Skip to content

Commit b4c4748

Browse files
committed
fix(compiler): better handling of interval arithmetic
1 parent 1032872 commit b4c4748

10 files changed

Lines changed: 907 additions & 188 deletions

File tree

AGENT.md

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# AGENT.md
2+
3+
Guidance for coding agents working in this repository. Derived from `CLAUDE.md`.
4+
5+
## Common Development Commands
6+
7+
### Building
8+
9+
- `npm run build` - Development build in `/build`
10+
- `npm run build watch` - Development build with file watching
11+
- `npm run build production` - Production build in `/dist`
12+
- `npm run clean` - Remove `/build` and `/dist`
13+
- `npm run typecheck` - Run TypeScript type checking (always when completing a task)
14+
15+
### Testing
16+
17+
- `npm run test` - Requires a specific test suite to be specified
18+
- `npm run test compute-engine/<test-name>` - Run specific test file (e.g., `npm run test compute-engine/arithmetic`)
19+
- `npm run test snapshot` - Update test snapshots
20+
- `npm test` - Alias for `npm run test`
21+
22+
### Development
23+
24+
- `npm start` - Development build with watch and local server
25+
- `npm run lint` - Run ESLint with auto-fix
26+
- `npm run doc` - Generate documentation
27+
28+
### Test File Patterns
29+
30+
- Tests live under `/test/`
31+
- Pattern: `npm run test compute-engine/<test-name>` maps to `/test/compute-engine/<test-name>.test.ts`
32+
33+
## Architecture Overview
34+
35+
### Core Components
36+
37+
- `src/compute-engine/index.ts` - ComputeEngine: parsing, evaluation, manipulation, scopes, precision, validation
38+
- `src/compute-engine/boxed-expression/` - Boxed expression types
39+
- `BoxedExpression`, `BoxedNumber`, `BoxedSymbol`, `BoxedFunction`, `BoxedString`
40+
- `src/common/type/` - Type system with inference and subtype checking
41+
- `src/compute-engine/latex-syntax/` - LaTeX parsing to MathJSON
42+
- `src/math-json/` - MathJSON structures and utilities
43+
44+
### Key Architecture Patterns
45+
46+
- Boxing system: expressions are boxed with consistent interfaces
47+
- Canonical forms: normalized representation for efficiency
48+
- Scoped evaluation: symbol definitions and assumptions in lexical scopes
49+
- Library system: domain-specific math libraries loaded selectively
50+
- Validation modes: `strict` (default) vs non-strict
51+
- Numeric precision: machine (64-bit) and arbitrary precision (Decimal.js)
52+
53+
## Expression Creation Modes
54+
55+
### Canonical vs Structural vs Non-Canonical
56+
57+
1. **Canonical** (`{ canonical: true }` or default)
58+
- Fully canonicalized, `bind()` is called, `isCanonical` true
59+
2. **Structural** (`{ structural: true }`)
60+
- Bound, only structural normalization, `isStructural` true
61+
- Use to avoid specific canonical transforms (e.g., keep `Power(x, 1/3)`)
62+
3. **Non-canonical** (`{ canonical: false }` without `structural`)
63+
- Not bound, not canonical/structural
64+
- **Cannot be used** in arithmetic operations (`.mul()`, `.add()`, etc.)
65+
66+
**Pitfall**: `ce._fn('Power', ..., { canonical: false }).mul(...)` will assert.
67+
Use structural mode instead:
68+
```ts
69+
ce.function('Power', [base, exp], { structural: true }).mul(other)
70+
```
71+
Note: `ce._fn()` does not support `structural`.
72+
73+
## Simplification and Recursion Prevention
74+
75+
**Critical**: Do not call `.simplify()` inside simplification rules or functions
76+
used by them. This can cause infinite recursion.
77+
78+
### Do not call `.simplify()` in:
79+
- `src/compute-engine/symbolic/simplify-rules.ts`
80+
- Polynomial helpers called by rules (e.g., `polynomialDivide`, `polynomialGCD`, `cancelCommonFactors`)
81+
- Any function in the simplification pipeline
82+
83+
### Safe to call `.simplify()` in:
84+
- Top-level APIs
85+
- Tests
86+
- Evaluation contexts
87+
- On simple numeric coefficients (with care)
88+
89+
### Best practice
90+
Return canonical expressions and let callers decide whether to simplify.
91+
92+
### Recursion Guards (do not rely on these if you recurse)
93+
- Expression deduplication in `simplify.ts`
94+
- Step limit guard (max 1000 steps)
95+
- Loop detection in main simplify loop
96+
97+
## Expression Lifecycle
98+
99+
1. Parse (LaTeX → MathJSON → BoxedExpression)
100+
2. Canonicalize
101+
3. Evaluate
102+
4. Simplify
103+
5. Serialize (LaTeX/MathJSON)
104+
105+
## Important Files
106+
107+
- Core engine: `src/compute-engine/index.ts`
108+
- Base expression: `src/compute-engine/boxed-expression/abstract-boxed-expression.ts`
109+
- Arithmetic: `src/compute-engine/boxed-expression/arithmetic-*.ts`
110+
- Validation: `src/compute-engine/boxed-expression/validate.ts`
111+
- Libraries: `src/compute-engine/library/`
112+
- Type system: `src/common/type/`
113+
- Test helpers: `test/utils.ts`
114+
- Generated: `API.md` (do not edit manually)
115+
116+
## Circular Dependency Resolution
117+
118+
Common problematic chain:
119+
```
120+
abstract-boxed-expression.ts → compile.ts → library/utils.ts → collections.ts → box.ts → abstract-boxed-expression.ts
121+
```
122+
123+
Resolution strategy:
124+
- Extract shared utilities (e.g., `canonical-utils.ts`)
125+
- Prefer static imports after breaking cycles
126+
- Verify via:
127+
```bash
128+
npx tsx -e "import {ComputeEngine} from './src/compute-engine'; new ComputeEngine()"
129+
```
130+
131+
Warning signs:
132+
- `ReferenceError: Cannot access '_BoxedExpression' before initialization`
133+
- Build failures with circular dependency warnings
134+
- ESLint: `Dependency cycle detected`

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
## [Unreleased]
2+
3+
### Bug Fixes
4+
5+
- **Interval Arithmetic (JS/GLSL)**: Fixed interval evaluation of compound
6+
arguments (e.g. `sin(2x)`, `sin(x+x)`, `sin(x^2)`, `cos(2x)`) by propagating
7+
interval results through trig, elementary, and comparison functions in
8+
`interval-js`, and by adding `IntervalResult` overloads to the GLSL interval
9+
library for `interval-glsl`.
10+
111
## 0.35.0 _2026-02-02_
212

313
### Parsing

src/compute-engine/compilation/base-compiler.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,14 @@ export class BaseCompiler {
145145

146146
if (h === 'If') {
147147
if (args.length !== 3) throw new Error('If: wrong number of arguments');
148+
const fn = target.functions?.(h);
149+
if (fn) {
150+
if (typeof fn === 'function') {
151+
return fn(args, (expr) => BaseCompiler.compile(expr, target), target);
152+
}
153+
if (args === null) return `${fn}()`;
154+
return `${fn}(${args.map((x) => BaseCompiler.compile(x, target)).join(', ')})`;
155+
}
148156
return `((${BaseCompiler.compile(args[0], target)}) ? (${BaseCompiler.compile(
149157
args[1],
150158
target

src/compute-engine/compilation/interval-glsl-target.ts

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ IntervalResult ia_partial(vec2 v, float clip) {
7979
return IntervalResult(v, clip);
8080
}
8181
82+
bool ia_is_error(float status) {
83+
return status == IA_EMPTY || status == IA_ENTIRE || status == IA_SINGULAR;
84+
}
85+
8286
// Addition
8387
IntervalResult ia_add(vec2 a, vec2 b) {
8488
return ia_ok(vec2(a.x + b.x - IA_EPS, a.y + b.y + IA_EPS));
@@ -394,6 +398,198 @@ IntervalResult ia_cosh(vec2 x) {
394398
IntervalResult ia_tanh(vec2 x) {
395399
return ia_ok(vec2(tanh(x.x) - IA_EPS, tanh(x.y) + IA_EPS));
396400
}
401+
402+
// IntervalResult overloads for propagation
403+
IntervalResult ia_add(IntervalResult a, IntervalResult b) {
404+
if (ia_is_error(a.status)) return a;
405+
if (ia_is_error(b.status)) return b;
406+
return ia_add(a.value, b.value);
407+
}
408+
409+
IntervalResult ia_add(IntervalResult a, vec2 b) {
410+
if (ia_is_error(a.status)) return a;
411+
return ia_add(a.value, b);
412+
}
413+
414+
IntervalResult ia_add(vec2 a, IntervalResult b) {
415+
if (ia_is_error(b.status)) return b;
416+
return ia_add(a, b.value);
417+
}
418+
419+
IntervalResult ia_sub(IntervalResult a, IntervalResult b) {
420+
if (ia_is_error(a.status)) return a;
421+
if (ia_is_error(b.status)) return b;
422+
return ia_sub(a.value, b.value);
423+
}
424+
425+
IntervalResult ia_sub(IntervalResult a, vec2 b) {
426+
if (ia_is_error(a.status)) return a;
427+
return ia_sub(a.value, b);
428+
}
429+
430+
IntervalResult ia_sub(vec2 a, IntervalResult b) {
431+
if (ia_is_error(b.status)) return b;
432+
return ia_sub(a, b.value);
433+
}
434+
435+
IntervalResult ia_mul(IntervalResult a, IntervalResult b) {
436+
if (ia_is_error(a.status)) return a;
437+
if (ia_is_error(b.status)) return b;
438+
return ia_mul(a.value, b.value);
439+
}
440+
441+
IntervalResult ia_mul(IntervalResult a, vec2 b) {
442+
if (ia_is_error(a.status)) return a;
443+
return ia_mul(a.value, b);
444+
}
445+
446+
IntervalResult ia_mul(vec2 a, IntervalResult b) {
447+
if (ia_is_error(b.status)) return b;
448+
return ia_mul(a, b.value);
449+
}
450+
451+
IntervalResult ia_div(IntervalResult a, IntervalResult b) {
452+
if (ia_is_error(a.status)) return a;
453+
if (ia_is_error(b.status)) return b;
454+
return ia_div(a.value, b.value);
455+
}
456+
457+
IntervalResult ia_div(IntervalResult a, vec2 b) {
458+
if (ia_is_error(a.status)) return a;
459+
return ia_div(a.value, b);
460+
}
461+
462+
IntervalResult ia_div(vec2 a, IntervalResult b) {
463+
if (ia_is_error(b.status)) return b;
464+
return ia_div(a, b.value);
465+
}
466+
467+
IntervalResult ia_negate(IntervalResult x) {
468+
if (ia_is_error(x.status)) return x;
469+
return ia_negate(x.value);
470+
}
471+
472+
IntervalResult ia_sqrt(IntervalResult x) {
473+
if (ia_is_error(x.status)) return x;
474+
return ia_sqrt(x.value);
475+
}
476+
477+
IntervalResult ia_square(IntervalResult x) {
478+
if (ia_is_error(x.status)) return x;
479+
return ia_square(x.value);
480+
}
481+
482+
IntervalResult ia_exp(IntervalResult x) {
483+
if (ia_is_error(x.status)) return x;
484+
return ia_exp(x.value);
485+
}
486+
487+
IntervalResult ia_ln(IntervalResult x) {
488+
if (ia_is_error(x.status)) return x;
489+
return ia_ln(x.value);
490+
}
491+
492+
IntervalResult ia_abs(IntervalResult x) {
493+
if (ia_is_error(x.status)) return x;
494+
return ia_abs(x.value);
495+
}
496+
497+
IntervalResult ia_sign(IntervalResult x) {
498+
if (ia_is_error(x.status)) return x;
499+
return ia_sign(x.value);
500+
}
501+
502+
IntervalResult ia_floor(IntervalResult x) {
503+
if (ia_is_error(x.status)) return x;
504+
return ia_floor(x.value);
505+
}
506+
507+
IntervalResult ia_ceil(IntervalResult x) {
508+
if (ia_is_error(x.status)) return x;
509+
return ia_ceil(x.value);
510+
}
511+
512+
IntervalResult ia_min(IntervalResult a, IntervalResult b) {
513+
if (ia_is_error(a.status)) return a;
514+
if (ia_is_error(b.status)) return b;
515+
return ia_min(a.value, b.value);
516+
}
517+
518+
IntervalResult ia_min(IntervalResult a, vec2 b) {
519+
if (ia_is_error(a.status)) return a;
520+
return ia_min(a.value, b);
521+
}
522+
523+
IntervalResult ia_min(vec2 a, IntervalResult b) {
524+
if (ia_is_error(b.status)) return b;
525+
return ia_min(a, b.value);
526+
}
527+
528+
IntervalResult ia_max(IntervalResult a, IntervalResult b) {
529+
if (ia_is_error(a.status)) return a;
530+
if (ia_is_error(b.status)) return b;
531+
return ia_max(a.value, b.value);
532+
}
533+
534+
IntervalResult ia_max(IntervalResult a, vec2 b) {
535+
if (ia_is_error(a.status)) return a;
536+
return ia_max(a.value, b);
537+
}
538+
539+
IntervalResult ia_max(vec2 a, IntervalResult b) {
540+
if (ia_is_error(b.status)) return b;
541+
return ia_max(a, b.value);
542+
}
543+
544+
IntervalResult ia_pow(IntervalResult base, float exp) {
545+
if (ia_is_error(base.status)) return base;
546+
return ia_pow(base.value, exp);
547+
}
548+
549+
IntervalResult ia_sin(IntervalResult x) {
550+
if (ia_is_error(x.status)) return x;
551+
return ia_sin(x.value);
552+
}
553+
554+
IntervalResult ia_cos(IntervalResult x) {
555+
if (ia_is_error(x.status)) return x;
556+
return ia_cos(x.value);
557+
}
558+
559+
IntervalResult ia_tan(IntervalResult x) {
560+
if (ia_is_error(x.status)) return x;
561+
return ia_tan(x.value);
562+
}
563+
564+
IntervalResult ia_asin(IntervalResult x) {
565+
if (ia_is_error(x.status)) return x;
566+
return ia_asin(x.value);
567+
}
568+
569+
IntervalResult ia_acos(IntervalResult x) {
570+
if (ia_is_error(x.status)) return x;
571+
return ia_acos(x.value);
572+
}
573+
574+
IntervalResult ia_atan(IntervalResult x) {
575+
if (ia_is_error(x.status)) return x;
576+
return ia_atan(x.value);
577+
}
578+
579+
IntervalResult ia_sinh(IntervalResult x) {
580+
if (ia_is_error(x.status)) return x;
581+
return ia_sinh(x.value);
582+
}
583+
584+
IntervalResult ia_cosh(IntervalResult x) {
585+
if (ia_is_error(x.status)) return x;
586+
return ia_cosh(x.value);
587+
}
588+
589+
IntervalResult ia_tanh(IntervalResult x) {
590+
if (ia_is_error(x.status)) return x;
591+
return ia_tanh(x.value);
592+
}
397593
`;
398594

399595
/**

0 commit comments

Comments
 (0)