1- ### Unreleased
1+ ### 0.56.0 _ 2026-03-10 _
22
33#### Added
44
5- - ** Function-style aliases for collection / arithmetic operators** —
6- the lowercase ` \operatorname{...} ` forms common in Desmos-style and
7- procedural notations now parse to their existing capitalized
8- counterparts:
9- - ` \operatorname{mod}(a, b) ` → ` Mod `
10- - ` \operatorname{var}(L) ` → ` Variance `
11- - ` \operatorname{shuffle}(L) ` → ` Shuffle `
12- - ` \operatorname{random}() ` → ` Random `
13- - ` \operatorname{repeat}(x) ` → ` Repeat `
14- - ` \operatorname{join}(L1, L2, ...) ` → ` Join `
15- No semantic change — the operators themselves were already
16- registered, just under their capitalized names.
17-
18- - ** ` Distance(p1, p2) ` ** — Euclidean distance between two points
19- represented as tuples. Accepts any positive dimension; mismatched
20- dimensions return a typed error. LaTeX trigger
21- ` \operatorname{distance}(p1, p2) ` .
22-
23- - ** Geometric primitive heads** — ` Triangle ` , ` Sphere ` , ` Segment `
24- registered as opaque typed function heads (signature only, no
25- evaluator). Parsed but preserved structurally; consumers branch on
26- the operator name. LaTeX triggers ` \operatorname{triangle} ` ,
27- ` \operatorname{sphere} ` , ` \operatorname{segment} ` . Same shape as the
28- existing ` ["To", a, b] ` pattern.
29-
30- - ** ` To ` library entry** — ` \to ` already parsed to ` ["To", a, b] ` ,
31- but the head wasn't in the standard operator set, so rows containing
32- it were classified as ` unsupported-operator ` . The library entry
33- declares the head's signature (no evaluator) so the classification
34- reflects reality: this is a known typed action node.
35-
36- - ** ` GeometricVector ` head** — ` \operatorname{vector}(p1, p2) ` (Desmos's
37- geometric form: a directed segment between two points) parses to a
38- new opaque typed head. Distinct from the existing ` Vector ` operator
39- for column-vector construction (` (number+) -> vector ` signature), so
40- aliasing the LaTeX trigger to it would have either constrained
41- Desmos's input shape or forced a signature widening. New head, no
42- evaluator — same shape as ` Triangle ` /` Sphere ` /` Segment ` .
43-
44- - ** First-class color values** — colors are now typed values with their
45- own primitive type (` color ` ) and per-colorspace constructor heads,
46- rather than anonymous tuples.
47-
48- - ** Constructor heads** (one per colorspace): ` Rgb ` , ` Hsv ` , ` Hsl ` ,
49- ` Oklab ` , ` Oklch ` . Each takes 3 components plus an optional 4th
50- alpha argument. Components are interpreted per the colorspace's
51- own conventions — ` Rgb ` channels 0–1 sRGB (no parse-time clamp),
52- HSV/HSL hue in degrees with saturation/value/lightness 0–1,
53- Oklab/Oklch with their standard ranges. The operator name
54- preserves the colorspace through evaluation.
55-
5+ - ** First-class color values** — colors are now typed values with a dedicated
6+ ` color ` primitive type and per-colorspace constructor heads, rather than
7+ anonymous tuples.
8+ - ** Constructor heads** : ` Rgb ` , ` Hsv ` , ` Hsl ` , ` Oklab ` , ` Oklch ` . Each takes 3
9+ components plus an optional alpha. Channels follow each colorspace's own
10+ conventions (RGB: 0–1 sRGB; HSV/HSL: hue in degrees, S/V/L 0–1; Oklab/Oklch:
11+ standard ranges).
5612 - ** LaTeX** : ` \operatorname{rgb}(...) ` , ` \operatorname{hsv}(...) ` ,
5713 ` \operatorname{hsl}(...) ` , ` \operatorname{oklab}(...) ` ,
58- ` \operatorname{oklch}(...) ` . 1:1 with the heads, both directions.
59-
60- - ** Conversions** : ` AsRgb ` , ` AsHsv ` , ` AsHsl ` , ` AsOklab ` , ` AsOklch `
61- convert any color to the named space (identity if already there).
62- Oklab↔Oklch routes directly via polar/cartesian conversion;
63- sRGB-based spaces go through RGB.
64-
65- - ** ` ColorDelta(a, b) ` ** — perceptual color difference (ΔE_OK,
66- Euclidean distance in OKLab). Wide-gamut inputs are not clipped
67- before measurement.
68-
69- - ** ` color ` primitive type** — added to ` PrimitiveType ` . Subtype of
70- ` value ` . Used in the signatures of all color constructors,
71- conversions, ` Color ` , ` ColorMix ` , ` ContrastingColor ` , and the
72- color-input slots of ` ColorDelta ` /` ColorContrast ` /` ColorToString ` /
73- ` ColorToColorspace ` /` ColorFromColorspace ` .
74-
75- - Driven by the Graph Paper team's roadmap for parsing Desmos-style
76- color formulas (` \operatorname{rgb} ` , ` \operatorname{hsv} ` ,
77- ` \operatorname{oklab} ` were the most common unsupported color
78- operators in their corpus).
79-
80- - ** JavaScript compile-target support for color values** — all five
81- color constructor heads, the five ` As* ` converters, ` ColorDelta ` ,
82- and ` Distance ` compile to runtime ` _SYS.* ` calls. At runtime a color
83- is a 3- or 4-element ` [L, C, H] ` (or ` [L, C, H, alpha] ` ) array in
84- canonical OKLCh — the same shape produced by ` Color() ` , ` ColorMix ` ,
85- ` Colormap ` , etc. (Mirrors the GPU target's design: color values are
86- ` vec3 ` OKLCh in shader code.) ` AsOklch ` compiles as a no-op pass-
87- through. The compile-target representation is identical across JS,
88- GLSL, and WGSL, so values move between contexts without conversion.
14+ ` \operatorname{oklch}(...) ` , parsing and serialization both directions.
15+ - ** Conversions** : ` AsRgb ` , ` AsHsv ` , ` AsHsl ` , ` AsOklab ` , ` AsOklch ` convert any
16+ color to the named space (identity if already there).
17+ - ** ` ColorDelta(a, b) ` ** — perceptual color difference (ΔE_OK, Euclidean
18+ distance in OKLab). Wide-gamut inputs are not clipped before measurement.
19+
20+ - ** JavaScript compile-target support for color values** — all color
21+ constructors, the ` As* ` converters, ` ColorDelta ` , and ` Distance ` are
22+ supported. At runtime a color is a 3- or 4-element OKLCh array
23+ (` [L, C, H] ` or ` [L, C, H, alpha] ` ), matching the GPU target's ` vec3 ` /` vec4 `
24+ representation, so values move between JS, GLSL, and WGSL without conversion.
25+
26+ - ** ` Distance(p1, p2) ` ** — Euclidean distance between two points represented as
27+ tuples. Accepts any positive dimension; mismatched dimensions return a typed
28+ error. LaTeX trigger ` \operatorname{distance}(p1, p2) ` .
29+
30+ - ** Geometric primitive heads** — ` Triangle ` , ` Sphere ` , ` Segment ` , and
31+ ` GeometricVector ` are now recognized as typed function heads (no evaluator,
32+ preserved structurally for downstream consumers). LaTeX triggers
33+ ` \operatorname{triangle} ` , ` \operatorname{sphere} ` , ` \operatorname{segment} ` ,
34+ ` \operatorname{vector}(p1, p2) ` . ` GeometricVector ` is distinct from the
35+ existing ` Vector ` (column-vector construction).
36+
37+ - ** ` To ` head registered** — ` \to ` already parsed to ` ["To", a, b] ` but was
38+ classified as ` unsupported-operator ` ; it is now a known typed head.
39+
40+ - ** Function-style aliases** — lowercase ` \operatorname{...} ` forms common in
41+ Desmos-style notation now parse to their existing capitalized operators:
42+ ` \operatorname{mod} ` → ` Mod ` , ` \operatorname{var} ` → ` Variance ` ,
43+ ` \operatorname{shuffle} ` → ` Shuffle ` , ` \operatorname{random} ` → ` Random ` ,
44+ ` \operatorname{repeat} ` → ` Repeat ` , ` \operatorname{join} ` → ` Join ` .
45+
46+ - ** ` ce.latexOptions ` ** — new mutable, engine-wide bag of LaTeX parse/serialize
47+ options (e.g. ` decimalSeparator ` , ` digitGroupSeparator ` ). Available as a
48+ constructor option and as a read/write property:
49+ ``` ts
50+ const ce = new ComputeEngine ({ latexOptions: { decimalSeparator: ' {,}' } });
51+ // or post-construction:
52+ ce .latexOptions = { decimalSeparator: ' {,}' };
53+ ```
54+ These options are merged into every ` ce.parse() ` and ` expr.toLatex() ` call.
55+ Precedence (most-specific wins): ` LatexSyntax ` instance defaults <
56+ ` ce.latexOptions ` < per-call options. Previously, options like
57+ ` decimalSeparator ` could only be changed per call post-construction (and
58+ ` expr.latex ` could not be customized at all).
8959
9060#### Changed
9161
92- - ** ` Color('...') ` ** now returns an ` Oklch ` head instead of a 0–1 sRGB
93- ` Tuple ` . Oklch is the canonical wide-gamut representation; downstream
94- code can convert via ` AsRgb ` etc. The string parser still accepts the
95- same set of CSS-style inputs.
96-
97- - ** ` ColorMix ` ** now returns an ` Oklch ` head regardless of input form
98- (was: 0–1 sRGB ` Tuple ` ). When both inputs are typed color heads, the
99- mix happens in OKLCh directly without an sRGB pinch, preserving
100- out-of-gamut chroma. Hue interpolation takes the shortest path around
101- the wheel; mixing with an achromatic endpoint carries the other
62+ - ** ` Color('...') ` ** now returns an ` Oklch ` head instead of a 0–1 sRGB ` Tuple ` .
63+ The string parser still accepts the same set of CSS-style inputs.
64+ - ** ` ColorMix ` ** now returns an ` Oklch ` head and mixes in OKLCh directly,
65+ preserving out-of-gamut chroma. Hue interpolation takes the shortest path
66+ around the wheel; mixing with an achromatic endpoint carries the other
10267 endpoint's hue (matches CSS Color 4 ` color-mix ` ).
103-
104- - ** ` ContrastingColor ` ** now returns an ` Rgb ` head (was: 0–1 sRGB
105- ` Tuple ` ). White and black are sRGB by definition, so ` Rgb(1, 1, 1) `
106- and ` Rgb(0, 0, 0) ` are the natural representations.
107-
108- - ** ` Colormap ` ** now returns ` Oklch ` heads — either a ` List(Oklch, ...) `
109- for full-palette / N-color resampling, or a single ` Oklch ` for
110- position-sampling.
111-
112- - ** ` ColorToString ` ** with ` 'oklch' ` format now serializes typed color
113- inputs without an sRGB round-trip; out-of-gamut chroma serializes
114- losslessly. ` 'hex' ` /` 'rgb' ` /` 'hsl' ` paths unchanged (sRGB is the
115- destination, so clipping is correct).
116-
68+ - ** ` ContrastingColor ` ** now returns an ` Rgb ` head (was: 0–1 sRGB ` Tuple ` ).
69+ - ** ` Colormap ` ** now returns ` Oklch ` heads — either a ` List(Oklch, ...) ` or a
70+ single ` Oklch ` for position-sampling.
71+ - ** ` ColorToString ` ** with ` 'oklch' ` format serializes typed color inputs
72+ without an sRGB round-trip; out-of-gamut chroma serializes losslessly.
73+ ` 'hex' ` /` 'rgb' ` /` 'hsl' ` paths are unchanged.
11774- ** Color-consuming signatures tightened** — ` (any, any) ` →
11875 ` (color | string | tuple, color | string | tuple) ` for ` ColorDelta ` ,
11976 ` ColorContrast ` , ` ColorMix ` , ` ContrastingColor ` , ` ColorToString ` ,
120- ` ColorToColorspace ` . The ` As* ` converters tightened further to
121- ` (color) -> color ` since they only accept typed color heads.
77+ ` ColorToColorspace ` . The ` As* ` converters take ` (color) -> color ` .
12278
12379#### Migration notes
12480
125- Code that consumed ` Color('...') ` 's tuple output, or the tuple output
126- of ` ColorMix ` / ` ContrastingColor ` / ` Colormap ` , now sees a typed color
127- head. To reconstruct the previous shape, wrap with the appropriate
128- converter:
81+ Code that consumed the tuple output of ` Color('...') ` , ` ColorMix ` ,
82+ ` ContrastingColor ` , or ` Colormap ` now sees a typed color head. To get the
83+ previous 0–1 sRGB shape, wrap with ` AsRgb ` :
12984
13085``` ts
13186// Before: const tuple = ce.expr(['Color', "'red'"]).evaluate(); // [r, g, b] in 0-1
@@ -134,122 +89,45 @@ const rgb = ce.expr(['AsRgb', ['Color', "'red'"]]).evaluate();
13489// rgb is ['Rgb', r, g, b] with channels 0-1
13590```
13691
137- ** RGB channel convention** : ` Rgb ` head components are ** 0–1 sRGB** across
138- all layers (engine, JS compile, GPU compile). This is a uniform convention
139- chosen for shader-pipeline interoperability and for round-trip simplicity
140- (` Rgb(r, g, b) → AsRgb → [r, g, b] ` matches the input). Consumers parsing
141- Desmos-style formulas (which use 0–255) should normalize at the
142- adapter/importer layer — ` Rgb(255, 0, 0) ` in Desmos source maps to
143- ` Rgb(1, 0, 0) ` in CE.
144-
145- - ** ` ce.latexOptions ` ** — new mutable, engine-wide bag of LaTeX
146- parse/serialize options (e.g. ` decimalSeparator ` , ` digitGroupSeparator ` ).
147- Available as a constructor option and as a read/write property:
148- ``` ts
149- const ce = new ComputeEngine ({ latexOptions: { decimalSeparator: ' {,}' } });
150- // or post-construction:
151- ce .latexOptions = { decimalSeparator: ' {,}' };
152- ```
153- These are merged into every ` ce.parse() ` and ` expr.toLatex() ` call.
154- Precedence (most-specific wins): LatexSyntax instance defaults <
155- ` ce.latexOptions ` < per-call options. Previously, the only way to change
156- options like ` decimalSeparator ` post-construction was per call (and the
157- ` expr.latex ` getter could not be customized at all).
92+ ` Rgb ` head components are ** 0–1 sRGB** across all layers (engine, JS compile,
93+ GPU compile).
15894
15995#### Fixed
16096
16197- ** Super-linear parse time on deeply-nested parametric expressions** —
162- ` ce.parse() ` could exhibit exponential blowup on inputs like nested
163- rotation matrices ` \left(\cos(\theta)\cdot S+\sin(\theta)\right) `
164- (depth 6 took ~ 44s; depth 7 would take minutes). Two underlying causes:
165-
166- - The ` cachedValue() ` helper that backs type / sign caches on
167- ` BoxedFunction ` had its early-return guard commented out, so every
168- ` .type ` access recomputed the type by recursing into all operand
169- types. With cache disabled, an expression of nesting depth ` d `
170- triggered O(2^d) type recomputations during canonicalization.
171-
172- The guard had been disabled because the original form was buggy:
173-
174- ``` js
175- if (v .generation === undefined || v .generation === generation) {
176- if (v .value === null ) v .value = fn ();
177- return v .value ;
178- }
179- ```
98+ ` ce.parse() ` could exhibit exponential blowup on inputs like nested rotation
99+ matrices ` \left(\cos(\theta)\cdot S+\sin(\theta)\right) ` (depth 6 took ~ 44s).
100+ Two underlying causes were addressed: the type/sign cache on ` BoxedFunction `
101+ was effectively disabled (causing every ` .type ` access to recurse through all
102+ operands), and ` parseEnclosure ` was speculatively trying matchfix definitions
103+ whose close-delimiter token wasn't even present in the input. Parse time on
104+ the affected inputs is now linear.
105+
106+ - ** ` ce.parse() ` ignored the injected ` LatexSyntax ` instance's
107+ ` decimalSeparator ` ** — ` ce.parse() ` hardcoded ` decimalSeparator: '.' ` ,
108+ silently overriding any value configured on a ` LatexSyntax ` passed via the
109+ constructor's ` latexSyntax ` option. The injected instance's configured
110+ separator now takes effect end-to-end.
180111
181- On a fresh cache ` v.generation` is ` undefined` , so the first call
182- took the early- return branch, computed ` v.value` , and returned —
183- but never updated ` v.generation` from ` undefined` . Every later
184- call, regardless of the requested generation, also matched
185- ` === undefined` and returned the same stale value . The cache
186- effectively never invalidated . Disabling it fixed staleness at the
187- cost of all caching.
188-
189- Re- enabled with a corrected guard that compares against the
190- requested generation directly, so first- compute updates
191- ` v.generation` and subsequent calls invalidate correctly when the
192- engine generation advances:
193-
194- ` ` ` js
195- if (v.generation === generation && v.value !== null) return v.value;
196- v.generation = generation;
197- v.value = fn();
198- return v.value;
199- ` ` `
112+ - ** ` expr.toMathJson({ metadata: ['latex'] }) ` was silently dropped** — passing
113+ a metadata array of specific fields (e.g. ` ['latex'] ` or ` ['wikidata'] ` ) was
114+ ignored; only ` metadata: 'all' ` worked. The array form now correctly
115+ populates the requested fields.
200116
201- - ` parseEnclosure` tried every matchfix definition that matched the
202- open delimiter, parsing the body twice for each (once with the
203- boundary, once without). For invalid inputs containing many ` .`
204- tokens (e .g . Desmos ' s `p.x` field-access syntax), the
205- `EvaluateAt` matchfix (`.` … `|`) was attempted on every `.` even
206- though `|` was nowhere in the input, and each speculative body
207- parse contained more nested `.` triggers. Two changes address this:
208- a pre-check (using a `closeTokens` set pre-computed at dictionary
209- indexing time) skips matchfix defs whose close trigger can' t appear
210- ahead in the token stream; and the ` EvaluateAt` def is no longer
211- tried on bare ` .` (the canonical form is ` \l eft.expr\r ight|_{x=0}` ,
212- which the pre- check still admits via the ` \l eft` prefix), so a ` .`
213- without a ` \l eft` / ` \m athopen` / etc . prefix never speculates as
214- ` EvaluateAt` .
215-
216- Combined, deeply- nested expressions and large invalid inputs (the
217- Desmos corpus' s worst row was 1006 chars and previously hung
218- indefinitely) now parse in milliseconds. On the Desmos corpus
219- benchmark (2,092 probes at 5s budget), parse-timeouts dropped from
220- 62 to 3 — a 95% reduction.
221-
222- - **`ce.parse()` ignored the injected LatexSyntax instance' s
223- ` decimalSeparator` ** — ` ce.parse()` hardcoded ` decimalSeparator: '.'` ,
224- silently overriding any value configured on the ` LatexSyntax` passed
225- via the constructor 's `latexSyntax` option. The hardcoded default has
226- been removed; the injected instance's configured separator now takes
227- effect end-to-end.
228-
229- - **`expr.toMathJson ({ metadata: [' latex' ] })` was silently dropped** —
230- passing a metadata array containing only specific fields (e.g.
231- `['latex']` or `['wikidata']`) was ignored because the option fell
232- through and the final spread overrode it with the empty default. Only
233- `metadata: ['all']` worked. The array case now correctly populates
234- the requested metadata fields.
235-
236- - **`expr.toMathJson({ shorthands: [' all' ] })` disabled all shorthands** —
237- the ` ' all' ` branch correctly expanded the defaults to the full kind
238- list, but a following unconditional ` if (Array .isArray (... ))`
239- overwrote it back to the literal ` [' all' ]` , which matches no actual
240- shorthand kind. So passing ` [' all' ]` had the opposite effect of what
241- was intended. The string form ` ' all' ` and explicit lists like
242- ` [' function' ]` were unaffected.
117+ - ** ` expr.toMathJson({ shorthands: ['all'] }) ` disabled all shorthands** — the
118+ ` ['all'] ` array form had the opposite of its intended effect. The string form
119+ ` 'all' ` and explicit lists like ` ['function'] ` were unaffected.
243120
244121### 0.55.6 _ 2026-03-08_
245122
246123#### Fixed
247124
248- - **LaTeX parsing: ` \lim` with postfix operators** — ` \lim_{x\to 0 }\left (x\right)^ x`
249- now correctly parses as ` Limit (x^ x)` instead of ` Power (Limit (x), x)` . The
250- ` \lim` parser was using ` parseArguments (' implicit' )` which stripped the
251- delimiters and left the ` ^ x` unconsumed; it now uses ` parseExpression` so
252- postfix operators are included in the limit body.
125+ - ** LaTeX parsing: ` \lim ` with postfix operators** —
126+ ` \lim_{x\to 0}\left(x\right)^x ` now correctly parses as ` Limit(x^x) ` instead
127+ of ` Power(Limit(x), x) ` . The ` \lim ` parser was using
128+ ` parseArguments('implicit') ` which stripped the delimiters and left the ` ^x `
129+ unconsumed; it now uses ` parseExpression ` so postfix operators are included in
130+ the limit body.
253131
254132- ** LaTeX parsing: style, size, and color switch commands** — ` \displaystyle ` ,
255133 ` \textstyle ` , ` \scriptstyle ` , ` \scriptscriptstyle ` , ` \tiny ` ..` \Huge ` (10 size
0 commit comments