<!-- https://mathlive.io/compute-engine/changelog/ -->

# Compute Engine Changelog

import ChangeLog from '@site/src/components/ChangeLog';

<ChangeLog>
## Coming Soon

### Breaking Changes

- **The Cortex language has been renamed Epsil.** The experimental scripting
  language previously called Cortex is now Epsil, and every public surface
  follows: the CLI binary is `epsil` (was `cortex`), the conventional source
  file extension is `.epsil` (was `.cortex` / `.cx`), the package subpath is
  `@cortex-js/compute-engine/epsil` (was `…/cortex`), and the API entry points
  are `parseEpsil()`, `serializeEpsil()` and `executeEpsil()` (with the
  corresponding `ExecuteEpsilOptions`/`ExecuteEpsilResult` types). The npm
  package name and `@cortex-js` scope are unchanged. There are no compatibility
  aliases: update imports and scripts to the new names. A `./cli` package
  export exposes the CLI entry point so the standalone `epsil` launcher
  package (and other tools) can forward to it.

- **The `structural: true` boolean is no longer part of `ce.function()`'s typed
  signature — use `{ form: 'structural' }`.** The `form` option has been the
  documented spelling for the creation modes since the structural tier was
  introduced, and `ce.expr()`, `ce.box()` and `ce.parse()` had already dropped
  the boolean from their public types; `ce.function()` was the last one still
  carrying it, which made the surface inconsistent and advertised a spelling the
  guide tells you not to use. The boolean is still accepted **at runtime** —
  `optionsToInternal()` maps `{ structural: true }` and `{ form: 'structural' }`
  to the identical internal form — so only code type-checked against
  `ComputeEngine`/`IComputeEngine` is affected, and the fix is a rename at the
  call site. `{ canonical: false }` (equivalently `{ form: 'raw' }`) is in the
  same position and was already untyped.

- **Cortex: `=` is now positional — it assigns only as a whole statement, and
  compares everywhere else.** `:=` always assigns and `==` always compares; a
  bare `=` means `Assign` when it is the top-level operator of a statement whose
  left side is a binding target (a name, or a field/index path rooted at one),
  and `Equal` in every other position. The canonical trap simply works:
  `Solve(x^2 = 4, x)` is the equation and returns `[2, -2]`, where it used to
  assign and report no solutions. So do `if a = true { … }`, `while x = 5 { }`
  and `[a = 1]`, each of which previously assigned **silently** — the C footgun
  no longer exists in Cortex.

  The decision is purely syntactic: it never depends on evaluating a value, on
  scope, or on which definitions are installed. As a comparison `=` binds at the
  relational tier, so `if x = 5 && y` groups as `(x = 5) && y`; as an assignment
  it binds loosest and takes the whole right-hand side.

  Two consequences. A statement whose left side is _not_ a binding target now
  compares — `x^2 = 4` on its own line is the equation, not an assignment to a
  power. And `a = b = 5` would assign `a` the boolean `b == 5`, which is never
  what a chained assignment means, so it reports `chained-assignment`; write
  `a := b := 5` to chain, or `a = (b = 5)` if the comparison was meant.

  `:=` is unconditional, so it still reaches a condition where a bare `=` no
  longer can. `if flag := true { … }` assigns and then uses the assigned value
  as the test — with no type error to catch it — so it now reports
  `assign-in-condition`, a **warning** (the spelling is deliberate). It fires
  only where a value is consumed as a boolean, not for `f(a := 1)`.

  The `assign-in-argument` diagnostic is **removed**: `f(x = 4)` is now an
  ordinary comparison, so there is nothing left to diagnose. Serialized output
  always uses the explicit `:=` and `==`, never a bare `=`, so a round-trip is
  exact regardless of position — which means the formatter rewrites an authored
  `=` to `:=` where it assigns.

### New Features

- **Epsil debugging support in the engine.** Two additions that let a
  debugger (such as the VS Code Epsil extension's DAP adapter) pause and
  inspect Epsil programs at statement granularity:
  - **Source positions survive canonicalization.** The `sourceOffsets`
    metadata the Epsil parser attaches to every node is now preserved
    through canonical boxing — custom canonical-handler results and the
    numeric fast-path constructors re-attach it, the `.canonical` getters
    (function and symbol) thread it, closure capture keeps it on rebuilt
    bodies, and the recursion knot-tying re-box serializes it. Positions are
    advisory metadata: default JSON serialization does not emit them
    (opt-in via `metadata: ['sourceOffsets']`), and interned singletons are
    never stamped.
  - **Debug statement hooks.** `src/common/debug-hook.ts` exposes a
    synchronous, module-global pre-statement hook and post-statement result
    hook, fired by the statement sequencer (`Block` bodies, lambda bodies,
    `if` branches) for source-mapped statements only. One comparison per
    statement when unset; not part of the public engine API.
  - Function-application scopes are now pushed with the context name
    `'call'` (previously an anonymous placeholder name), so `ce.trace` and
    debuggers can delimit activation frames.

- **Destructuring assignment — `(a, b) := (b, a)`.** A tuple pattern may now
  appear on the left of a Cortex assignment, writing bindings that already
  exist instead of declaring new ones. The pattern grammar is the destructuring
  `let`'s — at least two elements, each a bare symbol, a `_` skipping that
  position, or a nested tuple pattern — and a shape mismatch is the same
  `incompatible-type` error value.

  The right-hand side is evaluated **once, in full, before any target is
  written**, which is what makes a swap mean what it reads: `(a, b) := (b, a)`
  exchanges the two values rather than assigning `b` to both. The same holds
  for a rotation (`(a, b, c) := (c, a, b)`) and for the pair-carrying loop step
  that is the usual reason to want this — `(a, b) := (b, a + b)` is an entire
  Fibonacci iteration, and `(a, b) := (b, a % b)` an entire Euclid step, with
  no temporary.

  Unlike a destructuring `let`, the targets keep their identity and their
  declared type: a value that does not fit a target's type is an error value,
  and assigning to a `const` fails. Those two are found only by attempting the
  write, so they are not atomic — targets earlier in the pattern stay written.
  A shape mismatch is atomic and writes nothing, including when it is nested
  under a position that would have bound; the destructuring `let` gained the
  same guarantee, which it did not previously have. Lowers to the `Assign`
  primitive with a `Tuple` pattern in the target position, held raw
  (canonicalizing it would fold a single-letter target such as `i` into the
  constant of that name), and accepted on all routes.

  In **compiled** code it lowers to per-leaf temporaries followed by per-leaf
  writes — `(a, b) := (b, a + b)` becomes `let _tv1 = b; let _tv2 = a + b; a = _tv1; b = _tv2` — which is what keeps the compiled form honest: the
  targets already exist, so the naive `a = b; b = a` would read the `a` it just
  clobbered. Temporaries never capture a name the program already uses. Every
  target — JavaScript, Python, GLSL and WGSL — compiles it in any statement
  position, including a loop body, so the Fibonacci and Euclid steps above
  compile. Value position (a block's last statement, whose value is the
  block's) and a non-literal tuple value fail closed (D6) and the interpreter
  takes over; a destructuring `let` in value position is now refused the same
  way, explicitly, rather than by emitting source that happens not to parse.
  (This also fixes a silent divergence in the same family as the
  destructuring-declare one: a tuple target previously compiled
  as `_ = …`, leaving every target at its old value behind `success: true`.)

  ```js
  ce.box(['Assign', ['Tuple', 'a', 'b'], ['Tuple', 'b', 'a']]).evaluate();
  ```

- **Cortex diagnoses a tuple pattern written with a bare `=`.** A parenthesized
  left side is not a binding target, so `(a, b) = (b, a)` resolves — correctly,
  under the positional-`=` rule — to a *comparison* of two tuples whose result
  is discarded: the swap it looks like silently does nothing. That shape is
  almost always a typo for the destructuring assignment above, so it now
  reports `destructuring-bare-equal`; write `(a, b) := (b, a)` to destructure,
  or `==` if the comparison was meant. The node is unchanged — the diagnostic
  reports, it does not reinterpret.

  The check is deliberately narrow: it fires only statement-leading, and only
  when the left side is shaped exactly like a destructuring pattern (bare
  names, `_`, nested tuples), so a genuine tuple equation with computed
  components — `(x + 1, y) = t` — stays silent.

- **Parameterized nominal types: `type tree<T> = tuple<value: T, children: list<tree<T>>>`.** A nominal `type` declaration now takes the same
  type-parameter clause a generic alias takes, in Cortex as above and from the
  host with
  `ce.declareType('tree', '…', { typeParams: [{ name: 'T', variance: 'out' }] })`.
  Unlike an alias, an application is **opaque** — `tree<integer>` is never
  expanded — which is precisely what lets the definition refer to itself, so a
  recursive parametric container (a rose tree, a JSON with a payload, a zipper)
  is expressible for the first time. The arity, bound and unused-parameter rules
  are the alias's, shared and generalized; self-reference, which an alias
  forbids, is the point here.

  A parameter carries a **variance** marker — `out` (covariant), `in`
  (contravariant) or `inout` (invariant) — saying how two applications relate:
  under `out`, a `tree<integer>` is usable where a `tree<number>` is expected.
  **No marker means `out`**, declared rather than inferred and verified against
  the definition like a written one: values are immutable, so covariance is
  sound and is what a payload container wants, and only the consuming minority
  pays an annotation. Because the default is declared, a definition that uses
  its parameter in an input position does not quietly change the type's
  subtyping contract — it is a `variance-violation` at the declaration, naming
  the violated variance and where it came from, the offending occurrences by
  path (`notify.(arg 1)`), and exactly the markers that would verify. `inout`
  verifies against any definition. Variance and bounds do not interact.

  A `tuple` definition mints a **quantified** constructor
  (`tree: forall T. (T, list<tree<T>>) -> tree<T>`), so `tree(1, [])` solves
  `T = finite_integer` from its arguments; a `record` definition is still
  inhabited by a constructor function, whose own clause is independent of the
  type's. Field access reads the definition **instantiated at the
  application's arguments** — with `t: tree<number>`, `t.value` is a `number`.
  `match` is a binding of values, not a projection of the annotation: each
  capture takes the matched **value's own** type, usually narrower — on a `t`
  built as `tree(1, [])`, `match t { tree(v, cs) => … }` binds `v: integer` and
  `cs: list<never>`, not `number` and `list<tree<number>>`. Compilation
  erases the tag at the instantiated definition, as it already did for an
  unparameterized nominal type: `tree<integer>` compiles like the equivalent
  tuple, and declines identically where that would. One documented limitation: a
  construction solves its parameters from its arguments alone and an annotation
  does not widen them, so an explicitly `inout`/`in` parameterized type can only
  be constructed at exactly its argument type. See the new "Parameterized
  Nominal Types" section of the types guide.

- A type variable may now appear in one arm of a **union**: `type opt<T> = T | missing` and `forall T. (T | missing) -> list<T>` are accepted, and at a call
  the argument takes exactly one arm — the open arm binds the variable
  (refutation included), a ground arm binds `never`, the narrowest member of
  the family. At most one arm of a union may mention a variable (`T | U` is
  unsolvable by construction). Intersections and negations remain rejected
  wherever the declaration mints a constructor — the minted signature is what
  is checked, so a `record` body or a `mint: false` declaration goes
  unchecked — and the intersection diagnostic now steers to the spelling that
  replaces it, a bound (`forall T: number.`).

- A Cortex `type` statement re-declaration (a notebook re-run, or an edited
  definition) now UPDATES the existing type record in place instead of
  installing a new one. Types that mention the name — and applied references
  such as `box<integer>` already built — follow the new definition, so a node
  parsed before the re-run and one parsed after can no longer give different
  subtyping answers for the same pair of types, and a mutually recursive set
  converges on the second run rather than the third. A re-declaration that
  breaks a type depending on it now fails on the run that introduces it, rather
  than silently leaving that type reading a stale definition: an edit that
  changes the type-parameter count while a dependent still applies the old
  arity is a `generic-alias-arity` error, and one that makes a dependent's
  declared variance unsound is a `variance-violation`. Both are attributed to
  the dependent and name the re-declaration as the trigger, and both roll the
  statement back completely — definition, type-parameter clause, verified
  variance and minted constructor all restored. Re-declaring a type through the
  host `ce.declareType()` API still throws, unchanged.

- **Cortex: `break` and `continue`.** They leave, or skip to the next iteration
  of, the innermost enclosing `while`/`for` loop, and lower to the engine's
  existing `Break()`/`Continue()` primitives. Valid anywhere in a loop body,
  including inside an `if`, a `match` case, or a `do` block. The loop context
  resets at every function and lambda boundary — a `break` written inside a
  lambda defined in a loop body does not target that loop, and is a
  `control-outside-loop` diagnostic — because the engine's `Block`
  short-circuits on `Break`/`Continue` structurally, so a laxer rule would
  permit non-local control flow. Value-carrying `break value` remains unspelled,
  pending the ruling on a general `return`.

- **Cortex: `??` for absence coalescing.** `a ?? b` is `Coalesce(a, b)`: the
  value of `a` unless it is absent (`Missing` or `NaN`). It discharges
  _absence_; it does not rescue an `Error`. Right-associative, at precedence 18
  — looser than `|>` so `xs |> f ?? 0` defaults the pipeline's result, tighter
  than `|->` so `x |-> x.a ?? 0` defaults inside the body.

- **Cortex: `is` for dynamic type tests.** `x is integer` lowers to
  `Element(x, integer)` — the same test a `match` type pattern performs. The
  right operand is a type name, so a typo (`x is intger`) is a parse-time
  diagnostic rather than a comparison against an undeclared symbol. Simple named
  types only for now: a compound type (`!error`, `integer | string`,
  `list<integer>`) reports `type-pattern-unsupported`, as the equivalent typed
  pattern already does. `is` is a contextual word — `let is = 5` stays legal.

### Improvements

- **Membership in a value collection now types the tested function
  parameter.** `Element(c, digits)` — Epsil `c in digits` — inside a function
  body narrows a not-yet-typed parameter to the collection's element type
  (`digits: list<string>` ⇒ `c: string`), the membership counterpart of the
  collection evidence `Length(cs)` and `cs[i]` already contribute. The
  evidence lands on the parameter's binding only: the function's arrow still
  reports a scalar parameter slot as `unknown`, so the lambda auto-broadcast
  default is unchanged (`isDigit(["5", "x"])` still maps elementwise). Two
  deliberate exclusions: a **global** symbol is never retyped — membership is
  a predicate (`x in [1, 2, 3]` on a string-valued `x` is `False`, not a type
  error), and a Solve domain spec such as `Element(x, Range(1, 9))`
  constrains its unknown without narrowing it — and membership in a **set**
  (`x ∈ ℤ`, `x ∈ {1, 2, 3}`) stays with the assume machinery, which applies
  such refinements scoped.

- **Epsil debugger: function signatures in the Variables panel show inferred
  parameter evidence.** The engine's arrow deliberately hides evidence that
  does not rule out broadcasting, so a function like
  `skipWs(cs, i) = … cs[i] …` displayed as
  `(dictionary | indexed_collection, unknown) -> …`. The debugger now reads
  the parameter bindings instead and shows names alongside everything
  inference recorded: `(cs: dictionary | indexed_collection, i: boolean | indexed_collection | number | string) -> …`, and `isDigit` shows
  `(c: string) -> boolean`. Display-only; the engine's types are untouched.

- **Cortex: most reserved words are now ordinary identifiers.** Only the words
  the grammar actually consumes are reserved: the literals (`true`, `false`,
  `Infinity`, `oo`, `NaN`) and the active keywords and word operators (`break`,
  `const`, `continue`, `do`, `else`, `for`, `function`, `if`, `in`, `match`,
  `while`). The other 76 words in the documented list — `set`, `with`, `label`,
  `where`, `to`, `each`, and so on — can name a binding, be assigned to, be a
  `|->` parameter, and be called. Previously a binding name accepted them but a
  bare assignment target, a mapsto parameter, and a call's callee did not, so
  `label(6) = 1` was accepted while `label(6)` was an error.

  Relatedly, assigning to a literal word is no longer silently accepted:
  `true = 5` and `NaN = 1` now report `reserved-word` like every other binding
  position.

### Resolved Issues

- **Compiled string comparisons fail closed instead of returning wrong
  values.** The JavaScript compile target is numeric at heart: `Equal` and
  `NotEqual` lower to a tolerance test (`Math.abs(a - b) <= tol`), which for
  string operands is `NaN <= tol` — so `s == "a"` compiled to `false` behind
  `success: true`, and `IndexOf` over a list of strings returned 0 for the
  same reason. Both now fail closed (D6) when an operand is *provably* a
  string (a string literal, or statically string-typed — an unknown-typed
  symbol never gates, so inferred-parameter plot equalities compile
  byte-identically), and the interpreter fallback returns the correct value.
  Orderings (`Less`, `Greater`, …) are gated more narrowly, on the **mixed**
  case only (`"a" < 1` — inert in the interpreter, `false` compiled): an
  all-string comparison compares strings exactly as the interpreter does
  (raw code-unit order) and keeps compiling, with parity pinned. `match` on
  string constants was never affected — it emits a real `===` — and is now
  pinned too. The Python target needed no gate: its wrong shapes raise a
  loud runtime `TypeError` rather than a silent value, and its `IndexOf` is
  genuinely correct.

- **The broadcast route no longer miscompiles string comparisons, and
  whole-array string equality fails closed.** Two stragglers of the
  string-comparison class above reached the emitter through different doors:
  a mixed ordering over a collection (`Less("a", [1, 2])`) broadcast to
  `[false, false]` via `_SYS.bcast` before any gate ran, and whole-array
  equality (`Equal(["a","b"], ["a","b"])`) compiled to `false` because
  `_SYS.eq`'s per-element tolerance test makes equal strings unequal — the
  gate tested *operands*, and neither operand is a string scalar. The string
  gates are now element-aware ("participants": scalar operands and the
  provable element types of collection operands), and the string-evidence
  test is **recursive**: nested string lists (`[["a"]] == [["a"]]`),
  heterogeneous literals (`["a", 1] == ["a", 1]`), a symbol typed
  `broadcastable<string>` or `list<string>` in an ordering, and — via the
  same walk — whole-value equality over `dictionary`/`record`/`tuple`-typed
  symbols (whose element types reach `string`) all previously compiled to
  wrong booleans and now fail closed. Admission is deliberately narrower
  than decline: only *flat* all-string orderings keep compiling (their
  interpreter parity is pinned); nested all-string shapes decline. Numeric
  shapes keep byte-identical codegen. (The Python target has the same
  broadcast-route defect — `np.less("a", [1, 2])` — recorded as a known open
  hole, not yet fixed.)

- **A destructuring `Declare` with a positional initial value now binds
  (tuple patterns).** `["Declare", ["Tuple", "x", "y"], "unknown", ["Tuple", 3, 4]]` — the positional-value spelling the `Declare` contract
  documents and the scalar path already honors — silently declared nothing
  on the tuple path, which only read the trailing-attributes dictionary. The
  two forms now share one value resolution. A positional *type* on a tuple
  pattern, previously a silent no-op on this dead path, is now applied per
  bound name and surfaces an `incompatible-type` error value when it doesn't
  fit — loud over silent, and **atomic**: every leaf is validated against
  the type before any binding is installed, so a failure on the second leaf
  no longer leaves the first one declared. (No surface route emits either
  spelling: the Epsil parser uses the dictionary form.)

- **A destructuring assignment is now atomic too.** `(x, y) := (7, 4.5)` with
  both targets declared `integer` used to write `x` and then fail on `y`,
  leaving the tuple half-assigned; the same happened when a later target was a
  constant. Every leaf is now validated against its target's existing binding
  — declared type, constness — before the first write, using the very check the
  write itself performs, so the diagnostic is unchanged and a rejected pattern
  leaves every target at its OLD value. (Failures raised deeper inside the
  install machinery — function-literal reconciliation, effect contracts — stay
  sequential.)

- **A destructuring declare or assign whose right-hand side is a
  tuple-valued expression now compiles (JavaScript target).**
  `let (v, j) = parseValue(cs, i)` and the state-threading idiom
  `(v, j) := step(j)` previously failed closed unless the right-hand side
  was a literal tuple. When the pattern is flat and the right-hand side's
  static type pins the tuple arity (`-> tuple<T1, T2>`), the compiler now
  binds the whole result to one temporary and reads components positionally
  — `let _tv1; _tv1 = step(k); let v = _SYS.at(_tv1, 1); …` — preserving the
  interpreter's evaluate-once-then-write order (so swaps and `_` positions
  behave identically). A tuple-typed *symbol* right-hand side rides the same
  path. Nested patterns, statically-unknown arity, and every non-JavaScript
  target (GLSL, WGSL, Python, interval) keep the fail-closed refusal.

- **A function no longer broadcasts over a collection argument its body
  consumes whole.** A user function's unannotated parameters default to
  scalar, and a scalar-parameter function maps over an indexed-collection
  argument (the vectorization convention: `f(x) = 2x` applied to `[1, 2, 3]`
  is `[2, 4, 6]`). But the *collection evidence* a body provides was being
  lost in three ways, so functions that plainly consume a collection whole
  were broadcast too — the body then saw a single element, and conditions
  inside it failed (`Condition must evaluate to "True" or "False"`) or loops
  never terminated. All three are fixed, and each writes its evidence onto
  the parameter so the inferred signature reflects the use:
  - a parameter referenced only from a **nested block scope** (an `if`
    branch, a `while` body — `while cs[j] != "z"`) auto-declared a throwaway
    per-scope shadow binding that swallowed the inference; bare parameters
    now share one cached binding across the whole body, which the literal's
    parameter declaration then adopts;
  - a function that merely **forwards its parameter** (`g(xs) = f(xs)`)
    learned nothing, because calls to inferred-signature functions skip
    argument validation — and with it, its narrowing side-channel; the
    collection-only parameter types of the callee now narrow unknown symbol
    arguments even on that route;
  - `Length(x)` contributed nothing because its parameter is deliberately
    `any` (`Length(5)` stays symbolic); it now treats a not-yet-typed symbol
    operand as collection evidence, like an indexed read does.

- **A `while` loop inside a zero-argument function now terminates.**
  `function f() { let j = 1; while j < 3 { j = j + 1 }; j }` hit the
  iteration limit: the nullary apply path skipped the sweep of stale
  canonicalization bookkeeping that the parameterized path performs, so the
  loop condition read a hoisted valueless binding forever. The nullary path
  now hides those bindings for the duration of the call, exactly like the
  parameterized path.

- **`And`/`Or`/`Not` accept a possibly-absent condition, Kleene-style.** A
  comparison on an indexed read — `cs[j] == "a"`, honestly typed
  `boolean | missing` since the index may be out of range — was rejected at
  canonicalization by the logic operators' `boolean` parameters, so the
  guarded loop condition `j <= Length(cs) && cs[j] == "a"` errored with
  `incompatible-type`. The three operators now declare the `handle`
  missing-value behavior and evaluate Kleene over absence: `False` dominates
  `And`, `True` dominates `Or`, `Not(Missing)` is `Missing`, and a surviving
  absent condition still surfaces through `If`'s absent-condition error.

- **Epsil: a pinned `match` case after a result line is a new case.** The
  case body `1 => "one"` followed by a line starting `== lim => …` fused into
  the comparison `"one" == lim` (leading-operator line continuation), and the
  `=>` then diagnosed. At the top level of a case body a linebreak now ends
  the body; parenthesized subexpressions keep the ordinary continuation.
  Two diagnostics were also sharpened: comma-separated cases get a targeted
  `match-case-separator` (with a fix-it to `;`, and parsing recovers instead
  of dropping the remaining cases), and a conditional tail accidentally
  placed at the start of a line (`x + 1` / `if x > 0 else 0`) reports
  `conditional-if-line-start` instead of the misleading
  `opening bracket expected`.

- **A shader loop body no longer emits a `return` inside the loop.** On the
  GLSL and WGSL targets, a `for` body with more than one statement compiled as
  a *value* — its last statement became `return <statement>`, so the shader
  returned on the first iteration while reporting `success: true`. Two plain
  scalar assignments (`a := a + k; b := b * 2`) were enough to hit it. A loop
  body is now compiled as a statement list, which is also what lets a
  destructuring assignment lower on those targets.

- **An even root of an even power reduces, and no longer does so for complex
  values.** `\sqrt[4]{x^2}` now returns `\sqrt{|x|}`. It did not before, because
  the result is structurally larger and the cost check rejected it — which was
  quietly masking a soundness bug: the rewrite had no real-domain guard, so for
  a value declared complex it would have produced `\sqrt{|z|}`, where the
  principal value of `\sqrt[4]{z^2}` at `z=i` is `e^{i\pi/4}`, not 1. The guard
  is now in place and the reduction is kept for being the reduced real-domain
  form rather than the smaller one. A complex-declared base is left alone.

- **A closure returned from a function now resolves captured variables from
  inside a nested block.** `k ↦ (x ↦ if x > 1 { k } else { 0 })` applied at
  `k = 100` returned the symbol `k` instead of `100`, while the same body
  without the branch blocks — or a plain `do { k }` — returned `100`.
  `captureClosures` rebinds a returned function literal so its body block
  closes over the call's frame, but it reused the body's operands verbatim, so
  a *scoped block nested inside* that body kept its canonicalization-time
  parent chain and reached the stale copies of the same lexical levels. The
  walk now re-roots nested blocks onto the captured chain, keeping their own
  locals. Held operands are what introduce such a block — `If` branches are the
  common case, and Cortex compiles every `if` branch to a block, so any `if`
  inside an escaping lambda was affected, including one drained later from a
  lazy `Map`.

- **An annotated function parameter read from inside a nested block now
  resolves.** In Cortex, `function s(k: number) { if 1 > 0 { k } else { 0 } }`
  returned the *symbol* `k` rather than the argument — while the same function
  with a bare `k` returned it correctly. `evaluateBlock` sweeps stale
  canonicalization bookkeeping from the block's scope, and its keep-test was
  "is this binding's type inferred". An auto-declared shadow inherits the
  DECLARED type of the outer binding it shadows, so an annotated parameter left
  an explicitly-typed valueless shadow that survived the sweep and hid the call
  value in the lambda's fresh scope. The keep-test is now "was this created by
  a `Declare` statement", which is what the sweep meant to ask; genuine
  block-locals still survive, including across a re-entered block. Cortex wraps
  each `if` branch in a block, which is why the conditional shape surfaced it.

- **A lazy `Map` returned from a function no longer loses the variables its
  mapping function closed over.** `f(k) = Map([1,2], x ↦ x + k)` drained by the
  caller produced `[k + 1, k + 2]` instead of `[101, 102]` — a silently wrong
  value, with no error. Drain-time Map fusion serves each element with a direct
  operator application in the **ambient** scope, bypassing `makeLambda` and so
  its scope push, and the shape gate treated every parameter-free operand as a
  "closed" value. A free symbol is not closed: it resolves by binding lookup,
  and once the defining call has returned that binding is no longer ambient.
  The closure chain was always intact — `captureClosures` rebinds the literal
  to the call's frame — so the fix is for the drain to evaluate inside it. A
  level whose operands are all literals, the shape the fusion was built for
  (`1 + Mod(Range(0,899) + 29, 900)`), records no scope and keeps the original
  zero-scope-work path; the 3×900 witness is unchanged.

- **Annotating a callback parameter no longer switches off broadcasting for the
  whole function.** `function map(f: (A) -> B, t)` stopped broadcasting over
  *every* parameter the moment `f` was annotated, so a recursive
  `map(f, t.children)` over a tree went inert while the identical function with
  a bare `f` worked. Broadcast eligibility (`paramsAreScalar`) is
  all-or-nothing across the parameter list and a function type is not a scalar
  type, so one callback annotation vetoed the rest. A function-typed parameter
  receives a function, never a collection, so it can never be broadcast over
  and now abstains instead of vetoing — which is the position the inference
  path already took, so a declared `(A) -> B` parameter and an inferred one of
  the same shape no longer disagree. A **collection**-typed parameter still
  suppresses broadcasting for the function: it consumes a whole collection, and
  that suppression is what keeps a nested collection argument from being
  descended into elementwise.

- **Reading a field through a recursive type's own recursive field no longer
  fails with `Converting circular structure to JSON`.** With
  `type tree = tuple<value: any, children: list<tree>>`, the expression
  `t.children[1].value` produced that error rather than the field's value.
  Joining the element types of a `list<tree>` reaches `unionTypes`, whose
  de-duplication key was `JSON.stringify(type)` — and a recursive type
  reference reaches itself through its resolved `def`. The key now omits `def`,
  which is both cycle-safe and lossless: a reference is identified by its name,
  and `def` is the only edge by which a type cycle can close.

- **A recursive type alias no longer overflows the stack when a value fails to
  match it.**
  `type alias json = missing | boolean | finite_real | string | list<json> | dictionary<json>`
  accepted every JSON shape correctly, but any _rejected_ value — a function, a
  complex number, `NaN` — produced `Maximum call stack size exceeded` instead of
  an `incompatible-type` error. `hasValueComponent` unfolded structural alias
  references with no cycle guard, unlike `isSubtype`, which has had one at each
  of its own unfold sites. Cutting the back edge is exact rather than merely
  conservative here: every component reachable around the cycle is already
  reachable on the first unfold.

- **A forward type reference can now be fulfilled by a later declaration, so a
  mutually recursive set of types is writable.** The documented spelling —
  `ce.declareType("json", "… | type json_array")` followed by
  `ce.declareType("json_array", "list<json>")` — failed with
  `The type "json_array" is already defined in the current scope`: the forward
  reference installed a type record, and the declaration meant to complete it
  read that record as a redeclaration conflict. The declaration now completes
  the record **in place**, so the types that captured the reference resolve
  through to the definition (a fresh record would leave them pointing at the
  empty one, and the recursion could never close). Only an unfulfilled reference
  is completable; a name that already has a definition is still a redeclaration
  error. This applies to the Cortex `type` statement equally.

- **A parameter typed by an alias of a collection now binds its argument whole
  instead of broadcasting over it.** With `type alias u = list<number>`, a
  function `(u) -> …` applied to `[1, 2]` was mapped over the list and each
  element then failed the parameter check — while the inline
  `(list<number>) -> …` spelling bound correctly. `isScalarType` did not unfold
  alias references, so an alias of a collection read as a scalar. Nominal types
  are unaffected and still broadcast: their values are tagged applications,
  never collections, so a list of them is a genuine elementwise call.

- **`simplify()` now reduces inside `\int`, `\sum`, `\prod` and
  `\frac{d}{dx}`.** The body of those operators was never simplified, so
  `\int(\sin^2x+\cos^2x)dx` stayed as written rather than becoming `\int 1\,dx`,
  and `\sum(n+n)` never reached its closed form. That was deliberate: the
  closed-form rules match on the body's _shape_, and simplifying first rewrites
  the shape out from under them — `\sum k(k+1)` becomes `\sum(k^2+k)` and the
  sum-of-products rule stops recognising it. The body is now simplified once at
  the fixpoint, after those rules have had every chance and none has fired, so
  both work: the integrand above collapses AND `\sum k(k+1)` still returns its
  closed form.

  This is new work on expressions that contain a binder — roughly 3x on
  binder-heavy input in exchange for simplification that previously did not
  happen at all. Expressions without a binder are unaffected.

- **`Coalesce` no longer evaluates its tail past an undecided operand.** When an
  operand still carried free variables, the handler evaluated every remaining
  operand before returning the partially evaluated expression — so a later
  operand's effects ran, and its errors surfaced, on a path that a decided first
  operand would never have taken. The tail is now left unevaluated, which also
  makes the nested form `Coalesce(a, Coalesce(b, c))` and the flat
  `Coalesce(a, b, c)` observationally equal.

- **The cost function no longer prices the same expression differently depending
  on which form it is handed.** `Square(x)` cost 6 unevaluated but 1 canonical,
  and `Exp(x)` cost 10 versus 1 — yet both canonicalize to a `Power`. Since
  `simplify()`'s cost gate compares an incoming expression against a rule's
  result, and the two need not be in the same form, that made some comparisons
  off by up to 2×. Both now price through the same helper as `Power`, so the
  cost is a property of the expression rather than of its representation.

  Relatedly, a negation is now priced by what it is applied to: a sign on a
  _term_ costs 1, while negating a whole _sum_ keeps the higher cost of 4, since
  that forces delimiters. Previously both cost 4, which said `-a - b - c` is
  nearly twice as complicated as `-(a + b + c)` when it reads more simply — and
  made `Subtract(a, b)` cost less than the `Add(a, Negate(b))` it canonicalizes
  to. One visible consequence: `\int\sqrt{x^2-1}dx` now returns
  `\frac12(x\sqrt{x^2-1} - \operatorname{arcosh} x)`, matching the factored form
  its two sibling trig-substitution integrals already returned.

- **A coefficient no longer jumps in cost at an arbitrary size.** The cost
  function treated a numeral times something as cheap only for an integer up to
  10 (but any rational), and discarded the coefficient's own size entirely — so
  `10x` cost 4 while `11x` cost 10, a 2.5× step for the same shape. The
  coefficient's cost is now counted, which prices size continuously (integer
  literals are already priced by digit count), and the magnitude test is gone:
  `11x` costs 5, `1000x` costs 7. `2x` still costs less than `x + x`, which is
  what the discount exists for. One visible consequence: `\sum n^2` returns
  `\frac16(2b^3+3b^2+b)` rather than `\frac13 b^3+\frac12 b^2+\frac16 b`,
  matching the shape `\sum n` already returned.

- **Raising something to a power now accounts for what is being raised.** The
  cost function priced a power by its exponent alone, so `(a+b+c+d)^{20}` —
  which expands to 1,771 terms — scored 2, the same as `x^{20}` and barely above
  `x^2`. The base is now counted. `2q^2` is still cheaper than the repeated
  multiplication it replaces, which is what that rule exists for. One visible
  consequence: `(\sqrt2+\sqrt3)^2` now reaches its closed form `5+2\sqrt6`
  instead of staying unexpanded.

  Two long-standing shortcuts came out with it. A negated power was priced
  without its base, so `-\sin^2x` scored 4 where `\sin^2x` scored 12 — the same
  subexpression valued three-fold apart on nothing but a leading sign. And
  `\sqrt{}` carried hidden surcharges for a perfect-square or odd-power
  argument, added to push factoring rewrites like `\sqrt{x^2y}\to|x|\sqrt y`
  past the cost check. Those rewrites introduce an absolute value, so they
  genuinely do grow the expression; they are kept because they are correct on
  the reals, not because they are smaller, and they now say so directly rather
  than relying on a surcharge to disguise their size. Same for distributing an
  exponent over a product. Behavior is unchanged in every case.

- **A radical is no longer priced as if it were an ordinary decimal.** The cost
  function reduced an exact value to its floating-point magnitude, so `\sqrt3`,
  `2\sqrt3` and `\sqrt{17}` all scored the same as the plain decimal `0.5` — the
  radical was invisible to every comparison. An exact value is now priced as
  `rational × \sqrt{radical}` with the radical counted, calibrated so a radical
  literal costs about what the equivalent expression costs (`\sqrt3` and
  `\sqrt y` both score 6). Plain integers, decimals and fractions are unchanged.

  Because a radical now carries weight, keeping one factored out beats spreading
  it across several terms, so a number of antiderivatives come back in a tidier
  form: `\int\frac{1}{x^2+x+1}dx` returns
  `\frac{2\sqrt3}{3}\arctan\left(\frac{\sqrt3}{3}(2x+1)\right)` rather than
  distributing the `\sqrt3` over both terms of the argument. Collapsing nested
  radicals (`\sqrt{\sqrt{12}} \to \sqrt[4]{12}`) also no longer needs a
  cost-gate exemption — it now wins on its own merits.

- **An unevaluated integral now weighs heavily against a closed form.**
  `Integrate` was priced like any unrecognized operator, but an antiderivative
  can be much larger than its integrand — `\int\sec^3x\,dx` scored 49 against a
  closed form of 78, and `\int\frac{1}{x^4+1}dx` 62 against 145 — so a rewrite
  that resolved such an integral could be rejected for being "more complicated".
  Integrals now carry a large flat premium. It is a weight rather than an
  absolute rule — a closed form vastly larger than its integral could still
  lose. It is flat rather than proportional so that comparing two expressions
  which each contain one integral still turns on the integrands, and so that an
  expression with fewer integrals still wins.

- **`ce.parse()` now accepts a `scope` option in its public type signature.**
  The implementation has always honored it — the whole parse runs with the
  supplied scope as the current lexical scope, so name resolution walks
  `scope → parents` and every auto-declare and inference lands rooted there —
  but the option was missing from `IComputeEngine.parse()`, which is the type
  the public `ComputeEngine` resolves to. Passing it was therefore a compile
  error (`TS2769`) even though it worked at runtime. `ce.expr()`, `ce.box()`,
  `ce.function()` and `ce._fn()` all already declared it; `parse()` was the lone
  omission. No runtime change.

- **`Pochhammer()`, `Degrees()` and `DMS()` no longer numericize an exact
  irrational argument.** Found by auditing for the `Beta()` bug below, which
  turned out to be one instance of a small class. `(\sqrt2)_2` returned
  `3.41421356…` instead of the exact `2 + \sqrt2`, and
  `\mathrm{Degrees}(\sqrt2)` returned `0.0246826…` instead of `\sqrt2\pi/180`.
  Two different causes, one symptom: `Pochhammer` built its rising-factorial
  terms with the `.add()` method, which folds two exact literals to a machine
  float (the same slip as `Beta`, and its own symbolic branch alongside already
  did it correctly); `Degrees` fell back to `ce.number(arg.re)` for a
  non-rational argument, and `.re` is a machine float. Exact rationals,
  integers, floats, poles and symbolic arguments are unchanged in all three.

  The audit also found the nightly exactness grid was covering only 104 of the
  engine's numeric operators — which is why these went unnoticed. It now covers
  28 more. `Mandelbrot`/`Julia` are deliberately excluded (a float is the answer
  for an escape-time sampler), as are `Rational`/`Rationalize` (they exist to
  turn a float into an exact value).

- **`Beta()` no longer numericizes an exact irrational argument.**
  `\mathrm{B}(\sqrt2, 2)` evaluated to `0.2928932188…` instead of the exact
  `1/(\sqrt2(1+\sqrt2))`, breaking the contract that `evaluate()` returns the
  most exact form and only `N()` produces a float. The closed form
  `\mathrm{B}(a, m) = (m-1)!/(a(a+1)\cdots(a+m-1))` was being built with the
  `.add()`/`.mul()` methods, which fold two exact literals to a machine float —
  so `\sqrt2 + 1` collapsed on the very first factor and the whole result went
  inexact. Integer and rational arguments were unaffected (they fold exactly),
  which is why only an irrational argument showed it. Poles
  (`\mathrm{B}(-1, 2) = \tilde\infty`), the finite negative cases
  (`\mathrm{B}(-2, 2) = 1/2`) and float arguments are unchanged.

## 0.102.0 _2026-08-05_

### New Features

- **Two ring constructions, `Adjoin` and `QuotientRing`, with their standard
  notations.** `\mathbb{Z}[\sqrt2]`, `\mathbb{Z}[\sqrt2,\sqrt3]`,
  `\mathbb{Z}[i]` and `\mathbb{Z}[x]` now parse as ring adjunction
  (`["Adjoin", "Integers", …]`), and both `\mathbb{Z}_n` and
  `\mathbb{Z}/n\mathbb{Z}` as the quotient ring
  (`["QuotientRing", "Integers", "n"]`), which serializes back to the subscript
  form. The blackboard-bold ring and field constants — `\mathbb{Z}`,
  `\mathbb{Q}`, `\mathbb{R}`, `\mathbb{C}` — are accepted as bases. Both
  operators are **inert** in this version: they stay symbolic, carry no
  membership test and no arithmetic in the constructed ring, but they do report
  an honest type — `\mathbb{Z}[\sqrt2]` is a `set<finite_real>`, `\mathbb{Z}[i]`
  a `set<finite_complex>`, `\mathbb{Z}_n` a `set<finite_integer>`. Note that
  `\mathbb{Z}_p` is read as the integers modulo `p` — the quotient reading — not
  as the alternative number-theoretic reading the same notation carries in some
  texts, and that field adjunction written with parentheses
  (`\mathbb{Q}(\sqrt2)`) is not parsed. Sign-restricted spellings are
  unaffected: `\mathbb{Z}_+`, `\mathbb{R}_-` and `\mathbb{Z}_{\ge0}` still name
  `PositiveIntegers`, `NegativeNumbers` and `NonNegativeIntegers`.

- **Quantifiers accept an undelimited parenthesized body.**
  `\forall x > 0 (x^2 > 0)` — a condition followed by a parenthesized body, with
  no comma between them — now parses as `["ForAll", <x > 0>, <x^2 > 0>]` for all
  five quantifiers (`\forall`, `\exists`, `\exists!` and the negated forms).
  Previously the group was absorbed into the condition as an implicit product.
  The split is accepted only when the group reads as a proposition — a relation,
  a logic connective, a membership, or a predicate application such as `(P(x))`
  — so `\forall x > 2 (\sin y)` still reads the group as a factor of the
  condition.

- **More spellings of the quotient ring and the sign-restricted number sets
  parse.** `\frac{\Z}{n\Z}` (and `\dfrac`, `\tfrac`) now reads as
  `["QuotientRing", "Integers", "n"]`, like the inline `\mathbb{Z}/n\mathbb{Z}`,
  and serializes back to `\Z_{n}`. The terse blackboard-bold family also accepts
  the short comparison commands — `\R_{\ge 0}`, `\R_{\gt0}`, `\N_{\ge1}` and
  their siblings — which previously required the spelled-out `\geq`/`\geqslant`
  forms.

- **Cortex: the conditional expression `a if c else b`.** When both branches are
  single expressions, the braces of the block form are noise, and the
  conditional spells the same `["If", c, a, b]` without them:
  `let y = 10 if x > 3 else 20`. It is the same operator the block form builds —
  only the branches differ, plain expressions instead of `Block`s, so the
  conditional introduces no scope and no statement can appear in a branch. The
  `else` is **mandatory** (it is what ends the condition; a missing branch would
  leave the false case with no value to name), and `1 if c` reports the new
  `conditional-else-expected` diagnostic — use the block form `if c { 1 }` when
  there is nothing to return. Chains nest to the right
  (`"zero" if n == 0 else "negative" if n < 0 else "positive"`), so there is no
  `else if` spelling to learn. The conditional binds looser than every operator
  that computes — as in Python, looser than `||` — but tighter than the four
  that bind or pair, `=`, `|->`, `|>` and `->`, so the whole conditional is the
  right-hand side of an assignment, the body of a function, the piped value, or
  the value of a dictionary entry. Used as an operand it needs parentheses:
  `1 if c else 2 + 3` reads as `1 if c else (2 + 3)`. One layout rule: the `if`
  must be on the **same line** as the value before it — a line break separates
  statements, so an `if` that starts a line always begins a new `if`-statement.
  The block form is unchanged, and a match-case guard (`n if n > 0 => …`) is
  unaffected: patterns have their own grammar, which has no conditional.

### Improvements

- **Cortex: `If` now serializes as `if`, not as a function call.** The
  `if`-expression syntax has always _parsed_, but the serializer had no rule to
  emit it, so a program written `if c { 1 } else { 2 }` came back from the
  formatter as `If(c, do {1}, do {2})`. It now round-trips to the form it was
  written in. The spelling is chosen by the shape of the branches, which is
  exactly what the parser distinguishes: `Block` branches give the block form
  (chaining to `else if` when the alternative is itself a block-form `If`),
  plain expression branches give the conditional form `a if c else b`. A shape
  with neither spelling — mixed branches, or an `If` with no `else` and a
  non-`Block` consequent — keeps the generic `If(c, …)` call form, which also
  re-parses faithfully. An `If` in operand position is parenthesized according
  to the conditional's precedence, so `Add(If(c, 1, 2), 3)` serializes
  `(1 if c else 2) + 3` rather than the differently-parsing `1 if c else 2 + 3`.

- **`simplify()` no longer returns a result more complicated than its input.**
  The cost gate that decides whether to keep a rewrite used to tolerate growth
  of up to 30% — a proportion, so the bigger the expression, the more growth it
  allowed. It is now **strict**: a rewrite is kept only if it does not increase
  the cost.

  Two things were wrong with the tolerance. It let large expressions run away:
  instrumenting the gate across the test suite caught a single rewrite adding
  1,693 cost units, and a chain walking one expression from cost 7,098 to 10,133
  — every step recorded as a "simplification". And 90% of what the tolerance
  actually bought was the generic expansion rule, which was making results
  **worse** by blowing factored closed forms apart.

  Removing it restores them. `\sum_{n=0}^{b}(a + dn)` now returns the textbook
  `(b+1)(a + bd/2)` instead of a four-term polynomial; `\int\sqrt{1-x^2}dx`
  returns `\frac12(x\sqrt{1-x^2} + \arcsin x)`; `\int e^x\sin x\,dx` returns
  `\frac12(\sin x - \cos x)e^x`; solving `x^2 - 2x\cos t + 1 = 0` for `t`
  returns `\arccos\frac{x^2+1}{2x}`, which now agrees with the validity
  condition reported beside it; and `\frac{-b+\sqrt{b^2-4ac}}{2a}` stays in
  closed form instead of splitting into `-b/(2a) + \sqrt{b^2-4ac}/(2a)`.

  A rule that should apply regardless of cost is tagged `purpose: 'transform'`,
  which bypasses the gate entirely — that, not a numeric tolerance, is the
  supported way to express "preferred even though larger".

  Making that work meant tagging the rewrites that had been surviving on the
  tolerance. Nine families now declare `purpose: 'transform'` explicitly: the
  power combinations `x^n·x^m → x^{n+m}`, `x·x^n → x^{n+1}` and
  `x^n·x → x^{n+1}` (whose equivalent for three or more factors already carried
  the tag, so the same rewrite had been obeying two different cost policies
  depending on which implementation caught it); collapsing nested radicals
  (`√√12 → ⁴√12`); removing a logarithm from under an exponential
  (`e^{\ln x + y} → x·e^y` and its `\log_c` sibling);
  `\log_c(x^n) → n·\log_c(x)`; the geometric-series and
  shifted/falling-factorial closed forms for `Sum` and `Product`; the
  `\sin/\cos(π ± x)` argument reductions; rationalizing a radical denominator;
  and `\sqrt{x^{2n+1}} → |x|^n\sqrt{x}` (a branch-cut correctness rewrite, not a
  size optimization). Several were untagged only because the original
  string-matching exemption list never named them. Tagging them also makes them
  robust to a caller-supplied `costFunction`. Distributing a negation over a sum
  (`-(x+1)` → `-1-x`) is tagged for the same reason — it trades one negation for
  one per term, so it always scores worse, but it is the form the rest of the
  engine works in.

- **`ComputeEngine`, `expr.engine` and `ExpressionComputeEngine` are now one
  interchangeable type — no more casts between them.** The `ComputeEngine`
  exported from the package (and from the `/core` sub-path) is now a constructor
  value paired with the structural `IComputeEngine` interface, rather than the
  class itself, whose private fields made its type nominal. An engine obtained
  from `expr.engine` (typed `ExpressionComputeEngine`) can now be assigned or
  passed wherever a `ComputeEngine` is expected, and vice versa.
  `new ComputeEngine()`, `instanceof ComputeEngine`,
  `InstanceType<typeof ComputeEngine>` and the static
  `ComputeEngine.getStandardLibrary()` all work as before (the constructor's
  type is the new `ComputeEngineConstructor` interface). The interface also
  gained members that were previously only on the class: `Two`, `toJSON()`,
  `suggestOperatorName()` and `functionProperties()`. One typed-surface
  consequence: the legacy `canonical`/`structural` options of `ce.expr()` and
  `ce.box()` are not part of the interface — use the equivalent `form` option
  (`{ canonical: false }` → `{ form: 'raw' }`, `{ structural: true }` →
  `{ form: 'structural' }`); the legacy options still work at runtime. The
  `ExpressionComputeEngine` type is now **deprecated**: it is interchangeable
  with `ComputeEngine`, which should be used instead.

### Resolved Issues

- **Serialization no longer writes to the current scope.** `toLatex()` and
  `toMathJson()` internally re-canonicalize parts of the expression they lay out
  (for example to display a product with negative exponents as a fraction), and
  that re-canonicalization could **declare** an undeclared function head — or
  write an inferred type onto a declaration — in whatever scope was ambient at
  serialization time. In particular, serializing an expression that had been
  parsed against a per-call `scope` leaked its function heads into the
  surrounding scope, changing how later parses read (`aQ_{z}(x,y)` parsed before
  the leak as an implicit product, after it as a function application).
  Serialization now runs in a resolve-only region: names resolve against the
  scope chain but are never declared, and no inference is written. The same
  region now also covers the undeclared-head auto-declaration during
  partial-form boxing, which the resolve-only contract already promised but did
  not enforce.

- **Cortex: a wrapped operator chain no longer ends in a dangling operator.**
  When an infix chain was long enough to wrap, the formatter emitted the
  operator after _every_ element, including the last. With no closing fence to
  absorb it the output ended on the operator, and the result did not re-parse:
  `Add(accumulator, someLongVariableName, x, y)` at a narrow margin produced
  `accumulator +` / `someLongVariableName + x + y +`, which reports
  `unexpected-symbol "+"`. Only the separators _between_ elements are kept now;
  those are fine across a line break, since an operator with whitespace on both
  sides stays infix. Fenced lists are unaffected — a trailing `,` before `]` or
  `;` before `}` is legal Cortex and still emitted.

- **Cortex: a statement block that has to wrap is indented, not staircased.**
  `do { … }` (and the new `if` block form) laid its body out with the generic
  fenced-list layout, which aligns continuation lines to the _opening brace_.
  For a statement block that pushed the body out to the brace's column, and at a
  realistic margin left so little width that the statements broke apart in turn.
  Both are now laid out anchored at the **keyword**: body one indent in, closing
  brace under the keyword, statements separated by the line break itself. An
  `else if` chain is flattened first, so every clause stays at the same column
  instead of nesting a level deeper per `else if`. Expressions and collections
  keep the existing brace-aligned layout.

- **A subscript or bracket on a set constant is no longer read as an index.**
  `\mathbb{Z}_n` parsed as `["At", "Integers", "n"]` — indexing into a set —
  which is not a valid type, and serialized back to `\Z[n]`, which parsed as an
  `incompatible-type` error, so the expression did not survive a round trip.
  Those spellings now produce the ring constructions above. Indexing a genuine
  indexed collection is unchanged.

- **A bare `N` or `D` used as a variable now binds the same way everywhere in an
  expression.** A single-uppercase-letter name that is also a standard library
  operator reads as a variable when it appears where a value is required
  (`N + 1`), and the engine declares that variable the moment it first sees such
  an occurrence. Occurrences boxed _before_ that point — the first `N` of
  `N, N+1` — kept the operator binding, so one expression carried two different
  bindings for one name (`isSame()` was false between two occurrences) and
  boxing the same input a second time produced a third. An expression such as
  `N, N+1, N+2` or `(DB+BC)^2 = AD^2+AC^2` therefore did not survive a
  serialize-and-reparse round trip. When the variable is declared partway
  through a boxing, the expression is now rebuilt against it, so every
  occurrence shares one binding. What the names mean is unchanged: `N(2.3)` and
  `D(x^2, x)` still apply the builtin operators, `x^2 |> D` still pipes into the
  derivative operator, and a bare mention on its own (`ce.parse('N')`) still
  leaves the operator definition intact.

- **A symbol spelled by the generic (name-based) speller now reads back as the
  same symbol.** Two cosmetic spellings changed the symbol's identity on a round
  trip. A plain trailing digit run became a subscript, so the symbol `x2`
  serialized as `x_2` and parsed back as the _different_ symbol `x_2` (and
  `Arctan2` as `\mathrm{Arctan_2}` → `Arctan_2`). Such a name is now spelled
  verbatim and upright — `\mathrm{x2}`, `\mathrm{Arctan2}` — which reads back as
  the original symbol; a name that already uses the `_` subscript convention is
  unaffected (`x_2` still serializes as `x_2`). **This changes the rendering of
  digit-suffixed names:** they display upright as `x2` rather than as `x₂`;
  write `x_2` for the subscripted form. Separately, a name from the Greek-letter
  table whose command the LaTeX dictionary gives to a constant was spelled with
  that command: the symbol `pi` serialized as `\pi` and parsed back as `Pi`,
  `zeta` as `\zeta` → `Zeta`, `phiLetter` as `\varphi` → `GoldenRatio`. Those
  names are now spelled `\mathrm{pi}`, `\mathrm{zeta}` and `\mathrm{phiLetter}`.
  The set is derived from the dictionary, not hardcoded: a constant that yields
  its bare command to a declaration does not claim the spelling, so the symbol
  `gamma` still serializes as `\gamma` — that command reads back as `EulerGamma`
  only in an engine where `gamma` is undeclared, i.e. never in an engine holding
  the symbol.

- **The imaginary unit now has a single canonical spelling:
  `["Complex", 0, 1]`.** A bare `i` parsed to the complex literal
  `["Complex", 0, 1]`, but `\imaginaryI` (and `\mathrm{i}`, `\operatorname{i}`,
  and the MathJSON symbol `"ImaginaryUnit"`) canonicalized to the _symbol_
  `ImaginaryUnit`. Since the serializer emits `\imaginaryI` for both,
  `ce.parse('i')` did not round-trip, and two structurally identical expressions
  could compare unequal with `isSame()`. The `ImaginaryUnit` definition has
  always been declared `holdUntil: 'never'` — meaning its value is substituted
  at canonicalization — but the symbol was interned in the engine's
  common-symbol table, which short-circuited that substitution. It no longer is,
  so `ce.parse('i')`, `ce.parse('\imaginaryI')` and `ce.box('ImaginaryUnit')`
  now all canonicalize to the same complex literal. **Migration:** raw MathJSON
  `"ImaginaryUnit"` is still accepted and still round-trips non-canonically
  (`ce.box('ImaginaryUnit', { canonical: false })`), but code matching on the
  canonical form should test for the complex literal — `expr.isSame(ce.I)` or
  the `isImaginaryUnit()` helper — rather than for the symbol name.

- **The interned imaginary unit is now an exact value**, so exactness-gated
  folds fire on it. It was built from a float-lane numeric value while the
  identical `["Complex", 0, 1]` literal boxed exact, which made canonicalization
  disagree with itself depending on how `i` reached it:
  `["Power", "ImaginaryUnit", 2]` stayed `i^2` (and `ce.parse('i^2').simplify()`
  did not reduce) while `["Power", ["Complex", 0, 1], 2]` folded to `-1` — so
  `ce.box(expr.json)` did not round-trip to `expr`. Small integer powers of `i`
  now fold on every route, and `\sqrt{-1}`, `(-1)^{1/2}` and `\frac{a}{i}`
  canonicalize to `i`, `i` and `-ia` respectively.

- A product of an infinity and the imaginary unit no longer collapses to `NaN`
  at canonicalization. `["Multiply", "PositiveInfinity", ["Complex", 0, 1]]`
  canonicalized to `NaN` while the other operand order stayed symbolic: the
  `n·i` promotion (which folds `2·i` to the exact `2i`) accepted a non-finite
  left operand and built a value out of an infinite component. Infinities are
  excluded from that fold, so both operand orders now keep the symbolic product.
  `evaluate()` still returns `NaN` for the indeterminate form.

- `Degrees()` of a non-real argument no longer drops the imaginary part.
  `\imaginaryI\degree` canonicalized to `0` (and `(2+3i)\degree` to
  `\frac{\pi}{90}`) because the conversion read only the real part of its
  operand. The linear conversion is now applied to the whole value:
  `Degrees(i) = i\pi/180`.

- A canonical `Multiply` is now always flat. A product built by the invisible
  (juxtaposition) operator could keep a nested `Multiply` operand — for example
  `ce.parse('2f(ab)')` canonicalized to `Multiply(2, f, Multiply(a, b))` instead
  of `Multiply(2, a, b, f)` — which broke the associativity contract and made
  two structurally identical products compare unequal with `isSame()`. Note the
  MathJSON serializer flattens on output, so `expr.json` printed the same
  `["Multiply", 2, "a", "b", "f"]` either way; only `expr.ops` showed the
  difference. As a consequence exact numeric factors separated by parentheses
  now fold as they should: `2(3x)` canonicalizes to `6x`. One visible knock-on:
  two closed forms returned by `Sum` — `(b+1)(a+bd/2)` and its sibling — now
  come back from `simplify()` expanded (`1/2·d·b² + a·b + 1/2·b·d + a`) rather
  than factored. The values and the canonical forms are unchanged; flattening
  shaved the expanded rewrite's cost just under `simplify()`'s 1.3× acceptance
  gate (35 vs 35.1), so the rewrite is now accepted where it used to be
  rejected. That margin shows how finely the cost gate discriminates between a
  factored and an expanded form, and it is a candidate for tuning.

- An operator used as a value — unapplied, as in `["Tuple", "A", "Abs"]` or as a
  callback in `["Map", xs, "Factorial"]` — no longer serializes to a fragment of
  its own notation. The serializer reached for the operator's LaTeX notation,
  which is written in terms of operands, so with none it emitted `\vert\vert`
  for `Abs`, `!` for `Factorial` or `\sum` for `Sum`; none of those re-parse. A
  LaTeX dictionary entry now declares whether its notation stands on its own
  with the new `standaloneSymbol` property — true of the function commands
  (`\sin`, `\ln`, `\arctan`) and of the constant and set notations (`\Z`,
  `\emptyset`, `\varphi`) — and only a flagged entry's notation is used for an
  unapplied symbol. Every other operator is spelled out as `\mathrm{Abs}`,
  `\mathrm{Factorial}`, `\mathrm{Sum}`, which re-parses to the same symbol.
  Leaving `standaloneSymbol` unset on a custom dictionary entry is always safe:
  it only costs the nicer spelling.

- The Fungrim identity artifact was regenerated against the canonical forms
  above (1445 rules: 1435 simplify + 10 solve). Five imaginary-unit identities
  were retired because canonicalization now performs them natively, which made
  each rule a no-op: `\sqrt{-1} = i`, `i^2 = -1`, `1/i = -i`, `i^3 = -i` and
  `i^4 = 1`. Every rule matching on the imaginary unit was re-encoded to the
  `["Complex", 0, 1]` canonical spelling; none was lost, and no rule's meaning
  changed. Separately, the offline rule compiler's self-test no longer rejects a
  rewrite whose result is structurally identical to the expectation but carries
  a different binder identity (the fallback compared with `isSame` and `isEqual`
  only, and `isEqual` no longer proves identities in free variables) — this
  recovers the `Sinc` derivative identity.

- **A negated product now has a single canonical spelling.** `canonicalMultiply`
  normalizes signs before folding exact numeric factors, so a fold that itself
  produced a negative real coefficient — only a product with complex factors
  can, e.g. `i \cdot i = -1` — stranded a literal `-1` operand:
  `["Multiply", "ImaginaryUnit", "ImaginaryUnit", "a", "b"]` canonicalized to
  `["Multiply", -1, "a", "b"]`, which serializes as `-(ab)` and re-parses as the
  structurally different `["Negate", ["Multiply", "a", "b"]]`. A fold-produced
  negative coefficient now re-enters the sign normalization, so both spellings
  converge on `Negate(Multiply(a, b))` — the same form literal input has always
  produced. With this, every remaining serialize-and-reparse exception in the
  MathNet corpus ledger is a documented-lossy prettification: the ledger carries
  zero bug classes (385/391 round-trip).

- **A function assigned at the top level is no longer mistaken for an un-applied
  builtin.** The single-uppercase-letter fallback (see the `N`/`D` entry above)
  decided "standard library" by scope position, but
  `ce.assign('F', ce.parse('x \mapsto x^2'))` lands its definition in the same
  scope as the library — so `F + 1` silently shadowed the function with an
  unknown variable, and from then on `F(2)` evaluated to the product `2F`. The
  fallback now discriminates on the definition's origin: a user-defined function
  used as a numeric operand surfaces an `incompatible-type` error and the
  function stays intact. Devolution of the builtins themselves (`N + 1`, `S/D`)
  is unchanged.

- **A raw (non-canonical) symbol now evaluates through its binding**, matching
  the behavior raw and structural _function_ nodes gained in 0.101.0. With `x`
  assigned `5`, `ce.box('x', { canonical: false }).evaluate()` returned the
  symbol `x`; it now returns `5`, while the receiver stays on its tier. A symbol
  with no assigned value still evaluates to itself.

- **The ellipsis fold barrier holds at any depth, on every route.** A product
  carrying a `ContinuationPlaceholder` (`\dots`) in a nested operand could still
  be spliced — and its factors folded or reordered across the ellipsis — when it
  arrived through raw MathJSON (`ce.box`), wrapped in a `Sequence`, or as an
  explicit `\cdot`/`*` chain hanging off a juxtaposed run:
  `(px_1 + 1) \cdots (px_n + 1) \cdot p^m` re-serialized with the `\dots` moved
  to the front. All flattening sites now share one depth-aware barrier, and an
  explicit multiplication folds a juxtaposed ellipsis run into a single flat
  notational product, which round-trips.

- **An unknown function head whose name collides with a claimed constant
  spelling is spelled upright.** A function head literally named `pi` serialized
  as `\pi(x)`, which re-parses as `Pi` — the constant, a different symbol. The
  head-spelling path now consults the same claimed-spellings table as bare
  symbols: `\mathrm{pi}(x)`, `\mathrm{zeta}(x)`. A custom dictionary entry for
  such a head with a notation of its own is honored — only the colliding
  auto-generated spelling is bypassed.

- `InterpolatingFunction` used as a bare symbol no longer serializes with a
  trailing empty subscript (`\operatorname{InterpolatingFunction}_{}`); it is
  spelled `\mathrm{InterpolatingFunction}`, which re-parses to the symbol. The
  applied form keeps its domain-subscript notation.

## 0.101.0 _2026-08-04_

### Breaking Changes

- **A bare `G` is now a variable, not Catalan's constant.** The LaTeX dictionary
  claimed the bare letter `G` for `CatalanConstant` ahead of any declaration, so
  a formula such as `\int_{t_i}^{t_e}(G - F)\,dt` silently read as
  `Subtract(CatalanConstant, F)`, and declaring `G` could not reclaim it. As
  with `e` and `i`, the constant is now reachable only through explicit upright
  markup: `\operatorname{G}` — also its serialized form, so `CatalanConstant`
  still round-trips — and `\mathrm{G}`. Two consequences: `G`, `G(2)` and `G_x`
  are ordinary symbols, and `\mathrm{G}` no longer parses as the upright symbol
  `G_upright`.

- **`EulerGamma` now serializes as `\operatorname{EulerGamma}`, not `\gamma`.**
  Bare `\gamma` still _parses_ as the constant, but it now yields to a
  declaration (see Improvements), which would have made the old serialized form
  ambiguous: in an engine where `gamma` is a variable, serializing an expression
  carrying the constant and parsing it back silently returned the variable.
  `\gamma` could not be disambiguated with upright markup the way `G` was —
  `\operatorname{\gamma}` already means the plain symbol `gamma` — so the
  constant serializes to its MathJSON name, which reaches `EulerGamma` through
  the generic symbol path regardless of what is declared. Rendered output that
  used to show `γ` now shows an upright `EulerGamma`.

- **An ungrouped full-signature marker on a function literal is now always the
  literal's own contract.**
  `["Function", ["Typed", body, "'(x: number) -> number'"], "x"]` previously
  read the marker as a _return type_ — the literal typed
  `(unknown) -> (x: number) -> number` — unless the signature carried an effect
  specifier. It now declares the literal's own signature (parameter types, arity
  — now checked — and return), whether or not effects are stated, matching the
  `forall` and effect-bearing readings. A plain arrow still states **no** effect
  contract. To ascribe a function-_returning_ type, use the grouped spelling,
  which is unchanged: `["Typed", body, "'((x: number) -> number)'"]`.

- **`expr.isEqual()` and `=` no longer prove symbolic identities.** Equality is
  now _arithmetic_: the operands are evaluated, compared structurally, and —
  when their difference has no unknowns — compared numerically within
  `ce.tolerance`. No expansion, no simplification and no sampling is attempted.
  An identity between expressions with free variables, such as
  `\sin^2 x + \cos^2 x = 1` or `(x+1)^2 = x^2 + 2x + 1`, therefore returns
  `undefined` instead of `true`, and the corresponding `Equal` expression stays
  inert (as it already did for any undetermined comparison, so an equation is
  still usable as an argument to `Solve`). Every `=` comparison is now cheap and
  predictable. **Migration:** wherever an identity was being proven, call
  `expr.isIdenticallyEqual(other)`, or evaluate `["IdenticallyEqual", lhs, rhs]`
  (LaTeX `lhs \equiv rhs`) instead of `["Equal", lhs, rhs]`. Comparisons of
  numbers, of constant expressions, and of expressions that evaluate to the same
  structure are unaffected.

- **`expr.isSame()` no longer follows the value of a symbol.** It is now
  strictly syntactic everywhere: it compares canonical forms as written and
  never substitutes an assigned value. Previously a top-level
  symbol-versus-literal comparison dereferenced the binding — with `one := 1`,
  `ce.symbol('one').isSame(1)` was `true` — while the same comparison between
  two symbols, or nested inside a larger expression, did not, which made the
  method inconsistent with itself. With `x := 5`, `ce.symbol('x').isSame(5)` is
  now `false`. **Migration:** use `expr.isEqual(value)` — or `expr.is(value)`,
  which adds a numeric fallback for constant expressions — to compare values;
  `.isSame()` answers "is this the same expression?" only. Internal checks
  against a literal operand, such as `op.isSame(0)`, are unaffected. One
  canonicalization consequence: the `x^0 → 1` fold is now a pure generic-symbol
  fold (like `x/x → 1`), so `z^0` canonicalizes to `1` even while `z := 0` —
  previously the assigned value was peeked and the fold was blocked. The literal
  `0^0` still canonicalizes to `NaN`.

- **A bare `\equiv` now parses as `IdenticallyEqual`, not `Equivalent`.**
  `\equiv` is the mathematical identity sign, so `p \equiv q` parses as
  `["IdenticallyEqual", "p", "q"]` (see New Features). The biconditional keeps
  all of its other notations — `\iff`, `\Leftrightarrow`, `\leftrightarrow`,
  `\Longleftrightarrow`, `\longleftrightarrow` — and `Equivalent` still
  serializes as `\iff`, so logic round-trips are unaffected; only the reading of
  `p \equiv q` as a biconditional changes. Over boolean operands the two
  operators agree in any case, since two propositions are identically equal
  exactly when they are equivalent. **Migration:** write `p \iff q` where a
  biconditional is meant. A `\equiv` followed by `\pmod{n}` is still a
  `Congruent`, and `\not\equiv` still negates a congruence.

- **`ce.expr()`'s `scope` option now RECEIVES the boxing's writes.** It used to
  steer _lookup_ only: auto-declared free symbols, undeclared call heads and
  type inference all still landed in the engine's current scope, so the option
  half-contained a boxing. The whole box now runs with the supplied scope as the
  current lexical scope, so every declaration and inference lands rooted there
  and discarding the scope discards the writes. Code that passed `scope` to
  redirect lookups while deliberately keeping the declarations in the engine's
  scope must now box without the option (or re-declare the harvested names). The
  same semantics are what the new `scope` option on `ce.parse()` provides — see
  New Features.

- **`evaluate()` on a raw or structural expression now evaluates through its
  canonical form.** Binder machinery — declaring a `Sum` index, normalizing
  `Tuple → Limits` — is a canonicalization step, so evaluating a structural
  binder ran its handler against an unbound index and returned a _silently wrong
  value_: `["Sum", "n", ["Tuple", "n", 1, 3]]` boxed with `structural: true`
  evaluated to `9` instead of `6`, and the same tree boxed raw evaluated to
  itself. Both routes (and `.N()` and async evaluation) now produce the
  canonical result: the receiver stays on its tier, but `.evaluate()` leaves the
  tier and every tier agrees on the value. Two consequences: a raw `2 + 3` now
  evaluates to `5` instead of echoing itself, and arity or type errors that only
  canonicalization checks can now surface from evaluating a raw tree (a raw
  `5 |> 3` evaluates to the same `incompatible-type` error the canonical route
  reports, where it used to stay an inert `Pipe`). A bare unbound _symbol_ still
  evaluates to itself.

- **Quantifiers now canonicalize their operands.** `ForAll`, `Exists`,
  `NotForAll`, `NotExists` and `ExistsUnique` held their operands without a
  canonical handler, so a parsed quantifier kept raw parse sugar in its body:
  `InvisibleOperator` where `Multiply` was meant, `Delimiter(Sequence(…))` where
  a `Tuple` was meant, stray `HorizontalSpacing` from `\quad`. The condition and
  body are now canonical (e.g. a condition `x > 0` normalizes to `0 < x` like
  every other comparison), which also makes the serialized form round-trip.

### New Features

- **Evaluate handlers now receive the expression being evaluated.** The handler
  options carry an optional `expression` field — the canonical node, whose
  `.ops` are the raw (pre-numericization) operands, unlike the handler's first
  parameter which holds the evaluated operands (see the `EvaluateHandlerOptions`
  documentation for the caveats: positional correspondence does not survive
  associative flattening, `ReleaseHold`, or dropped operands, and for `lazy`
  operators the first parameter is also unevaluated). The first consumer is
  `Power`: under `.N()` a negative base's real-vs-complex branch is now decided
  from the exponent's **exact** rational — read from the raw operand, or through
  a symbol's binding — so `.N()`, the type handler, and the compiled constant
  fold agree for exact odd-denominator exponents of any term size
  (`(-2)^{1000003/1000001}` is now real on every leg; parity is decided on the
  exact bigint terms, so denominators beyond 2⁵³ do not corrupt the branch).
  Exponents with no exact provenance (floats, `π`, a lambda parameter, a `Sum`
  body) keep the rate-bounded float reconstruction.

- **`expr.hash` is now a documented public property.** A structural,
  bucketing-grade hash suitable as an **in-memory** cache or bucket key with a
  deep compare on hit. The documented contract: `a.isSame(b)` implies
  `a.hash === b.hash` (the hash is a pure function of the canonical tree — a
  symbol's assigned value never affects it); it is deterministic within a
  release but **not stable across releases**, so it must never be persisted; it
  is 32-bit-class, so a hash hit must be verified with `isSame()`; and it folds
  bound-variable names (binding-identity, not alpha-equivalence), matching
  `isSame()`. The property itself is unchanged — it was previously marked
  `@internal`.

- **Parametric polymorphism: `forall` type variables in function signatures.**
  The type language gains rank-1 (prenex) type variables with optional ground
  upper bounds: `forall T. (list<T>) -> T`,
  `forall T: indexed_collection. (T) -> T`,
  `forall T, U. (list<T>, (T) any -> U) -> list<U>`. Any identifier can be a
  variable — the clause declares it, and within its arm the quantified name
  shadows a nominal type of the same name (`forall` itself is now reserved in
  type strings). User functions can be declared generically
  (`ce.declare('swap', 'forall T, U. (tuple<T, U>) -> tuple<U, T>')`); each call
  solves the variables from the operands by local type inference (repeated
  variables join — `forall T. (T, T) -> T` at an `integer` and a `real` gives
  `real`), the result type is obtained by substitution, and a violated bound
  reports the instantiated expected type. Overload sets quantify per arm, with a
  ground arm beating an equally-specific generic one. On the query APIs,
  `matches` with a generic pattern answers existentially (`(number) -> number`
  matches `'forall T. (T) -> T'`) while `couldMatch` reads each variable as its
  declared bound. A generic declaration may be implemented by an `evaluate`
  handler or by a function body — see the two entries below. See the new
  "Generic Signatures" section of the types guide.

  Twenty-five library operators now state their contracts declaratively with
  generic signatures instead of imperative type handlers, preserving operand
  kinds and dimensions exactly: `Identity`, `Prime`, `BaseForm`, `Chop`,
  `PlusMinus`, `Remainder`, `Conjugate`, `Inverse`, `Reverse`, the tuple
  constructors (`Single`, `Pair`, `Triple`, `KeyValuePair`), and the collection
  family (`Take`, `Drop`, `Slice`, `DeleteAt`, `Insert`, `ReplaceAt`, `Sort`,
  `Unique`, `RandomShuffle`, `Tally`, `Partition`, `ChunkBy`) — e.g. `Reverse`
  of a `matrix<integer^(2x3)>` is now statically a `matrix<integer^(2x3)>`, and
  `Take` of it a `list<vector<integer^3>>`. As part of this, a dimensioned
  collection is now also a subtype of a collection of its rows
  (`matrix<integer^(2x3)> <: indexed_collection<vector<integer^3>>`), matching
  how single-index access has always evaluated.

- **Generic function literals: a `forall` signature can now be implemented by an
  inline body.** A whole-signature clause makes a `["Function"]` literal generic
  — written as a signature string
  (`["Function", body, "'forall T. (x: T) -> T'"]`), as a `Typed` marker on the
  body, or as the declared type of the symbol the literal is assigned to — and
  it works on every route: `ce.assign`, the `Assign` operator, and an annotated
  Cortex `const`/`let`. Each call instantiates the clause, so on one engine
  `f(5)` types `finite_integer` and `f("a")` types `string`, bounds are enforced
  at the call, and a collection argument still broadcasts at the variable's
  bound. Declaring first and assigning after —
  `ce.declare('nest', 'forall T. (x: T, n: integer) -> T')` — makes generic
  _recursion_ work for the first time. The body is canonicalized once with the
  quantified parameters **erased**: inside it, `x: T` is an ordinary unannotated
  parameter, a bound does not narrow it, two parameters sharing `T` are not
  known to have the same type, and the variable-correlated result is a trusted
  ascription rather than a run-time check. Four boundaries are rejected with
  dedicated diagnostics: partial application of a generic function, a generic
  clause in a multi-clause set (in either direction), a function-literal body
  for a generic overload set, and a `forall` clause on an individual parameter
  annotation.

- **Cortex: generic function definitions, `function f<T>(…)`.** A definition
  takes a **type-parameter clause** between its name and its parameter list:
  `function g<T: number, U>(x: T, k: (T) any -> U) -> list<U> { … }`. Bounds
  must be ground types, the effect specifier and return type are unchanged, and
  the clause names scope over the definition's **head** only — its parameters,
  effect specifier and return type — so a body-local annotation such as
  `let y: T` is an ordinary unknown-type error. Unused variables, result-only
  variables and non-ground bounds are diagnosed at parse time by the type
  grammar itself; an empty clause, a duplicate name and a generic clause in a
  multi-clause set have their own codes (`empty-type-parameter-clause`,
  `duplicate-type-parameter`, `generic-clause-unsupported`). A generic
  definition serializes back to the sugared form losslessly. The math definition
  form does not take a clause: `f<T>(x) = x` remains an ordinary expression,
  since it is genuinely ambiguous with a relational one.

- **Transparent generic type aliases: `type alias Pair<T> = tuple<T, T>`.** A
  structural type alias can now take a **type-parameter clause**, in Cortex as
  above and from the host with
  `ce.declareType('Pair', 'tuple<T, T>', { alias: true, typeParams: ['T'] })` —
  parameter names, `{ name, bound }` records or one clause string
  (`'T, U: number'`). The applied spelling is usable anywhere a type is written:
  an annotation, a parameter, the element position of another type. It expands
  **eagerly**, at type resolution, into the substituted definition, so nothing
  downstream ever meets an applied reference: `Pair<integer>` _is_
  `tuple<integer, integer>`, and that expansion is what `.type`, `toString()`,
  `matches()` and error messages show — the source keeps the spelling it was
  written with, and a Cortex program round-trips `let p: Pair<integer> = (1, 2)`
  verbatim. Arguments nest (`Pair<Pair<integer>>`, `list<Pair<integer>>`) and
  aliases compose (`type alias Wrap<T> = list<Pair<T>>`). A parameter may carry
  a ground bound, enforced wherever the alias is applied; an argument that is
  itself a type **variable** — a `forall` clause's, or the enclosing alias's own
  parameter — is admitted by comparing bounds: the variable's declared bound
  must satisfy the parameter's (an unbounded variable is bounded by `any`, so a
  bare `forall T. (Keyed<T>) -> T` reports `generic-alias-bound` naming both
  bounds). Four limits, each with its own diagnostic: a generic alias may not
  refer to **itself** (recursive generic aliases are out of scope), every
  parameter must be **used** in the definition, a bare or wrongly-sized
  application is an arity error, and a parameterized **nominal** type is still
  unsupported. No constructor is minted for a generic alias and its name is not
  claimed in the value namespace at all, so a function of the same name stays
  legal, before or after. A dependent alias **snapshots** what it was built
  from: re-running a `type` statement replaces that alias, and re-running the
  cell re-declares the dependents in order. See the new "Generic Type Aliases"
  section of the types guide.

- **`IdenticallyEqual`: a dedicated operator for mathematical identities.**
  `["IdenticallyEqual", lhs, rhs]`, the method `expr.isIdenticallyEqual(other)`
  and the LaTeX notation `\equiv` (the `≡` character parses the same way, and
  the operator serializes back to `\equiv`) ask whether two expressions have the
  same value for **every** value of their free variables. This is the tier that
  proves an identity: it applies expansion and simplification and evaluates both
  sides at pseudo-random sample points, so a `True` verdict may rest on sampling
  — a very strong indication rather than a formal proof, and the only comparison
  in the engine that can answer this way. It is three-valued: an identity that
  can neither be established nor refuted stays unevaluated. The machinery itself
  is not new — it is the prover that `Equal` used to run — but it is now reached
  explicitly. On the compile targets, `Equal` keeps its tolerance comparison
  while `IdenticallyEqual` and `Same` decline to compile.

- **`Same` (the Cortex `===` operator, also written `≣`) is now specified as
  canonical-syntactic equality**, matching `expr.isSame()`. It compares the
  canonical form of its operands as written and **never dereferences the value
  of a symbol**: with `x := 5`, `["Same", "x", 5]` is `False` while
  `["Equal", "x", 5]` is `True`. It remains total — always `True` or `False`,
  with no tolerance — and keeps no IEEE exemption for `NaN`:
  `["Same", "NaN", "NaN"]` is `True` where `["Equal", "NaN", "NaN"]` is `False`,
  and the same holds inside a collection
  (`["Equal", ["List", "NaN"], ["List", "NaN"]]` is `False`). Together with
  `Equal` and `IdenticallyEqual`, this gives three tiers of comparison —
  syntactic, same value, and same function of the free variables — described in
  the "Comparing Expressions" section of the Symbolic Computing guide.

- **Per-call scope control: `ce.parse(latex, { scope })` and
  `ce.createScope()`.** Canonical parsing writes to the engine's lexical scope —
  free symbols are auto-declared, undeclared call heads become inferred
  functions, types are narrowed by usage — which consumers parsing untrusted or
  out-of-order input had to contain with `pushScope`/`popScope` discipline.
  `scope` makes the containment first-class: the whole parse runs with the
  supplied scope current, so name resolution walks `scope → parents` and every
  auto-declare and inference lands rooted there.
  `ce.createScope(bindings?, parent?)` builds one from a declarations table,
  which turns each parse into a function of (latex, dictionary, declarations):

  ```js
  const scope = ce.createScope({ h: 'function', p: 'tuple<3>' });
  const expr = ce.parse('h(u) = u^2', { scope });
  ```

  One binding per definition head is enough to make a definition parse against a
  predeclared name of a different arity — or against a builtin
  (`N(x, m, s) = …`) — with no mutation and no ordering requirement, and a
  binding for a subscripted spelling (`{ theta_z: 'number' }`) is what
  `\theta_z` resolves to instead of being declared. Trigger-spelled names now
  participate fully: a subscripted Greek-letter base consults the same
  joined-name resolution as ASCII names, so a `function`-typed binding (or
  `resolveSymbol` answer) for `alpha_1` makes `\alpha_1(x)` parse as a function
  application — previously only ASCII bases could commit a joined name. The
  scope is caller-owned and readable: `declarations()` returns its entries with
  their post-inference types (as `BoxedType`, so `entry.type.toString()` is the
  canonical, fingerprintable spelling) and an `inferred` flag, sorted by name;
  `narrowings()` reports definitions in _enclosing_ scopes that a contained
  parse narrowed (the one write an ephemeral scope cannot contain); `dispose()`
  releases the scope's definitions from configuration-change tracking. A
  definition harvested from one scope can seed the next —
  `ce.createScope({ f: def })` installs the same object, preserving binding
  identity — and the scope's definitions are never auto-disposed, so a harvested
  definition outlives the call.

### Issues Resolved

- **`Real`, `Imaginary` and `Argument` are real-by-definition for the compile
  targets** (Tycho item 147). The complexness analysis judged these heads by
  their operands, so `Mod(Im(z), 1)` over a declared-complex `z` tripped the
  GLSL real-only helper gate and failed closed — even though the projections
  always lower to a real scalar (`(z).y`, `atan(z.y, z.x)`, `.im`) on every
  target. The analysis now short-circuits these heads as real-shaped regardless
  of their operand's type, so real projections of complex interiors
  (`Mod(Im(b + a·ln(x+iy)), Im(w))`, domain-coloring rows) compile again.
  Provably complex operands (`Mod(√-2, 1)`, `Mod(i·x, 1)`,
  `Mod(Conjugate(z), 1)`) still fail closed.

- **A function literal with an invalid explicit `Block` body no longer prints a
  bare internal `TypeError` while boxing** (Tycho item 150). Canonicalizing
  `["Function", ["Block", ⟨body with an Error node⟩], …]` dereferenced the
  invalid block's missing scope, and the caught
  `Cannot read properties of undefined (reading 'bindings')` was printed raw to
  `console.error` before recovering to a non-canonical literal. The block is now
  rebuilt so scope creation runs even over an invalid body: the literal boxes
  canonically (still `isValid: false`), with no console noise. Two siblings
  fixed alongside: the nullary form silently produced a canonical literal with
  an _unscoped_ block, and an **empty** `Block` body threw the same TypeError
  (it now follows the annotated branch's convention: an empty body is
  `Nothing`). The two recovery catch sites in `applyOperatorDefinition` now
  attribute anything they print (`ComputeEngine: error canonicalizing \`op\`: …`) instead of emitting a bare message.

- **Machine-precision `.N()` of a negative base to a rational power took the
  wrong branch.** At machine precision, `(-2)^{100/3}` numericized to `-1.08e10`
  (wrong sign — p = 100 is even), `(-2)^{7/3}` took the complex branch where the
  real root exists, and `(-2)^{7/6}` — a genuine even-q case — wrongly produced
  a real value. The exponent is numericized at the engine's 15-digit precision
  before the real-root convention applies, which put it far outside the branch
  decision's reconstruction tolerance. The tolerance now scales with the engine
  precision, threaded identically through the type handler and the compiled
  constant fold. A 288-cell rational sweep against an independent reference went
  from 53 mismatches to 0.

  Fixing this exposed a deeper, longstanding defect in the same fallback: every
  irrational's continued-fraction convergents eventually fall within any fixed
  tolerance, so a negative base raised to an **irrational** exponent could
  silently take the real branch — `(-2)^{\sqrt2}` and `(-2)^{1/\pi}` were
  wrong-real on _both_ precision lanes, `(-2)^e` on the bignum lane, `(-2)^\pi`
  on the machine lane. The reconstruction now also requires a coincidence bound
  (the candidate rational must be identifiable as _the_ value the double was
  rounded from, not merely a nearby convergent): π, e, √2, √5, ln 2 and plain
  non-rational floats now take the complex principal branch identically at every
  precision, and the compiled constant fold follows (`(-2)^{\sqrt2}` folded to a
  wrong real constant; it now folds to the complex value, matching `.N()`).
  Exact `Rational` exponents are unaffected by the bound. Known limitation: once
  an exponent has been numericized, a rational whose terms exceed what a
  15–17-digit double preserves (denominator ≳ 3·10⁵) is indistinguishable from
  an irrational and takes the complex branch.

- **Symbols declared with a non-finite type now report their finiteness.** A
  symbol declared `non_finite_number` answered `isFinite`/`isInfinity` with
  `undefined`; both predicates now decide from the declared type (`isFinite` is
  `false`, `isInfinity` is `true`), completing the type-consult work started for
  function expressions in 0.100.2. The workaround checks this blindness had
  required in the `Add`/`Multiply`/ `Divide` type handlers were retired after an
  instrumented full-suite run showed the getters now subsume them everywhere.

- **A parse or box under a partial canonical form no longer declares free
  symbols.** `ce.parse(s, { canonical: ['Number'] })` — any partial form —
  auto-declared every free symbol into the current scope, so containing the
  writes of an untrusted or out-of-order parse required `pushScope`/`popScope`
  discipline even though the result is not fully canonical. A partial form now
  follows the same symbol contract as the structural route: names resolve
  against the scope chain — an existing declaration still binds, and a
  `holdUntil: 'never'` constant still substitutes its value — but a name that
  resolves to nothing stays unbound instead of being declared.
  `canonical: true`, `canonical: false` and `structural: true` are unchanged.

- **A function declared with a scalar return type is now rejected when added to
  a tuple.** `scalar + tuple` is an error when the scalar is provable, and a
  declared (non-inferred) numeric result counts as proof. That guard never fired
  for a head declared with a function _type_ —
  `ce.declare('f', '(number) -> number')` — because such a declaration produces
  a value definition rather than an operator definition, and the guard consulted
  only the latter: `f(x) + (1, 2)` stayed a symbolic `Add`. It now falls back to
  the head's value definition. The inferred cases are unchanged and still stay
  symbolic, an inferred numeric type being retractable evidence rather than
  proof: a signature inferred from a `:=` body, or a function type inferred from
  earlier use.

- **A built-in operator name used as a callback no longer compiles to a broken
  artifact.** `Map(xs, Sin)`, `CountIf(xs, IsPrime)` and the like compiled
  "successfully" and then threw `_f is not a function` at run time: the callback
  symbol fell through to a free-variable lookup instead of resolving to a
  function. Such a name is now eta-expanded into a shared emitted wrapper
  (`const _fn_Sin = (_tv1) => Math.sin(_tv1)`) — the same machinery a
  user-defined function callback already used — so the artifact runs and agrees
  with the interpreter. The expansion happens at the operator's **required**
  arity, so an operator with an OPTIONAL tail works too (`Sum(Map(xs, Ln))`: a
  callback site applies `Ln` unary and the optional base defaults), as does a
  unary operator with a target operator mapping (`Negate`), which the
  bare-operator-symbol path used to refuse outright. Where a built-in cannot be
  expanded at all — a variadic tail (`Less`), no required parameter (`Random`),
  or a wrapper body with no lowering on the target (`IsPrime` on JavaScript, any
  function value on the shader targets) — compilation now fails closed at
  compile time and the caller falls back to the interpreter, instead of
  producing an artifact that throws. (A bare single-uppercase-letter operator
  name such as `D` or `N` is exempt: the engine reads those as variables when
  they appear un-applied, so they keep their free-symbol reading.)

- **Six classes of LaTeX round-trip defects are fixed** (found by the corpus
  round-trip lane, `npm run check:roundtrip` — 18 of its 37 recorded failures
  cleared):
  - A symbol naming an operator, used as a value, serialized to the _empty
    string_, silently deleting the operand: `(A, +)` — the tuple of a set and
    its operation — parsed to `["Tuple", "A", "Add"]` but serialized as `(A,)`.
    Such a symbol now falls back to `\mathrm{Add}`, which parses back to the
    same symbol.
  - A one-operand `Tuple` serialized as `(x)`, which parses back as plain `x`.
    It now serializes with a trailing comma, `(x,)`, a spelling the parser
    already accepted.
  - A product containing an ellipsis (`ContinuationPlaceholder`) serialized with
    mixed separators (`ab\times\dots\times z`), so the juxtaposed run regrouped
    into a nested `Multiply` when parsed back; and a product of rationals with
    an ellipsis was merged into a single `\frac`, moving factors across the
    ellipsis and folding them (`3/2 · 6/5 · … · X` came back with a spurious
    `9/5`). An ellipsis product now joins every factor with an explicit
    multiplication sign and is never merged into one fraction.
  - `Prime` serialized its exponent unbraced (`A^\prime`), so a following letter
    was swallowed into the command name: `\angle BA'C` serialized back to
    `\primeC`, which does not parse. The exponent is now braced.
  - A quantifier body serialized without delimiters, so
    `\forall k\ge0, a_0=9\land a_1=3` parsed back with the `\land` bound _above_
    the `ForAll`. The body is now parenthesized when its precedence requires it,
    and — with quantifier operands now canonical (see Breaking Changes) — the
    body round-trips structurally.

- **`Find` now types as the element, not the collection.** Its static type was
  the whole collection's (`Find([1,2,3], p)` claimed `list<integer>`) while
  evaluation returns a single element or `Nothing`; it is now
  `element | nothing`.
- **`BaseForm`'s declared result contradicted its behavior**
  (`-> string | nothing` while echoing its numeric operand); it now echoes the
  operand type.
- **`If` without an else branch keeps the `nothing` arm in its type.** A false
  condition yields `Nothing`, but the static type silently dropped that arm.

### Improvements

- **Structural-tier `freeVariables`, `unknowns` and `references` now derive
  bound variables from the operator's binding sites.** On a structural tree
  (`ce.box(…, { structural: true })`) a binder's bound variable leaked as a free
  variable whenever its operand carried a raw parse spelling the free-variable
  walk did not know: `["Sum", ["Power", "n", 2], ["Tuple", "n", 1, 10]]`
  reported `n` as free, while the canonical route reported nothing. The bound
  names now come from the operator definition's binding-site selectors, which
  read every spelling the binder accepts (`Tuple`, `Element`, `Limits`, a bare
  symbol, held or not), so the structural and canonical routes agree. Consumers
  that pattern-match raw binder spellings to build capture-avoidance sets can
  retire those collectors. A node with no operator definition
  (`canonical: false`) has no binding sites to consult and keeps the previous
  spelling-based recognition, and a binder whose variable survives into its
  result (`D`, `Series`) still reports that variable as free.

- **The bare-assign route now broadcasts like the value route.** Assigning an
  annotated or generic function literal directly (`ce.assign('f', …)` with no
  prior declaration) installs an operator definition whose broadcastability is
  derived from its parameter types, so `f([1,2,3])` with
  `f: (x: number) -> number` — or `forall T: number. (T) -> T` — maps over the
  list instead of rejecting, matching both the declare-then-assign route and
  what compiled code already did. Unbounded generic identities still return
  their operand whole, and an empty source answers `[]` on every route.
- **Re-assigning a function literal no longer changes its representation.**
  Assigning the same annotated literal twice (the notebook re-run pattern) used
  to silently convert the operator definition into a value definition, losing
  the derived broadcast behavior; re-assignment now rebuilds the same
  representation as the first assignment.
- **Function-literal signature markers may reference user-declared types when
  serialized to Cortex**, and anonymous literals carrying a signature marker
  round-trip losslessly instead of dropping the ascription.

- **A broadcast argument at a generic parameter now binds the _element_ type.**
  When a collection is admitted against a scalar-bounded type variable — the
  lift that makes `Conjugate([1, 2, 3])` legal — the variable used to bind the
  whole argument, and the result was un-wrapped again only where it was the bare
  variable. A result that merely _mentioned_ the variable then came out one rank
  too high: `forall T. (T) -> tuple<T, T>` over `[1, 2]` typed
  `list<tuple<vector<…^2>, vector<…^2>>>` against the value `[(1, 1), (2, 2)]`.
  The bound is still checked at the scalar base — admission is unchanged — but
  the variable now binds the argument's element type and the call site's
  ordinary broadcast wrap re-adds the rank, which gives one rule for every
  result shape: an echo (`Chop`, `Conjugate`) types exactly as before, and a
  result that mentions the variable is the per-element result with the
  argument's shape around it. Two static types change: a mixed-rank or
  union-typed argument now takes the broadcast wrapper's (coarser) shape answer,
  the same one its ground counterpart gets, and `Remainder(M, 7)`-shaped calls
  no longer widen to a union at all — `matrix<finite_integer^(2x2)>`, where the
  whole-argument bind gave
  `list<finite_integer | vector<finite_integer^2>^(2x2)>`. Only the kinds a
  broadcast actually maps are peeled: a `set` argument is admitted but never
  mapped (`Conjugate(Set(1, 2))` stays a `set`), and a tuple stays atomic. In
  the same pass, a rank ≥ 2 argument to a generic **function literal** now maps
  to the scalar leaves on the value route too, matching the operator route and
  compiled code: with `f: forall T. (x: T) -> tuple<T, T>` assigned
  `x |-> (x, x)`, a 2×2 argument evaluates to a 2×2 of pairs of scalars instead
  of applying the literal to whole rows.

- **A malformed type annotation no longer swallows the statement after it.**
  Recovery from a bad annotation ran twice — once inside the type subparser and
  once in its caller — so a declaration such as `let x: )bad( = 1`
  resynchronized one statement too far and discarded the line that followed. The
  subparser now only diagnoses, and each caller resynchronizes at the unit its
  own grammar uses: a statement boundary for a declaration, the next `,` or
  closing bracket for one element of a list. A malformed annotation in a
  function's parameter list, in a `|->` parameter list or in a `match` tuple
  pattern therefore costs only its own annotation — the parameter survives
  untyped and the rest of the list still parses. The `|->` case is the one that
  was silently wrong rather than merely noisy: the parameters after the
  malformed one were dropped, so the lambda went on to parse at a different
  arity. The resync honors `<…>` nesting, so an unclosed applied alias
  (`f(x: Pair<integer, string)`) no longer mints a bogus parameter out of the
  type's own argument list.

- **Generic values describe themselves, and an anonymous generic application is
  typed.** A function literal assigned to a symbol declared with a polytype now
  carries that polytype as its own type on all three routes (`ce.assign`, the
  `Assign` operator, an annotated Cortex `const`/`let`), so the stored value
  reports `forall T. (x: T) -> T` instead of the arrow its erased body would
  infer — as long as every parameter of the clause mentions a quantified
  variable (a literal with a ground parameter still self-describes as its
  inferred arrow). Applying a generic literal _anonymously_ —
  `["Apply", literal, arg]`, and the bare `[literal, arg]` it canonicalizes from
  — instantiates the clause too, so the application types `finite_integer` at
  `5` and `string` at `"a"` rather than falling back to `unknown`. And
  re-assigning an **untyped** literal over a signature that was itself _derived_
  from an earlier assignment now fully replaces it, arity included; a signature
  the author declared (any `ce.declare` form) stays sticky, as before.

- **Euler derivative notation and `\gamma` now yield to a declaration.** Both
  spellings are claimed by a parselet that runs ahead of symbol resolution, so
  no declaration could reclaim them: `D_x + 1` parsed as the derivative of the
  constant function `x ↦ 1`, and `\gamma` was always the Euler-Mascheroni
  constant. Each parselet now consults the symbol oracle first — the engine
  scope, supplemented by the `resolveSymbol` parse option. `D_x` reads as a
  symbol when the joined name `D_x` is declared (the same sibling-name rule
  subscripted spellings already use) or when `D` itself is shadowed by a
  non-function declaration; a function-typed `D` keeps the derivative reading.
  `\gamma` reads as the symbol `gamma` when `gamma` is declared. Left
  undeclared, both notations parse exactly as before. This gives a host
  embedding the engine a way to say "these are my variables" without having to
  replace the LaTeX dictionary.

- **Degenerate big operators (`Σ_{i=a}^{a}`, `Π_{i=a}^{a}`) now reduce.** A big
  operator whose lower and upper bounds are structurally equal has a one-point
  domain, so it has exactly one term. Two reductions follow. First, at
  **evaluation**: a symbolic bound made the domain non-enumerable, so
  `\sum_{i=x}^{x} i^2` stayed inert; one point needs no enumeration, and it now
  evaluates to `x^2` (`Product` likewise). Second, at **canonicalization**: when
  the index does not occur in the body, the indexing set carries no information
  at all, so the "identity wrapper" spelling `\sum_{i=d}^{d} f(x)` folds to
  `f(x)` — the same generic-symbol fold family as `x/x → 1`. With several
  indexing sets only the degenerate, unused ones are dropped — and never one
  whose index a sibling indexing set's bounds reference. The canonicalization
  fold compares the bounds strictly syntactically: it never reads a symbol's
  assigned value (`\sum_{i=a}^{5}` with `a := 5` keeps its `Sum` structure —
  values belong to evaluation, where the reduction does follow them). Bounds
  that are not provably a fixed one-point domain are unaffected: `±∞` and `NaN`
  bounds keep their previous behavior, impure or invalid bounds (two
  syntactically identical `RandomInteger(1,6)` draws) stay symbolic, and literal
  equal bounds with the index used (`\sum_{i=5}^{5} i^2` → `25`) still enumerate
  as before. The evaluate-path reduction substitutes the bound for the index
  under a capture guard; a body whose inner binder could capture it stays
  symbolic rather than being silently corrupted (and the guard's refusal is
  final — it does not fall through to the closed-form rewrites, which carry no
  such guard).

- **Compile-time CSE now merges repeated pure user-function calls everywhere,
  including named callbacks.** The 0.100.0 admission of pure user-function
  applications applied only inside emitted definition bodies (where a repeated
  recursive self-call made compiled recursion exponential); a repeated call at
  the top level of the compiled expression — `f(x+1) + f(x+1)^2` — still
  compiled to two calls. Both compiler harvest routes now admit them, behind the
  same transitive callee-body validation (each level's purity is re-derived
  against current bindings at compile time, so a callee that draws, writes, or
  splices caller-supplied source stays un-merged). In addition, a **named**
  callback that resolves to a validated pure function literal no longer blocks
  eligibility: two identical `Map(xs, f)` applications with a pure user-defined
  `f` now compile to one traversal, and the same applies to typed callbacks of
  eager operators such as `CountIf` (a drawing `f` still compiles to two — draw
  streams and call counts are preserved; a callback or callee name shadowed by
  an enclosing parameter is conservatively never merged). Callbacks naming
  **built-in** operators (`Map(xs, Sin)`, `CountIf(xs, IsPrime)`) are now
  admitted as well: the compiler eta-expands them into a shared emitted wrapper,
  so what they do is the built-in's own deterministic, effect-free emission.
  They merge when the operator is pure, is the engine's own definition for that
  name, and has at least one required parameter and no variadic tail — a drawing
  built-in (`Random`), a variadic one (`Add`, `Less`), a name a user definition
  shadows, a name a caller `vars` entry maps, and a name a caller `functions` /
  `operators` mapping overrides all stay conservatively excluded. Opt out as
  before with `compile(expr, { cse: false })`.

- **`FindFit`/`FindRoot` now report a setup-phase deadline in band** (Tycho item
  118 addendum). A time budget consumed entirely before the solver started —
  evaluating the data operand, differentiating the model, compiling it — used to
  escape as a bare `CancellationError` the caller could only duck-type, which an
  interpreted model over a few hundred rows hits routinely under a tight ambient
  budget. Such an expiry now answers the same record shape a mid-solve expiry
  does, with `timedOut: True` and a new `phase` entry naming how far the call
  got: `"setup"` (nothing was fitted — `iterations` is `0`, `residualNorm` is
  `NaN`, and the reported parameters are the starting guesses) or `"solve"`
  (genuine best-so-far, as before). Only an expired time budget converts: every
  other failure during setup — a malformed model, bad data, an abort signal —
  behaves exactly as it did. The keys appear only on a timed-out record, so a
  successful fit is unchanged.

### Performance

- **Symbolic equality of free-variable expressions now samples before it
  simplifies.** The `eq()` free-variable branch ran expand+simplify on both
  sides to try a structural proof, and only then fell back to stochastic
  sampling — on large trees the symbolic pass costs hundreds of milliseconds
  and, whenever the sides genuinely differ, contributes nothing the sampler
  doesn't decide alone. The order is now reversed: sample first (a compile plus
  ~50 deterministic point evaluations), and run the expand+simplify proof only
  when sampling is uninformative (no compilable/finite sample points). Verdicts
  are unchanged: a sampled agreement was already accepted as `true`, a sampled
  disagreement already degraded to `undefined` under the truth-under-constraints
  contract, and an identity provable by simplify cannot genuinely disagree at a
  shared sample point. On a consumer's Voronoi document whose piecewise rows
  compare each broadcast element against `min(⟨list⟩)` (18 identical
  expand+simplify passes of the same min-expression), the document build drops
  from 14.6 s to 6.2 s — faster than releases that predate the 0.100.2 `\bmod`
  serialization fix, which had made the comparison trees honest (and bigger).

- **Compile-time complexness analysis is no longer quadratic on large
  expressions** (Tycho item 148). The 0.100.2 operand-consulting fixes (items
  144/143) made `isComplexValued` walk a node's whole subtree per query, and the
  GPU emitters query per node — a deeply nested expression (a textually inlined
  user-function chain) paid O(n²): a depth-6 nested chain spent 82% of its GLSL
  compile (1.5 million analysis calls) in the walk, roughly doubling large
  shader compiles relative to 0.100.1. The analysis is now memoized per
  compilation with a LAYERED memo that mirrors the context's lexical nesting:
  entering a block frame or binder mask pushes a fresh layer (an answer cached
  under a mask can never be reused outside it), and leaving restores the
  enclosing layer — so binder-dense bodies (nested `Sum`/`Product`) memoize too,
  instead of wiping the cache at every mask crossing. Compiled output verified
  byte-identical across targets. The depth-6 chain compiles 7× faster than
  unmemoized (and ~2× faster than 0.100.1, which did fewer, cheaper walks);
  scaling on the regressed class — deeply nested inlined expression chains — is
  near-linear in expression size again. (Deeply nested _binder_ chains,
  `Sum`-in-`Sum`, keep a pre-existing superlinear analysis cost that 0.100.1
  shares — measured, not part of this regression.)

## 0.100.2 _2026-08-03_

### New Features

- **`Repeat` compiles on the JavaScript target.** `Repeat(7, 3)` previously
  failed closed (interpreted fallback); it now lowers to a native array
  construction with interpreter parity at the edges: the value is evaluated
  exactly once and replicated (`Repeat(Random(), 3)` yields three copies of a
  single draw — and consumes its draw even when the count is ≤ 0, matching the
  interpreter), a zero or negative count yields `[]`, and the 1-argument
  infinite form and a statically non-finite count still decline — the
  interpreter leaves `Repeat(7, ∞)` unevaluated, so a compiled `[]` would be a
  valid-looking value with the wrong meaning.

- **`Binomial`/`Choose` compile on the GPU targets.** A literal k ∈ 0…8 unrolls
  to the falling-factorial form on GLSL and WGSL (`Binomial(x+1, 2)` →
  `(((x + 1.0) * ((x + 1.0) - 1.0)) / 2.0)`), matching the interpreter's
  generalized semantics for non-integer and negative first operands
  (`Binomial(5.5, 2)` = 12.375, `Binomial(-1, 2)` = 1). An impure
  (Random-family) operand is hoisted and drawn exactly once;
  `Binomial(Random(), 0)` declines rather than folding to `1.0` (the interpreter
  consumes that draw); a statically non-finite first operand declines
  (`Binomial(∞, k)` is NaN in the interpreter for every k, including k = 0);
  non-literal, negative, non-integer, or larger k fail closed.

### Improvements

- **Products and quotients with a provably non-finite real factor now type
  `non_finite_number`.** New ratified rule: a provably non-finite real factor is
  implicitly nonzero — proven signs are required only of the finite factors.
  `2\ln(0)` and `\ln(0)/2` now type `non_finite_number` (previously the top type
  `number`); shapes admitting `0·∞`, `∞/∞` or `∞·i` keep the sound widen.
  Structurally, `Ln(0)` now reports `isFinite === false` and
  `isInfinity === true` from its static type (both were `undefined`),
  `valueOf()` projects a direction-proven infinity (`Ln(0).valueOf()` is
  `-Infinity`; `~oo` is reserved for provably non-real values), `\ln(0)/\pi`
  canonicalizes to `\ln(0)` and `2/\ln(0)` to `0`, and an unfolded
  finite-real-over-±∞ quotient types `finite_integer` (the value is exactly 0).
  Compiled emissions are unchanged.

### Resolved Issues

- **`.subs()` on a structural expression now preserves the structural form.** A
  structural receiver requested a CANONICAL rebuild, so substituting into one
  silently canonicalized it: the parse vocabulary structural form exists to
  preserve (`Subtract`, `Divide`, `InvisibleOperator`, `Delimiter`, operand
  order) was erased and exact literals were folded — substituting `k+1` for `x`
  in a structural `2(x+1)-\frac{y}{3}` returned `Add(Multiply(2, Add(k, 2)), …)`
  instead of the structural
  `Subtract(InvisibleOperator(2, Delimiter(Add(Add(k, 1), 1))), Divide(y, 3))`.
  The receiver's form is now preserved three ways (canonical → canonical,
  structural → structural, raw → raw); an explicit `canonical` option is
  unchanged. The binder-rebuild internals (`rewriteWithBinders`, used by the
  escaping-scope re-bind and by binding-keyed substitution) had the same
  conflation and are fixed the same way. `.map()` had the mirror-image defect —
  a structural receiver fell into the RAW rebuild arm, keeping the shape but
  silently losing the binding — and now preserves the receiver's form under the
  same three-way rule.

- **GLSL compile of `Mod` over wide-typed real expressions.** The shader
  targets' real-only helper gate refused operands whose type merely _could_ be
  complex: the complex type a `Sqrt`/`Ln` of unknown sign carries since 0.100.0
  propagated to enclosing arithmetic (`10^5·√(⌈x⌉²+⌈y⌉²)`), through boolean
  nodes, and into piecewise conditions, so `Mod` expressions over plot variables
  failed closed (D6) where 0.99.0 compiled them. The complexness analysis now
  short-circuits boolean- and string-typed nodes and, for arithmetic heads that
  only propagate complexness (`Add`, `Subtract`, `Multiply`, `Divide`,
  `Negate`), consults the operands — honoring the unknown-sign `Sqrt`/`Ln`/`Log`
  real-kernel contract — instead of the widened type. Provably complex operands
  still fail closed.

- **`Min`/`Max` over a collection whose element type is unknown.** With a base
  declared `indexed_collection`, `Distance(S, p)`'s result type degraded to
  scalar `number` (the broadcast arm was invisible through the elementless
  type), and `Min` then compiled to the variadic-scalar `Math.min(...)` — which
  returns `NaN` when handed the runtime array, silently, behind `success: true`.
  `Distance` now reports `number | list<number>` when an operand's collection
  element type is undecidable, and the `Min`/`Max` JavaScript lowering emits a
  runtime shape projection (reduce an array, pass a scalar through) for operands
  that could be collections, matching the interpreter both ways.

- **`invisibleMultiply` serialization option vs `\bmod`.** With
  `invisibleMultiply: '\\cdot'`, `Mod(k·f, 1)` serialized as `k\cdot f\bmod1`,
  which re-parses as `k·Mod(f, 1)` — the `Mod` serializer decided
  parenthesization assuming juxtaposition, which binds tighter than `\bmod`
  while an explicit `\cdot` binds looser. A `Multiply` operand of an infix
  `\bmod` is now parenthesized whenever the option is set (products that
  serialize as `\frac` remain unwrapped).

- **Ordering comparisons over a provably complex operand now fail closed at
  compile time.** `Less`/`LessEqual`/`Greater`/`GreaterEqual` with a
  complex-valued operand (`i·x < 0`) compiled to a raw JavaScript comparison of
  a `{re, im}` object — a silent `false` behind `success: true` — while the
  interpreter correctly leaves the comparison symbolic (the complex numbers are
  not ordered). Such comparisons now decline with a clear diagnostic on every
  compile target. `Equal`/`NotEqual` keep their complex support, and real-kernel
  expressions of unknown sign (`√x < 2`) still compile.

- **Non-canonical trees are no longer restructured by pretty serialization.**
  Serializing a `canonical: false` expression (`toMathJson`/`toLatex` with
  `prettify`) could rebuild a `Multiply` containing a symbolic `Divide` factor
  through the canonical product machinery: explicit `Delimiter` fences were
  dropped, factors reordered, and the round-trip changed the expression
  (`Mod((k·f), 1)/n + y` re-parsed with the dividend split). Pretty rewrites on
  a non-canonical tree are now shape-preserving — order-preserving
  numerator/denominator split, no factor sorting, fences kept. Canonical
  serialization is unchanged.

- **Impure operands spliced by multi-use compile templates drew more than
  once.** A lowering that splices a compiled operand string into its emitted
  code more than once re-evaluates a `Random`-family operand at run time — a
  silent wrong value that also shifts every later draw. A full audit of the
  JavaScript, GLSL, WGSL, interval, and Python targets fixed twelve such sites:
  `Equal`/`NotEqual` over a complex operand and across n-ary chains,
  `Range(start, stop, step)` (which re-drew once per element), `Round`,
  odd-degree `Root`, `Variance` (12 draws where the interpreter makes 2),
  complex `Argument`/`Conjugate`, chained relations and element-wise selection
  masks, `Match` subjects, and a double-compile in the complex `Add` fallback
  that orphaned a hoisted draw. Impure operands are now bound to a temporary
  exactly once and **in argument order** — binding only the middle of
  `Random() < Random() < 0.9` had executed the second draw first, inverting the
  comparison. Pure emissions are byte-identical throughout, and regression tests
  count draw sites in the emitted code.

- **`ContrastingColor` emitted invalid WGSL.** Both the 1-argument and
  3-argument forms lowered to a GLSL-only `?:` ternary on every GPU language;
  WGSL now emits `select(…)` (the GLSL emission is byte-identical), and an
  impure (Random-family) color operand fails closed instead of being spliced
  twice.

## 0.100.1 _2026-08-02_

### Breaking Changes

- `PointZ` applied to a 2-D point is now a typed `incompatible-dimensions` error
  instead of the `NaN` absence marker — at type-check time when the operand's
  type statically proves 2-D (`tuple<number, number>`, or a list or set of such
  points, in either the tuple or coordinate-row spelling), and at evaluation
  time otherwise. The broadcast over a list of 2-D points errors identically,
  and the JavaScript compile of a statically 2-D operand declines. A
  statically-absent component is a type-level fact: the silent `NaN` masked
  upstream pipeline defects. `PointX`/`PointY`, all 3-D behavior, and the
  compiled runtime `NaN` marker for dynamically-shaped bases are unchanged.

### New Features

- Multi-clause function definitions can now be declared before they are defined.
  `declare("J", "(number, complex) -> complex")` followed by clauses such as
  `J(0, z) = z` previously failed with `incompatible-type` (a literal-parameter
  clause is a narrowed arm of the declared signature, not a function subtype) —
  and failed silently, leaving the symbol undefined. Clauses are now checked
  arm-shaped against the declaration (parameters and result must be subtypes of
  the declared ones), a genuinely incompatible clause errors loudly without
  corrupting the definition, and the declared signature is preserved on the
  installed function — making declare-then-define usable for recursive clause
  sets
  (`let fact: (number) -> number; fact(0) = 1; fact(n: integer) = n * fact(n-1)`).

- Complex-valued multi-clause function definitions now compile. A recursive
  clause set such as
  `J(0, z: complex) = z; J(n: integer, z: complex) = J(n-1, z)^2 + z_0`
  evaluated correctly but declined the JavaScript compile target while its
  real-valued twin compiled; the clause dispatcher now carries the complex
  `{re, im}` convention through parameter guards, call-site coercion, and mixed
  real/complex clause bodies.

- The LaTeX serialization style options (`rootStyle`, `fractionStyle`,
  `indexStyle`, `powerStyle`, `logicStyle`, `numericSetStyle`, `groupStyle`,
  `applyFunctionStyle`) can now be specified as a constant, in addition to a
  function of the expression and of its nesting level:

  ```ts
  ce.latexOptions = { rootStyle: 'solidus' };
  expr.toLatex({ fractionStyle: 'inline-solidus' });
  ```

  Previously, only the function form was supported, and a string value
  serialized to an empty string. A string that is not one of the values accepted
  for that option now throws when the option is set, instead of producing an
  empty serialization.

- `Distance` broadcasts over point lists. `Distance(S, p)` (either argument
  order) where `S` is a list of points — spelled as tuples `[(0,0),(3,4)]` or as
  coordinate rows `[[0,0],[3,4]]` — returns the list of per-point distances, on
  both the interpreted and compiled routes, so `min(Distance(S, p))` computes
  the nearest-point distance directly. `Distance(S, T)` over two point lists is
  pairwise with strict length matching. Scalar and string arguments are still
  rejected.

- Point operators are aligned over point lists: `PointX/Y/Z` project a
  coordinate-row list (`PointX([[10,11],[20,21]])` is now `[10, 20]`, not the
  first row), and `Norm` over a list of point tuples returns per-point norms
  `[0, 5, 10]` on both routes, matching `Abs`. `Norm` over a plain matrix (list
  of lists) keeps its Frobenius meaning. Compiled `Norm` and `Abs` previously
  disagreed with the interpreter on these shapes (a flattened norm and
  componentwise values behind `success: true`).

### Performance

- Registering many interdependent user functions (`declare` + `assign` chains,
  the shape a document importer produces) was quadratic in the number of
  functions: the `type` accessor's cache key evaluated the effect projection
  (`isPure`, made binding-aware in 0.100.0) before the cheap constant-operand
  short-circuit, forcing every stored function literal's full signature to
  re-derive down the call chain on each registration. Reordering the check and
  adding a per-generation fast path makes registration near-linear — 9–14×
  faster at 120 functions, 13.7× at 240 — with byte-identical
  `isPure`/`effects`/`type` answers. New benchmark:
  `benchmarks/effects-registration.ts`.

### Resolved Issues

- `isPure` and `.effects` no longer claim a seed frame contains a lazy view that
  escapes it. `WithRandomSeed(42, Map(xs, x => Random()))` — and the
  `[Random() for k = [1...6]]` comprehension spelling of it — reported
  `isPure: true` with no effects, even though a lazy view draws at
  materialization, from whatever frame is active _then_, so the values were
  genuinely live. Such an expression now reports `isPure: false` and
  `["random"]`, including when the view leaves the frame as a cell of a returned
  `List`/`Tuple`/`Pair` or as a block's result. The runtime semantics are
  unchanged: materializing inside the frame
  (`WithRandomSeed(42, ListFrom(Map(...)))`, an index, a reducer) still replays,
  and still reports pure — as do a view whose element body draws nothing and a
  seeded frame around an ordinary draw.

- Two-sample bracket ranges with decimal anchors now compute their step exactly.
  `[1.008, 1.016...5]` previously differenced its anchors in binary floating
  point, baking the step `0.008000000000000007` into the range — 499 elements
  ending at ~4.992 instead of 500 landing on the 5 anchor. The step is now
  derived from the anchors' decimal digits in exact integer arithmetic
  (`0.008`), matching the compound-anchor spelling `[1+0.008, 1+0.016...5]`.
  Anchors with no short decimal form keep their float step.

- Symbolic differentiation declines honestly instead of blowing up. The
  derivative of a deeply self-nested expression (e.g. a 12-deep `√(x+√(x+…))`
  chain, whose second derivative is a 6.5-million-character expression taking
  45+ seconds) doubles its tree per level; differentiation now tracks the size
  of what it builds and, past an internal budget (25,000 nodes, ~30× the largest
  derivative in the test corpus), aborts and leaves the `D`/`Derivative` inert —
  the established decline convention — in bounded time rather than hanging.

- A divergent definite integral is no longer reported as a confident
  measurement. `\int_0^1 \frac{1}{x}\,dx` numericized to
  `709.08956571281 ± 0.00000000074` — the value at which the adaptive
  quadrature's refinement toward the singularity ran out of floating-point
  range, dressed up as a measured quantity. Numeric integration now checks the
  series of dyadic shells it sheds while refining toward each endpoint: a
  convergent improper integral's shells shrink geometrically, a divergent one's
  do not. Divergent integrals return `NaN` on all three numeric routes (`.N()`,
  iterated `.N()`, and compiled code), and detection also stops the refinement
  early, so `\int_0^\infty x\,dx` resolves in ~1 ms instead of ~560 ms.
  Legitimate improper integrals — `\int_0^1 \frac{1}{\sqrt{x}}\,dx`,
  `\int_0^1 \ln x\,dx` — are unaffected, as are proper integrals, whose results
  are bit-for-bit unchanged.

- The order in which functions are defined no longer changes their meaning. A
  definition body that referenced a function _before_ it was defined — for
  example `g(t) \coloneq 2a(t)` registered ahead of
  `a(t) \coloneq [\cos t, \sin t]` — froze the reference as a multiplication
  (`2 \cdot a \cdot t`), producing scalar results interpreted and `null`/`NaN`
  from compiled functions behind `success: true`. Such provisional readings are
  now re-derived when the referenced name later gains a function definition, so
  every registration order yields the same interpreted and compiled results.
  Genuinely scalar juxtaposition (`2x(t+1)` where `x` never becomes a function)
  is unchanged.

- Function parameters whose bodies index them are now treated as collections. A
  definition such as `h(v) \coloneq v_1 + v_2` with `h` declared
  `(list<real>) -> real` parsed the subscripts as unrelated symbols, an
  `At`-indexing body inferred a scalar parameter (so list arguments broadcast
  elementwise instead of applying), and the JavaScript compile target declined
  `At` over a declared list parameter. All three are fixed: parameters are bound
  at their declared types while the body is parsed, an indexing body infers a
  collection parameter, and the compile target admits indexing over such
  parameters with a runtime shape guard.

- The derivative of trigonometric functions now honors `angularUnit`. In degree
  mode, `D(Sin(x), x)` evaluates to `\frac{\pi}{180}\cos x` (and inverse
  trigonometric derivatives are divided by the conversion factor), on both the
  interpreted and compiled routes, for both by-reference functions (`f'` where
  `f := x \mapsto \sin x`) and inline `\frac{d}{dx}` expressions. Previously the
  interpreted derivative and the compiled by-reference derivative returned
  radian-convention values, and compiled `ND` double-converted.

- `Sqrt` no longer claims a complex result type for a radicand whose
  non-negativity only constant folding can establish. Machine floats are not
  folded at canonicalization, so `\sqrt{1-0.2^2}` reached the type handler with
  an undecided sign and typed as `finite_complex`, while the folded
  `\sqrt{0.96}` typed as `finite_real`. A radicand that is pure and has no
  unknowns is now folded to decide the sign. A negative radicand
  (`\sqrt{0.2^2-1}`) still types as complex, and a radicand with unknowns
  (`\sqrt{x}`) is unchanged.

- Declared types are enforced for symbols named with a single uppercase letter.
  An argument-validation repair intended for bare standard-library operator
  symbols such as `N` and `D` used in value position also fired for any
  user-declared single-letter symbol that failed a parameter type check,
  silently skipping the declared-type check. The repair is now gated on the
  provenance of the shadow it rebinds to.

- Arguments with free variables are no longer exempt from declared-type checking
  when their type is provably incompatible. Applying a function whose parameter
  is, e.g., `tuple<number, number, number>` to an unassigned symbol declared
  `string` (or any provably disjoint type) now reports `incompatible-type`
  instead of silently accepting the call. Arguments whose type could still turn
  out compatible — `unknown`, inferred symbols, unions with a matching arm,
  same-category collections — continue to defer to runtime exactly as before.

### Performance

- GPU compilation no longer re-merges its function table on every compile. The
  table reached V8's fast-property limit, so the per-call object spread
  allocated ~45 KB of transient dictionary-mode garbage per compilation; the
  merged table is now memoized per target instance.

## 0.100.0 _2026-08-02_

### Breaking Changes

- **Assignments now enforce declared types consistently.** Declare-with-value,
  `Assign`, and `ce.assign()` all reject values incompatible with an explicit
  symbol type. Inferred types retain their widening behavior.

- **Purity and effects reporting is more precise.** `expr.isPure` and
  `expr.effects` now account for which operands an operator evaluates and for
  the current bindings of referenced functions. Consequently, held expressions
  and seeded-random blocks can be pure, function literals do not inherit the
  effects of calling them, and named effectful callbacks are no longer reported
  as pure. Unresolved forward references conservatively have unknown effects.

- **Effects from partially applied functions occur only when all required
  arguments have been supplied.** Partial application no longer evaluates an
  effectful body early or repeats its effects.

- **Contradictory operator declarations are rejected.** In particular,
  `pure: true` cannot be combined with `drawsRandom: true`; inconsistent legacy
  flags, effect annotations, and `effects:` declarations also fail registration.

- **Errors now propagate through strict built-in operators and function
  application.** An invalid operand generally produces the underlying `Error`
  value instead of leaving an inert expression. Collection constructors and
  error-observing or lazy operators can still contain or inspect errors.
  `Assume` returns string status values such as `"ok"` and `"not-a-predicate"`.

- **Literal `Nothing` arguments are removed consistently.** `f(Nothing)`,
  `Apply(f, Nothing)`, and `Nothing |> f` now all behave like `f()`. An
  expression that evaluates to `Nothing` is still passed as an argument. Invalid
  `Pipe` callees now return the error directly.

- **Positional collection operators require indexed collections.** `First`,
  `Second`, `Third`, and `Last` now reject sets and other non-indexed
  collections. Empty indexed collections still return `Missing`.

- **Several elementary functions now report complex result types when their
  real-valued domain cannot be proven.** This includes `Sqrt`, `Ln`, `Log`,
  `Arcsec`, and `Arccsc`. Provably in-domain operands retain a real type;
  provably negative operands use complex helpers when compiled.

### New Features

#### Functions, types, and effects

- **Multi-clause function definitions** can dispatch by arity, literal value,
  and parameter type. The most specific clause wins, declaration order breaks
  ties, and redefining the same parameter domain replaces that clause. Recursive
  clause sets compile to JavaScript. Partial application of a clause set is not
  supported.

- **Cortex programs can declare nominal types and structural aliases** with
  `type name = …` and `type alias name = …`. Declarations provide checked
  constructors where appropriate; nominal values are opaque, support structural
  equality by tag and payload, and can expose named fields with `value.field`.
  Record-shaped nominal types can use a same-name constructor function for
  validation or normalization. Type tags are erased by compiled output.

- **Function signatures can declare effects.** Types and Cortex definitions
  accept `pure`, `any`, or effect labels including `random`, `scope`, `network`,
  `time`, and file-system effects. Inferred effects follow the function body;
  explicit effects are checked contracts. Callback parameters can use effect
  annotations to restrict accepted functions.

- **Operator definitions accept an `effects:` field.** The existing `pure` and
  `drawsRandom` properties remain supported as shorthand.

- **Expressions and function types expose effects directly** through
  `expr.effects` and `type.effects`. Effect-discharging operators such as
  `WithRandomSeed` can absorb an effect, while `Hold` defers the effects of its
  contents until release.

#### Cortex language

- **Function definitions accept literal parameters**, including strings,
  booleans, finite numbers, `NaN`, and infinities. Unicode mathematical symbols
  such as `π`, `∞`, `ⅈ`, and `ℝ` now resolve to their standard constants or
  sets. Non-finite literal names are reserved; verbatim identifiers remain
  available when those spellings are needed as names.

- **`match` supports inclusive numeric range patterns**, including negative and
  infinite bounds. Range patterns can be combined with alternatives and guards
  and compile on every target.

- **Errors can be handled in Cortex.** `match` can catch error values, and the
  new held predicate `IsError(x)` detects errors without propagating them.
  Propagated errors include a non-rendered trace available through MathJSON and
  the error-trace APIs.

- **The structural equality operator `===` now evaluates.** It performs total,
  exact structural comparison, including for symbolic expressions, `Missing`,
  and `NaN`.

- **`Count` accepts either a value or a predicate**, and **`Table` accepts tuple
  iterator specifications**. Tuple and brace iterator forms now behave
  consistently for `Sum`, `Product`, and `Integrate`.

- **Static type errors are reported by `cortex check` and before program
  evaluation.** Checking canonicalizes without executing effects.

#### Compilation

- **`PointList` with collection-valued components compiles to JavaScript.** GPU
  coordinate accessors can also project compatible point-list components.

- **`At` compiles to GLSL and WGSL** for statically sized numeric collections,
  including guarded dynamic scalar indexes and literal gathers or masks.

- **`D`, `Derivative`, and `ND` compile on all targets** when they can be
  reduced to a compilable form.

- **Non-finite numeric literals compile to GLSL and WGSL.**

- **Python compilation supports collection equality and inequality.**
  `Norm(matrix, 2)` now compiles with the interpreter's Frobenius-norm
  semantics.

- Added Cortex transition guides for Python and Mathematica users.

### Improvements

- Unknown Wolfram Language and JavaScript collection-function names now suggest
  the corresponding Cortex operators.

- Static result types are tighter for trigonometric, hyperbolic, extrema,
  Pochhammer, and collection-rank operations when the result can be proven real
  or finite.

- User-declared type names now resolve consistently in annotations, signatures,
  type predicates, collection operators, and MathJSON declarations. Literal
  value types and bounded numeric refinements now accept their own values.

- `Range` and `Linspace` can enumerate exact symbolic bounds and steps that have
  a numeric value, such as multiples of π. Bracket range syntax also recognizes
  more exact arithmetic progressions.

- `Sum` and `Product` evaluate pure, closed bound expressions such as
  `Length(P)` while retaining symbolic behavior for genuinely free bounds.

- `Distance` accepts the same point-or-point-list union types as related point
  operators and reports a clear error if a point list reaches scalar distance
  evaluation, in both interpreted and compiled code.

### Performance

- Numeric integration retains a sufficiently accurate deterministic quadrature
  estimate instead of replacing it with a much slower Monte Carlo estimate. This
  substantially improves nested and difficult integrals.

- Lazy function-applying collections, including `Map`, `Filter`, `FlatMap`,
  `Scan`, `Tabulate`, and `Iterate`, memoize evaluated elements per instance.
  Cache invalidation now follows actual symbol and configuration dependencies,
  so unrelated assignments no longer discard cached collection elements.

- Exact evaluation of sufficiently large, bounded integer `Map` broadcasts can
  use compiled float64 arithmetic when exactness is statically guaranteed.

- Compiled function bodies reuse repeated pure user-function calls, avoiding
  exponential work in recursive definitions. Common-subexpression elimination
  now also recognizes engine-provided compiled operators.

### Issues Resolved

#### Parsing and serialization

- Chained postfix indexes parse uniformly as nested `At` expressions for
  symbols, subscripted expressions, and literal collections.

- Ranges with compound first anchors, such as `n+1..n+10`, `[2n..3n]`, or
  `x = m+n...m+n+4`, bind the whole anchor expression instead of absorbing part
  of it into the surrounding addition, multiplication, or negation. This applies
  in brackets, bare expressions, and relations. A parenthesized range opts out:
  `n+(1..10)` remains a broadcast addition.

- Dictionary-valued function results preserve parameter bindings through
  MathJSON round trips, including in recursive functions.

- Roots serialized with solidus or quotient notation now delimit a `Power` base
  correctly.

- Invalid multi-argument `Exp` and `Exp2` expressions remain well-formed inert
  expressions with an arity error.

#### Evaluation and functions

- `At` reports incompatible dimensions when more indexes are supplied than a
  collection can consume.

- `match` inside a function uses the current call's parameters rather than
  retaining values from the first call.

- Seeded-random frames no longer incorrectly mark enclosing function literals or
  `Map` callbacks as random.

- Purity and effect results for recursive functions are stable and no longer
  depend on which expression is queried first.

- Compiled lambdas with unbound symbols, including symbols reached through
  assigned values or function bodies, decline cleanly instead of throwing a
  JavaScript `ReferenceError`. Interpreter fallback numericizes symbolic results
  and is also available for failed interval compilation.

- Numeric use of a list-valued function no longer widens its inferred result to
  a scalar or causes compiled reductions to use scalar arithmetic on arrays.

- `FindFit` and `FindRoot` observe ambient time limits during expensive
  iterations and return their best result with `timedOut: True` when possible.

- Differentiating control-flow or binding operators such as `Which`, `Sum`, and
  `Integrate` stays symbolic instead of throwing or producing an invalid
  slot-wise derivative.

#### Collections

- Bounded `Take` expressions are recognized as finite even when the source
  length is unknown, allowing finite prefixes of infinite filtered collections
  to materialize.

- Bare `_` works as the identity function in function slots, and wildcard
  predicate shorthand is accepted consistently by eager and lazy collection
  operators.

- Unary `Iterate` functions receive the accumulator, and indexed access now
  agrees with iteration about the first emitted value.

- Collection operators resolve evaluable numeric arguments such as `N-1`. They
  remain symbolic, rather than using a default, when a required numeric argument
  is unresolved.

- `Partition` distinguishes unresolved integer sizes from predicates and no
  longer throws on an unbound size.

- Seeded comprehensions materialize consistently with other lazy collections.

#### Numerical evaluation and types

- Numeric roundoff cleanup is independent of `ce.tolerance`; comparison and
  explicit `Chop` operations continue to honor the configured tolerance.
  Compiled `Chop` now uses that tolerance, and exact operands remain exact.

- The reported uncertainty of an iterated numeric integral includes inner-level
  quadrature error. Previously only the outermost level's own estimate was
  reported, which could present a result with inner error as exact.

- Bignum `Sin` and `Cos` preserve small representable values near zero
  crossings.

- Static types for non-finite, complex, and out-of-domain results were corrected
  across arithmetic, special functions, elliptic functions, inverse
  trigonometric functions, and complex component operators.

- Domain boundaries are classified with exact comparisons for exact literals.

- Non-radian angle conversion preserves the imaginary component of complex
  results.

- `LCM(0, 0)` returns `0`, and `SigmaMinus1` preserves exact results under
  `evaluate()`.

#### Compilers

- Compiled `Mod`, `Remainder`, division, and negation now parenthesize compound
  operands correctly across JavaScript, GPU, and Python targets.

- Impure operands used by compiled remainder, modulus, and selected GPU
  functions are evaluated exactly once.

- Broadcasts over literal lists emit target-appropriate code for JavaScript,
  Python, GLSL, and WGSL instead of leaking JavaScript syntax into other
  targets.

- GPU compilation rejects unsupported alpha colour constructors, non-finite loop
  bounds, mismatched shapes, and scalar-only operations on vectors or matrices
  instead of emitting invalid or silently incorrect shaders.

- GPU `Max` and `Min` over one collection reduce to a scalar instead of
  returning the input vector.

- JavaScript compilation now broadcasts `Sign`, `Arctan2`, `Hypot`, and `Sinc`
  over lists.

- Complex constant folding is consistent with `realOnly`: non-real constants
  produce `NaN` in real-only mode and principal complex values when complex
  compilation is supported.

- Compiled `InverseHaversine` supports complex results on JavaScript and reports
  an appropriate complex static type for symbolic inputs.

### Benchmarks

#### Numeric performance (200-digit precision)

Median time per call, in **microseconds — lower is better**. `—` means the tool
returned no usable result at that precision.

| Expression         | CE 0.100.0 | CE 0.99.0 | SymPy | math.js | Mathematica |
| ------------------ | ---------: | --------: | ----: | ------: | ----------: |
| $\pi^2$            |        8.2 |       8.7 |   201 |     190 |         3.7 |
| $\sin 1$           |         25 |        23 |   253 |     599 |         5.8 |
| $\cos 1$           |         24 |        23 |   240 |     668 |         7.6 |
| $\ln 2$            |         15 |        15 |   375 |   4,939 |         4.1 |
| $e^{\pi}$          |         15 |        15 |   238 |   5,375 |         5.1 |
| $\zeta(3)$         |      1,707 |     1,716 |   290 |       — |          54 |
| $\Gamma(\tfrac13)$ |        915 |       933 |   382 |       — |         233 |
| $\psi(\tfrac13)$   |        797 |       786 | 3,051 |       — |         192 |

#### Symbolic capability & performance

Each cell is **how many times faster than Mathematica** that engine is on the
case (`Mathematica ÷ engine`, so **higher is better**; Mathematica itself is
`1×`). `—` means the engine can't do the case; `✓` means it solves a case
Mathematica can't. Compare the **CE 0.100.0** and **CE 0.99.0** columns to see
what is _new this release_ (a `—` under `0.99.0` next to a number under the
current build). The **CE + R/F** column is the current build with the opt-in
Rubi integrator + Fungrim identities loaded (`loadIntegrationRules` /
`loadIdentities`), on the same minified bundle.

| Operation                              | CE 0.100.0 | CE + R/F | CE 0.99.0 | SymPy  | math.js | Mathematica |
| -------------------------------------- | :--------: | :------: | :-------: | :----: | :-----: | :---------: |
| **Antiderivatives**                    |            |          |           |        |         |             |
| $\int\frac{1}{\sqrt x}\,dx$            |    3.5×    |   1.8×   |   2.8×    |  0.4×  |    —    |     1×      |
| $\int\frac{x}{\sqrt{1-x^2}}\,dx$       |    6.4×    |   1.0×   |   5.7×    | 0.08×  |    —    |     1×      |
| $\int\frac{1}{x^3+1}\,dx$              |    3.3×    |   0.4×   |   2.7×    |  0.3×  |    —    |     1×      |
| $\int\frac{\sqrt x}{1+x}\,dx$          |     —      |   1.5×   |     —     |  0.1×  |    —    |     1×      |
| $\int\frac{x}{(1+x)^{1/3}}\,dx$        |     —      |   0.9×   |     —     | 0.009× |    —    |     1×      |
| $\int\frac{x^2}{(1+x)^{1/3}}\,dx$      |     —      |   0.8×   |     —     | 0.006× |    —    |     1×      |
| **Derivatives**                        |            |          |           |        |         |             |
| $\tfrac{d}{dx}\sqrt{1-x^2}$            |   0.03×    |  0.03×   |   0.03×   | 0.001× | 0.003×  |     1×      |
| **Simplification**                     |            |          |           |        |         |             |
| $\sqrt{3+2\sqrt2}$                     |    40×     |   29×    |    28×    |   —    |    —    |     1×      |
| $\sqrt6\,x+\sqrt2\,x$                  |    73×     |   38×    |    51×    |  3.1×  |   16×   |     1×      |
| **Evaluation**                         |            |          |           |        |         |             |
| $\lim_{x\to0}\tfrac{\sin x}{x}$        |    36×     |   17×    |    34×    |  3.0×  |    —    |     1×      |
| $\lim_{x\to\infty}(1+\tfrac1x)^x$      |    4.9×    |   3.6×   |   4.7×    |  2.2×  |    —    |     1×      |
| $\int_1^2\tfrac1x\,dx$                 |   18356×   |  20053×  |  17734×   |  310×  |    —    |     1×      |
| $\int_{-\infty}^{\infty} e^{-x^2}\,dx$ |    258×    |   110×   |   251×    |  2.6×  |    —    |     1×      |
| **Solving**                            |            |          |           |        |         |             |
| $x^4+x^2-1=0$                          |    0.3×    |   0.2×   |   0.3×    | 0.06×  |    —    |     1×      |
| $x^3-x-1=0$                            |    1.4×    |   1.6×   |   1.3×    | 0.04×  |    —    |     1×      |

Across the cases both solve, Compute Engine is a **median 3.5× faster than
Mathematica** (up to 18356×) — in the browser, not a proprietary kernel.

<sub>Measured 2026-08-02 · Compute Engine `0.100.0` (current build @ `14fc06c2`)
· published `0.99.0` · SymPy `1.14.0` · math.js `15.2.0` · Mathematica
`14.3.0 for Mac OS X ARM` · Node `v22.13.1`. Correctness is verified numerically
against an independent `mpmath` reference, never another tool. Reproduce with
`npm run build production && ./venv/bin/python3 benchmarks/gen_cases.py && node benchmarks/report.mjs && node benchmarks/report_changelog.mjs`.</sub>

## 0.99.0 _2026-07-30_

### New Features

- **User-defined functions now compile to GLSL and WGSL.** A function declared
  in the engine (`f(u,v) := …`) and called from a GPU-compiled expression is
  emitted **once** as a real shader function and called by name — matching what
  the JavaScript and interval targets already did — instead of failing as an
  unknown operator and forcing callers to inline the body at every call site.
  Definitions arrive on the compilation result's preamble alongside the `_gpu_*`
  helpers, so existing shader assembly keeps working; when `f` calls `g`, `g` is
  declared first. Signatures are synthesized statically (declared parameter
  types, or the body's inferred shape: `float`, `bool`, `vec2`–`vec4`, complex
  as `vec2`); anything a shader cannot express fails closed with a diagnostic
  naming the parameter — including recursion (GLSL/WGSL forbid it; it still
  compiles on the JavaScript target), collection-valued arguments beyond the
  static `vec2`–`vec4` shapes, and argument shapes that disagree with the
  declared parameter type. Undeclared parameters default to `float`.
  Caller-declared types on `compileFunction` parameters and on shader
  inputs/uniforms are authoritative for these checks, with element-aware
  matching across both languages' spellings (`bvecN`, `ivecN`, `vec2<i32>`, …) —
  a `vec2<bool>` argument no longer passes for a `vec2<f32>` parameter. A WGSL
  body referencing a declared input now correctly emits `input.<name>`
  (previously a bare, undeclared identifier).

### Performance

- **Compiled code now shares repeated subexpressions** (common-subexpression
  elimination) on the `javascript`, `interval-js`, and `python` targets: a pure
  subtree occurring several times inside one compiled expression is bound to a
  temporary once and reused, instead of being recomputed at every site.
  Corpus-extreme shapes (a 500-node subtree repeated 128×) compile ~50% faster
  because the emitted source collapses, and run up to ~1.9× faster when the
  repeats involve runtime helpers the JS engine cannot eliminate itself. Sharing
  is conservative by construction: random draws, user-defined function
  applications, named callbacks, caller-supplied custom lowerings, and anything
  inside a conditionally-evaluated position (unselected `Which`/`If` arms,
  short-circuited `And`/`Or` tails) are never merged, so values, draw streams,
  and selection laziness are unchanged. Opt out per call with
  `compile(expr, { cse: false })`. Design notes:
  `docs/plans/2026-07-28-compile-cse-design.md`.

- **Compiled output is now byte-for-byte deterministic on every target**:
  compiler-generated temporaries (chained-relation operand bindings, loop
  accumulators, complex power chains — and the new CSE temps) draw deterministic
  `_tvN`/`_cseN` names from a per-compilation counter instead of
  `Math.random()`, and the allocator avoids capture against every symbol in the
  expression. Two compilations of the same expression now emit identical source,
  on the GPU targets included.

- **Random draws are up to 100x faster when the engine runs inside a secondary
  JavaScript realm** — a `vm` context, a sandboxed worker, or an
  embedder-supplied global. V8 compiles `Math.imul(...)` down to a single
  machine instruction only when `Math` is the host realm's; reached through
  another realm's global it becomes a property lookup plus a call, and the PCG3D
  hash behind every draw performs six of them. Binding the function once at
  module scope removes the lookup. Measured on node 22: 10 million draws went
  from ~880 ms to ~40 ms in a `vm` context (and from ~36 s to ~0.5 s for a
  10-million-sample Monte-Carlo integral under Jest). Draw values are unchanged
  — the stability vectors are untouched.

### Resolved Issues

- **A Leibniz derivative no longer swallows the comparison that follows it.**
  `\frac{d}{dx}x^2 > 0` parsed as `D(x^2 > 0, x)` — the derivative of a
  _boolean_ — which then evaluated to an `incompatible-type` error. The operand
  of `\frac{d}{dx}` / `\frac{\partial}{\partial x}` is now parsed as a term: it
  still takes a trailing sum (`\frac{d}{dx}x^2 + 1`, matching the `\int … dx`
  integrand convention), but stops before a relational, assignment or arrow
  operator, so the expression parses as `D(x^2, x) > 0`.

- **An undecidable comparison no longer discards its evaluated operands.**
  `x^2 + x^2 > 0` evaluated to `0 < x^2 + x^2` and `\frac{d}{dx}x^2 > 0` to
  `0 < D(x^2, x)`: `Equal`, `NotEqual`, `Less` and `LessEqual` evaluate their
  own operands (they are lazy, so their `canonical` handlers can see raw
  operands for chain decomposition), then threw that work away when the
  comparison itself could not be decided. They now report the evaluated operands
  — `0 < 2x^2` and `0 < 2x` — which is what the non-lazy relations (`Approx`,
  `Tilde`, `Precedes`…) already did. The comparison itself is unchanged: an
  undecidable one still stays inert, since `x^2 = 4` is a _condition_ rather
  than a falsity, and decidable ones still fold to `True`/`False`.

- **Monte-Carlo integration and stochastic equality now replay under
  `WithRandomSeed()`.** Both sampled `Math.random()` directly, so a seeded block
  did not reproduce: `WithRandomSeed(42, \int_0^1 \sin(1/x) dx)` returned a
  different estimate on every evaluation, and a seeded `isEqual()` verdict could
  not be replayed at all.

  They now draw from a _derived sub-stream_ — a private stream seeded from the
  ambient frame that consumes **none** of its indices. This matters because an
  integral takes up to 1e7 samples and the sampling loop is deadline-truncated:
  charging those to the frame would make adding an integral shift every later
  `Random()` draw in the block, and would make replay depend on wall-clock time.
  Adding or removing an integral now leaves sibling draws untouched, and the
  same integral samples the same points wherever it appears in the frame.

  Outside a frame both remain live, as before. `monteCarloEstimate()` (exported
  from `@cortex-js/compute-engine/numerics`) gains an optional trailing `draw`
  parameter defaulting to `Math.random`, so existing callers are unaffected.

  **This applies to evaluation, not to compiled code.** An integral inside a
  compiled function still samples live, even within a seed frame, and that is
  deliberate: the generated code emits one independent quadrature call per
  limit, so a sub-stream would restart at the same sample points on every outer
  node of a nested integral — trading reproducibility for a biased estimate. In
  practice a smooth integrand never reaches the stochastic estimator anyway (it
  folds to a constant at compile time, or converges under deterministic
  Gauss–Kronrod); only a pathological one does. Use `.N()` when a seeded
  integral has to reproduce. See `docs/plans/2026-07-28-derived-substreams.md`
  §5.1.

  An integral that cannot finish inside a seed frame — a bound or parameter is
  still unbound — now keeps the frame, so completing it later reproduces the
  estimate the frame would have produced. Operator definitions gain a
  `readsRandomFrame` flag for this: it means "reads the seed frame, consumes
  none of its indices", and is inferred for a user function whose body reaches
  an estimator. Unlike `drawsRandom`, it does not make the operator impure.

- **A collection operator now resolves an unevaluated numeric argument.**
  Collection handlers are consulted on the canonical expression — `.at()`,
  `.each()` and `.count` are available on any canonical expression, and the
  broadcast that zips `Add`/`Multiply` operands runs before they are evaluated.
  An argument spelled `N-1` was therefore still an unevaluated sum at that
  point, and each operator silently fell back to its default: with `N` assigned
  `6`, `RotateLeft(S, N-1) + RotateLeft(S, N-2)` returned `2·RotateLeft(S, 1)`,
  and `Take(S, N-2) + Take(S, N-2)` returned nothing at all. Literal arguments
  were never affected. Fixed for `Take`, `Drop`, `RotateLeft`, `RotateRight`,
  `Repeat`, `Fill`, `Partition`, `Tabulate`, `Insert`, `DeleteAt`, `ReplaceAt`,
  `Slice`, `Permutations` and `Combinations`.

- **`Take`, `Drop`, `RotateLeft`, `RotateRight` and `Slice` now stay symbolic
  when their numeric argument has no value.** An argument that is absent and one
  that is present but unresolved (a free variable) were treated alike, so each
  operator answered a collection it does not denote: `Take(S, n)` returned `[]`,
  `Drop(S, n)` returned all of `S`, and `RotateLeft(S, n)` and
  `RotateRight(S, n)` rotated by one. They now report an unknown count and
  evaluate to themselves until `n` has a value — matching `Repeat`, `Insert`,
  `DeleteAt`, `ReplaceAt`, `Permutations`, `Combinations`, `Tabulate`, `Chunk`
  and `Range`, which already did. An omitted argument still takes the operator's
  default, so `RotateLeft(S)` still rotates by one; a `NaN` argument counts as
  unresolved, not omitted. `Fill` joins them: `Fill(f, (n, 3))` used to produce
  an empty matrix and `Fill(f, (2, n))` two empty rows. `IsEmpty` of such a
  `Take` over an infinite collection no longer answers `False` either — a zero
  count would make it empty.

  Membership is deliberately unaffected: `Contains(RotateLeft(xs, n), x)` still
  answers, because a rotation is a permutation.

- **`Partition(xs, n)` with an unbound `n` no longer throws.** `Partition`
  accepts either a chunk size or a predicate, and chose the arm by whether the
  size read as an integer — so an argument that is _typed_ `integer` but has no
  value yet was applied as a predicate, and the resulting exception escaped
  `evaluate()`. The arm is now chosen by type, and an unresolved predicate (a
  symbol declared `function` with no value) leaves the expression unevaluated as
  well. A predicate that does resolve, to something other than a boolean, still
  reports the error with its spelling hint.

- **`WithRandomSeed(seed, [… for …])` draws again.** A comprehension is a lazy
  view, like `Map`: its body draws per element when the collection is
  materialized. The rule that keeps the seed frame around a body that could not
  finish its draws (0.98.0) read the comprehension's unevaluated body as
  unfinished work, so the expression evaluated to itself — the collection was
  inert and yielded no elements, and nothing would ever complete it. A
  comprehension now follows the same convention as `Map`: materialize it inside
  the frame (`WithRandomSeed(1, ListFrom([Random() for k = [1...6]]))`) for
  reproducible draws. A comprehension whose _clause_ still owes draws keeps the
  frame, as before.

- **A user-defined function now infers its `pure` and `drawsRandom` flags from
  its body.** Previously every user function was born pure, so `f() := Random()`
  reported `isConstant: true` while drawing from the random stream on every
  call. Two things broke as a result: `N(f() + \pi)` consumed two draws where
  `N(\mathrm{Random}() + \pi)` consumed one, and a partially evaluated
  `WithRandomSeed()` body calling `f` lost its seed frame — silently resuming
  with live, unseeded draws.

  The flags are derived from the heads the body applies: a body reaching a
  known-impure head is impure, and one reaching a stream-drawing head also
  draws. A head with no definition at the point of definition — a higher-order
  parameter (`f(g) := g()`), or a callee defined after its caller — is still
  assumed pure; set the flag explicitly on the definition when that matters.

- **`RandomPrime()` is now reproducible under `WithRandomSeed()`.** It declared
  `drawsRandom: true`, but its draws bypassed the seed frame, so
  `WithRandomSeed(42, RandomPrime(1000))` returned a different prime on every
  evaluation. It now draws from the frame like the rest of the random family.

- **`RandomExpression()` is now declared impure.** It was declared pure, which
  made `isConstant` true for a generator that returns a different expression on
  every call, and admitted it to common-subexpression elimination.

### Improvements

- **A loop-form `Sum`/`Product` can now be a sub-expression in GLSL and WGSL.**
  A shader has no expression-level loop, so a `Sum` with a symbolic bound was
  emitted as statements — valid as a whole function body, but nothing more:
  `\sum_{k=0}^{n}kx` compiled while `1 + \sum_{k=0}^{n}kx` and
  `0.03\sum_{k=0}^{n}kx` failed closed, which demoted the whole class to the CPU
  path (corpus rows are almost never a bare sum). The loop is now **hoisted**
  ahead of the value it feeds and referenced through its accumulator, so these
  compose. Constant bounds still unroll to an expression as before, and a loop
  nested inside an unrolled term hoists alongside it. Nested sums hoist into
  their enclosing loop body, not out of it.

  A loop inside a **conditionally-evaluated branch** (an `If`/`When`/`Which`/
  `Match` arm) still fails closed, with a diagnostic that says so. A shader
  conditional is an expression, not a statement, so hoisting the loop out of the
  branch would run it unconditionally — and because a compiled `Random()`
  advances a counter at run time, a loop stranded ahead of a branch it never
  feeds would change the value of every later draw.

  `Loop` and `Block` still fail closed as sub-expressions on these targets.

- **A compile decline now names its actual cause.**
  `Unknown operator \`X\`` was reported for three different situations, so a failing compile band could not be triaged by message. They are now distinct: an operator whose compile handler declined a particular _operand shape_ says which operand and why (`PointList: cannot compile — component 1 is collection-valued …`); a head the engine knows but the target cannot lower says so (`Integrate: cannot compile — the operator is known to the engine but target 'glsl' has no lowering for it.`); and `Unknown operator`is now reserved for a head with no operator definition at all. The reason reaches callers as`CompilationResult.error`on the`success: false` paths, and as the thrown message on the direct-target path.

### Benchmarks

#### Numeric performance (200-digit precision)

Median time per call, in **microseconds — lower is better**. `—` means the tool
returned no usable result at that precision.

| Expression         | CE (current) | CE 0.98.0 | SymPy | math.js | Mathematica |
| ------------------ | -----------: | --------: | ----: | ------: | ----------: |
| $\pi^2$            |          6.8 |       7.4 |   173 |     106 |         3.9 |
| $\sin 1$           |           20 |        21 |   218 |     442 |         5.3 |
| $\cos 1$           |           20 |        21 |   219 |     573 |         7.1 |
| $\ln 2$            |           14 |        14 |   332 |   4,344 |         3.8 |
| $e^{\pi}$          |           12 |        13 |   216 |   4,751 |         4.7 |
| $\zeta(3)$         |        1,511 |     1,547 |   265 |       — |          49 |
| $\Gamma(\tfrac13)$ |          831 |       812 |   346 |       — |         213 |
| $\psi(\tfrac13)$   |          712 |       712 | 2,762 |       — |         171 |

#### Symbolic capability & performance

Each cell is **how many times faster than Mathematica** that engine is on the
case (`Mathematica ÷ engine`, so **higher is better**; Mathematica itself is
`1×`). `—` means the engine can't do the case; `✓` means it solves a case
Mathematica can't. Compare the **CE (current)** and **CE 0.98.0** columns to see
what is _new this release_ (a `—` under `0.98.0` next to a number under the
current build). The **CE + R/F** column is the current build with the opt-in
Rubi integrator + Fungrim identities loaded (`loadIntegrationRules` /
`loadIdentities`), on the same minified bundle.

| Operation                              | CE (current) | CE + R/F | CE 0.98.0 | SymPy  | math.js | Mathematica |
| -------------------------------------- | :----------: | :------: | :-------: | :----: | :-----: | :---------: |
| **Antiderivatives**                    |              |          |           |        |         |             |
| $\int\frac{1}{\sqrt x}\,dx$            |     4.6×     |   2.5×   |   3.5×    |  0.5×  |    —    |     1×      |
| $\int\frac{x}{\sqrt{1-x^2}}\,dx$       |     8.3×     |   1.5×   |   6.6×    | 0.09×  |    —    |     1×      |
| $\int\frac{1}{x^3+1}\,dx$              |     5.3×     |   0.9×   |   4.2×    |  0.3×  |    —    |     1×      |
| $\int\frac{\sqrt x}{1+x}\,dx$          |      —       |   1.9×   |     —     |  0.1×  |    —    |     1×      |
| $\int\frac{x}{(1+x)^{1/3}}\,dx$        |      —       |   1.1×   |     —     | 0.01×  |    —    |     1×      |
| $\int\frac{x^2}{(1+x)^{1/3}}\,dx$      |      —       |   1.1×   |     —     | 0.007× |    —    |     1×      |
| **Derivatives**                        |              |          |           |        |         |             |
| $\tfrac{d}{dx}\sqrt{1-x^2}$            |    0.04×     |  0.04×   |   0.03×   | 0.001× | 0.004×  |     1×      |
| **Simplification**                     |              |          |           |        |         |             |
| $\sqrt{3+2\sqrt2}$                     |     41×      |   28×    |    31×    |   —    |    —    |     1×      |
| $\sqrt6\,x+\sqrt2\,x$                  |     79×      |   45×    |    48×    |  3.1×  |   19×   |     1×      |
| **Evaluation**                         |              |          |           |        |         |             |
| $\lim_{x\to0}\tfrac{\sin x}{x}$        |     39×      |   16×    |    34×    |  3.0×  |    —    |     1×      |
| $\lim_{x\to\infty}(1+\tfrac1x)^x$      |     5.2×     |   4.0×   |   6.9×    |  2.1×  |    —    |     1×      |
| $\int_1^2\tfrac1x\,dx$                 |    4458×     |  4875×   |   4193×   |  77×   |    —    |     1×      |
| $\int_{-\infty}^{\infty} e^{-x^2}\,dx$ |     316×     |   128×   |   274×    |  2.5×  |    —    |     1×      |
| **Solving**                            |              |          |           |        |         |             |
| $x^4+x^2-1=0$                          |     0.3×     |   0.3×   |   0.3×    | 0.07×  |    —    |     1×      |
| $x^3-x-1=0$                            |     1.6×     |   1.8×   |   1.4×    | 0.04×  |    —    |     1×      |

Across the cases both solve, Compute Engine is a **median 5.2× faster than
Mathematica** (up to 4458×) — in the browser, not a proprietary kernel.

<sub>Measured 2026-07-30 · Compute Engine `0.98.0` @ `ef394659` (current build)
· published `0.98.0` · SymPy `1.14.0` · math.js `15.2.0` · Mathematica
`14.3.0 for Mac OS X ARM` · Node `v22.13.1`. Correctness is verified numerically
against an independent `mpmath` reference, never another tool. Reproduce with
`npm run build production && ./venv/bin/python3 benchmarks/gen_cases.py && node benchmarks/report.mjs && node benchmarks/report_changelog.mjs`.</sub>

## 0.98.0 _2026-07-28_

### New Features

- **`Which` and `If` now broadcast over list-valued conditions.** Selection is
  performed element by element, with the first matching clause winning. Scalar
  conditions and results are broadcast as needed, and list-valued inputs must
  have the same length. Results are evaluated at most once and only when needed
  by at least one element. A position with no matching clause returns `NaN`.

  ```text
  Which([3, 2, 1, 3] == 3, 1, True, 0) → [1, 0, 0, 1]
  ```

  JavaScript compilation supports the same behavior. GLSL and WGSL compilation
  support fixed-size vectors of two to four elements.

- **New operator-definition attribute `drawsRandom`** marks operators that
  consume the engine's random stream (`Random`, `RandomShuffle`, …). It is
  narrower than `pure: false`, which also covers side-effecting operators such
  as `Assign`, and is used by `WithRandomSeed` to decide whether a partially
  evaluated body still owes draws to its seed frame.

### Breaking Changes

- **Multiplying lists of different lengths now returns an
  `incompatible-dimensions` error**, consistent with other element-wise
  operations. Matrix multiplication is unchanged.

- **Assigning a value to a builtin operator name now creates a symbol in the
  current scope instead of replacing the builtin globally.** For example,
  `ce.assign("Sin", 30)` changes the value of the symbol `Sin`, but `Sin(0)`
  continues to call the builtin function.

- **The unused `BindingSite.shield` field has been removed.** Setting it had no
  effect.

### Improvements

- **JavaScript compilation now supports more collection-valued expressions:**
  user-defined function calls, comparisons and logical operators, and indexed
  `Sum`/`Product` bodies all broadcast element-wise. Length mismatches compile
  to `NaN`.

- **Unsupported collection-valued conditions and vector operations now fail
  compilation with a diagnostic** instead of producing incorrect Python,
  interval-js, GLSL, or WGSL code.

- **`RandomChoice` now infers more precise result types**, including
  `finite_real` for finite intervals and `finite_integer` for integer ranges.

- **Repeated compilation with the same external target is now deterministic**
  and produces identical generated code.

- **Evaluating chained broadcast operations over lazy collections is faster:**
  approximately 3× faster with `evaluate()` and 4× faster with `.N()` at default
  precision in the benchmark used for this release.

### Issues Resolved

- **Partially evaluated `WithRandomSeed` expressions now retain their seed**, so
  later substitutions produce the same result as substituting before evaluation.

- **Nested `N` and `Evaluate` expressions now preserve numeric approximation and
  precision.** For example, `N(Evaluate(pi))` now returns a numeric value
  instead of the symbolic `pi`.

### Benchmarks

#### Numeric performance (200-digit precision)

Median time per call, in **microseconds — lower is better**. `—` means the tool
returned no usable result at that precision.

| Expression         | CE (current) | CE 0.97.0 | SymPy | math.js | Mathematica |
| ------------------ | -----------: | --------: | ----: | ------: | ----------: |
| $\pi^2$            |          7.9 |       8.5 |   203 |     288 |         4.5 |
| $\sin 1$           |           23 |        23 |   253 |     598 |         6.0 |
| $\cos 1$           |           21 |        22 |   253 |     781 |         8.1 |
| $\ln 2$            |           16 |        16 |   386 |   5,471 |         4.7 |
| $e^{\pi}$          |           15 |        15 |   273 |   7,477 |         5.9 |
| $\zeta(3)$         |        1,738 |     1,702 |   337 |       — |          56 |
| $\Gamma(\tfrac13)$ |          924 |       913 |   407 |       — |         268 |
| $\psi(\tfrac13)$   |          781 |       790 | 3,496 |       — |         199 |

#### Symbolic capability & performance

Each cell is **how many times faster than Mathematica** that engine is on the
case (`Mathematica ÷ engine`, so **higher is better**; Mathematica itself is
`1×`). `—` means the engine can't do the case; `✓` means it solves a case
Mathematica can't. Compare the **CE (current)** and **CE 0.97.0** columns to see
what is _new this release_ (a `—` under `0.97.0` next to a number under the
current build). The **CE + R/F** column is the current build with the opt-in
Rubi integrator + Fungrim identities loaded (`loadIntegrationRules` /
`loadIdentities`), on the same minified bundle.

| Operation                              | CE (current) | CE + R/F | CE 0.97.0 | SymPy  | math.js | Mathematica |
| -------------------------------------- | :----------: | :------: | :-------: | :----: | :-----: | :---------: |
| **Antiderivatives**                    |              |          |           |        |         |             |
| $\int\frac{1}{\sqrt x}\,dx$            |     4.5×     |   2.2×   |   3.5×    |  0.5×  |    —    |     1×      |
| $\int\frac{x}{\sqrt{1-x^2}}\,dx$       |     8.1×     |   1.6×   |   6.8×    | 0.08×  |    —    |     1×      |
| $\int\frac{1}{x^3+1}\,dx$              |     4.9×     |   0.6×   |   3.7×    |  0.3×  |    —    |     1×      |
| $\int\frac{\sqrt x}{1+x}\,dx$          |      —       |   1.7×   |     —     | 0.08×  |    —    |     1×      |
| $\int\frac{x}{(1+x)^{1/3}}\,dx$        |      —       |   1.0×   |     —     | 0.008× |    —    |     1×      |
| $\int\frac{x^2}{(1+x)^{1/3}}\,dx$      |      —       |   1.0×   |     —     | 0.006× |    —    |     1×      |
| **Derivatives**                        |              |          |           |        |         |             |
| $\tfrac{d}{dx}\sqrt{1-x^2}$            |    0.04×     |  0.03×   |   0.03×   | 0.001× | 0.004×  |     1×      |
| **Simplification**                     |              |          |           |        |         |             |
| $\sqrt{3+2\sqrt2}$                     |     37×      |   27×    |    31×    |   —    |    —    |     1×      |
| $\sqrt6\,x+\sqrt2\,x$                  |     74×      |   42×    |    44×    |  3.1×  |   17×   |     1×      |
| **Evaluation**                         |              |          |           |        |         |             |
| $\lim_{x\to0}\tfrac{\sin x}{x}$        |     41×      |   17×    |    38×    |  2.9×  |    —    |     1×      |
| $\lim_{x\to\infty}(1+\tfrac1x)^x$      |     7.2×     |   4.6×   |   6.8×    |  1.9×  |    —    |     1×      |
| $\int_1^2\tfrac1x\,dx$                 |    5778×     |  6836×   |   5520×   |  107×  |    —    |     1×      |
| $\int_{-\infty}^{\infty} e^{-x^2}\,dx$ |     352×     |   146×   |   298×    |  2.7×  |    —    |     1×      |
| **Solving**                            |              |          |           |        |         |             |
| $x^4+x^2-1=0$                          |     0.3×     |   0.3×   |   0.3×    | 0.05×  |    —    |     1×      |
| $x^3-x-1=0$                            |     2.2×     |   2.4×   |   1.9×    | 0.05×  |    —    |     1×      |

Across the cases both solve, Compute Engine is a **median 4.9× faster than
Mathematica** (up to 5778×) — in the browser, not a proprietary kernel.

<sub>Measured 2026-07-28 · Compute Engine `0.97.0` @ `bff3c3b1` (current build)
· published `0.97.0` · SymPy `1.14.0` · math.js `15.2.0` · Mathematica
`14.3.0 for Mac OS X ARM` · Node `v22.13.1`. Correctness is verified numerically
against an independent `mpmath` reference, never another tool. Reproduce with
`npm run build production && ./venv/bin/python3 benchmarks/gen_cases.py && node benchmarks/report.mjs && node benchmarks/report_changelog.mjs`.</sub>

## 0.97.0 _2026-07-27_

### Breaking Changes

- **A broadcast operand is evaluated ONCE.** Broadcasting is an operation on
  values (the NumPy/Julia/R model), so an operand that is lifted into every cell
  is evaluated a single time and the operation then maps over the cells:

  ```
  L < Random()        // ONE draw, compared against every element of L
  Map(L, l ↦ l < Random())   // a draw per element, written explicitly
  ```

  Comparisons and logical connectives used to re-evaluate a lifted operand per
  element, so `[0.5, 0.5, 0.5] < Random()` could answer `[True, False, True]`.
  Arithmetic (`L + Random()`) already drew once; the disagreement was an
  implementation artifact of the element-wise zip, not a semantic. Only impure
  _scalar_ operands under a broadcast are affected — an impure operand that IS
  the traversed collection (`[Random(), Random()] < 0.5`) still draws per cell,
  because those draws are the cells.

- **A length mismatch in a broadcast is an error, not a truncation.** Zipping to
  the shortest operand silently discarded the tail of the longer one:

  ```
  [1,2,3] < [2,2]     // was: [True, False]   now: incompatible-dimensions
  ```

  `Add` already answered `incompatible-dimensions` for this shape, so the engine
  disagreed with itself depending on the head. One check now governs every
  broadcast path — the eager zip, the arithmetic broadcast, and the lazy `Map`
  form — so the ordering relations, the logical connectives,
  `Divide`/`Power`/`Mod`, `Add`/`Multiply`, and `ElementMax`/`ElementMin`/
  `Clamp` all answer alike. In particular the SIZE of a collection no longer
  decides the semantics (a mismatch used to error below the eager threshold and
  truncate above it), and neither does the shape of the source:
  `Add(Filter(…), L)` used to truncate where `Less` on the same operands
  errored.

  An **unbounded** operand against a finite one is a mismatch too (`count` is
  `Infinity`, which agrees with no finite length). A scalar operand is a LIFT,
  not a participant, so it never mismatches; an operand whose length is not yet
  known is not compared, since there is nothing to compare until it resolves. An
  empty operand alongside a non-empty one is a mismatch, while a lone empty
  operand still broadcasts to `Nothing` (`Not([])`).

  `PointList` is deliberately unaffected: it ZIPS components rather than
  broadcasting an operator over them, and its shortest-zip
  (`PointList([1,2,3],[10,20])` → two points) is an existing consumer contract.
  The general rule: an operator LIFTED over collections requires length
  agreement, while an explicit PAIRING constructor (`Zip`, the variadic `Map`,
  `PointList`) defines its length as the shortest input. See
  `docs/BROADCAST-MODEL.md` for the full policy.

### New Features

- **Compiled comparisons and logical connectives broadcast element-wise.** The
  ordering relations (`<`, `<=`, `>`, `>=`) and the connectives (`And`, `Or`,
  `Not`) compile over collection-valued operands on the JavaScript target
  instead of failing closed, so the Desmos filter form compiles rather than
  falling back to the interpreter:

  ```js
  compile(ce.parse('[10,20,30][|[1...3]-k|>0]')).run(); // [10, 30]
  ```

  These heads lower to raw JS infix operators, which are silently wrong on an
  array (`0 < [1,0,1]` stringifies it; an array is truthy, so `m1 && m2` returns
  a whole operand). They now wrap the head's own scalar codegen in the
  `_SYS.bcast` runtime helper, which recurses per POSITION — an empty or
  mismatched position projects to NaN without poisoning its siblings
  (`Not([[], [True]])` → `[NaN, [false]]`, matching the interpreter's
  `[Nothing, [False]]`).

  The scalar path is untouched: `x < 3` with an `unknown`-typed plot variable
  still emits `_.x < 3`, with no runtime guard. Three shapes deliberately keep
  failing closed, because the compiled answer would disagree with
  interpretation: a CHAINED ordering (`0 < xs < 5`, whose pairwise `&&` is sound
  only over scalars); `Equal`/`NotEqual` over two collections (whole- collection
  equality, which keeps its `_SYS.eq` dispatch and stays element-wise for the
  list-vs-scalar case); and an operand that types as a list but does not COMPILE
  to one — notably a user-function application over a collection argument
  (`q(L) < y`), since a user function's body compiles as scalar code and returns
  NaN on an array, which a comparison would turn into a plausible `false`.

### Issues Resolved

- **`Covariance`/`Correlation` report a length mismatch as
  `incompatible-dimensions`.** They were already strict — a ragged pair of data
  collections errored — but with their own `unexpected-argument` ("collections
  differ in length") rather than the `incompatible-dimensions` error every
  broadcast path answers. One error tag now covers every length-mismatch
  diagnosis (see `docs/BROADCAST-MODEL.md`). The other argument errors
  (`at least 2 data points required`, the shape error, `zero variance`) are
  unchanged.

- **`Multiply` over operands of mixed collection kinds is element-wise again.**
  A `List` literal packs as a tensor value while a `Range`/`Filter`/`Take`/
  `Reverse` result does not, so the non-tensor operand was classified as a
  SCALAR factor — and applying a scalar multiplies every CELL by it, turning
  each cell into a list:

  ```
  [1...3] · [4,5,6]        // was: [[4,8,12],[5,10,15],[6,12,18]]   now: [4,10,18]
  Range(1,3) · [1,2,3]     // was: [[1,2,3],[2,4,6],[3,6,9]]        now: [1,4,9]
  Range(1,3) + [1,2,3]     // [2,4,6] — `Add` was always element-wise
  ```

  Not a length problem: it misfired at _matched_ lengths, and same-kind pairs
  (`List`×`List`, `Range`×`Range`) were always element-wise, because neither
  operand reached the scalar bucket. `Add` avoids it by declining its tensor
  kernel when fewer than two operands pack; `Multiply` now declines when a
  non-tensor collection would land among the scalars, and falls through to the
  same element-wise broadcast — which also brings mixed-kind mismatches under
  the length ruling above. Scalar×list scaling, matrix products, and
  component-wise tuple scaling are unchanged.

- **A symbol bound to a derived collection serializes as its name again.**
  Regression in 0.96.0. `.latex` and `toString()` materialize a lazy collection
  before serializing, and a symbol whose assigned value is a _derived_
  collection (the result of evaluating a `Join`, a comprehension, …) delegates
  `isLazyCollection` to that value — so the symbol serialized as its
  materialized value while `.json` still answered the symbol's name:

  ```js
  ce.assign('L', ce.box(['List', 1, 2, 3]));
  ce.assign('L', ce.box(['Join', 'L', ['List', 4]]).evaluate());
  ce.symbol('L').json;   // 'L'
  ce.symbol('L').latex;  // was: '\bigl\lbrack1, 2, 3, 4\bigr\rbrack'  now: 'L'
  ```

  A literal `List` value (eager, not lazy) never triggered it, which is what
  made the failure value-provenance-dependent and hard to spot: a consumer that
  serialized a symbol to persist a document wrote the value where the name
  belonged. A symbol now never takes the materialize-before-serialize path — a
  name's spelling does not depend on what the name currently holds. Lazy
  collection _expressions_ (`Range`, `Map`, comprehensions) serialize as before.
  Reported by the Tycho team.

- **`Add` and `Multiply` fold a `Measurement` operand on the first `.N()`.**
  Every unary/binary arithmetic head already folded a quadrature result
  (`Power`, `Divide`, `Sqrt`, `Sin`, `Negate`, `Abs` of a `Measurement` yield
  another `Measurement`), but the two n-ary heads checked for `Measurement`
  operands against the _plainly evaluated_ operands. A quadrature operand only
  becomes a `Measurement` under numeric approximation — `\int_0^1 \sin x\,dx`
  evaluates to the exact `1 - \cos 1` — so the check saw nothing, and the first
  `.N()` returned an inert `Add`/`Multiply` whose `.re` was `NaN` (the same dead
  numeric read fixed for `Measurement` itself in 0.96.0, one level up). A second
  `.N()` folded it:

  ```js
  const once = ce.parse('1 + \\int_0^1 \\sin(x) dx').N();
  once.re;      // was: NaN     now: 1.4596976941318605
  once.operator // was: 'Add'   now: 'Measurement'
  ```

  The handlers now re-dispatch the Measurement fold on the numericized result,
  so the parse route agrees with the (already correct)
  `ce.box(['Add', <measurement>, 1]).N()` route exactly — value and error bar.
  The sibling `Quantity` check in the same handlers had the identical structural
  gap (an operand that only becomes a `Quantity` under numeric approximation was
  invisible to it); it is closed the same way. `evaluate()` is unaffected and
  still returns the exact symbolic form. Reported by the Tycho team.

- **Compiling a definite integral that cannot close symbolically is now
  bounded.** The JavaScript compilation target first attempts to resolve an
  `Integrate` to a closed form (so a plotted `∫₀ˣ f` costs ~µs per sample
  instead of a quadrature); that attempt ran under whatever deadline the caller
  had armed — and under none by default. An integrand with a _symbolic_ exponent
  (`y^{k/2-1}` with `k` a free document parameter) sent the integration-by-parts
  search into a cycle with no shrinking measure: bounded in depth but not in
  cost, it did not return in over 5 minutes, synchronously, with no way to
  interrupt it:

  ```js
  // χ² tail with shape parameter k left free: >5 min → ~2 s, success: true
  ce.getCompilationTarget('javascript')
    .compile(ce.parse('\\int_x^\\infty \\frac{e^{-y/2} y^{k/2-1}}{(k/2-1)!\\, 2^{k/2}} dy',
             {strict: false}), {realOnly: true});
  ```

  The antiderivative-first attempt now arms its own 2-second span (an enclosing
  caller span still tightens it, per the timeout model's `min()` nesting — it
  can only shorten, never extend, a caller's bound) and degrades to the GK15
  quadrature emitter on expiry, so the compiled integral samples numerically
  with the parameter bound at run time — which is the desired behavior for a
  non-elementary integrand. Note that `ce.timeLimit` was retired in 0.89.0: to
  bound `evaluate()` itself, use `ce.withTimeLimit({ms, label}, () => …)`, which
  this shape honors to the millisecond. Reported by the Tycho team.

- **Compiled `Factorial` of a non-integer computes Γ(x+1) instead of NaN.** The
  interpreter has always extended the factorial to the reals (`(\frac12-1)!` →
  `Γ(\frac12)` = `√π`), but the compiled runtime used the integer-only helper,
  so the same expression compiled successfully and returned `NaN` — silently
  zeroing out, e.g., a χ² density's normalizing constant `(k/2-1)!` at odd `k`:

  ```js
  const f = ce.getCompilationTarget('javascript')
              .compile(ce.parse('(\\frac{1}{2}-1)!'));
  f.run({}); // was: NaN    now: 1.7724538509055159 (= √π, identical to .N())
  ```

  Non-integer arguments route through the same `gamma` the interpreter uses, so
  compiled and interpreted values are bit-identical; the non-negative integer
  fast path is unchanged, and a negative integer stays at the Γ pole (`NaN`, the
  real projection of the interpreter's `ComplexInfinity`). The Python target
  likewise now emits `scipy.special.gamma(x + 1)` instead of
  `scipy.special.factorial` (which answers `0` for a negative non-integer). The
  GPU targets already extended via `Γ`; the interval target remains deliberately
  integer-only. Reported by the Tycho team.

### Benchmarks

#### Numeric performance (200-digit precision)

Median time per call, in **microseconds — lower is better**. `—` means the tool
returned no usable result at that precision.

| Expression         | CE (current) | CE 0.96.0 | SymPy | math.js | Mathematica |
| ------------------ | -----------: | --------: | ----: | ------: | ----------: |
| $\pi^2$            |          7.3 |       7.5 |   180 |     110 |         3.9 |
| $\sin 1$           |           20 |        21 |   218 |     439 |         5.2 |
| $\cos 1$           |           20 |        20 |   221 |     456 |         7.1 |
| $\ln 2$            |           14 |        14 |   347 |   4,406 |         3.7 |
| $e^{\pi}$          |           13 |        13 |   214 |   4,655 |         4.5 |
| $\zeta(3)$         |        1,534 |     1,562 |   272 |       — |          49 |
| $\Gamma(\tfrac13)$ |          840 |       834 |   350 |       — |         213 |
| $\psi(\tfrac13)$   |          724 |       720 | 2,776 |       — |         174 |

#### Symbolic capability & performance

Each cell is **how many times faster than Mathematica** that engine is on the
case (`Mathematica ÷ engine`, so **higher is better**; Mathematica itself is
`1×`). `—` means the engine can't do the case; `✓` means it solves a case
Mathematica can't. Compare the **CE (current)** and **CE 0.96.0** columns to see
what is _new this release_ (a `—` under `0.96.0` next to a number under the
current build). The **CE + R/F** column is the current build with the opt-in
Rubi integrator + Fungrim identities loaded (`loadIntegrationRules` /
`loadIdentities`), on the same minified bundle.

| Operation                              | CE (current) | CE + R/F | CE 0.96.0 | SymPy  | math.js | Mathematica |
| -------------------------------------- | :----------: | :------: | :-------: | :----: | :-----: | :---------: |
| **Antiderivatives**                    |              |          |           |        |         |             |
| $\int\frac{1}{\sqrt x}\,dx$            |     4.6×     |   2.3×   |   3.4×    |  0.5×  |    —    |     1×      |
| $\int\frac{x}{\sqrt{1-x^2}}\,dx$       |     7.9×     |   1.5×   |   6.1×    | 0.08×  |    —    |     1×      |
| $\int\frac{1}{x^3+1}\,dx$              |     5.2×     |   0.8×   |   4.0×    |  0.3×  |    —    |     1×      |
| $\int\frac{\sqrt x}{1+x}\,dx$          |      —       |   1.9×   |     —     |  0.1×  |    —    |     1×      |
| $\int\frac{x}{(1+x)^{1/3}}\,dx$        |      —       |   1.1×   |     —     | 0.01×  |    —    |     1×      |
| $\int\frac{x^2}{(1+x)^{1/3}}\,dx$      |      —       |   1.1×   |     —     | 0.007× |    —    |     1×      |
| **Derivatives**                        |              |          |           |        |         |             |
| $\tfrac{d}{dx}\sqrt{1-x^2}$            |    0.04×     |  0.03×   |   0.03×   | 0.001× | 0.003×  |     1×      |
| **Simplification**                     |              |          |           |        |         |             |
| $\sqrt{3+2\sqrt2}$                     |     42×      |   29×    |    29×    |   —    |    —    |     1×      |
| $\sqrt6\,x+\sqrt2\,x$                  |     77×      |   45×    |    51×    |  3.0×  |   18×   |     1×      |
| **Evaluation**                         |              |          |           |        |         |             |
| $\lim_{x\to0}\tfrac{\sin x}{x}$        |     15×      |   16×    |    33×    |  3.1×  |    —    |     1×      |
| $\lim_{x\to\infty}(1+\tfrac1x)^x$      |     7.3×     |   4.9×   |   6.8×    |  2.2×  |    —    |     1×      |
| $\int_1^2\tfrac1x\,dx$                 |    4819×     |  4740×   |   4259×   |  80×   |    —    |     1×      |
| $\int_{-\infty}^{\infty} e^{-x^2}\,dx$ |     293×     |   124×   |   250×    |  2.6×  |    —    |     1×      |
| **Solving**                            |              |          |           |        |         |             |
| $x^4+x^2-1=0$                          |     0.3×     |   0.3×   |   0.3×    | 0.06×  |    —    |     1×      |
| $x^3-x-1=0$                            |     1.7×     |   1.9×   |   1.5×    | 0.04×  |    —    |     1×      |

Across the cases both solve, Compute Engine is a **median 5.2× faster than
Mathematica** (up to 4819×) — in the browser, not a proprietary kernel.

<sub>Measured 2026-07-27 · Compute Engine `0.96.0` @ `9eb6538a` (current build)
· published `0.96.0` · SymPy `1.14.0` · math.js `15.2.0` · Mathematica
`14.3.0 for Mac OS X ARM` · Node `v22.13.1`. Correctness is verified numerically
against an independent `mpmath` reference, never another tool. Reproduce with
`npm run build production && ./venv/bin/python3 benchmarks/gen_cases.py && node benchmarks/report.mjs && node benchmarks/report_changelog.mjs`.</sub>

## 0.96.0 _2026-07-26_

### Breaking Changes

- **The 0.95.0 random-family tombstones are deleted, completing the one-release
  migration window.** In 0.95.0, evaluating a removed head (`RandomInteger`,
  `RandomList`, `RandomSeed`, `Sample`, `Shuffle`) threw an `operator-removed`
  error naming its replacement, and `ce.randomSeed` was a throwing accessor.
  Those guards are now gone: the removed heads behave like any other
  unrecognized operator — a valid, inert expression — and `randomSeed` is no
  longer a property of the engine. Migrate on 0.95.0 (where every legacy call
  site fails loudly with its replacement named) before adopting this release;
  the migration table is in the 0.95.0 notes below.

- **`evaluate()` on a symbol now resolves the free symbols of its stored
  value.** A symbolic value was returned verbatim, so a symbol assigned _after_
  it was stored never reached it:

  ```
  let d = 3x^2 + 1
  let x = 2
  d          // was: 3x^2 + 1     now: 13
  N(d)       // 13 — unchanged
  ```

  `N()` and `compile()` already resolved it, so plain `evaluate()` was the
  outlier and disagreed with both; a second `evaluate()` used to resolve one
  more level ("one-evaluate-late"). Assignment remains **eager**, so declaration
  order still decides what a value snapshots:
  `let x = 2; let d = 3x^2 + 1; x = 3; N(d)` is `13`, while
  `let d = 3x^2 + 1; let x = 2; x = 3; N(d)` is `28`. The residual for a cyclic
  binding is unchanged (`s = s + 1; s` → `s + 1`).

- **A stored value's free symbols are no longer captured by a same-named
  parameter.** They now denote the binding they were canonicalized against, not
  whatever an inner scope calls that name:

  ```
  let a = x + 1
  g(x) = a
  g(5)       // was: 6            now: x + 1
  N(g(5))    // was: 6            now: x + 1
  ```

  With a global `x = 100`, `g(5)` is `101` — the lexically correct binding —
  where it used to be `6`. This closes the same defect on three paths that
  disagreed with each other: the parameter substitution applied after a call,
  the constant dereference path, and the numeric (`N`) re-evaluation inside a
  call frame. A dictionary-valued symbol is covered too. Behavior that was
  already correct is unchanged: a block-local `let` does not leak into a stored
  value, and a renamed parameter never captured.

- **Only a value SHIELD hides a stored value's own binding.** Dereferencing a
  symbol's stored value deferred to the ambient lookup whenever ANY valueless
  shadow of a free symbol's name was in scope. That was a proxy for the shield
  idiom — `Solve`, `D`, `Integrate`, `Limit` and `simplify()` all hide a
  symbol's value by shadow-declaring it valueless — and it swept in ordinary
  declarations, which shield nothing:

  ```
  a = x + 1                          // x still valueless: a captures the global x
  x = 100
  a + 5                              // 106 — unchanged
  Block(Declare(x, "real"), a + 5)   // was: x + 6        now: 106
  ```

  `a` captured a _binding_; the inner `Declare` created a _different_ variable,
  and a different variable has no business intercepting. The asymmetry that made
  the old rule indefensible: give that shadow a value —
  `Block(Declare(x, "real"), Assign(x, 7), a + 5)` — and it already did not
  intercept, evaluating to `106` before and after. The genuine shields are
  unaffected: with a global `x = 100`, `Solve(x + 1 = 0, x)` is still `[-1]`,
  and `simplify()` remains value-blind.

- **A union type is assignable only to a target that covers EVERY arm.**
  `isSubtype()` (and therefore `type.matches()`) used the any-branch rule when
  the target was a composite type and the all-branch rule when it was a
  primitive, so the answer depended on the shape of the target:
  `list<tuple<number,number,number>> | tuple<number,number,number>` matched
  `tuple<number, number, number>`, while `number | list<number>` matched neither
  `number` nor `collection`. Assignability is now all-branch throughout — the
  dual of the intersection rule, where `A & B` is assignable as soon as one arm
  is. A union still matches any target covering all its arms (`integer | real` ⊑
  `real`, `number | list<number>` ⊑ `collection | number`).

  Code that asked `unionType.matches(T)` to mean "could this be a `T`" was
  asking the wrong question and now gets `false`; ask it the other way round
  (`ce.type(T).matches(unionType)` — "would a `T` satisfy this"), which is how
  the matrix-inference repair gate on union-typed parameters such as
  `LinearSolve`'s `matrix | vector` is now spelled.

### Issues Resolved

- **Adaptive quadrature no longer reports a sharply-peaked integral as a
  converged zero.** The adaptive loop started from a single 15-node panel over
  the whole interval, and could never recover from a first panel that read as
  zero: with every node returning ~0, the Gauss/Kronrod difference also
  vanished, met the absolute tolerance, and the integral "converged" after 15
  evaluations. The witness is a peak far narrower than the interval whose weight
  vanishes at the center node:

  ```js
  ce.parse('\\int_{-50}^{50} x^2 \\frac{1}{\\sqrt{2\\pi}} e^{-x^2/2} dx').N();
  // was: 3.2e-21 ± 5.1e-21     (true value: 1)
  // now: 1.0000000000000002
  ```

  The whole Gaussian moment family failed this way (moments 1, 2, 4 and 6 were
  each 100% wrong while claiming an error bar around `1e-21`); the bare `∫φ`
  over the same interval was correct, because `φ(0)` is sampled. Quadrature now
  starts from 16 equal panels, which fixes the family exactly and brings the
  comb `∫₋₁₅¹⁵ φ(x)³⁵⁰` to 0.14% relative error. This is a mitigation, not a
  guarantee — a peak narrower than a starting panel can still be missed, and the
  error estimate still cannot report it — a peak-discovery strategy (shifted or
  low-discrepancy probes) would close the family rather than move the threshold,
  and is not attempted here. Iterated integrals reduce the per-level count so
  the floor applies to the whole integral rather than multiplying per dimension.
  The cost falls on integrals that used to converge on the first panel:
  `∫₀¹ sin x` goes from 15 to 240 evaluations, so a compiled integral called per
  plotted sample goes from ~40 µs to ~130 µs. Reported by the Tycho team.

- **A definite integral whose integrand is identically non-finite now fails fast
  instead of burning the full Monte-Carlo budget.** Adaptive quadrature can
  never converge on a `NaN` integrand, so it fell through to the Monte-Carlo
  estimator, which spent 1e7 samples — 250–450 ms per call — to return the `NaN`
  that was decidable up front. The usual trigger is an unbound variable reaching
  a compiled artifact as `undefined`; a plotted curve then froze the thread and
  drew nothing. `monteCarloEstimate` now probes a few samples and returns `NaN`
  immediately when they are all non-finite. Measured: 100 calls on such an
  integrand went from 30.5 s to 22 ms. Integrands with a genuine endpoint
  singularity are unaffected — the bail requires _every_ probe to be non-finite.
  Reported by the Tycho team.

- **`.N()` of a definite integral with a free symbol in the integrand no longer
  throws a raw `ReferenceError` out of generated code.** The integrand was
  handed to the implicit compiler regardless, and the generated body read the
  free symbol from a scope slot the numeric caller never supplies:

  ```js
  ce.parse('\\int_0^1 (x+q)\\,dx').N();
  // was: throws ReferenceError: _ is not defined
  // now: stays symbolic
  ```

  A parameter with no value leaves nothing to integrate numerically, so the
  expression now stays symbolic; it evaluates as before once the symbol has a
  value. Both the single-limit and the iterated multi-limit paths are covered.

- **A `Measurement` now answers the numeric accessors.**
  `Measurement(value, error)` types as its nominal's scalar type, so
  `Integrate(…).N()` reports `finite_real` and `isNumber === true` — but every
  numeric read was dead, which silently poisoned any consumer reading `.re`, and
  propagated, since `Measurement + 1` is another `Measurement`:

  ```js
  const n = ce.parse('\\int_0^1 \\sin(x) dx').N();
  n.re;            // was NaN,       now 0.45969769413186023
  n.numericValue;  // was undefined, now the nominal
  n.valueOf();     // was the string '0.4596… ± 0.0000…', now the number
  n.sgn;           // was undefined, now 'positive'
  ```

  `.re`, `.im`, `.bignumRe`, `.bignumIm`, `.sgn` and `valueOf()` project the
  nominal — all of them on the public `Expression` type, so `.re` is the channel
  and no cast is needed. The uncertainty stays reachable through the MathJSON,
  or through `op2` after narrowing with `isFunction()`, and `toString()` still
  shows `±`.

  Three members are deliberately NOT projected, all for the same reason: a
  `Measurement` is a **function expression**, and the number-literal surface
  belongs behind the `isNumber()` guard, which narrows on expression kind.
  `numericValue` stays `undefined` — it is declared only on
  `NumberLiteralInterface`, and projecting it would advertise an exact numeric
  representation that a quadrature result does not have; `isNumberLiteral` and
  the `isNumber()` guard stay `false`; and `.value` stays `undefined` (it is the
  expression for a literal and `undefined` for a symbolic expression).
  `.re`/`.im` are sufficient for a `Measurement` precisely because its nominal
  is never an exact number. Reported by the Tycho team.

- **A shorthand-lambda placeholder in a pipeline stage no longer picks up a
  same-named global.** `Pipe` is lazy, so it canonicalizes its right operand in
  the caller's scope — before the operand is wrapped into the implicit lambda it
  denotes. A global `_1` holding a value therefore captured the placeholder, and
  the stage silently failed to canonicalize:

  ```js
  ce.box(['Assign', '_1', 7]).evaluate();
  ce.parse('[1,2,3] \\rhd \\mathrm{Map}(\\_1, k \\mapsto k^2)').evaluate();
  // was: Map([1,2,3], (k) |-> k^2)   now: [1,4,9]
  ```

  The placeholders (`_`, `_1`…`_9`) mentioned by the stage are now bound to
  fresh, valueless locals for the duration of that canonicalization. A genuine
  free variable in the stage still resolves — and auto-declares — in the
  caller's scope, unchanged.

- **The empty list is now a member of every list type.** `[]` typed
  `list<nothing>`, which made `[] <: list<integer>` **false** — an empty list
  satisfied no list type at all:

  ```js
  ce.parse('[]').type.toString(); // was: "list<nothing>"   now: "list<never>"
  ce.parse('[]').type.matches('list<integer>'); // was: false        now: true
  ```

  The cause was in the type lattice rather than in lists: `widen()` (a join)
  returned `nothing` for an empty input, where the join of no types is the
  _bottom_ type. `nothing` is the unit type of the value `Nothing`, not the
  bottom type — `never` is. With `never`, covariance does the rest, since
  `never <: X` gives `list<never> <: list<X>`. `narrow()` had the mirror bug and
  now returns the top type for an empty input.

  Visible effects: the rendered type of an empty collection changes
  (`list<nothing>` → `list<never>`, `list<list<nothing>>` →
  `list<list<never>>`), and an empty list now satisfies a list-typed parameter
  of any element type. Note that `couldMatch()` deliberately ignores the empty
  list as a witness — `list<integer>.couldMatch('list<string>')` stays `false`,
  since that question is about element shape.

- **`nothing` and `missing` were reported as disjoint from any union containing
  them** — `nothing` vs `boolean | nothing` claimed disjointness, refuted by the
  value `Nothing`, which inhabits both. The unit-type short-circuit ran before
  the union was examined and compared a type name against a composite type
  object. This surfaced as an unsound `nothing <: !(boolean | nothing)`.

- **A quantifier's variable is no longer captured by a same-named global
  value.** `ForAll`, `Exists`, `NotExists`, `ExistsUnique` and `NotForAll`
  declared a local scope that was created and then stayed empty, so the
  quantified variable was bound wherever the caller had it. With `x` assigned,
  the bound occurrence resolved the assigned value and the proposition was
  discharged from it:

  ```
  x = 5
  \forall x, x > 4      // was: True     now: ForAll(x, x > 4)
  \exists x, x > 4      // was: True     now: Exists(x, x > 4)
  ```

  Quantification over a finite domain (`\forall x \in \{1,2,3\}, x > 0`) is
  unchanged, and the global `x` is untouched in both cases. The quantifiers now
  use the sanctioned binder mechanism (`scoped: limitsIndexSites(0)`).

- **`Integrate`'s integration variable is bound by the integral.** The index of
  a `Limits` operand was bound nowhere: it was left raw on the parse route and
  carried the caller's binding on the `ce.function` route, so the same integral
  written two ways did not compare equal.

  ```js
  const parsed = ce.parse('\\int_0^1 x^2 \\,dx');
  const built = ce.function('Integrate', [
    ce.parse('x^2'),
    ce.function('Limits', [ce.symbol('x'), ce.number(0), ce.number(1)]),
  ]);
  parsed.isSame(built); // was: false    now: true
  ```

  `Integrate` now uses the sanctioned binder mechanism
  (`scoped: indexingSetSites(1)`), so `expr.localScope` reports the integration
  variable(s), and an indefinite integral's open result is re-bound to the
  enclosing scope on the way out.

- **`D`'s differentiation variables are bound by the derivative.** `D` declared
  a local scope that was minted and then never populated, so its variable
  operands were bound wherever the caller had them and the same derivative
  written two ways did not compare equal:

  ```js
  const parsed = ce.parse('\\frac{d}{dx} x^2');
  const built = ce.function('D', [ce.parse('x^2'), ce.symbol('x')]);
  parsed.isSame(built); // was: false    now: true
  ```

  `D` now uses the sanctioned binder mechanism, with a new variadic selector
  (`scoped: operandsFrom(1)`) for an operator whose bound variables are a
  trailing list of arbitrary length. `expr.localScope` reports every
  differentiation variable, and a derivative — an open expression in that
  variable — is re-bound to the enclosing scope on the way out.

  **Visible consequence**: a body handed to `D` already boxed is re-bound to the
  derivative's own scope, so it stops comparing equal to the expression it was
  built from — `ce.box(['D', body, 'x']).op1.isSame(body)` is now `false`, as it
  has been for a `Sum`'s index since that operator was migrated. Code that lifts
  a differentiand back OUT of a `D` node into the ambient scope has to re-bind
  it; `expr.explain('D')`, which presents the differentiand as a free-standing
  expression, now does.

- **A `Function` literal's parameter operand denotes the literal's own
  parameter.** The body `Block`'s binding was already the authority for
  occurrences in the body, but the parameter operand itself was left raw on the
  parse and `ce.box` routes and carried the CALLER's binding on the
  `ce.function` route — the same route disagreement `Series` and `Integrate`
  were migrated to fix. A canonical literal now has exactly one binding per
  parameter, referenced from both the parameter operand and the body.

- **A bound variable named after a library constant (`Pi`, `e`, `i`, ...) is now
  bound like any other.** A binder's variable was resolved by NAME, and a name
  owned by a constant short-circuits to the interned constant before the scope
  chain is consulted — so the binder declared a binding that nothing referenced.
  With an already-canonical body, the parameter was silently lost:

  ```js
  const f = ce.function('Function', [ce.parse('\\pi + 1'), ce.symbol('Pi')]);
  ce.box(['Apply', f, 10]).evaluate(); // was: 1 + pi    now: 11
  ```

  The parse and `ce.box` routes gave `11` throughout, so this was also a route
  disagreement. Bound variables are now built from the binder scope's own
  binding, which fixes the binding identity for every binder that accepts a bare
  symbol — including `D(Pi^2, Pi)`, whose value was already right while the
  binding underneath was the constant's.

- **`Interval` now survives a LaTeX round-trip.** A closed interval serialized
  as `\lbrack a, b\rbrack`, which the parser reads as a two-element `List`, and
  a fully-open one as `\lparen a, b\rparen`, read back as a parenthesized
  `Sequence`. The substitution was silent and could be destructive: under
  `RandomChoice` — the documented migration target for the removed
  `RandomList(n)` — `RandomChoice(Interval(0, 1), n)` came back as
  `RandomChoice(List(0, 1), n)`, demoting a uniform real draw to a Bernoulli
  pick of the two _values_ 0 and 1, with nothing downstream able to detect it.

  Serialization now depends on whether the position disambiguates the interval:

  - In a **set position** of a set operator — the right side of `\in`/`\notin`,
    either side of `\cup`, `\cap`, `\setminus`, `\subset`, `\subseteq`,
    `\supset`, `\supseteq` — the conventional bracket notation is kept
    (`x\in\lbrack0, 1\rbrack`), because the operator forces the set reading when
    it is parsed back. This is unchanged, and stays the common display case.
  - **Anywhere else** the serialization has to stand on its own: half-open
    intervals use the unambiguous American spellings (`\lbrack a, b\rparen`,
    `\lparen a, b\rbrack`), an open interval uses ISO reversed brackets
    (`\rbrack a, b\lbrack`), and a **closed** interval uses the function form
    `\mathrm{Interval}(a, b)` — `[a, b]` is also how a two-element list is
    written, so no bracket spelling is available for it.

  Consumers storing a uniform draw as LaTeX can now use the closed
  `RandomChoice(Interval(0, 1), n)` from the migration table directly. The
  half-open `Interval(0, Open(1))` remains the more precise spelling for what
  `RandomList(n)` meant (upper-exclusive) and also round-trips.

- **`ListFrom` now compiles.** It had no compile handler, so the one eager
  materializer that works over an arbitrary collection body could not be used in
  compiled code. That matters under `WithRandomSeed`: a frame around a _lazy_
  comprehension does not make it replayable, because the view materializes after
  the frame has exited and the draws escape it. `ListFrom` inside the frame
  makes it eager, and the compiled form now agrees with the interpreter
  draw-for-draw:

  ```
  WithRandomSeed(12345, ListFrom([Random() for k=[1...6]]))   // replays, compiles
  ```

  On the JavaScript and Python targets the splice is decided at runtime, per
  operand, so an operand typing as `unknown` — a free symbol in a compiled body
  — works. The GPU targets have no runtime splice (their lists are fixed-size
  `vecN`/array literals), so an all-scalar `ListFrom` compiles exactly like the
  equivalent `List` and anything with a provably collection operand fails
  closed. Note that `Repeat(Random(), n)` is eager and round-trips but draws
  **once**, yielding n copies of a single value — it is not a uniform batch.

- **A cycle between symbol values no longer overflows the stack.** A pair of
  bindings such as `a := b` with `b := a` is individually well-formed — each
  value mentions no symbol of its own name — so the self-reference guard never
  fired and any query that resolves a symbol's value and delegates to it
  recursed until the stack blew. This reached far more than the reported
  `isFiniteCollection`: `count`, `at`, `each`, `N()`, `isEqual`, `isSame`, and
  the scalar predicates (`sgn`, `isFinite`, `isNaN`, `re`/`im`) all crashed on a
  cyclic binding. Such a query now **fails closed** — `undefined`/`false`, never
  a throw. An enumeration that traverses a cycle yields the elements it gathered
  before closing it, matching what a direct self-reference (`d := Append(d, 1)`
  → `[1]`) has always produced.

- **Comparison, ordering and rationalization no longer numericize an argument
  they are about to reject.** `isEqual`/`Equal`, `Sort`, `assume`,
  `ApproxEqual`, `Rationalize` and the `.N()` trig path each called `.N()` on an
  operand and then discarded the result when it turned out not to be a number
  literal. An operand with unknowns can never become one, and over nested
  applications of a user function that discarded walk is exponential in the
  nesting depth: at depth 12, `isEqual` against such a chain took ~1.8 s and
  `Sort` of five of them far longer; both are now milliseconds. Arguments that
  _can_ numericize — including partially numericizable symbolic ones such as
  `sin(2) + x` — are unaffected. (Same class as the elementwise-`Sin` fix below;
  that one was the exact path, these are the `.N()` path.)

- **`Round`/`Floor`/`Ceil`/`Truncate` of a symbolic term keeps its integer
  type.** `Round(4Q)` typed `number` while both the less informative `Round(Q)`
  and the fully known `Round(4.7)` typed `finite_integer`: an operand typed
  `finite_number` reports `isReal === false`, which means "not provably real",
  and the handler read it as "provably complex". Non-realness is now proven from
  a number literal or from a type that excludes the reals, so an operand of
  unknown realness keeps the generic-point convention. A wrap asserting
  integrality (`RandomChoice(Interval(0,1), Round(4N))` with `N` unbound) now
  type-checks.

- **`sin`/`cos`/`tan`… of a deeply nested symbolic argument no longer blows
  up.** The constructible-value lookup called `.N()` on every argument,
  including one with free symbols that can never numericize; over nested
  applications of a user function that wasted traversal re-walked shared
  sub-chains and grew exponentially with the nesting depth. Evaluating
  `sin(πR/8)` over a 17-element list whose element _k_ is a user function
  applied _k_ times took 44 s and now takes ~0.1 s. Arguments that can
  numericize — including a symbol with an assigned value — reduce exactly as
  before.

- **A function literal built around an already-canonical body now binds its
  named parameters.** Canonicalizing an expression that is already canonical is
  a no-op, so a body constructed _before_ the literal existed kept the bindings
  it was built with — and its parameter occurrences went on denoting the
  enclosing scope's variable of the same name instead of the literal's own
  parameter. The repair previously covered only the anonymous placeholders (`_`,
  `_1`, …) produced by the pipe/shorthand desugaring; it now covers named
  parameters as well, so anything keyed on a symbol's binding — the
  post-application substitution of a partially-symbolic result, symbol equality
  — sees the parameter for what it is:

  ```ts
  const body = ce.box(['Add', 'y', 1]); // canonical: `y` is the caller's
  const f = ce.function('Function', [body, ce.symbol('y', { canonical: false })]);
  // the body's `y` is now this literal's parameter, not the caller's `y`
  ```

  Values are unchanged; what changes is which variable an occurrence refers to.

- **An antiderivative is expressed in the caller's symbols.** `Integrate` binds
  its integration variable, and an integrand's free coefficients are declared
  alongside it — but the antiderivative machinery (and the Rubi rule driver
  installed by `loadIntegrationRules`) works on the bare integrand and creates
  its own occurrences of those names in the caller's scope. The two sets of
  occurrences denoted different variables, so the result could compare unequal
  to the same expression written by hand. The integrand is now re-bound as it is
  lifted, matching how a Jacobian's body is lifted from its literal.

### New Features

- **An operator definition can now declare its bound variables.** The `scoped`
  flag accepts a **binding-site selector** in addition to `true`:

  ```ts
  import { operandSites } from '@cortex-js/compute-engine';

  ce.declare('MyBinder', {
    lazy: true,
    scoped: operandSites(1), // operand 1 is my bound variable
    signature: '(expression, symbol) -> number',
  });
  ```

  A selector implies a scope, so `scoped` remains the complete inventory of
  scope-creating operators. When one is given, the engine declares each site's
  symbol in the operator's own scope _before_ the `canonical` handler runs, and
  binds every occurrence of those names to that scope afterwards — so the
  `parse`, `ce.box()` and `ce.function()` routes agree about which binding a
  bound variable denotes, whichever route built the expression. The prebuilt
  selectors (`operandSites`, `indexingSetSites`, `limitsIndexSites`,
  `lambdaParamSites`) and the `BindingSite`/`BindingSiteSelector` types are
  exported from `@cortex-js/compute-engine`. `Sum`, `Product`, `Loop`,
  `Comprehension`, `Series` and `NDSolveFunction` now use it in place of six
  hand-rolled conventions.

  An indexing-set selector marks its sites `clauseLocal`: later clauses see
  earlier bindings, but an _earlier_ clause's collection resolves a name a later
  clause binds in the enclosing scope
  (`Comprehension(…, Element(i, [j, j+1]), Element(j, […]))` draws `i` from the
  ambient `j`).

- **`BoxedType.couldMatch()`** — "could a value of this type be a `target`?",
  the predicate for classifying a value by shape. `matches()` answers the other
  question — "is _every_ value of this type a `target`" — so it reports `false`
  for a union whose members include exactly the shape being asked about, which
  is the steady state for a variable declared with more than one admissible
  shape:

  ```ts
  const t = ce.type('tuple<number, number> | list<tuple<number, number>>');
  t.matches('list<tuple<number, number>>'); // false
  t.couldMatch('list<tuple<number, number>>'); // true
  ```

  Unions are distributed at every depth, so a union nested inside a parameter is
  handled too: `list<integer | tuple<number, number>>` could be a
  `list<tuple<number, number>>` — witness `[(1,2)]`.

  The relation is symmetric and decisive for the composite shapes it models: a
  `tuple<number, number>` could not be a `list<tuple<number, number>>`,
  `list<integer>` could not be a `list<string>`, and a `set<T>` could not be a
  `list<T>`. List dimensions and tuple arity and element names are compared.
  Shapes it does not model fall back to assignability in either direction, so
  the answer is never narrower than `matches()` — with one deliberate exception:
  `never` is uninhabited, so nothing could be a `never`. `unknown` could be
  anything; consumers that treat an inconclusive type as "no" should check
  `isUnknown` themselves.

- **`BoxedType.unionMembers`** — the members of a union type, each boxed, or
  `[this]` for any other type. Lets a consumer reason arm-by-arm without reading
  the raw `Type` AST. It does not reach a union nested inside a parameter;
  `couldMatch()` covers that case directly.

- **`BoxedType.isDisjointFrom()`** — a type-overlap predicate for consumers that
  classify an expression by comparing its type against a set of candidates.
  `matches()` answers subtyping (`this <: other`), so two types that share
  values without either containing the other look unrelated in both directions:

  ```ts
  const a = ce.type('integer | string');
  const b = ce.type('integer | boolean');
  a.matches(b); // false
  b.matches(a); // false — yet they share `integer`
  a.isDisjointFrom(b); // false: they may overlap
  ```

  The predicate is conservative in the safe direction: when disjointness cannot
  be established the answer is `false` ("may overlap"), never a false claim of
  disjointness. `unknown` — the type of an undeclared symbol — therefore
  overlaps everything. It accepts a `Type`, a type string, or a `BoxedType`, and
  throws on a string that is not a valid type.

### Improvements

- Disjointness now distributes over unions, so `integer | string` is recognized
  as disjoint from `boolean` instead of falling through to "may overlap". This
  also makes a union a subtype of a negation when no member meets the negated
  type (`integer | boolean <: !string`).

- Disjointness is now decided by comparing the primitive _categories_ of the two
  types, so composite types are separated from each other and from primitives
  instead of falling through to "may overlap": a `list<integer>` is not a
  `string`, a `tuple<number, number>` is not a `list<tuple<number, number>>`, a
  `set` is not a `list`, and a `record` is not a `dictionary`. Broad categories
  still contain the narrow ones, so `integer` and `value`, or `list<integer>`
  and `collection`, correctly report "may overlap". This generalizes and
  replaces the narrower numeric-vs-non-numeric rule.

  Two same-category composites whose parameters cannot coincide (`list<integer>`
  vs `list<string>`) are deliberately _not_ claimed disjoint: `list<never>` is a
  subtype of both, so the claim would rest on how the empty list is typed rather
  than on the type lattice. `couldMatch()` answers that question decisively.

## 0.95.0 _2026-07-25_

### Breaking Changes

- **The random family is redesigned around block-scoped seeding.** Seeding moves
  out of argument lists and engine state entirely: there is no seed argument
  anywhere in the family, and no ambient seed to set. A
  `WithRandomSeed(seed, body)` frame makes every draw inside it — including
  draws in user-function calls (dynamic scoping) and in compiled code —
  deterministic and replayable, while draws outside any frame are live.

  ```js
  ce.box(['WithRandomSeed', 42, ['List', ['Random'], ['Random']]]).evaluate();
  // → two DIFFERENT values, and the same two values on every re-evaluation
  ```

  The _n_-th draw of a frame is `hash(seed, n)` — PCG3D, a pure function of the
  seed and the draw index, computed identically by the interpreter, the
  JavaScript target, and (as f32) the GPU targets. Draws are IEEE float64
  regardless of the engine's precision mode, and the seed→stream mapping is a
  **cross-version contract** pinned by published test vectors. Frames nest
  (innermost wins) with independent per-frame counters, so one document cell's
  frame cannot perturb another's. See
  [Random Numbers](/compute-engine/reference/arithmetic/#random-numbers) (and
  `docs/RANDOMNESS-MODEL.md` in the repository) for the full contract, including
  the draw-consumption table and the rule that **only evaluation consumes draw
  indices** (an untaken branch, an unmaterialized lazy view, or a
  canonicalized-away wrapper consumes none).

  The surface changes:

  - **`Random` is domain-only.** `Random()` draws a real in [0, 1);
    `Random(Interval(a, b))` a real in [a, b); `Random(Range(…))` an element of
    the (normalized, inclusive) range; `Random(xs)` an element of a finite
    collection. The old `Random(seed)`/`Random(m, n)` forms — where the first
    argument meant a _seed_ or a _bound_ depending on its numeric type — are
    rejected by the signature.
  - **`Sample` → `RandomSample`, `Shuffle` → `RandomShuffle`** — renamed,
    seedless. `RandomSample`'s domain must be an indexed collection (a `Set` or
    an `Interval` is now invalid), and `k < 0` or `k > n` is now an
    `out-of-range` error rather than `undefined`. Without-replacement remains
    over _positions_, not values: sampling a multiset can repeat a value.
  - **`RandomInteger`, `RandomList`, and `RandomSeed` are removed**, along with
    the **`ce.randomSeed`** property. For one release, evaluating a removed head
    throws an `operator-removed` error naming its replacement, and
    `ce.randomSeed` is a throwing accessor — nothing fails silently.

  Migration: `Random(seed)` → `WithRandomSeed(seed, Random())`;
  `Random(n)`/`Random(m, n)` → `Random(Range(0, n-1))`/`Random(Range(m, n-1))`
  (for the non-degenerate ranges — the old bounds were upper-exclusive);
  `RandomInteger(a, b)` → `Random(Range(a, b))`; `RandomList(n[, seed])` →
  `RandomChoice(Interval(0, 1), n)`, framed if seeded;
  `Shuffle(xs[, seed])`/`Sample(xs, k[, seed])` →
  `RandomShuffle(xs)`/`RandomSample(xs, k)`, framed if seeded;
  `RandomSeed(s)`/`ce.randomSeed = s` → `WithRandomSeed(s, …)` around the work.

### New Features

- **`RandomChoice(domain, k)`** — `k` independent draws **with** replacement,
  the twin of `RandomSample` (without replacement). The domain may be a bounded
  `Interval`, a `Range`, or any finite collection, and is never materialized:
  `RandomChoice(Range(1, 10^9), 5)` is O(k). The count is typed `number` (a
  computed count need not be pre-rounded; it is rounded on evaluation), and `k`
  may exceed the domain size — that is what replacement means.

- **Random draws now compile — including inside auto-compiled `Map` bodies and
  in shaders.** Every compiled draw goes through the same engine primitive as
  the interpreter, deciding framed-vs-unframed at **call time**, so a function
  compiled outside any frame is deterministic when later called inside one,
  bit-identical to the interpreter. On the GPU, `WithRandomSeed` frames compile
  lexically (per-invocation counters): per-pixel seeding is
  `WithRandomSeed(perPixelSeed, Random())`. Unsupported forms fail closed at
  compile time rather than drawing silently.

- **Overload sets: an intersection of function signatures is now resolved at the
  call site.** A function that can be called in several different ways is
  declared with `&`, and the arm whose parameters accept the arguments is
  selected. When several arms accept them the **most specific** one wins;
  incomparable arms are tried in declaration order.

  ```js
  ce.declare('Draw', {
    signature: '((set<real>) -> real) & ((collection) -> any)',
    evaluate: (ops) => {
      /* dispatch on ops at run time */
    },
  });

  ce.box(['Draw', ['Interval', 0, 1]]).type; // → "real"  (set<real> is the more specific arm)
  ce.box(['Draw', ['List', 1, 2, 3]]).type; // → "any"
  ce.box(['Draw', 5]).isValid; // → false
  ```

  Previously such a signature parsed but was inert: applications were never
  arity- or type-checked and always typed `unknown`. Argument validation, result
  typing and type inference now all understand overload sets, on the operator
  definition and the symbol declaration routes alike.

  When an argument's type is not yet known, it is inferred as the **union** of
  the parameters the surviving arms accept at that position — the constraint the
  call actually carries. Above, an unknown `x` in `Draw(x)` is inferred
  `collection`, not `set<real>`: assuming the more specific arm would wrongly
  reject a later list.

  Note that `->` binds looser than `&`, so each arm must be parenthesized:
  `(number) -> real & string` is a _single_ signature returning `real & string`.
  See [Overload Sets](/compute-engine/guides/types/#overload-sets).

### Issues Resolved

- **`.N()` evaluated the operands of `Add` and `Multiply` twice.** The numeric
  path evaluated every operand exactly, discarded the results, and re-evaluated
  them numerically — observably wrong for impure operands (a framed `Random()`
  consumed two draw indices under `N()` and one under `evaluate()`) and a 2×
  evaluation tax otherwise. Impure operands now evaluate exactly once; pure
  operands keep the substitute-once-guarded path.

- **`Sample` materialized its whole source to draw a few elements.**
  `Sample(Range(1, 1000000), 3)` allocated a million boxed numbers and ran a
  full Fisher-Yates (~300 ms) to return three; large sources were an uncatchable
  OOM. `RandomSample` now runs a sparse Fisher-Yates over the index space — O(k)
  time and memory — and `RandomShuffle` refuses sources past the element cap
  instead of exhausting the heap.

- **Compiled random draws bypassed the engine's stream.** A compiled `Random()`
  emitted a bare `Math.random()`, so a compiled `Map` silently stopped being
  reproducible under a seed. Compiled and interpreted draws now share one code
  path (and the auto-compile gate that excluded impure bodies from `Map`
  compilation is gone — per-sample-point draws stay in the hot path).

- **`compileShader` emitted shaders that referenced undefined helpers.** A
  shader whose body used any `_gpu_*` helper (`Gamma`, the fractal helpers, and
  now the random draw) compiled in CE but failed at GPU shader-compile time,
  because the helper preamble was never spliced into the emitted source.
  `compileShader` now derives the preamble from the compiled body and inserts it
  ahead of the entry point, on both GLSL and WGSL.

- **A function signature nested in a union or an intersection lost its
  parentheses when serialized**, and re-parsed as a structurally different type
  with an identical string. `((number) -> real) & ((string) -> boolean)` came
  back as the single signature `(number) -> (real & ((string) -> boolean))`.
  Type serialization now parenthesizes a signature wherever it is a member of a
  union, intersection or negation.

- **An intersection was not a subtype of its own members** when the members were
  composite types. `((number) -> real) & ((string) -> boolean)` did not match
  `(number) -> real`. `A & B` is now a subtype of `R` whenever _any_ arm is,
  matching the behavior that already applied to primitive types.

- **`ce.assume()` threw a `TypeError`** when applied to an operator whose
  signature had no single result type.

- **A function literal assigned to a symbol declared with an overload set was
  not arity-checked**, so a two-parameter literal could be stored against
  one-argument arms and every declared call would silently partial-apply.

- **`Shuffle` and `Sample` were treated as pure functions.** Neither declared
  `pure: false`, so `isPure` — and therefore `isConstant` — was `true` for a
  random permutation or sample of a literal collection, and `Sample` was not
  gated out of `Map` auto-compilation. Both are now declared impure.

- **A masked GPU branch could draw.** The `When`/`Which` fall-through NaN
  compiled to `0.0 / 0.0`, whose value is implementation-defined in GLSL: a
  driver may fold it to a finite, renderable value. The `_gpu_nan()` helper now
  returns `intBitsToFloat(0x7FC00000)`, a guaranteed quiet-NaN bit pattern.

- **Desktop GLSL 4.x shaders were emitted with GLSL ES 1.00 syntax.**
  `compileShader` chose `in`/`out` over `attribute`/`varying` by testing whether
  the version string began with `3`, so `450 core` fell through to the ES 1.00
  keywords. The version is now parsed rather than prefix-matched, and a version
  below 300 is rejected: the emitted code uses ES 3.00 constructs throughout, so
  a lower `#version` header could not have compiled.

### Improvements

- The result type of the bare `function` type is now reported as `unknown`
  rather than `any`. `function` is shorthand for `(any*) -> unknown` and carries
  no information about its result, so an application of an undeclared function
  types `unknown`. This is visible in derived types — `[h(x)]` for an undeclared
  `h` now types `list<unknown>` instead of `list<any>`.

- The result type of a union or intersection of function signatures is now the
  union of the arms' result types, instead of being undetermined.

### Benchmarks

#### Numeric performance (200-digit precision)

Median time per call, in **microseconds — lower is better**. `—` means the tool
returned no usable result at that precision.

| Expression         | CE (current) | CE 0.92.1 | SymPy | math.js | Mathematica |
| ------------------ | -----------: | --------: | ----: | ------: | ----------: |
| $\pi^2$            |          6.1 |       6.5 |   175 |     101 |         3.8 |
| $\sin 1$           |           19 |        20 |   219 |     483 |         5.2 |
| $\cos 1$           |           20 |        20 |   218 |     567 |         7.0 |
| $\ln 2$            |           14 |        14 |   340 |   4,558 |         3.8 |
| $e^{\pi}$          |           12 |        12 |   226 |   4,770 |         4.5 |
| $\zeta(3)$         |        1,519 |     1,542 |   268 |       — |          49 |
| $\Gamma(\tfrac13)$ |          831 |       827 |   348 |       — |         212 |
| $\psi(\tfrac13)$   |          717 |       713 | 2,778 |       — |         169 |

#### Symbolic capability & performance

Each cell is **how many times faster than Mathematica** that engine is on the
case (`Mathematica ÷ engine`, so **higher is better**; Mathematica itself is
`1×`). `—` means the engine can't do the case; `✓` means it solves a case
Mathematica can't. Compare the **CE (current)** and **CE 0.92.1** columns to see
what is _new this release_ (a `—` under `0.92.1` next to a number under the
current build). The **CE + R/F** column is the current build with the opt-in
Rubi integrator + Fungrim identities loaded (`loadIntegrationRules` /
`loadIdentities`), on the same minified bundle.

| Operation                              | CE (current) | CE + R/F | CE 0.92.1 |  SymPy  | math.js | Mathematica |
| -------------------------------------- | :----------: | :------: | :-------: | :-----: | :-----: | :---------: |
| **Antiderivatives**                    |              |          |           |         |         |             |
| $\int\frac{1}{\sqrt x}\,dx$            |     6.2×     |   2.9×   |   5.6×    |  0.5×   |    —    |     1×      |
| $\int\frac{x}{\sqrt{1-x^2}}\,dx$       |     11×      |   1.6×   |   8.8×    |  0.1×   |    —    |     1×      |
| $\int\frac{1}{x^3+1}\,dx$              |     6.1×     |   0.9×   |   4.7×    |  0.3×   |    —    |     1×      |
| $\int\frac{\sqrt x}{1+x}\,dx$          |      —       |   2.0×   |     —     |  0.1×   |    —    |     1×      |
| $\int\frac{x}{(1+x)^{1/3}}\,dx$        |      —       |   1.2×   |     —     |  0.01×  |    —    |     1×      |
| $\int\frac{x^2}{(1+x)^{1/3}}\,dx$      |      —       |   1.2×   |     —     | 0.007×  |    —    |     1×      |
| **Derivatives**                        |              |          |           |         |         |             |
| $\tfrac{d}{dx}\sqrt{1-x^2}$            |    0.04×     |  0.04×   |   0.04×   | 0.0009× | 0.003×  |     1×      |
| **Simplification**                     |              |          |           |         |         |             |
| $\sqrt{3+2\sqrt2}$                     |     41×      |   29×    |    36×    |    —    |    —    |     1×      |
| $\sqrt6\,x+\sqrt2\,x$                  |     83×      |   45×    |    58×    |  3.2×   |   15×   |     1×      |
| **Evaluation**                         |              |          |           |         |         |             |
| $\lim_{x\to0}\tfrac{\sin x}{x}$        |     44×      |   18×    |    49×    |  3.1×   |    —    |     1×      |
| $\lim_{x\to\infty}(1+\tfrac1x)^x$      |     8.5×     |   5.2×   |   8.5×    |  2.1×   |    —    |     1×      |
| $\int_1^2\tfrac1x\,dx$                 |    6294×     |  6380×   |   6457×   |   92×   |    —    |     1×      |
| $\int_{-\infty}^{\infty} e^{-x^2}\,dx$ |     388×     |   143×   |   389×    |  2.4×   |    —    |     1×      |
| **Solving**                            |              |          |           |         |         |             |
| $x^4+x^2-1=0$                          |     0.3×     |   0.3×   |   0.3×    |  0.06×  |    —    |     1×      |
| $x^3-x-1=0$                            |     1.7×     |   1.8×   |   1.5×    |  0.04×  |    —    |     1×      |

Across the cases both solve, Compute Engine is a **median 6.2× faster than
Mathematica** (up to 6294×) — in the browser, not a proprietary kernel.

<sub>Measured 2026-07-26 · Compute Engine `0.95.0` @ `6c188402` (current build)
· published `0.92.1` · SymPy `1.14.0` · math.js `15.2.0` · Mathematica
`14.3.0 for Mac OS X ARM` · Node `v22.13.1`. Correctness is verified numerically
against an independent `mpmath` reference, never another tool. Reproduce with
`npm run build production && ./venv/bin/python3 benchmarks/gen_cases.py && node benchmarks/report.mjs && node benchmarks/report_changelog.mjs`.</sub>

## 0.94.0 _2026-07-24_

### Breaking Changes

- **`Nothing` now erases inside collections, as it already did inside operator
  argument lists.** `Nothing` is the ERASURE marker — an empty-sequence splice —
  so a `Nothing` element is spliced out of a `List`, `Set` or `Tuple` literal
  instead of being retained. Length, arity, type and indexing all follow:

  ```js
  ce.box(['List', 12, 'Nothing', 34]); // → [12, 34]   (length 2, was length 3)
  ce.box(['Set', 1, 'Nothing', 3]); // → Set(1, 3)
  ce.box(['Tuple', 1, 'Nothing', 3]); // → (1, 3)
  ce.parse('(a,,b)'); // → (a, b)   (an empty slot is `Nothing`)
  ```

  A key–value pair tuple is a NON-erasing position, so a dictionary/record entry
  whose value is `Nothing` is dropped as a whole entry, but a caller that needs
  a fixed-arity positional pair whose slot may hold an absent value must build
  it with `ce._fn('Tuple', …)` and use `Missing` (below) for the hole. This also
  applies to lazy iteration: an element that _evaluates_ to `Nothing` is dropped
  (`Map(xs, _ ↦ Nothing)` is the empty collection — the `mapMaybe` idiom).

- **New `Missing` marker and `missing` type for an absent-but-positioned
  value.** `Missing` is the complement of `Nothing`: "a position exists, its
  value is absent" (Julia `missing`, R `NA`). It is never erased —
  `[1, Missing, 3]` is a 3-element `list<integer | missing>` — and `missing` is
  a primitive unit type (a subtype only of itself and `any`, mirroring
  `nothing`), reachable as `ce.Missing`.

  Absence is **domain-normalized at value construction**: absence flowing
  through an operator into a NUMERIC result cell becomes `NaN` (the numeric
  absent element), while a non-numeric result cell keeps `Missing`. So a numeric
  operator ABSORBS a `Missing` operand into `NaN` rather than carrying a
  `missing` arm:

  ```js
  ce.box(['Add', 'Missing', 1]).evaluate(); // → NaN   (Add(Missing, 1) : number)
  ce.box(['Sin', 'Missing']).evaluate(); // → NaN
  ce.box(['Sin', ['List', 1, 'Missing', 3]]).evaluate(); // → [Sin(1), NaN, Sin(3)]
  ```

- **Out-of-band access preserves position instead of yielding `Nothing` or
  dropping the entry.** An out-of-range index, or a dictionary key that is not
  present, now yields a position-preserving marker chosen by the collection's
  element domain: `NaN` when the elements are numeric, `Missing` otherwise.

  ```js
  ce.box(['At', ['List', 10, 20, 30], 9]).evaluate(); // → NaN   (numeric)
  ce.box(['At', ['List', 'a', 'b'], 9]).evaluate(); // → Missing (non-numeric)
  ```

  **Gather is now length-preserving** — `At([a, b], [1, 9, 2])` is
  `[a, hole, b]` (length 3), where the baseline dropped the out-of-range entry —
  and a **boolean mask whose length differs from the collection is now an
  error**, where the baseline silently applied the prefix.

- **The 15 data-consuming aggregates return `NaN` on an absent datum or empty
  input.** `Mean`, `Variance`, `PopulationVariance`, `StandardDeviation`,
  `PopulationStandardDeviation`, `Kurtosis`, `Skewness`, `Median`,
  `InterquartileRange`, `Quartiles`, `Max`, `Min`, `Supremum`, `Infimum`, and
  `Mode` — over both call shapes (`Max(1, Missing, 3)` and
  `Max([1, Missing, 3])`) — now evaluate to `NaN` when any datum is absent
  (`Missing` or `NaN`) or the input is empty (`Quartiles` → `(NaN, NaN, NaN)`).
  `Max([])`/`Min([])` are therefore `NaN` (was `∓∞`), and these operators now
  type as `number` rather than `finite_real`, since their result may be `NaN`.

  ```js
  ce.box(['Max', 1, 'Missing', 3]).evaluate(); // → NaN
  ce.box(['Mean', ['List']]).evaluate(); // → NaN
  ```

- **Comparisons follow IEEE 754 for `NaN` and Kleene for the `Missing` symbol,
  across the whole relational family** (`Equal`, `NotEqual`, `Less`,
  `LessEqual`, `Greater`, `GreaterEqual`). This is the Julia model:

  - The **`Missing` symbol is Kleene** — a comparison with a `Missing` operand
    is itself `Missing`: `Equal(x, Missing) = Missing`,
    `NotEqual(Missing, x) = Missing`, `Less(Missing, 1) = Missing`. (An ordering
    with a `Missing` operand previously stayed symbolically unevaluated.)
  - **`NaN` follows IEEE** — `NaN` is unequal to everything (including itself)
    and unordered: `Equal(NaN, NaN) = False`, `NotEqual(NaN, x) = True`, and
    every ordering with a `NaN` operand is `False`. The `Equal`/`NotEqual`
    results match native float `==`/`!=`; the **ordering comparisons with `NaN`
    previously stayed symbolic** (`NaN < 1` was inert), so they now resolve to
    `False`.

  ```js
  ce.box(['Equal', 'NaN', 'NaN']).evaluate(); // → False    (IEEE)
  ce.box(['Less', 'NaN', 1]).evaluate(); // → False         (IEEE unordered)
  ce.box(['Equal', 2, 'Missing']).evaluate(); // → Missing  (Kleene)
  ce.box(['Less', 'Missing', 1]).evaluate(); // → Missing   (Kleene)
  ce.box(['Equal', 2, 2]).evaluate(); // → True             (unchanged)
  ```

  Absence for **discharge** (`IsMissing`, `Coalesce`) and **aggregates** (`Max`,
  `Mean`, …) is unaffected — a `NaN` is still absent there
  (`IsMissing(NaN) = True`, `Coalesce(NaN, d) = d`, `Max(1, NaN, 3) = NaN`).
  Broadcast comparisons apply the rule per cell. Because `NaN` follows IEEE,
  **compiled and interpreted comparisons now agree by construction** on numeric
  operands (plain `==` is the IEEE semantics — no guard is emitted, and
  `NaN == NaN` compiles to `false`). A **numeric-domain** `missing` arm
  (`number | missing`) does not widen a comparison's result type — that slot's
  absence value is `NaN`, so the result is a plain `boolean`, a `Missing` value
  read through such a slot compares as `NaN` (IEEE), and the comparison
  **compiles on float-only targets (GLSL/WGSL)**. A `missing`-arm operand over
  an object domain (e.g. `string | missing`) still types `boolean | missing` and
  lowers via the guarded form (`isAbsent(a) || isAbsent(b) ? null : a == b`) so
  a `Missing` becomes the target null. A scalar `If`/`Which` condition that
  evaluates to **`Missing`** yields a catchable **error expression**
  (`The condition is absent…`) rather than crashing `evaluate()` — absence is a
  runtime data state, so it must be renderable and catchable; discharge with
  `Coalesce`/`IsMissing` to branch on possibly-absent data. (A condition that is
  not boolean at all, e.g. `If(3, …)`, keeps the existing spell-check throw.) A
  `NaN`-comparison condition yields a plain boolean (IEEE) and branches
  normally.

- **Compiled `Max([])` / `Min([])` now return `NaN`, matching the interpreter**
  (previously `-Infinity` / `+Infinity` from the identity-seeded reduce).
  Non-empty folds are unchanged.

### New Features

- **`cortex check` — validate a program without evaluating it.** Parses the
  source (a file, `--eval`, or stdin) and reports diagnostics; exit status is
  `0` when there are no errors. With `--json` it emits a machine-readable
  envelope (`{ ok, diagnostics }` with severities, codes, messages, 0-based
  source offsets, 1-based line/column, and fix-its). The same structured
  diagnostics are available during evaluation with `--diagnostics json`.
- **`cortex doc` — library documentation from the terminal.** `cortex doc Sin`
  shows a definition's kind, signature or type, description, keywords, and (for
  constants) value; a non-name argument searches the library by identifier,
  description, curated keywords, and LaTeX commands
  (`cortex doc greatest common divisor` → `GCD`, …). `--limit <n>` controls the
  number of matches and `--json` emits a structured `{ query, matches }`
  envelope.
- **`Cortex for AI Agents` language card.** A condensed, machine-verified
  reference for LLMs and coding agents writing Cortex (`/cortex/for-agents/`):
  core semantics, an operator-precedence summary, a table of Python/JavaScript
  reflexes that don't transfer, and verified idioms. Every example on the page
  is executed by the documentation test suite, so the card cannot drift from the
  implementation.

- **`cortex mcp` — a Model Context Protocol server for Cortex.** Starts an MCP
  server on stdio (the default) or native Streamable HTTP, giving AI agents
  structured access to the same operations as the CLI: an `evaluate` tool (each
  call runs a complete, self-contained program in a fresh session and returns
  the value as display text, Cortex source and MathJSON, plus diagnostics),
  `check`, `doc`, `parse` and `serialize` tools, and the `Cortex for AI Agents`
  language card as the `cortex://docs/for-agents` resource. Register the stdio
  transport with, e.g.,
  `claude mcp add cortex -- npx -y @cortex-js/compute-engine mcp`, or start the
  URL endpoint with `cortex mcp --transport streamable-http`. The protocol
  implementation is self-contained: the package gains no new dependencies.

- **Spread arguments — `f(...t)` splices a tuple into a call's arguments.** New
  Cortex prefix syntax `...` (call argument lists only) and engine `Spread`
  marker: the elements of a tuple become ordinary positional arguments, so a
  point can be fed to a component function without manual indexing —
  `F(p) = (a(...p), b(...p), c(...p))` instead of `a(p[1], p[2], p[3])`. Several
  spreads splice in order (`g(...p, ...q)`), variadic built-ins accept them
  (`Max(...t)`), and the syntax round-trips through the Cortex serializer. A
  literal tuple splices at canonicalization; a symbolic argument defers —
  argument validation and the operator's canonical handler wait — until
  evaluation resolves the tuple and re-validates the real arguments. Tuples
  only: spreading a `List` or a scalar is an `incompatible-type` error, and an
  unresolved argument leaves the call symbolic.

  Spread also **compiles**, on every target: a literal tuple splices directly,
  and a tuple-typed argument (`p: tuple<number, number>`) is rewritten
  statically to positional accesses (`f(At(p,1), At(p,2))`). An argument whose
  tuple arity is not statically known fails closed — a dynamic JS/Python spread
  would silently mis-bind on an arity mismatch instead of erroring like the
  interpreter.

  ```js
  ce.box(['Add', ['Spread', ['Tuple', 1, 2, 3]]]).evaluate(); // → 6
  ```

- **Destructuring declarations — `let (q, r) = divmod(17, 5)`.** A Cortex
  `let`/`const` may bind the components of a tuple in one statement. Patterns
  are irrefutable in form — bare symbols, `_` to skip a position, nested tuple
  patterns; no literals or pins (use `match` for conditional destructuring) —
  and require an initializer. The value is evaluated once; a shape mismatch
  (wrong length, or not a tuple) yields an `incompatible-type` error value and
  binds nothing. `const (x, y) = …` makes each binding constant. Lowers to the
  `Declare` primitive with a `Tuple` pattern in the name position, which now
  accepts it on all routes.

  In **compiled** code, a destructuring declare with a literal tuple value
  desugars to per-leaf declares (each element bound once, in order); a
  non-literal value or a shape mismatch fails closed so the interpreter takes
  over. (This also fixes a silent divergence: the pattern previously compiled as
  a single `let _ = …`, and every pattern name read as NaN behind
  `success: true`.)

  ```js
  ce.box(['Declare', ['Tuple', 'x', 'y'], d]).evaluate(); // d = {value: (3, 4)}
  ```

- **`IsMissing` and `Coalesce` — absence testing and discharge.** `IsMissing(x)`
  is `True` when `x` is absent (the `Missing` symbol OR a `NaN`, R’s `is.na`);
  `IsNaN` remains a NaN-specific test. `Coalesce(a, b, …)` returns the first
  non-absent operand, evaluated left-to-right with short-circuit; if every
  operand is absent it returns the last one verbatim. Both discharge primitives
  work identically in the interpreter and compiled (JS/Python) — a numeric hole
  (`NaN`) and an object hole (`Missing`/null) are handled uniformly. On a target
  that cannot observe its absent element (a GPU shader, where fast-math may not
  preserve `isnan`), `IsMissing`/`Coalesce` fail closed with a compile error;
  propagation still works natively.

  ```js
  ce.box(['Coalesce', ['At', ['List', 10, 20], 9], 0]).evaluate(); // → 0
  ce.box(['IsMissing', 'NaN']).evaluate(); // → True
  ```

- **`HoldValues(body)` — the value-blind evaluation route from the operator
  surface.** A binder that shields the assigned free symbols of its body: for
  the duration of the evaluation each such symbol becomes a pure symbol (its
  declared type and in-scope assumptions apply, its assigned value does not),
  analogous to Mathematica's `Block[{x}, …]`. Now that the `Simplify` operator
  evaluates its argument first, `HoldValues` is the only value-blind route
  reachable from Cortex (the `.simplify()` method is not exposed there). Shield
  every assigned symbol, or a listed subset with a second `List`/`Set`/`Tuple`
  or single-symbol operand. Constants are never shielded, in-scope assumptions
  survive the shield, and the global values are intact afterwards.

  ```js
  ce.assign('x', 5);
  ce.assign('a', 3);
  ce.box([
    'HoldValues',
    ['Together', ['Add', ['Divide', 1, 'x'], ['Divide', 'a', ['Power', 'x', 2]]]],
  ]).evaluate(); // → (a + x) / x²   (without the wrapper: 8/25)
  ce.box(['HoldValues', ['Add', ['Power', 'x', 2], 'a'], ['List', 'a']]).evaluate();
  // → 25 + a   (x resolves, a shielded)
  ```

### Improvements

- **The Cortex serializer reconstructs `let`/`const` syntax.** A `Declare` node
  now serializes back to its statement form — `let x = 5`, `const c = 6.28`,
  `let x: real`, and destructuring patterns `let (x, y) = p` — instead of the
  generic `Declare(x, {value -> 5})` function spelling, so declarations
  round-trip source → MathJSON → source. Shapes with no `let` spelling (a
  `holdUntil` attribute, a computed name) keep the generic form.

- **Compiled comparisons and connectives look through provably-scalar user
  functions.** A helper declared with an open signature (`(unknown) -> unknown`,
  the shape that keeps list-broadcasting working) no longer makes a scalar
  comparison uncompilable: `q(x) < y` with `q(t) = n·t+1` compiles when every
  argument is scalar and the function's body provably maps scalar parameters to
  a scalar result (arithmetic/transcendental operators, scalar-typed captured
  symbols, and nested user helpers — analyzed recursively, with self-recursion
  declining). A call whose argument may be a collection (`q(L) < y`) still fails
  closed, so the sound half of the 0.93.0 rule is preserved — and unlike a
  `-> number` return annotation, the look-through never mis-compiles the
  broadcast call. Element-wise compiled comparisons over collections remain a
  separate roadmap item.

- **`declare()` accepts `inferredSignature: true`, to vouch that a name is an
  operator without pinning its types.** Declaring a `signature` normally makes
  it a contract, so a wide placeholder such as `(unknown) -> unknown` keeps
  every call typed `unknown` even after a function literal is assigned. That is
  the right default for a fixed API, but not for a name that must be declared
  _before_ its body exists — most often so that `f(x)` parses as an application
  rather than a multiplication:

  ```js
  ce.declare('q', { signature: '(unknown) -> unknown', inferredSignature: true });
  ce.assign('q', ce.parse('t \\mapsto 2t+1'));
  // signature is now `(unknown) -> finite_number`
  // `q(x) < y` types `boolean` and compiles;
  // `q(L) < y` over a list `L` types `list<boolean>` and still fails closed
  ```

  The flag was already honored at run time and is now part of the
  `OperatorDefinition` type, so it no longer needs a cast. A declaration that
  omits `signature` entirely behaves the same way.

- **An unapplied `Derivative(f)` now evaluates to a named-parameter function
  literal.** `Derivative(Sin)` evaluates to `x ↦ cos(x)`
  (`["Function", ["Cos","x"],"x"]`) instead of the hole-form `cos(_)`, which was
  typed `finite_number` — so a stored derivative is now callable:
  `let g = Derivative(f); g(2)` works instead of erroring with
  `incompatible-type`. Results with no closed form stay symbolic, and the
  multivariate mixed-partial form no longer throws
  `Function body must be a scoped Block expression` when applied.
- **The Cortex CLI's `--json` output materializes finite lazy collections.**
  `Range`, `Map`/`Filter` results, and loop-built `Join` chains now serialize as
  their elements (`["List", 1, 2, …]`, up to 10,000) instead of their
  unevaluated recipe; infinite collections keep the structural form.
- **Cortex trap lints and better "did you mean" suggestions.** Three common
  cross-language reflexes that previously failed _silently_ now produce an
  advisory warning (the parse and value are unchanged): `=` inside a call
  argument (`Solve(x^2 = 4, x)` is assignment, not an equation — use `==`), a
  literal index `0` (indexing is 1-based; `xs[0]` is `NaN`), and a `//` comment
  that reads as floor division (`7 // 2` is `7` followed by a comment; use
  `Floor(a / b)`). Calling `print` (or `println`, `printf`, `puts`, `echo`) now
  explains that a program's output is the value of its last statement. The "did
  you mean" matcher gained a curated cross-language tier: `split` →
  `StringSplit`, `push` → `Append`, `ceiling` → `Ceil`. The agent language card
  gained a verified library quick-roster, output-rendering notes (quoted
  booleans, list preview elision), and binder-variable semantics for
  `D`/`Integrate`.
- **`Pipe`/`Apply` reject or defer a non-function right operand more sensibly.**
  `x |> f` (`Pipe`) now returns an `incompatible-type` error when `f` is a
  number, string, or boolean literal (which can never be applied), instead of
  staying silently inert; a symbol or unevaluated `f` still defers (definitions
  may arrive later). `Apply` and `Pipe` no longer throw an uncaught
  `Invalid function literal` when given a string function operand — they decline
  gracefully. Applying a function-valued expression such as `InverseFunction(f)`
  now stays symbolic (`Apply(InverseFunction(f), 2)`) instead of misinterpreting
  it as a lambda body and substituting the argument for `f`.
- **Rubi integrator: Euler-substitution lever for √(quadratic)-nested
  radicals.** The experimental Rubi integrator now closes nested radicals whose
  inner radical is a square root of a quadratic with a positive leading
  coefficient — e.g. `∫ 1/(√(x+√(x²+1))+1) dx` (Bondarenko #9) — via an Euler I
  substitution `t = √a·x + √Q` that rationalizes `√Q` and reduces the integrand
  to a form the existing linear-radical machinery closes. This raises the
  Bondarenko benchmark to CE+R/F 21/35.
- **`simplify()` is value-blind for a symbol's sign and parity.** `.simplify()`
  no longer reads an assigned symbol's _value_ when applying a sign- or
  parity-driven rewrite: with `w := 5`, `|w|.simplify()` now stays `|w|` and
  `√(w²).simplify()` is `|w|` (previously both collapsed to `w`, silently baking
  in `w ≥ 0` and evaluating wrong after a later `w := -3`). Sign and parity are
  taken only from a symbol's declared type and in-scope assumptions — so
  `assume(w > 0)` still licenses `|w| → w` — never from its assigned value.
  `.evaluate()` and `.N()` are unchanged — they still substitute the value.
- **The `Simplify` operator now evaluates its argument before simplifying.**
  `Simplify(expr)` is now evaluate-then-simplify — the operator counterpart of
  the `expr.evaluate().simplify()` recipe — so it computes handler-driven
  results the value-blind `.simplify()` method never touches:
  `Simplify(Max(3, 5))` → `5`, `Simplify(D(x²+ax, x))` → `a + 2x`,
  `Simplify(∫x² dx)` → `x³/3`. Because evaluation substitutes assigned symbol
  values, the change is visible from Cortex: `let x = 5; Simplify(x^2 + x)` now
  gives `30`. The other transformers (`Expand`, `Factor`, `Together`,
  `Distribute`) are unchanged — they keep reduce-not-evaluate.
- **The `.simplify()` method no longer evaluates structural operators.**
  `Determinant`, `Trace`, `Transpose` and `Length` are no longer reduced by the
  `.simplify()` method (an unreleased whitelist added earlier in this cycle):
  the method is rule-driven and value-blind, and running an operator's
  `evaluate` handler is `.evaluate()`'s job. Use `expr.evaluate()` (or the
  `Simplify` operator, which now evaluates) to reduce them — e.g.
  `Determinant([[a,b],[c,d]]).evaluate()` → `a·d − b·c`.

## 0.93.0 _2026-07-23_

### New Features

- **Cortex CLI and interactive REPL.** Installing `@cortex-js/compute-engine`
  now provides a `cortex` executable. It evaluates inline source
  (`cortex -e '1 + 2'`), `.cx`/`.cortex` files, or a program read from standard
  input; with no input argument in a terminal it starts a stateful REPL.

  The REPL retains declarations across inputs, supports multiline programs and
  persistent history, and adds `.load`, `.clear`, `.ast`, and `.time` commands
  alongside Node's standard REPL commands. Non-interactive output can be emitted
  as text, MathJSON (`--json`), or Cortex source (`--cortex`). Diagnostics
  include source locations and are written to standard error. Evaluations have a
  10-second deadline by default; `--time-limit` changes it and `--time-limit 0`
  disables it.

- **`JacobianMatrix(fs, vars)`** — the matrix of partial derivatives ∂fᵢ/∂xⱼ,
  one row per function and one column per variable.

  ```js
  ce.box(['JacobianMatrix', ['List', fs…], ['List', 'x', 'y', 'z']]).evaluate();
  // → [[∂f₁/∂x, ∂f₁/∂y, ∂f₁/∂z], …]
  ```

  - `vars` may be **omitted**: the free variables of `fs` are used, in
    lexicographic order, so the column order is predictable.
  - A single (non-list) `fs` is the **gradient** case and yields a flat vector
    `[∂f/∂x₁, …, ∂f/∂xₙ]`, directly usable as one.
  - `JacobianMatrix(F)` accepts a **bare function** reference: its body is the
    system and its parameters are the differentiation variables, in declared
    order (`JacobianMatrix((z,y,x) ↦ …)` has columns z, y, x — an order
    free-variable inference could not preserve). Explicit variables then rename
    the parameters.
  - A square system composes with `Determinant`, which now also reduces under
    `simplify()` — so `JacobianDeterminant` is one composition away and is not
    provided separately.

  Beyond brevity this removes a real trap: the obvious hand-rolled form
  `Map([x,y,z], v |-> D(f, v))` silently returns **zeros**, because the lambda
  parameter shadows and `D` differentiates with respect to `v`.

  The operands are held — evaluating the variable list would replace a symbol
  carrying a value (`x := 5`) by that value, leaving nothing to differentiate
  against. Whether `fs` is a system or a single function is decided on what the
  operand _denotes_, not its syntax, so a user-defined function returning a list
  and a symbol bound to a list are both treated as systems.

### Improvements

- **The LaTeX/ASCII-math pipeline operators now preserve `Pipe`.** A bare
  pipeline stage such as `x |> f` previously lowered immediately to
  `["Apply", "f", "x"]`; it now produces `["Pipe", "x", "f"]`, matching the
  Cortex parser and retaining `Pipe`'s held-operand semantics. Chained stages
  remain left-associated, prefix stages use `Function(Pipe(_, f), _)`, and a
  programmatic `Pipe` serializes with `\rhd`. Pipeline topic markers remain
  direct substitutions, so `x |> f(\square)` still becomes `f(x)`.

- **`simplify()` reaches a distributed form when it is cheaper.** Rules tagged
  `purpose: 'expand'` are excluded from its scan, because expansion usually
  grows an expression — but that left `simplify()` unable to reach a strictly
  _cheaper_ result. It was not cost-rejecting the better form; it never
  generated the candidate.

  ```js
  const f = ce.parse('(1 + x y)^3 z + y^2 (1 + x y) (4 + 3 x y)');
  f.subs({ z: ce.parse('-\\frac{3y}{x} + \\frac{2}{x^2}') }).simplify();
  // Before → unchanged (cost 76)      Now → y^2 + 3y/x + 2/x^2 (cost 27)
  ```

  `simplify()` now tries expansion once, at the fixpoint, and keeps the result
  only when the cost function says it is strictly cheaper. That cannot cycle
  (the inner call has the trial disabled) and cannot blow up (the cost gate is
  the acceptance test), so a factored form survives: `(x+1)^5`, `(a+b)(c+d)` and
  `x(y+z)` are all left alone. A structural pre-check keeps the trial off
  expressions with no product or power for expansion to act on.

- **`simplify()` now evaluates structural operators.** It is rule-driven and ran
  no operator `evaluate` handler at all, so an operator whose result comes from
  a handler rather than a rule was handed straight back.

  ```js
  ce.parse('\\det\\begin{bmatrix} a & b \\\\ c & d \\end{bmatrix}').simplify();
  // Before → Determinant(Matrix([[a,b],[c,d]]))    Now → a * d - b * c
  ```

  The members are a closed list — `Determinant`, `Trace`, `Transpose`, `Length`
  — chosen by a membership rule: the handler reduces its operands to a closed
  form determined by their _structure_ (a matrix to a scalar, a collection to a
  measure) rather than rewriting the expression, and the head carries no
  simplification rule of its own. `Max`/`Min` deliberately fail that rule and
  are not members: they reduce their operands' _values_, which is evaluation's
  job.

  **`simplify()` remains value-blind.** With `a := 5`, `(a + 2).simplify()` is
  still `a + 2`. A structural head whose operands mention a symbol carrying a
  value is left alone rather than substituting it.

  Operators outside the list are unchanged, so the ordering rule still stands:
  `evaluate()` then `simplify()`; `simplify()` alone is not a superset.

- **The `Simplify` operator resolves symbols bound to a value.** An operator
  normally evaluates its arguments; the transformers are `lazy` only to keep the
  operand's _structure_ from being rewritten early, not to keep its values
  symbolic. This applies to `Expand`, `ExpandAll`, `Factor`, `Together` and
  `Distribute` as well.

  ```js
  ce.assign('v', ce.parse('\\frac{x^2-1}{x-1}'));
  ce.box(['Simplify', 'v']).evaluate();
  // Before → v      Now → x + 1
  ```

  This is the operator, not the method: `ce.symbol('v').simplify()` is still
  `v`, because `.simplify()` is value-blind.

- **`Together` reduces its result to lowest terms.** It folds the terms over the
  _product_ of the denominators, which is correct but not reduced; the result is
  now divided through by the numerator/denominator GCD. This also yields the
  least common denominator, since product / gcd **is** the LCD.

  ```js
  ce.box(['Together', ce.parse('-\\frac{3y}{x}+\\frac{2}{x^2}')]).evaluate();
  // Before → (-3y * x^2 + 2x) / (x * x^2)    Now → (-3x * y + 2) / x^2
  ```

  The multivariate case needs Brown's algorithm: the univariate Euclidean GCD
  treats `y` as an opaque coefficient and reports `1` for that pair.
  `cancelCommonFactors` now falls back to the multivariate GCD when the
  univariate one comes back trivial and more than one unknown is present. The
  order matters — the cheap path runs first, so the common case is unaffected.

  The same-denominator simplification rule still uses the unreduced fold: it
  runs inside the `simplify()` fixpoint, where output stability and cost matter
  more than presentation.

- **`CircularIntegrate` (`\oint`) gained an operator definition.** It previously
  had only a parser entry, so it typed as `any` and its limits stayed a raw
  `Tuple`. It now types as `number` and canonicalizes its limits into `Limits`
  expressions, matching `Integrate` so a limits-consuming caller sees a uniform
  shape. `CircularIntegrate` remains inert — there is still no contour-
  integration evaluation.

  ```js
  ce.parse('\\oint_C f').json;
  // Before → ["CircularIntegrate", "f", ["Tuple", "Nothing", "C", "Nothing"]]  (type: any)
  // Now    → ["CircularIntegrate", "f", ["Limits", "Nothing", "C", "Nothing"]] (type: number)
  ```

- **`RandomList` now compiles on the JavaScript target.** `RandomList(n)` draws
  fresh values on every call of the compiled function, matching `evaluate()`.
  For draws that stay the same from call to call, use the explicit-seed form
  `RandomList(n, seed)`. A count that is negative, non-finite, or above the
  1,000,000 cap makes the compiled function throw, rather than silently clamping
  or returning `NaN`. (`evaluate()` reports an `out-of-range` error expression
  for the same input.)

- **`declare()` accepts a spread of an existing operator definition**, so you
  can override one handler and keep the rest:

  ```js
  ce.declare('At', { ...ce.lookupDefinition('At').operator, evaluate });
  ```

  The built-in's other handlers (`type`, `signature`, `canonical`, …) are
  preserved. Overriding with a bare `{ signature, evaluate }` instead **drops**
  them — prefer the spread.

- **Compiled `At` supports a collection-valued index.** An index that is a list
  of indices or a boolean mask now works when compiled, matching `evaluate()`:

  ```js
  const p = ['List', 10, 20, 30];

  ce.box(['At', p, ['List', 3, 1]]);                    // → [30, 10]
  ce.box(['At', p, ['List', 'False', 'True', 'True']]); // → [20, 30]

  ce.assign('p', ce.box(p));
  ce.assign('X', ce.box(['List', 1, 2, 3]));
  ce.parse('p_{X}'); // → [10, 20, 30]
  ```

  Negative indices count from the end and out-of-range entries are dropped, so a
  gather may be shorter than its index list. Previously these returned
  `undefined`, a wrongly-shaped scalar (`p[[2]]` gave `20` rather than `[20]`),
  or threw.

  An `At` with a collection-valued index now has type `list<T>` rather than the
  element type `T`. **If you dispatch on `.type`, expect the new value.** This
  is what lets surrounding operations compose: `At(p, I) + 1` broadcasts
  element-wise, and `Length(At(p, I))` compiles.

### Resolved Issues

- **`assume()` after `assign()` now records the assumption.** The predicate was
  evaluated through the symbol's assigned value before the assumption system saw
  it, so with `w := 5`, `assume(w > 0)` folded to `True`, returned
  `'tautology'`, and recorded **nothing** — the assumption was silently
  discarded on arrival (only the assume-before-assign order worked). Predicates
  mentioning assigned symbols are now recorded _value-blind_, as facts about the
  symbol: `assume(w > 0)` after `w := 5` returns `'ok'` and `|w|` then
  simplifies to `w`. A predicate that contradicts the current value (e.g.
  `assume(w > 0)` with `w := -2`) still returns `'contradiction'` and is
  rejected. `'tautology'` now means tautological relative to types and existing
  assumptions, never relative to an assigned value — re-asserting an equality a
  symbol's value already satisfies returns `'ok'`, not `'tautology'`.

- **`.N()` on a multi-limit `Integrate` no longer drops all but the first
  limit.** The numeric-approximation branch read only the first `Limits`
  operand, so `Integrate(f, Limits(x,0,3), Limits(y,0,2))` numericized as a
  single-variable integral — `∫∫ 1` over `[0,3]×[0,2]` gave `3` (a wrong value,
  not a decline) and a multivariate integrand gave `NaN`. Multiple limits now
  perform iterated adaptive Gauss–Kronrod quadrature (Monte-Carlo fallback per
  level, as for a single limit), following the Mathematica iterator convention
  the symbolic path already used: the FIRST limit is the OUTERMOST integral, so
  an inner bound may reference the outer variables —
  `Integrate(1, Limits(x,0,1), Limits(y,0,x))` (the triangle) numericizes to ½.
  A bound that references an inner integration variable or a foreign free symbol
  declines (the integral stays inert) rather than integrating wrongly. The same
  fix applies to `compile()`: a multi-limit integral that does not close
  symbolically now compiles to nested quadrature calls — dependent bounds
  included — where it previously truncated to the first limit.

- **`Transpose`/`ConjugateTranspose` report the transposed static type.** They
  had no type handler, so an unevaluated `Transpose(m)` typed as the generic
  `value` and was rejected by matrix arithmetic: `Multiply(Transpose(J), J)` for
  `J = JacobianMatrix(…)` — the Gram-matrix idiom JᵀJ — errored with
  `incompatible-type` unless `J` was evaluated first. Both now preserve the
  element type and swap the shape's axes (`matrix<T^(2x3)>` →
  `matrix<T^(3x2)>`).

- **`simplify()` reaches trig identities inside a quotient.** Recursion into
  `Divide` operands is deliberately withheld to preserve factored structure for
  common-factor cancellation, but that also blocked operand-local trig
  reductions: `(r·cosθ)/(r·sin²θ + r·cos²θ)` didn't simplify even though the
  denominator alone reduces to `r`. Trig-bearing operands of a `Divide` now get
  the full recursion (the same carve-out `Add`/`Multiply` already made), so it
  simplifies to `cos(θ)`; factored-polynomial cancellation is unaffected.

- **Value-resolution overreach in the lazy-operand fixes.** The machinery that
  lets `Solve`/`Simplify`/`JacobianMatrix` see through a held operand resolved
  bound symbols too aggressively. Fixed:
  - `resolveBoundSymbols` is now **binder-aware**: it no longer resolves a
    variable bound by a `Function`, `Block`, `Sum`, … to a same-named global
    value (`Simplify(x ↦ x+1)` no longer corrupts the body's bound `x`).
  - A transformer nested in a `Solve` equation could substitute an unknown that
    also carries a value — `Solve(Simplify(x-2)=0, x)` with `x:=5` returned
    `[]`. The unknown is now shielded across transformer reduction.
  - `JacobianMatrix` differentiated a system in which a diff variable had
    already been replaced by its global value (`JacobianMatrix(g,[x,y])` with
    `x:=5`, `g:=[x²y,x+y]` gave a wrong matrix). It now resolves the operand's
    _shape_ without substituting values, and differentiates against a fresh
    symbol when a diff variable carries a value.
  - `simplify()` evaluated a structural head's whole operand tree, running an
    impure descendant — `simplify(Transpose([[Random()]]))` drew a random
    number. It now declines when the expression is impure.

- **`D` no longer evaluates its result at the differentiation variable's
  assigned value.** With `x := 5`, `D(x², x)` is now `2x` (was `10`). Under the
  ratified binding convention (ARCHITECTURE.md, "Bound variables, free symbols,
  and assigned values"), the variable a binder owns is a pure symbol — its
  declared type and assumptions apply, its assigned value never does, INCLUDING
  in the result. A caller who wants the value evaluates the result again or
  substitutes explicitly. Free symbols are unaffected: with `a := 3` too,
  `D(a·x², x)` is `6x`.

- **`Integrate`, `Limit`, and bundled `Solve` shield a value-bound bound
  variable.** The same convention: a same-named global assignment no longer
  leaks into these binders or their results. With `x := 5`, `Integrate(x², x)`
  is `x³/3` (was `125/3`), `∫₀¹ x² dx` is `1/3` (was `0`), and
  `Limit(Simplify(x²), x, 0)` is `0` (was `25`, on both the box and parse
  routes). A bundled `Solve(\{Simplify(9 − w²) = 8, w ∈ -3..3\})` with `w := 9`
  now returns `[1, -1]` (was `[]`): the value-bound bundled unknown is now
  discovered before the shield is computed. A shared `withValueShield` helper
  implements the shield and replaces `JacobianMatrix`'s per-variable rename.

- **`Distribute` fold, `numeratorDenominator`, `Together`, transformers, and
  `toString()` grouping** — as previously listed.

- **Beta-reduction completeness.** A finite self-composition (`g(g(x))` for a
  non-recursive `g`) only inlined one level, so `Solve(g(g(x))=0, x)` returned
  `[]`; the recursion guard is now a total-count budget that still terminates
  genuine recursion. Typed function parameters (`(x: real) ↦ …`) are unwrapped,
  so a typed function inlines like a bare one.

- **`Pipe` (`|>`) declines a non-function right-hand side** (`5 |> 3` stays
  inert) and its handler now delegates to `apply` instead of duplicating its
  named-operator path.

- **`Pipe` (`|>`) holds its operands, so `x |> f` behaves exactly like `f(x)`.**
  It evaluated the topic eagerly, regardless of `f` — which broke a chain whose
  right-hand side is a _lazy_ operator that needs its argument unevaluated. A
  bare function reference was the sharp case: `F |> JacobianMatrix` passed an
  evaluated `F`, stripped of its definition, so the Jacobian could not see the
  map's body.

  ```js
  // F |> JacobianMatrix |> Determinant |> Simplify
  ce.assign('F', /* the counterexample map */);
  // Before → Pipe(Pipe(F, JacobianMatrix), Determinant)   Now → -2
  ```

  `f` now decides whether the topic is evaluated (a lazy `f` receives it
  unevaluated). A chained topic — the inner pipe of `a |> g |> f` — is plumbing
  whose value flows on, so it is evaluated before reaching `f`. Eager stages,
  lambda right-hand sides, `N()`, and numeric chains are unaffected.

- **Indexing into a computed list hid the unknown from `Solve`.** A held
  equation containing `At(List(…), k)` was opaque to the solver — it saw no
  unknown and answered `[]`, which by contract means "proven no solutions". This
  is the last of the lazy-operand family: `At` is now projected structurally in
  a held operand.

  ```js
  ce.box(['Solve', ['Equal', ['At', ['List', 'Y', 2], 1], 5], 'Y']).evaluate();
  // Before → []      Now → [5]
  ```

  Projection, never evaluation: with `Y := 99`, `At([Y, 2], 1).evaluate()` is
  `99`, which inside a held equation would replace the unknown being solved for.
  Only a literal `List` with a literal in-range index is projected (negative
  indices count from the end); a symbolic list or index is left alone. The
  transformers (`Simplify`, `Expand`, …) get the same treatment.

- **`Solve` returned `[]` for an equation it could not see into.** A lazy
  operator holds its equation and takes only `.canonical`, which binds structure
  without resolving values, so two kinds of operand stayed opaque: a call to a
  user-defined function, and a symbol whose value _contains_ the unknown.
  Because `[]` means "proven no solutions", these were silent wrong answers
  rather than visible inertness.

  ```js
  ce.assign('g', ce.parse('t \\mapsto t^2 - 4'));
  ce.box(['Solve', ['Equal', ['g', 'x'], 0], 'x']).evaluate();
  // Before → []      Now → [2, -2]

  ce.assign('s', ce.parse('\\frac{9-w^2}{4}'));   // `s = 2` has no `w` in it
  ce.box(['Solve', ['Equal', 's', 2], 'w']).evaluate();
  // Before → []      Now → [1, -1]
  ```

  A transformer nested inside the equation (rather than at its root) is now
  reduced too, so `Solve(Simplify(u) = 2, w)` works.

  The unknown is never substituted, even when it has a value: the reduction is
  structural — `.subs` on the lambda body, `.value` on a binding — never
  `.evaluate()`. With `x` assigned `5`, `Solve(g(x) = 0, x)` still returns
  `[2, -2]`. A recursive definition expands one level and stops.

- **Expression transformers ignored a user-defined function in their operand.**
  Same root cause: `Simplify(g(a))`, `Expand`, `Factor`, `Together` and
  `Distribute` returned `g(a)` unchanged, and `Integrate(g(t), t)` stayed inert.

  ```js
  ce.assign('g', ce.parse('t \\mapsto t^2 - 4'));
  ce.box(['Factor', ['g', 'a']]).evaluate();
  // Before → g(a)    Now → (a - 2) * (a + 2)
  ```

  Beta-reduction substitutes the function _body_, so an assigned value for a
  symbol elsewhere in the operand is still left alone.

- **`Distribute` returned a product where it should return a sum.** The helper
  recombined the branches of a distributed sum with `Multiply` instead of `Add`,
  so `(a + b)·c` became `(a·c)·(b·c)`. Every input the operator acted on came
  back with a different value. The operator had no test coverage, which is why
  this survived; it is now covered by a numeric oracle.

  ```js
  ce.box(['Distribute', ce.parse('(a+b)c')]).evaluate();
  // Before → a * b * c * c      Now → a * c + b * c
  ```

- **`numeratorDenominator` reported a denominator of `1` for a bare negative
  power.** Canonical form writes `1/x^2` as `Power(x, -2)`, and the `Power`
  branch never moved a negative exponent into the denominator. The same factor
  inside a `Multiply` (`y/x^2`) routes through `Product.asNumeratorDenominator`,
  which splits on exponent sign and was already correct — so the two disagreed.
  This also affected the `NumeratorDenominator` operator and `.denominator`. A
  _symbolic_ exponent is still left alone: its sign is not decidable there.

  ```js
  ce.parse('\\frac{1}{x^2}').numeratorDenominator;
  // Before → [x^(-2), 1]        Now → [1, x^2]
  ```

- **`Together` dropped denominators written as negative powers.** It treated
  only a `Divide` node as carrying a denominator, so such terms were folded into
  the numerator and the combined fraction kept negative powers.

  ```js
  ce.box(['Together', ce.parse('\\frac{1}{x}+\\frac{1}{x^2}')]).evaluate();
  // Before → (x * x^(-2) + 1) / x    Now → (x + 1) / x^2
  ```

- **Expression transformers ignored a `ReplaceAll` in their operand.** `Expand`,
  `ExpandAll`, `Factor`, `Together`, `Distribute` and `Simplify` are lazy and
  took only `.canonical` of their held operand, so a `ReplaceAll` reached them
  as an unevaluated call with no polynomial structure and was silently returned
  unchanged. The reduction is recursive, so a producer head nested inside the
  operand is handled too.

  ```js
  ce.parse('\\mathrm{Expand}(\\mathrm{ReplaceAll}(x^2+x, x \\to a+1))').evaluate();
  // Before → ReplaceAll(x^2 + x, To(x, a + 1))    Now → a^2 + 3a + 2
  ```

  Note this is deliberately a different set from the transformer heads reduced
  by `Solve`/`Integrate`/`Limit`: `ReplaceAll` ends in `.evaluate()` and so
  substitutes assigned symbol values, which those algorithms must avoid.

- **`toString()` dropped the parentheses around a product-of-sums denominator**,
  producing text that reads back as a different expression. The MathJSON and
  LaTeX serializations were correct throughout; only the ASCII form was
  affected. The grouping check treated any string starting with `(` and ending
  with `)` as already parenthesized, which `(x + 1) * (x^2 - 1)` satisfies
  without being a single group.

  ```js
  ce.box(['Divide', 1, ['Multiply', ['Add', 'x', 1], ['Add', ['Power','x',2], -1]]]).toString();
  // Before → 1 / (x + 1) * (x^2 - 1)    Now → 1 / ((x + 1) * (x^2 - 1))
  ```

- **`Map` now evaluates over a source that only becomes a collection when
  evaluated.** `Map(X - 1, f)` stayed in its unevaluated lazy form while
  `Map(X, f)` and `Map([0, 1, 2], f)` both evaluated. The trigger was any source
  whose collection-ness is not visible before evaluation — a broadcast
  arithmetic result over a list, or an eager collection operator such as
  `UnicodeScalars`.

  ```js
  ce.assign('X', ce.box(['List', 0, 1, 2]));
  ce.box(['Map', ['Subtract', 'X', 1], sq]).evaluate();
  // Before → Map(X - 1, (x) |-> x^2)     Now → [1, 0, 1]

  ce.box(['Map', ['UnicodeScalars', { str: 'ab' }], sq]).evaluate();
  // Before → Map(UnicodeScalars("ab"), …) Now → [9409, 9604]
  ```

  Applies to the `zipWith` (multi-source) form as well, and to `.at()` — which
  previously returned `undefined` for such a source, so a result longer than the
  materialization head was silently rendered head-only instead of head-and-tail.
  Every other collection operator (`Filter`, `Take`, `Sort`, `Reverse`, `First`,
  …) already accepted these sources. Expressions that are genuinely not
  collections are unchanged: `Map(5, f)` still stays symbolic.

- **Compiled comparisons and logical connectives no longer return a wrong answer
  for a list operand.** `<`, `<=`, `>`, `>=`, `And`, `Or` and `Not` could
  produce a scalar `false` (or one of their operands) where `evaluate()` returns
  an element-wise list of booleans — a wrong result from a successful compile.
  Such expressions now decline to compile and fall back to interpretation, which
  gives the correct answer.

  Mostly affects a filter whose condition is computed, such as
  `L[|[1...n]-k|>0]`: it now evaluates correctly but is no longer compiled, so
  expect interpreter performance for that shape. Scalar comparisons and
  connectives, and list literals such as `Not([True, False])`, still compile.

- **Complex values are handled correctly when compiled.** `At(p, 1+2i)` and
  `RandomList(n, 7+3i)` now agree with `evaluate()`, which uses the real part of
  a complex index or seed. Previously the compiled forms returned `NaN` and a
  different random sequence respectively.

  A compiled scalar _comparison_ against a complex value is still wrong (it
  returns `false` rather than declining); this is unchanged and tracked in
  `ROADMAP.md`.

- **`At` with several indices reports the correct type.** For a 2×3 matrix `M`,
  `At(M, 1, [1,2])` is the 2-element list `[1,2]` and `At(M, 1, 2)` is a single
  element — both previously reported the type of a whole matrix row.

## 0.92.1 _2026-07-22_

### Breaking Changes

- **`Join` appends a tuple operand as a single element instead of splicing its
  components.** A tuple is an `indexed_collection`, so `Join` used to iterate it
  and concatenate its components. But a tuple is a _value_ — a point or vector —
  and the engine treats it as one everywhere else (`Abs(point)` → `Norm`,
  component-wise point arithmetic). Splicing silently degraded the point-list
  accumulation idiom `L → Join(L, P)`: the result grew by 2 instead of 1 and
  stopped matching `list<point>`.

  ```js
  // Before
  ce.box(['Join', pointList, ['Tuple', 2, 5]]).evaluate();
  // → ["List", …, 2, 5]        length +2, heterogeneous `number` tail
  // After
  // → ["List", …, ["Pair", 2, 5]]   length +1, still a list of points
  ```

  `Join` of collections is unchanged (`Join([1,2],[3,4])` → `[1,2,3,4]`;
  `Join([(0,3)],[(2,5)])` → `[(0,3),(2,5)]`). The one reversal is a tuple in
  operand position where it was previously flattened: `Join((1,2),(3,4))` was
  `[1,2,3,4]` and is now `[(1,2),(3,4)]`. `Join` now agrees with `Append` on a
  tuple element.

- **`Join` now reports the joined ELEMENT type instead of a bare `list`.** Its
  type handler returned `list` whatever it was given, so a joined point list did
  not match `list<tuple<…>>` and type-directed dispatch downstream stopped
  recognizing it. Each operand now contributes either its own type (an atomic
  tuple, which becomes one element) or its element type (a collection, which is
  spliced), widened into the result:

  ```js
  Join(pointList, point); // was: list      now: list<tuple<number, number>>
  Join([1, 2], [3, 4]); //   was: list      now: list<finite_integer>
  ```

  An operand whose element type is unknown still yields the bare `list`. This is
  a **type-surface change**: code pinning the exact string `'list'` for a `Join`
  result sees the narrowed type instead — but `matches()`-based queries only
  gain precision.

### Issues Resolved

- **`evaluateAsync()` no longer tears down a scoped operator's local scope while
  the operator is still running.** An `evaluateAsync` handler returns at its
  first suspension point, not at completion, so the dispatcher popped the
  operator's local evaluation context too early; everything the resumed handler
  did then ran against the enclosing scope. A big operator whose reduction
  outlived one time slice (roughly, more than ~16ms of work) assigned its loop
  index globally — leaking it, overwriting an outer binding of the same name,
  and throwing `Cannot assign a value to the constant "i"` for the commonest
  index spelling of all, since global `i` is `ImaginaryUnit`:

  ```js
  await ce.parse('\\sum_{i=1}^{200000} i').evaluateAsync();
  // Before: throws `Cannot assign a value to the constant "i"`
  // After:  20000100000  (matches the synchronous lane)

  ce.assign('n', 7);
  await ce.parse('\\sum_{n=1}^{200000} n').evaluateAsync();
  ce.box('n'); // Before: 200000 (clobbered). After: 7
  ```

  The synchronous lane was fixed in 0.87.1; this brings the asynchronous lane —
  the cancellation path `withTimeLimit` documents — into line with it.
  Cancellation behavior is unchanged.

  Because the context is now held across the `await`, a scoped operator's frame
  is no longer necessarily on top of the evaluation-context stack when it
  unwinds — a second evaluation started while the first is suspended pushes
  above it. The frame is therefore removed by identity rather than popped, so an
  unwinding evaluation cannot discard (and dispose the bindings of) one that is
  still running.

  One known limitation remains, unchanged by this release: while an async
  evaluation is suspended, its scope is the engine's current one, so code that
  enters the same engine in that window can see the operator's local bindings
  (e.g. a loop index). Enclosing bindings still resolve correctly through the
  scope's parent chain, and the local scope is gone once the evaluation settles,
  but a name that collides with an in-flight index resolves to that index.
  Removing this needs per-evaluation (task-local) context propagation; until
  then, use one engine per concurrent evaluation.

## 0.92.0 _2026-07-21_

### Breaking Changes

- **The parser's three symbol hooks are replaced by one oracle:
  `resolveSymbol`.** The `ParseLatexOptions` handlers `getSymbolType` and
  `hasSubscriptEvaluate` (and the internal `isSymbolDeclared`) are replaced by a
  single optional handler:

  ```js
  resolveSymbol?: (symbol) => { type, subscriptEvaluate? } | undefined
  ```

  Return `undefined` for a symbol the handler does not know; return a record
  (with a `BoxedType` or type-string `type`) for one it does. Declaration is the
  _presence_ of the record — `{ type: 'unknown' }` is a declared symbol of
  unknown type — so the previously inexpressible distinction between
  "undeclared" and "declared, type unknown" is now first-class, and inconsistent
  answers (a typed-but-undeclared symbol) are unrepresentable.

  Semantics also changed from _replace_ to _supplement_: through `ce.parse()`
  the handler is consulted first and any symbol it does not resolve falls back
  to the engine scope's definitions. Handlers no longer need to re-implement
  scope delegation (previously required boilerplate with `getSymbolType`):

  ```js
  // Before: vouch + hand-written scope delegation
  getSymbolType: (id) => {
    if (vouched(id)) return 'function';
    const def = ce.lookupDefinition(id); // boilerplate, easy to forget
    /* ... map def to a type ... */
  };
  // After: vouch only; unresolved symbols fall back to the scope
  resolveSymbol: (id) => (vouched(id) ? { type: 'function' } : undefined);
  ```

  Similarly, the `Parser` interface (custom LaTeX dictionary entries) replaces
  `getSymbolType()`/`hasSubscriptEvaluate()` with `parser.resolveSymbol()`, e.g.
  `parser.getSymbolType(id).matches('function')` becomes
  `parser.resolveSymbol(id)?.type.matches('function')`.

### Improvements

- **A declared name now outranks subscript-index capture.** Once a symbol `B` is
  bound to an indexed-collection value (a point, list, tuple…), the parser reads
  `B_{2}` as indexing (`At(B, 2)`), which made every subscripted sibling name
  (`B_2`, `B_3`, … alongside the point `B`) unspellable — and, since `B_{2}` and
  `B[2]` produce identical trees, unrecoverable after the parse. A subscripted
  spelling whose joined name is declared or assigned in scope now parses as that
  symbol; index capture applies only to undeclared joins, and bracket indexing
  (`B[2]`) is unaffected. Note that with the non-default
  `indexStyle: 'subscript'` serialization, `At(B, 2)` serializes as `B_{2}`,
  which re-parses as the symbol `B_2` when such a declaration exists.

- **Rubi integration (experimental) — nested-radical substitution fallback
  (R31).** `loadIntegrationRules` now closes nested-radical and
  sum-of-two-radical integrands the bundled algebraic rules leave inert. A
  nested radical (`√(x+√(x+1))/x²`, `√(1/x+√(1/x+1))`, the double-radical
  `√(x+1)/(x+√(√(x+1)+1))`) is rationalized by iteratively substituting
  `u = (a+b·x)^{1/k}` (or the Laurent `(a+b/x)^{1/k}`) at the innermost radical
  linear in `x`, keeping the resulting rational's denominator factored so the
  bundled partial-fraction rules close it; the conjugate shape
  `(√(x+1)+√(1−x))⁻²` is rationalized by its conjugate. Each result is accepted
  only after a domain-aware numeric derivative check against the integrand, so
  out-of-scope shapes stay cleanly unsolved. On the Bondarenko benchmark this
  lifts CE+Rubi from 12/35 to 20/35 (closing 8 previously unsolved
  nested-radical integrals; a ninth, #16, closes only under the production
  bundle's compiled rule set). Structurally inert off its family, and
  disableable with `RUBI_NO_R31`.

## 0.91.0 _2026-07-21_

### New Features

- **`FindFit` — general nonlinear least-squares fitting.**
  `FindFit(data, model, params, vars)` fits an arbitrary model expression to
  data by Levenberg–Marquardt, returning a record
  `{parameters, converged, residualNorm, iterations}`. Unlike the closed-form
  `LinearRegression`/`PolynomialFit`, the model may be any composition
  (`a·e^{b·x} + c`, Gaussians, power laws, cosines, …). `data` is a list of
  `(x…, y)` tuples or a plain list of `y` values (`x = 1, 2, …`). Each parameter
  spec is a bare symbol (start `1`, unbounded), `(a, a0)` (explicit start), or
  `(a, a0, lo, hi)` (start plus a box constraint, with `±∞` allowed for
  one-sided bounds). Convergence is first-class: non-convergence within the
  iteration budget is reported as `converged: False` with the best-so-far
  values, never a silent wrong answer. Jacobians are analytic (via `D`), with a
  per-column forward finite-difference fallback for components that cannot be
  differentiated symbolically. A **joint form** fits several models to several
  datasets sharing parameters (a list of models paired with a list of datasets,
  residuals stacked).

- **`FindRoot` — numerical equation solving.** `FindRoot(equations, params)`
  finds parameter values that zero one or more residuals, sharing the same
  parameter-spec grammar, box constraints, and result record as `FindFit`
  (root-finding is the zero-residual case of the same Levenberg–Marquardt core).
  `equations` is an equation (`lhs == rhs`), a bare residual expression (read as
  `= 0`), or a list of either.

### Resolved Issues

- **Applying a declared-then-assigned function to a large collection is now
  lazy, matching the hybrid-laziness contract.** Since 0.84.0, element-wise
  operations over collections of more than 100 elements (or of unknown length)
  evaluate to a lazy `Map` — but a function registered via
  `ce.declare('g', '(number) -> number')` + `ce.assign('g', x ↦ …)` resolved
  through a value definition whose application-site broadcast still zipped
  eagerly, walking the whole collection at `evaluate()` time. The
  value-definition application path now goes through the same laziness gate as
  parse-assigned functions (`g(x) := …`) and built-in broadcasts: past the eager
  threshold `g(X)` returns a lazy `Map`, results of ≤100 known-finite elements
  are byte-identical to before, collection-typed parameters still bind their
  argument whole, and tuples stay atomic. (Not a recent regression: this path
  had been eager on every release since laziness shipped in 0.84.0.)

### Benchmarks

#### Numeric performance (200-digit precision)

Median time per call, in **microseconds — lower is better**. `—` means the tool
returned no usable result at that precision.

| Expression         | CE (current) | CE 0.86.1 | SymPy | math.js | Mathematica |
| ------------------ | -----------: | --------: | ----: | ------: | ----------: |
| $\pi^2$            |          6.2 |       7.0 |   175 |     173 |         3.9 |
| $\sin 1$           |           20 |        20 |   220 |     506 |         5.2 |
| $\cos 1$           |           20 |        21 |   219 |     612 |         6.9 |
| $\ln 2$            |           14 |        14 |   348 |   4,616 |         4.0 |
| $e^{\pi}$          |           12 |        12 |   221 |   5,086 |         4.6 |
| $\zeta(3)$         |        1,580 |     1,619 |   261 |       — |          49 |
| $\Gamma(\tfrac13)$ |          842 |       839 |   345 |       — |         211 |
| $\psi(\tfrac13)$   |          728 |       720 | 2,806 |       — |         168 |

#### Symbolic capability & performance

Each cell is **how many times faster than Mathematica** that engine is on the
case (`Mathematica ÷ engine`, so **higher is better**; Mathematica itself is
`1×`). `—` means the engine can't do the case; `✓` means it solves a case
Mathematica can't. Compare the **CE (current)** and **CE 0.86.1** columns to see
what is _new this release_ (a `—` under `0.86.1` next to a number under the
current build). The **CE + R/F** column is the current build with the opt-in
Rubi integrator + Fungrim identities loaded (`loadIntegrationRules` /
`loadIdentities`), on the same minified bundle.

| Operation                              | CE (current) | CE + R/F | CE 0.86.1 | SymPy  | math.js | Mathematica |
| -------------------------------------- | :----------: | :------: | :-------: | :----: | :-----: | :---------: |
| **Antiderivatives**                    |              |          |           |        |         |             |
| $\int\frac{1}{\sqrt x}\,dx$            |     6.7×     |   3.1×   |   5.2×    |  0.5×  |    —    |     1×      |
| $\int\frac{x}{\sqrt{1-x^2}}\,dx$       |     10×      |   1.6×   |   8.4×    | 0.08×  |    —    |     1×      |
| $\int\frac{1}{x^3+1}\,dx$              |     6.3×     |   0.9×   |   4.4×    |  0.4×  |    —    |     1×      |
| $\int\frac{\sqrt x}{1+x}\,dx$          |      —       |   2.0×   |     —     |  0.1×  |    —    |     1×      |
| $\int\frac{x}{(1+x)^{1/3}}\,dx$        |      —       |   1.4×   |     —     | 0.01×  |    —    |     1×      |
| $\int\frac{x^2}{(1+x)^{1/3}}\,dx$      |      —       |   1.3×   |     —     | 0.007× |    —    |     1×      |
| **Derivatives**                        |              |          |           |        |         |             |
| $\tfrac{d}{dx}\sqrt{1-x^2}$            |     0.1×     |   0.1×   |   0.06×   | 0.003× |  0.01×  |     1×      |
| **Simplification**                     |              |          |           |        |         |             |
| $\sqrt{3+2\sqrt2}$                     |     41×      |   31×    |    32×    |   —    |    —    |     1×      |
| $\sqrt6\,x+\sqrt2\,x$                  |     104×     |   54×    |    59×    |  3.3×  |   16×   |     1×      |
| **Evaluation**                         |              |          |           |        |         |             |
| $\lim_{x\to0}\tfrac{\sin x}{x}$        |     58×      |   29×    |    47×    |  3.0×  |    —    |     1×      |
| $\lim_{x\to\infty}(1+\tfrac1x)^x$      |     9.3×     |   5.6×   |   8.0×    |  2.1×  |    —    |     1×      |
| $\int_1^2\tfrac1x\,dx$                 |    7405×     |  6951×   |   6437×   |  77×   |    —    |     1×      |
| $\int_{-\infty}^{\infty} e^{-x^2}\,dx$ |     423×     |   161×   |   363×    |  2.6×  |    —    |     1×      |
| **Solving**                            |              |          |           |        |         |             |
| $x^4+x^2-1=0$                          |     0.3×     |   0.3×   |   0.2×    | 0.06×  |    —    |     1×      |
| $x^3-x-1=0$                            |     1.7×     |   2.0×   |   1.4×    | 0.04×  |    —    |     1×      |

Across the cases both solve, Compute Engine is a **median 6.7× faster than
Mathematica** (up to 7405×) — in the browser, not a proprietary kernel.

<sub>Measured 2026-07-21 · Compute Engine `0.90.0` @ `8740998f` (current build)
· published `0.86.1` · SymPy `1.14.0` · math.js `15.2.0` · Mathematica
`14.3.0 for Mac OS X ARM` · Node `v22.13.1`. Correctness is verified numerically
against an independent `mpmath` reference, never another tool. Reproduce with
`npm run build production && ./venv/bin/python3 benchmarks/gen_cases.py && node benchmarks/report.mjs && node benchmarks/report_changelog.mjs`.</sub>

## 0.90.0 _2026-07-21_

### New Features

- **`RandomList(n)` — an eagerly-materialized list of `n` independent uniform
  reals in `[0, 1)`.** Draws come from the engine's random stream (honoring
  `ce.randomSeed` for reproducibility); the two-argument form
  `RandomList(n, seed)` produces a deterministic list from an explicit seed,
  independent of the engine stream. Eagerness is deliberate: the result is a
  concrete `List`, so every reference to it sees the same draws (a lazy
  collection of `Random()` calls re-draws on each traversal). With a literal
  count the length is part of the type (`RandomList(5)` types
  `vector<finite_real^5>`). The count is capped at 10⁶ elements; a larger count
  — or a negative one — returns an `out-of-range` error rather than silently
  misbehaving.

### Improvements and Resolved Issues

- **`Abs` of a fixed-arity point is now the Euclidean norm.** `|(3, 4)|`
  evaluates to `5` — the single-bar spelling of the vector magnitude, consistent
  with the `\lVert…\rVert` (`Norm`) parse and with tuple arithmetic treating
  points as vectors in ℝⁿ. Previously the expression stayed inert under
  `evaluate()`, and **compiled to garbage**: the JavaScript target returned the
  bare component array (which concatenated into a _string_ in downstream
  arithmetic), and the shader targets emitted invalid code. All targets now
  compile it as the norm (`_SYS.norm` on the js target, `length()` on GLSL/WGSL
  for 2–4 components; other arities and points with a broadcasting component
  fail closed to interpretation). Detection is type-based, so a tuple-_typed_
  symbol or parameter routes as a point too. `Abs` over a `List` is unchanged
  and still broadcasts elementwise. `Hypot` with a point argument now squares
  through the norm as well: `Hypot((3,4), 1) = √26` (previously an inert `Power`
  of a tuple). And `Norm`/`Abs` of a point now honor the exactness contract:
  `evaluate()` keeps the exact `√2`, `.N()` numericizes.

- **`Norm` compiles on the `interval-js` target** for fixed-arity points
  (default L2 norm; `hypot`-based in 2-D for a tighter enclosure). Implicit
  curves and line series expressed with `\lVert…\rVert` or `|…|` now get
  interval-arithmetic break detection instead of silently degrading to point
  sampling.

- **`Norm`/`Abs` of a point with a broadcasting component reports an honest
  `list<number>` type.** `‖(x+[0.5,1], y, z+2)‖` evaluates to a `List` (one norm
  per zipped element); its static type now says so instead of `number`, matching
  the equivalent `\sqrt{(x+[0.5,1])^2+…}` spelling.

- **A `Comprehension` serializes with bracket delimiters** so it survives its
  own round trip in every position: `H=[k^2 \operatorname{for} k=[1...4]]` now
  serializes as `H=\left[k^2 \operatorname{for} k = 1..4\right]` (previously the
  unfenced form re-parsed with the assignment swallowed into the comprehension
  body, and under an `Add` the trailing term was absorbed into the iteration
  range). The fence is unconditional — `[body for …]` parses back to the same
  `Comprehension`, so it is lossless. (Non-canonical serialization-shape
  callout, same class as the 0.83 big-operator body fence.)

- **`Expression.isValid` is now O(1) after the first query.** Validity is a
  structural property of an immutable expression, so it is computed once and
  cached; previously every query re-walked the whole subtree (and a parent's
  query re-entered each child's), which dominated large-document workloads — one
  profiled 75-row import spent 77% of its time in repeated `isValid` walks.

## 0.89.0 _2026-07-21_

### Breaking Changes

- **`ComputeEngine.timeLimit` is removed**, completing the deprecation begun in
  0.88.0. There is no implicit ambient deadline anymore: an `evaluate()` or
  `simplify()` outside a span runs unbounded, and `ce.withTimeLimit(ms, fn)` /
  `ce.withTimeLimit({ ms, label }, fn)` spans are the only way to arm a
  deadline. If you relied on the old 2000&nbsp;ms ambient default, wrap your
  evaluation entry point in a span:

  ```ts
  const r = ce.withTimeLimit({ ms: 2000, label: 'my-app:eval' }, () =>
    expr.evaluate()
  );
  ```

  The `engine.timeLimit:<operator>` attribution synthesized for ambient timeouts
  is gone with it — every `CancellationError.attribution` now names a span you
  created (or is `undefined` for an unlabelled span).

- **The `BoxedTensor` class is removed.** Tensor values (vectors, matrices) are
  now ordinary canonical `List` expressions; "tensor-ness" is a property
  (shape-regularity), not a distinct representation. The `BoxedTensor` type
  export and the `TensorInterface.tensor` accessor (which exposed the internal
  packed `Tensor` object) are gone. The `isTensor()` guard remains and now
  answers the representation-independent question "is this a shape-regular
  list?"; `.shape` and `.rank` remain and now report honestly on **every**
  tensor-shaped list — including broadcast results, which previously reported
  `[]`/`0`. Code that used `expr.tensor` should operate on `expr.ops` (the
  elements) or use the public linear-algebra operators.

- **Lists carry honest, shape-aware types.** A shape-regular list's type reports
  its actual element type and dimensions: `[1, 2, 3]` types
  `vector<finite_integer^3>` (previously `vector<3>`, i.e. `number` elements),
  `[[1, 2], [3.5, 4.5]]` types `matrix<finite_real^(2x2)>`, and a list of
  non-numeric values is no longer mistyped as a numeric vector —
  `[rgb(1,0,0), rgb(0,1,0)]` types `list<color^2>`, not `vector<2>`. The new
  types are strict subtypes of the old ones, so `type.matches('vector<3>')` and
  similar queries continue to answer `true`; only code comparing exact type
  _strings_ is affected. An evaluated broadcast result now carries its shape too
  (`Sin([0,1]).evaluate()` types `vector<2>`), and the declared type of a
  broadcast expression is always a sound upper bound of its evaluated value's
  type.

- **Signature validation of collection arguments is two-stage.** An operand
  whose static type could still conform to a collection parameter (a symbol
  declared plain `list`, `list<unknown>`, a `broadcastable<…>` intermediate) is
  accepted at canonicalization and checked against its actual value when the
  operator evaluates. Provably-wrong operands (`Determinant("abc")`,
  `Determinant(v)` with `v: list<number>` — a flat vector can never be a matrix)
  still error immediately. Consequently some `incompatible-type` errors that
  used to appear at parse/canonicalization time now surface at evaluation time
  instead (as the operator's specific error, e.g. `expected-square-matrix`, or
  as an inert expression).

### Improvements

- **Matrix operations work on computed matrices.** The static type of a
  broadcast application now mirrors its operand's shape (`Sqrt(M)` with `M` a
  2×2 matrix types as a 2×2 matrix), so expressions like `Determinant(Sqrt(M))`,
  `MatrixMultiply(Sqrt(M), Sqrt(M))`, or `Inverse(A + B)` — which used to fail
  with `incompatible-type` at canonicalization — now evaluate. Symbols declared
  `list` participate the same way once a conforming value is assigned.

- **Structural matrix operations work on any cell type.** `Transpose`,
  `ConjugateTranspose`, `Reshape`, `Flatten`, and the shape predicates
  (`IsSquareMatrix`, `IsSymmetric`, `IsDiagonal`) operate on any shape-regular
  list — a matrix of colors, tuples, or unevaluated function applications — not
  just numeric ones. Numeric kernels (`Determinant`, `Inverse`, …) still require
  numeric cells and decline others gracefully.

- **Exact integer matrices stay exact.** Linear-algebra kernels over exact
  integer matrices now use exact arithmetic under `evaluate()`:
  `Determinant([[9007199254740991, 0], [0, 3]])` returns the exact
  `27021597764222973` (previously rounded through float arithmetic). Under
  `.N()` results are floated, as before (`Inverse([[2,1],[1,3]]).N()` →
  `[[0.6, -0.2], [-0.2, 0.4]]`).

- **List equality is tolerant and NaN-aware in all cases.**
  `[1,2,3].isEqual([1,2,3+1e-11])` is `true` (engine tolerance),
  `[x,2].isEqual([y,2])` is `undefined` (symbolic), and a list containing `NaN`
  is never `isEqual` to itself (mirroring scalar `NaN ≠ NaN`) — uniformly for
  every list, whatever produced it. Ordering comparisons (`isLessEqual`, …)
  between equal tensors of any cell type now answer `true` instead of
  `undefined`.

- **Faster broadcast arithmetic.** Removing the eager tensor construction from
  the boxing path makes broadcast-heavy evaluation measurably faster (~30% on
  elementwise matrix expressions), with no per-expression packing cost until a
  numeric kernel actually runs.

### Issues Resolved

- **A function call over a collection-valued _expression_ keeps its broadcast
  type.** With `h` a function over numbers (declared or inferred) and `L` a
  list, `h(L)` already typed as a list — but `h(L + 1)` or `h(2L)` typed as a
  _scalar_ while still evaluating to a list. Both now type as the shaped vector
  (`h(L+1)` → `vector<3>` for a 3-element `L`), and a declared scalar signature
  no longer rejects a collection argument with `incompatible-type` —
  scalar-parameter functions are threadable, so the argument broadcasts,
  matching what evaluation always did. Scalar applications and genuinely invalid
  arguments (`h("abc")`) are unchanged.

### Benchmarks

#### Numeric performance (200-digit precision)

Median time per call, in **microseconds — lower is better**. `—` means the tool
returned no usable result at that precision.

| Expression         | CE (current) | CE 0.86.1 | SymPy | math.js | Mathematica |
| ------------------ | -----------: | --------: | ----: | ------: | ----------: |
| $\pi^2$            |          6.7 |       7.5 |   176 |     137 |         4.1 |
| $\sin 1$           |           21 |        21 |   220 |     444 |         5.1 |
| $\cos 1$           |           20 |        20 |   224 |     536 |         6.9 |
| $\ln 2$            |           16 |        16 |   352 |   5,754 |         3.8 |
| $e^{\pi}$          |           14 |        15 |   215 |   4,765 |         4.5 |
| $\zeta(3)$         |        1,734 |     1,790 |   282 |       — |          51 |
| $\Gamma(\tfrac13)$ |          957 |       950 |   356 |       — |         209 |
| $\psi(\tfrac13)$   |          806 |       795 | 2,818 |       — |         170 |

#### Symbolic capability & performance

Each cell is **how many times faster than Mathematica** that engine is on the
case (`Mathematica ÷ engine`, so **higher is better**; Mathematica itself is
`1×`). `—` means the engine can't do the case; `✓` means it solves a case
Mathematica can't. Compare the **CE (current)** and **CE 0.86.1** columns to see
what is _new this release_ (a `—` under `0.86.1` next to a number under the
current build). The **CE + R/F** column is the current build with the opt-in
Rubi integrator + Fungrim identities loaded (`loadIntegrationRules` /
`loadIdentities`), on the same minified bundle.

| Operation                              | CE (current) | CE + R/F | CE 0.86.1 | SymPy  | math.js | Mathematica |
| -------------------------------------- | :----------: | :------: | :-------: | :----: | :-----: | :---------: |
| **Antiderivatives**                    |              |          |           |        |         |             |
| $\int\frac{1}{\sqrt x}\,dx$            |     6.6×     |   3.1×   |   5.4×    |  0.5×  |    —    |     1×      |
| $\int\frac{x}{\sqrt{1-x^2}}\,dx$       |     8.0×     |   1.3×   |   6.8×    | 0.09×  |    —    |     1×      |
| $\int\frac{1}{x^3+1}\,dx$              |     5.6×     |   0.8×   |   4.3×    |  0.3×  |    —    |     1×      |
| $\int\frac{\sqrt x}{1+x}\,dx$          |      —       |   1.7×   |     —     |  0.1×  |    —    |     1×      |
| $\int\frac{x}{(1+x)^{1/3}}\,dx$        |      —       |   1.2×   |     —     | 0.01×  |    —    |     1×      |
| $\int\frac{x^2}{(1+x)^{1/3}}\,dx$      |      —       |   1.2×   |     —     | 0.007× |    —    |     1×      |
| **Derivatives**                        |              |          |           |        |         |             |
| $\tfrac{d}{dx}\sqrt{1-x^2}$            |    0.04×     |  0.04×   |   0.02×   | 0.001× | 0.003×  |     1×      |
| **Simplification**                     |              |          |           |        |         |             |
| $\sqrt{3+2\sqrt2}$                     |     44×      |   34×    |    34×    |   —    |    —    |     1×      |
| $\sqrt6\,x+\sqrt2\,x$                  |     131×     |   67×    |    72×    |  5.0×  |   23×   |     1×      |
| **Evaluation**                         |              |          |           |        |         |             |
| $\lim_{x\to0}\tfrac{\sin x}{x}$        |     53×      |   25×    |    43×    |  3.0×  |    —    |     1×      |
| $\lim_{x\to\infty}(1+\tfrac1x)^x$      |     8.7×     |   5.1×   |   7.2×    |  2.1×  |    —    |     1×      |
| $\int_1^2\tfrac1x\,dx$                 |    6954×     |  6552×   |   5758×   |  93×   |    —    |     1×      |
| $\int_{-\infty}^{\infty} e^{-x^2}\,dx$ |     382×     |   133×   |   341×    |  2.6×  |    —    |     1×      |
| **Solving**                            |              |          |           |        |         |             |
| $x^4+x^2-1=0$                          |     0.2×     |   0.3×   |   0.2×    | 0.06×  |    —    |     1×      |
| $x^3-x-1=0$                            |     1.5×     |   1.7×   |   1.4×    | 0.04×  |    —    |     1×      |

Across the cases both solve, Compute Engine is a **median 6.6× faster than
Mathematica** (up to 6954×) — in the browser, not a proprietary kernel.

<sub>Measured 2026-07-21 · Compute Engine `0.88.1` @ `afde4f88` (current build)
· published `0.86.1` · SymPy `1.14.0` · math.js `15.2.0` · Mathematica
`14.3.0 for Mac OS X ARM` · Node `v22.13.1`. Correctness is verified numerically
against an independent `mpmath` reference, never another tool. Reproduce with
`npm run build production && ./venv/bin/python3 benchmarks/gen_cases.py && node benchmarks/report.mjs && node benchmarks/report_changelog.mjs`.</sub>

## 0.88.1 _2026-07-20_

### Issues Resolved

- **Color converters (`AsRgb`, `AsHsv`, `AsHsl`, `AsOklab`, `AsOklch`) broadcast
  over lists**, like the color constructors already did:
  `AsRgb([Hsv(120,1,1), Hsv(0,1,1)])` → `[Rgb(0,1,0), Rgb(1,0,0)]` instead of an
  `incompatible-type` error. A non-color element produces a per-element error
  rather than rejecting the whole call. Out-of-range channels continue to pass
  through unchanged — they can represent valid out-of-sRGB-gamut colors, so
  constructors and converters neither clamp nor error.

- **`ce.box()` no longer throws on malformed MathJSON input.** A non-MathJSON
  plain object (`ce.box({foo: 1})`) or an array whose head is not a symbol used
  to throw a JavaScript `Error`; both now return a boxed
  `["Error", "'unexpected-mathjson'", …]` expression (with the offending input
  as context), consistent with how every other input problem is reported. The
  engine remains fully usable afterward.

## 0.88.0 _2026-07-20_

### Deprecations

- **`ComputeEngine.timeLimit` is deprecated** in favor of
  `ComputeEngine.withTimeLimit()`. `timeLimit` arms a hard-to-scope implicit
  deadline around each `evaluate()`/`simplify()`; wrap the work you want bounded
  in a span instead:

  ```ts
  // Before
  ce.timeLimit = 500;
  const r = expr.evaluate();

  // After
  const r = ce.withTimeLimit({ ms: 500, label: 'my-app:eval' }, () =>
    expr.evaluate()
  );
  ```

  `timeLimit` still functions exactly as before in this release; it will be
  removed in a future minor version.

### Improvements

- **`ComputeEngine.withTimeLimit()` accepts an attribution label.** In addition
  to the numeric form `withTimeLimit(ms, fn)`, an object form
  `withTimeLimit({ ms, label }, fn)` (preferred for new code) records a label on
  the span. Nesting still composes as `min()` — a labelled inner span can only
  shorten the effective deadline, never extend it past an enclosing one.

- **`CancellationError` now carries `attribution` and `spans`.** When a timeout
  fires, `attribution` is the label of the span that owns the deadline that
  fired (so a caller can compare it against the label it passed to distinguish
  "my sub-budget expired" from "my caller's budget expired"), and `spans` lists
  all active span labels, outermost first. Timeouts armed by the deprecated
  ambient `ce.timeLimit` are attributed as `engine.timeLimit:<operator>` (e.g.
  `engine.timeLimit:Integrate`).

- **Divisor functions are now O(√n) instead of O(n).** `Sigma0`, `Sigma1`,
  `SigmaMinus1`, `IsPerfect`, and `Totient` compute from the prime factorization
  rather than trial iteration: `Sigma1(1000000007)` went from ~14s to ~1ms, and
  11+-digit inputs that previously ran essentially forever return instantly.

- **Integer factorization is interruptible.** The Pollard-rho factorizer now
  honors the active deadline (a `withTimeLimit` span interrupts a hard semiprime
  factorization on time, with attribution) and is backstopped by an iteration
  budget (`cause: 'iteration-limit-exceeded'`) so it terminates even with no
  time limit set.

- **Symbolic integration no longer swallows a caller's timeout.** If a
  `withTimeLimit` span enclosing an integration expires mid-attempt, the
  `CancellationError` now propagates to the caller (identified via
  `attribution`) instead of being silently converted into "no antiderivative
  found". Rubi's own internal sub-budgets still degrade gracefully.

### Fixes

- **Unary `D` applications no longer serialize to a bare `D`.** An arity-1 (or
  otherwise unrecognized) `D` application — e.g. a document-defined function
  named `D` — used to serialize with its argument silently dropped (`["D","w"]`
  → `"D"`). It now serializes as `\operatorname{D}(w)`, which round-trips
  exactly. Recognized derivative shapes (`D(f,x)` → Leibniz notation) are
  unchanged.

- **A known-value uppercase symbol before a parenthesized group now parses as
  multiplication.** The predicate-notation heuristic (a single uppercase letter
  before `(...)` reads as a function application, e.g. `P(x)`) applied even when
  the scope knew the symbol was a value: with `K` assigned `-32.3`, `K(2-0.1)`
  parsed as a _call_ of `K` and evaluated to an `incompatible-type` error. The
  heuristic now consults the scope — a symbol with a known non-function type
  falls through to multiplication (`K(2-0.1)` → `-61.37`). Unknown and
  function-typed symbols are unchanged.

- **Juxtaposed-multiply serialization is round-trip safe for single-uppercase
  factors.** `Multiply(K, group)` serialized as `K(group)`, which re-parses as a
  function _call_ of `K` — corrupting the expression when `K` is a numeric
  symbol. A single-uppercase-letter factor directly against a parenthesized
  group now emits an explicit `\times` (`K\times(…)`). Other factor shapes
  (`x(y+z)`, `2(x+1)`, `\mathrm{abc}(x+1)`) already round-tripped and are
  untouched.

- **`ce.box()` now accepts native `bigint` values.** `bigint` was declared in
  `ExpressionInput` but unhandled by the boxing dispatch, so `ce.box(123n)` —
  bare or as an operand in `ce.box(['Add', 10n, 5])` — silently became the
  `Undefined` symbol. Bigints now box as exact integer literals, preserving
  exactness at any magnitude (never routed through a float), identical to the
  `ce.number(bigint)` path.

- **Unbounded collection walks over infinite lazy sources.** `First`/`At` on a
  `Filter` or `TakeWhile` whose predicate never (or eventually never) matches,
  and any walk over a `Dedup` of a source with infinitely repeating duplicates
  (e.g. `Dedup(Cycle([1,1]))`), could previously spin until the ambient time
  limit fired — or forever with `timeLimit = 0`. These walks are now bounded by
  `iterationLimit` and degrade gracefully (`Nothing`/`undefined`), consistent
  with the documented lazy-collection contract. Note: as a consequence,
  `Count(Dedup(...))` over a finite source **larger than `iterationLimit`** now
  returns `undefined` (unknown) instead of walking the whole source, matching
  `Filter`'s existing `count` behavior.

## 0.87.2 _2026-07-20_

### Breaking Changes

- **`Add` no longer widens an unreachable scalar arm into a broadcast collection
  type.** A sum mixing a scalar with a list-shaped operand typed as a union —
  `matrix + 1` was `finite_integer | matrix`, `2·[1,2,3] + a` was
  `number | vector<3>` — even though the value ALWAYS broadcasts elementwise and
  can never be a scalar (`[[1,2],[3,4]] + 1` → `[[2,3],[4,5]]`). These now type
  as the collection: `matrix`, `vector<3>`. The behavior was inconsistent as
  well as imprecise — a dimensionless `list<number> + 1` was already repaired to
  `list<number>` downstream, so only _dimensioned_ shapes carried the artifact.

  This is a **type-surface change**: code pinning the union spelling will see
  the narrowed type instead. It is a strict improvement for consumers that
  _dispatch_ on the type, and that is the motivation — union matching is
  all-members, so `type.matches('collection')` returned a confident `false` on a
  value that is always a collection, silently routing list-valued rows down
  scalar paths (reported by Tycho as item 67). It also unblocks expressions CE
  itself rejected: `MatrixMultiply([[x, y]], aM₁ + M₂)` failed signature
  validation on the union operand and now evaluates.

  Generic `collection`/`set`-typed operands are deliberately unchanged and keep
  the honest union — a non-indexed collection is never broadcast by the value
  path, so a scalar outcome stays reachable there.

### Resolved Issues

- **`isValid` now honors its documented contract through a list/tensor.**
  `isValid` is specified as `false` if the expression "or any of its
  subexpressions" is an `["Error"]`, but `BoxedTensor.isValid` returned `true`
  unconditionally — so a `List` whose every element was an `Error` reported
  `isValid: true`. `(1,2)+[3,4]` broadcasts to a list of `incompatible-type`
  errors and passed the gate. An embedded `Error` element now poisons the
  enclosing expression, matching what `BoxedFunction.isValid` already did.

  Behavior change worth noting even though the contract is unchanged: consumers
  using `isValid` as an admission gate before compiling or plotting will now
  correctly reject expressions they previously admitted.

  Only `expression`-dtype tensors are scanned; `float64`/`complex128`/`bool`
  fields cannot hold an `Error` by construction and keep the O(1) answer, so
  this does not add a per-element walk to large numeric tensors.

  The `isValid` documentation has been expanded to spell out the contract: the
  check is deep (including list elements and held operands), and it tests
  well-formedness rather than meaningfulness — free symbols, undeclared
  functions, `NaN` and `±∞` are all valid.

## 0.87.1 _2026-07-20_

### Breaking Changes

- **`compile(..., { realOnly: true })` now rejects a complex-valued tuple/list
  component.** `realOnly` coerced only the top-level result, so a `{ re, im }`
  object sitting in a component slot passed through untouched and reached the
  caller in a number slot — `(t, i t)` compiled successfully and returned
  `[0.5, { re: 0, im: 0.5 }]`. A complex component now fails the compile with a
  diagnostic naming the component, matching what the GPU targets already do and
  the existing `Sqrt(-1)` "no real value" error. The component's _type_ cannot
  decide this — with `t` undeclared, `(t, i t)` and `(t, t²)` both infer
  `finite_number` — so the check uses the same `isComplexValued` analysis the
  GPU targets fail closed on, and every target now rejects the same shapes.
  Real-valued tuples are unaffected, and the runtime `realOnly` coercion now
  also recurses into array results for values that only become complex when
  called (`(t, √t)` at `t = -4` → `[-4, NaN]`). The check follows only positions
  that can produce the compiled result, so a complex value consumed by an
  operation with a real result still compiles (`At([i, 2], 2)` → `2`).

### Resolved Issues

- **A `When` (restriction) whose value was a COLLECTION did not expose the
  collection interface.** `isCollection` was `false`, `count` `undefined` and
  `each()` yielded nothing, even though `.type` already reported `vector<N>` /
  `list<tuple<…>>` — the type system and the collection interface disagreed
  about the same value. Only a LIST-valued condition broadcast; the common case
  of one scalar restriction over a whole list left an opaque wrapper in place. A
  collection-valued `When` now behaves as `[When(L₁,c), …, When(Lₙ,c)]`: at or
  below `MAX_SIZE_EAGER_COLLECTION` it distributes into a `List`, and above the
  threshold it stays a held `When` that is nonetheless fully enumerable
  (`count`/`each()`/`at()`), following the same hybrid-lazy convention as
  `PointList`. A scalar `When` still reports as a scalar, and a `Tuple`-valued
  one is not split into its components so a restricted point stays a point.
  Operators whose collection-ness depends on their operands can now declare it
  with the new optional `BaseCollectionHandlers.isCollection` predicate. Every
  handler-backed collection API honors that opt-out — `count`, `each()`, `at()`,
  `get()`, `indexWhere()`, `subsetOf()`, `contains()`, `isLazyCollection` and
  `isIndexedCollection` — so none of them can report a collection answer for a
  value that says it is not one. The opt-out is deliberately narrower than
  `isCollection === false`: an _eager_ collection operator such as
  `UnicodeScalars` has no collection handlers at all until it is evaluated, and
  still goes through the materialize-then-iterate path.

- **A `Sum`/`Product` over three or more indexing sets silently dropped every
  index after the second.** The cartesian product of the indexing sets was built
  by a fold that returned tuples of the wrong length — for a 2×2×2 product,
  eight tuples of length 2, with pairs duplicated — and since the reducer reads
  the tuple positionally, the third and later loop indexes were assigned
  `undefined`. Any triple sum or product was therefore wrong, without a
  diagnostic. The full n-dimensional product is now iterated, with the last
  index varying fastest. One and two-index big-ops are unaffected.

- **A `Sum`/`Product` with a large finite bound exhausted the heap before it
  could be interrupted.** The whole index product was materialized up front, so
  `Σ_{i=1}^{10⁸}` allocated 10⁸ one-element arrays _before_ the reducer ran a
  single step — the process died before `run()`/`runAsync()` or any deadline
  could cancel it. Index tuples are now streamed one at a time, keeping
  allocation proportional to the number of indexes, and the engine deadline is
  checked between terms. Bounds whose magnitude exceeds
  `Number.MAX_SAFE_INTEGER` over a non-degenerate range can no longer be
  enumerated faithfully at `number` precision — adding one to such a value does
  not change it — and now evaluate to an `["Error", "out-of-range"]` rather than
  silently truncating to a single term. A degenerate range (`lower === upper`)
  still yields exactly one term.

- **A `List` element whose container type was revealed only by canonicalization
  was flattened into a numeric tensor.** Tensor eligibility is decided on raw
  operands so nested `List`s stay visible, but a wrapper such as a parsed
  `Delimiter`, `If`, `Which`, `When` or `Hold` reports its `tuple`/`set`/
  `dictionary`/`record` type only after it is canonicalized. Such elements were
  taken for scalar tensor components, so a list of tuples collapsed into a
  `vector<N>` and lost its structure. Container-valued elements are now
  recognized both by operator name and by type — primitive and structured alike,
  including as a member of a union — and keep the expression a `List`.

- **A `Sum`/`Product` index named `i` was read as `ImaginaryUnit` by the
  compiler's complex-valuedness analysis, silently corrupting the enclosing
  arithmetic on every target.** The binder's bound name reached the engine-value
  fallback as if it were a free symbol, so the analysis complex-tainted the
  _sibling_ operand of any enclosing arithmetic — the `Sum` itself emitted
  correctly. `\sum_{i=0}^{2}\cos(it)+2.5` compiled (`success: true`) to `NaN`,
  and `\sin(\sum_{i=0}^{2}\cos(it))+2.5` to a silently wrong `2.5`; the
  interpreter was correct throughout. The index need not appear in the body, and
  `\prod` was affected identically. A binder's bound names are no longer
  analyzed as free symbols; loop/summation indices are treated as the integer
  counters they are, while function parameters keep their declared types (a
  complex parameter stays complex).

- **A per-evaluate `ce.timeLimit` was silently inert inside a
  `ce.withTimeLimit(ms, fn)` span.** The span deadline replaced, rather than
  min-ed with, every inner clamp, so a pipeline wrapped in a span lost its inner
  bounds — a 500 ms clamp ran the full 60 s span. The effective deadline is now
  `min(ambient, now + timeLimit)`. Plain nested evaluations are unaffected. Note
  this applies to synchronous `evaluate()`: a span's deadline is still restored
  when its callback returns, so `evaluateAsync()` under a span remains
  unbounded.

- **GPU targets emitted invalid shader source for vector-valued block locals and
  over-wide `vecN` constructors.** A tuple/point-valued block local was declared
  `float` while being assigned a `vecN`/array, vector width did not propagate
  through an aliased local (`q := p`), and `vecN` constructor arity was chosen
  from the _argument_ count when it is a _component_ count — so a complex or
  nested-tuple element overflowed the constructor. Locals now declare the
  matching type, width propagates through aliases, and an aggregate-valued
  component fails closed with a diagnostic instead of emitting source no driver
  accepts. Failing closed now also covers a matrix-valued component, an empty
  tuple/list (neither language has a zero-length array type), and a block local
  bound to values of disagreeing shapes within one block (a shader local has a
  single declared type, and there is no declaration a scalar and a `vecN`
  assignment both satisfy).

- **A delimited `\mapsto` body was read as a statement `Block` rather than a
  `Tuple`, so a point-valued lambda silently dropped all but its last
  component.** `t \mapsto (\cos t, \sin t)` applied at `t = 0.5` returned
  `0.479…` — just `sin 0.5` — on every target, `js` included; the equivalent
  `g(t) := (\cos t, \sin t)` was already correct. A delimited lambda body is now
  data (a `Tuple`) whatever its separator; a genuine statement block is built by
  the `;` infix parser when the sequence contains an `Assign` and reaches the
  lambda parser already formed, so `(x := 1; x+1)` is unchanged.

## 0.87.0 _2026-07-19_

### Resolved Issues

- **Exponential blowup evaluating float-carrying symbolic bodies inside function
  applications.** The "inexact operand numericizes a closed-constant
  sum/product" rule (`0.5 + π` → `3.64…`) decided "closed constant" by resolving
  symbols through the _dynamic_ scope chain, so inside a function application a
  bound-but-symbolic parameter counted as known: a body term like `z² + 0.3`
  fired a full-subtree `N()` walk that could make no progress, at every nested
  level, mutually recursive with `evaluate` — ~×7.5 work per nesting level. The
  canonical victim was interpreted evaluation of a recursive function over a
  symbolic argument (`Q(n, z) = Q(n-1, z)² + 0.3`, the iterated-map shape):
  depth 7 took ~8 s and depth 8+ hit the time limit, where the same recursion
  with an exact constant (`3/10`) unwound in milliseconds. The gate is now the
  lexical `isConstant` (every symbol a constant binding) — depth 7 drops ~500×
  to ~15 ms, float and exact now cost the same, and `0.5 + π`, `0.5 + √2`, and
  `0.5 + x` all behave exactly as before. Two neighboring sites sharing the
  wrong predicate returned flat-wrong _values_ inside applications and are fixed
  the same way: `KroneckerDelta(w)` over a bound symbolic parameter returned `0`
  (now stays symbolic), and `Degree(w²)` returned `0` (now `2`).

  Relatedly, many **non-lazy** evaluate handlers re-evaluated operands the
  evaluation driver had already evaluated. Each such call re-descends the whole
  operand subtree, so under nesting the waste compounded — a residual
  ×2-per-level re-walk on top of the bug above. All library handlers now follow
  the handler contract (a `lazy` operator's handler owns its operands' single
  evaluation; a non-lazy handler receives them already evaluated and must not
  re-evaluate): `Power`, `Sqrt`, `Root`, `Divide`, `Ln`, `Log`, `Negate` in
  arithmetic; the linear-algebra operators (`Transpose`, `Determinant`,
  `Inverse`, `MatrixMultiply`, `Norm`, the eigen/decomposition family, matrix
  constructors and predicates, ~30 sites); the statistics reducers
  (`Mean`/`Median`/`Variance`/… — 11 sites); and `Text`. Symbolic recursive
  unwinding is now linear: depth 80 unwinds in ~95 ms where depth ~20 previously
  hit the time limit.

- **`Timing` now measures the actual evaluation.** `Timing` was a non-lazy
  operator, so the engine evaluated its argument _before_ the handler ran and
  the handler then timed a redundant second walk of the already-evaluated result
  — reported times measured cache-warm re-walks, not the computation. `Timing`
  is now `lazy`: the handler receives the raw argument, canonicalizes it outside
  the timed region, and times the real evaluation.

- **One-time cache builds are no longer charged against the time limit.** The
  engine builds some internal tables lazily on first use (constructible trig
  values on the first `sin(π/6)`-style evaluation, the standard simplification
  rule set, etc.). Previously this warm-up ran inside the caller's
  `timeLimit`/`withTimeLimit()` budget, so a tight deadline on a fresh engine
  could lose a large fraction of its budget — or fire mid-build — on the very
  first call. The deadline is now suspended while a cache builds and then pushed
  back by the build's duration, so a time budget measures only the caller's own
  evaluation. Relatedly, a timeout that did fire during a cache build was
  swallowed and resurfaced as an unrelated `TypeError`; an interruption now
  propagates as the `CancellationError` it is, leaving the cache unbuilt so a
  later call retries.

- **Compiled real/complex convention mismatch in branch arms** (js target). A
  provably-real branch arm (`If`/`Which`/`When`) alongside a complex-valued arm
  compiled to a plain number while consumers of the branch read `{ re, im }`
  slots — so a constant base-case arm in a complex-ascribed recursive function
  (`M(0, z) = 0`, the canonical base-case shape) returned NaN at **every**
  point, including points that never left the base clause. Real arms are now
  coerced to the complex convention when any arm is complex (the no-match
  default likewise emits `{ re: NaN, im: NaN }`); wide-typed pass-through arms
  (a `z` slot declared `number` carrying a complex value at run time) stay bare.
  The same coercion now applies at the two sibling seams: a `Typed` complex
  ascription over a provably-real operand (previously silently inert in compiled
  code — an all-real body under a declared `-> complex` return), and a
  provably-real call-site argument bound to a complex-typed parameter of a
  user-defined function (`M(10, 0)` — `Complex(0, 0)` canonicalizes to the real
  literal `0`).

### New Features

- **New engine flag `ce.jit: 'auto' | 'off'`** governing every **implicit**
  compilation path — the new lazy-`Map` auto-compilation (below) and the
  pre-existing compiled numeric kernels (`NIntegrate`/`ND`/`NLimit`, the
  `Integrate`/`Limit` numeric fallbacks, `NDSolve` right-hand sides, the
  solve-domain enumeration sieve, the stochastic-equality probes, the compiled
  `Reduce` fast path). Default `'auto'`: attempts run, and on the first
  environment-level `EvalError` (a strict-CSP host refusing dynamic code) the
  engine latches to `'off'` engine-wide, capping CSP violation reports at one.
  Set `'off'` up front on strict-CSP pages, MV3 extensions, or hardened runtimes
  — or as a diagnostic kill switch. Explicit `compile()` is exempt and keeps
  failing loudly. Implicit compile failures now fall back to the interpreter
  **silently** (previously some of these paths logged a `Compilation fallback`
  warning).

### Performance

- **Lazy-`Map` element lambdas auto-compile on numeric drains.** Draining a lazy
  broadcast (`f(Range(1, 10^5)).N()`, a `PointList` sweep, an `addN`/`mulN`
  broadcast) whose element lambda applies interpreted user-defined functions
  previously paid the full symbolic pipeline per element (~ms/element). At
  **machine precision** (the gate: the default engine precision is bignum and
  never triggers this), such drains now compile the element lambda once per
  logical `Map` — eligibility-gated (pure bodies, literal-bounded loops, no
  unbound free symbols, ambient-scope captures only) — and serve elements from
  the compiled function, ~30–2500× faster with digit parity against the
  machine-precision interpreter. The compiled function is validated before every
  invocation against the same two-axis mutation keys as the comprehension memo,
  so reassigning a captured symbol (even mid-drain) recompiles, while unrelated
  assignments don't thrash the cache. Per-element fallback to the interpreter is
  silent and exact: non-numeric rows, NaN results (re-checked through the
  interpreter so `√x` over a sign-crossing source still yields complex values),
  and ineligible bodies (e.g. containing `Random`) behave exactly as before.

### Benchmarks

#### Numeric performance (200-digit precision)

Median time per call, in **microseconds — lower is better**. `—` means the tool
returned no usable result at that precision.

| Expression         | CE (current) | CE 0.86.1 | SymPy | math.js | Mathematica |
| ------------------ | -----------: | --------: | ----: | ------: | ----------: |
| $\pi^2$            |          7.2 |       8.1 |   179 |     102 |         3.9 |
| $\sin 1$           |           23 |        24 |   224 |     442 |         5.2 |
| $\cos 1$           |           23 |        23 |   224 |     571 |         7.0 |
| $\ln 2$            |           15 |        15 |   344 |   4,392 |         3.7 |
| $e^{\pi}$          |           13 |        13 |   212 |   4,930 |         4.5 |
| $\zeta(3)$         |        1,732 |     1,733 |   265 |       — |          48 |
| $\Gamma(\tfrac13)$ |          928 |       942 |   347 |       — |         219 |
| $\psi(\tfrac13)$   |          777 |       814 | 2,967 |       — |         196 |

#### Symbolic capability & performance

Each cell is **how many times faster than Mathematica** that engine is on the
case (`Mathematica ÷ engine`, so **higher is better**; Mathematica itself is
`1×`). `—` means the engine can't do the case; `✓` means it solves a case
Mathematica can't. Compare the **CE (current)** and **CE 0.86.1** columns to see
what is _new this release_ (a `—` under `0.86.1` next to a number under the
current build). The **CE + R/F** column is the current build with the opt-in
Rubi integrator + Fungrim identities loaded (`loadIntegrationRules` /
`loadIdentities`), on the same minified bundle.

| Operation                              | CE (current) | CE + R/F | CE 0.86.1 | SymPy  | math.js | Mathematica |
| -------------------------------------- | :----------: | :------: | :-------: | :----: | :-----: | :---------: |
| **Antiderivatives**                    |              |          |           |        |         |             |
| $\int\frac{1}{\sqrt x}\,dx$            |     5.4×     |   2.4×   |   4.3×    |  0.5×  |    —    |     1×      |
| $\int\frac{x}{\sqrt{1-x^2}}\,dx$       |     8.2×     |   1.1×   |   7.0×    | 0.09×  |    —    |     1×      |
| $\int\frac{1}{x^3+1}\,dx$              |     4.3×     |   0.5×   |   3.3×    |  0.3×  |    —    |     1×      |
| $\int\frac{\sqrt x}{1+x}\,dx$          |      —       |   1.4×   |     —     |  0.1×  |    —    |     1×      |
| $\int\frac{x}{(1+x)^{1/3}}\,dx$        |      —       |   1.1×   |     —     | 0.01×  |    —    |     1×      |
| $\int\frac{x^2}{(1+x)^{1/3}}\,dx$      |      —       |   1.1×   |     —     | 0.007× |    —    |     1×      |
| **Derivatives**                        |              |          |           |        |         |             |
| $\tfrac{d}{dx}\sqrt{1-x^2}$            |    0.03×     |  0.03×   |   0.02×   | 0.001× | 0.003×  |     1×      |
| **Simplification**                     |              |          |           |        |         |             |
| $\sqrt{3+2\sqrt2}$                     |     33×      |   19×    |    25×    |   —    |    —    |     1×      |
| $\sqrt6\,x+\sqrt2\,x$                  |     88×      |   48×    |    63×    |  3.4×  |   16×   |     1×      |
| **Evaluation**                         |              |          |           |        |         |             |
| $\lim_{x\to0}\tfrac{\sin x}{x}$        |     64×      |   31×    |    60×    |  3.3×  |    —    |     1×      |
| $\lim_{x\to\infty}(1+\tfrac1x)^x$      |     8.5×     |   5.1×   |   8.0×    |  1.9×  |    —    |     1×      |
| $\int_1^2\tfrac1x\,dx$                 |    6388×     |  6536×   |   6482×   |  75×   |    —    |     1×      |
| $\int_{-\infty}^{\infty} e^{-x^2}\,dx$ |     375×     |   144×   |   356×    |  2.2×  |    —    |     1×      |
| **Solving**                            |              |          |           |        |         |             |
| $x^4+x^2-1=0$                          |     0.4×     |   0.3×   |   0.3×    | 0.08×  |    —    |     1×      |
| $x^3-x-1=0$                            |     1.6×     |   1.7×   |   1.6×    | 0.04×  |    —    |     1×      |

Across the cases both solve, Compute Engine is a **median 5.4× faster than
Mathematica** (up to 6388×) — in the browser, not a proprietary kernel.

<sub>Measured 2026-07-20 · Compute Engine `0.87.0` @ `0158397a` (current build)
· published `0.86.1` · SymPy `1.14.0` · math.js `15.2.0` · Mathematica
`14.3.0 for Mac OS X ARM` · Node `v22.13.1`. Correctness is verified numerically
against an independent `mpmath` reference, never another tool. Reproduce with
`npm run build production && ./venv/bin/python3 benchmarks/gen_cases.py && node benchmarks/report.mjs && node benchmarks/report_changelog.mjs`.</sub>

## 0.86.3 _2026-07-19_

### New Features

- **Recursive user-defined functions now compile.** Self- and mutually recursive
  functions (`fact(n) := n ≤ 1 ? 1 : n · fact(n-1)`) compile on the `javascript`
  and `interval-js` targets to true recursion, instead of failing closed.
  Termination is the caller's contract, matching compiled unbounded `Loop`: on
  the `javascript` target runaway recursion throws a catchable `RangeError`; on
  `interval-js` the runner converts runtime errors to the _entire_ interval
  ("cannot bound"), per that target's error philosophy. (The interpreter throws
  `CancellationError` on its time limit instead.) A complex-valued recursive
  function needs a `Typed` `complex` **return** ascription on the function
  literal so the self-call types as a scalar — without it the application types
  `broadcastable<number>` and complex arithmetic over it does not compile. GPU
  targets (GLSL/WGSL) are unchanged: shaders cannot recurse, so recursion stays
  fail-closed there. Measured on a depth-10 iterated Julia map, the recursive
  form runs ~0.18 µs/pt — about an order of magnitude faster than the equivalent
  hand-unrolled closed form compiled before this release.

- **Function literals accept a signature-string shorthand.**
  `["Function", body, "'(n: integer, z: number) -> complex'"]` desugars at
  canonicalization into the structural `Typed` form (typed parameters plus a
  return-type ascription) — one compact string instead of nested `Typed`
  wrappers, reusing the full type grammar. Signatures must name every parameter;
  optional/variadic markers are not yet supported and fall through to the
  standard parameter validation error.

### Performance

- **Small literal integer powers of complex values compile to inline multiply
  chains.** `z^k` for literal `k` = 2…8 with a complex-valued `z` emitted the
  general polar-form power helper (`hypot`/`atan2`/`exp`/…) per evaluation; it
  is now an inline square-and-multiply chain, with the base bound to a const
  exactly once. Iterated-map workloads speed up ~9× (depth-10 Julia closed form:
  1.25 → 0.14 µs/pt). The square is digit-compatible with the interpreter; for k
  ≥ 3 both routes go through different roundings and agree to ~1 ulp. Exponents
  ≥ 9, negative, and non-integer still use the general helper.

### Resolved Issues

- **Degree-mode compilation now reaches user-defined function bodies.** The
  angular-unit rewrite (scaling trig arguments/results so radian-based compiled
  math reproduces `angularUnit` semantics) was applied only to the top-level
  expression: compiling `t ↦ f(t)` where `f(x) := sin(x)` under
  `angularUnit: 'deg'` emitted radian-based trig inside `f`'s definition, while
  the inlined `t ↦ sin(t)` correctly scaled. The rewrite is now applied to each
  emitted user-function body. (Compiled-vs-interpreted agreement for unit-scaled
  trig is ~1 ulp, not digit-exact — the two routes round the unit conversion in
  different orders.)

- **`ce.assign(name, fn)` ties the recursion knot for pre-boxed function
  literals.** A `Function` literal canonicalized _before_ `ce.assign(name, …)`
  (the programmatic box-then-assign route) left its self-reference bound to a
  stale auto-declaration, so the body's types were wrong — for most names the
  self-call typed `any` and compiling the function fail-closed on the collection
  guard, while a lucky subset of names (pre-declared shells such as `K` or `J`)
  masked the bug. `ce.assign` now pre-declares the target as function-typed and
  re-canonicalizes such a literal, matching the behavior of the `Assign`
  operator and of `f(n) := …` parsing. (Naming a function after a built-in
  operator, e.g. `N`, still collides — unchanged.)

## 0.86.2 _2026-07-19_

### Resolved Issues

- **An assigned complex symbol now compiles as complex without an explicit
  declaration.** `ce.assign("z_0", <complex value>)` then compiling an
  expression using `z_0` emitted the binding as a complex object literal while
  the operand analysis read only the DECLARED type (wide `number` / `unknown` ⇒
  real) — `number + {re, im}` arithmetic, silently `NaN` at every point. The
  analysis now derives complex-ness from the assigned value, mirroring the fold.
  Compile-bound variables (loop indices, lambda parameters) shadow the engine,
  so an index named `i` does not pick up the imaginary unit.
- **`Block`/`\coloneq` locals infer complex-ness from their assigned right-hand
  side.** In `w_1 ⩴ (x+iy)² + z_0; w_2 ⩴ w_1² + z_0` the local `w_1` was emitted
  as a complex object but consumed as REAL by later statements (its type
  defaulted to real; outer declares don't reach block locals) — silent all-NaN.
  Locals' complex-ness is now inferred in statement order — a later local
  reading an earlier complex local is itself recognized — shared by every target
  (this also extends the GPU `vec2` local hints to chained locals).
- **Complex `Add` binds compound operands once — nested complex arithmetic now
  compiles in O(tree size).** Each `{re: …, im: …}` slot spliced the full
  operand subexpression twice, doubling code size and runtime per nesting level:
  the depth-10 Julia closed form compiled to ~360 KB (~713 µs/pt). Compound
  complex operands are now bound to consts emitted exactly once: the same form
  compiles to ~1.9 KB and runs ~1.3 µs/pt, with digit-for-digit interpreter
  parity. Symbols and number literals stay inline, so simple shapes emit
  byte-identically.
- **`Max`/`Min` (and `Supremum`/`Infimum`) now type as `number`.** Their
  declared result type was the vestigial union `number | list`, so even
  `Max(1, 2)` typed as `list | number`, a comparison over one typed
  `list<boolean>`, and the compilation targets' scalar-condition assert
  fail-closed every `When` restriction containing a reduction
  (`y = x \{\max(a,x) < 2\}` masked its whole curve). These operators always
  REDUCE — including a collection argument's elements — to a single scalar
  extremum (`ElementMax`/`ElementMin` are the broadcasting variants), so the
  result type is `number` unconditionally. Evaluation is unchanged.

## 0.86.1 _2026-07-19_

### Resolved Issues

- **Materializing a list no longer restructures its eager elements.**
  `evaluate({materialization: true})` on a literal list spliced the contents of
  ANY collection-valued element into the parent — a list of `Tuple` pairs came
  back flattened (`[("a",1), ("b",2)]` → `["a",1,"b",2]`, so the result no
  longer fed `DictionaryFrom`), a nested list literal lost its nesting, and an
  _infinite_ lazy element (`Cycle`) was spread until the evaluation deadline.
  Only finite **lazy** sub-collections are now flattened-and-materialized (the
  documented intent: `[Range(1,3)]` still materializes to `[1,2,3]`); eager
  literals are preserved, and an infinite lazy element stays put as a bounded
  preview.
- **`Take` of an infinite collection with a finite bound is now finite.**
  `Take(Range(1,+∞), 3)` reported `count` 3 but `isFiniteCollection` false (it
  propagated the _source's_ finiteness), which left
  `ListFrom(Take(<infinite>, n))` symbolic. `Take` now reports finite whenever
  its own element count is known-finite; `ListFrom(Take(Range(1, +∞), 3))` →
  `[1,2,3]`. (When the source's count is genuinely unknown —
  `Take(ChunkBy(<infinite>, f), 3)` — finiteness stays unknown, keeping
  materialization previews honest.)
- **`Sort` and `Shuffle` type as `list<…>`.** Both always rebuild a `List`, but
  their static type claimed the _source's_ type (`Sort(Range(1,5))` typed as an
  `indexed_collection`-shaped Range). Both now report `list<element-type>`,
  matching `Take`.
- **`Slice` facets are now coherent over infinite and unknown-length sources.**
  `Slice` claimed `isFiniteCollection` unconditionally, and a negative _start_
  over an infinite source produced a `NaN` count while `at(1)` fabricated the
  element `+oo` (from `source.at(Infinity)`). The facets now share one bounds
  resolver: a negative **end** over an infinite source means "through the end" —
  an honest infinite tail (`Slice(Range(1,+∞), 5, -1)`: count `∞`, not finite,
  `at(1)` = 5, `Take(…, 3)` → `[5,6,7]`); a negative **start** over an infinite
  source ("the last k elements") is unresolvable and stays inert; an
  unknown-length source now reports finiteness as unknown rather than true.
  Bounded positive windows are unchanged (`ListFrom(Slice(Range(1,+∞), 1, 5))` →
  `[1,2,3,4,5]`).
- **`Sum`/`Product` bodies that bind looser than multiplication are now fenced
  when serialized.** The big-op body is parsed back at multiplication
  precedence, so an additive body's trailing terms escaped the operator on
  re-parse: `Sum(i + 1, i=1..3)` serialized as `\sum_{i=1}^3i+1`, which
  re-parses as `(\sum_{i=1}^{3}i)+1` — 9 became 7 — and a body-bound index in
  the escaped terms degenerated to a free symbol (`i` → the imaginary unit,
  turning real product expansions complex-valued). Additive (and other
  looser-than-multiplication) bodies now serialize parenthesized
  (`\sum_{i=1}^3(i+1)`); tighter-binding bodies (`2i`, `\frac{1}{i}`, a bare
  symbol) are unchanged.
- **Fused stepped ranges with a fraction or compound second anchor now parse to
  the intended `Range`.** `[0,\frac{1}{6}...1]` parsed to
  `List(0, Range(1/6, 1))` — silently wrong values — because the sample reader
  did not recognize fraction literals; it now yields `Range(0, 1, 1/6)` with an
  EXACT rational step (a float `0.1666…` step would drift and miss the end
  anchor; the range lands exactly on `1`). And `[m+n,m+n+15...m+n+60]` parsed to
  a nested-`Range` `List` — the `...` infix binds its left operand tight, so the
  continuation range was embedded in the additive tail
  (`Add(m, n, Range(15, m+n+60))`); the normalization now recovers the true
  second sample and end anchor, yielding `Range(m+n, m+n+60, 15)`. Both rewrites
  keep the provenance guard: only ellipsis/`..`-written ranges participate — an
  explicit `\operatorname{Range}(…)` element (bare or embedded in a sum) stays a
  literal `List` entry.

## 0.86.0 _2026-07-19_

### New Features

- **`ce.withTimeLimit(ms, fn)`** — run a block of work under a single evaluation
  deadline. Ordinarily each top-level `evaluate()` arms its own `timeLimit`
  budget, so a long sequence of short evaluations — e.g. draining a lazy
  collection element by element via `each()`/`at()` — can run unboundedly
  without ever tripping the limit. Wrapping the loop in `withTimeLimit()` arms
  one shared deadline for its full duration: any evaluation inside throws
  `CancellationError` (`cause: 'timeout'`) once the deadline is exceeded.
  Re-entrant (an inner call can only shorten the effective deadline, never
  extend it).

### Resolved Issues

- **`.N()` of `Tuple ± scalar·Tuple` no longer throws at machine precision.**
  When every term of a component sum was an integer-valued machine float
  (`20 − 0.1·20`), the exact summation path read `bignumRe` — which is undefined
  on a machine numeric value — and threw
  `TypeError: Cannot read properties of undefined (reading 'toFixed')`. At scale
  this killed composed lazy streams mid-drain (a 4001-point
  `PointList − scalar·PointList` died at the first integer-valued element) and
  made `at(k)` return `undefined` at the crashing indices. The integer fold now
  converts the integral machine value directly.
- **A solidus-rendered fraction juxtaposed with following material keeps
  explicit grouping.** Serializing a non-canonical
  `InvisibleOperator(Divide(1, 2), Delimiter(…))` at nesting depth > 3 (where
  the default fraction style switches to an inline solidus) emitted `1/2(sq)` —
  which re-parses as `1/(2·s·q)`, silently changing the value. The solidus form
  is now parenthesized (`(1/2)(sq)`); `\frac`-rendered fractions and
  trailing-position solidus fractions are unchanged.
- **`Multiply(symbol, Tuple)` serializes with an explicit multiplication sign.**
  `s(1,2,3)` re-parses as a function CALL `["s", 1, 2, 3]` for any symbol the
  parser cannot prove non-applicable, silently turning a product into an
  application. A bare-symbol factor followed by a parenthesized comma-group now
  serializes as `s\times(1,2,3)`. Single-expression groups (`s(x+1)`) and
  number-led products (`2(1,2)`), which re-parse as products, keep
  juxtaposition.
- **`CountIf`, `Position`, `Ordering`, `DictionaryFrom`, and `RecordFrom` stay
  inert on an infinite or unknown-length collection.** These operators require
  walking every element, so on an infinite input (`Range(1, +∞)`, `Cycle`,
  `Iterate`) they previously consumed the entire evaluation time limit and then
  threw `CancellationError` instead of returning a result. They now detect the
  non-finite input structurally and stay symbolic immediately. (`Ordering`
  previously returned a spurious _empty list_, claiming a complete ordering it
  never computed.) Huge-but-finite inputs still walk under the deadline as
  before, and `Find` is unchanged: it streams and short-circuits, so
  `Find(Range(1, +∞), x ↦ x > 5)` still returns `6`.

### Collections

- **`Insert`, `DeleteAt`, `ReplaceAt`, `Partition` (chunk and window forms),
  `SlidingWindow`, and `ChunkBy` are now hybrid-lazy.** Inputs at or below the
  100-element eager threshold evaluate to an eager `List` exactly as before
  (byte-identical shapes); larger, lazy, or infinite inputs stay symbolic and
  serve their elements on demand through `count`/`at`/ iteration, following the
  same convention as the hybrid-lazy broadcast forms. `Insert` into a
  million-element `Range` no longer materializes the whole list to answer
  `Count` or an index probe, and streaming prefixes of infinite results now
  work: `Take(Partition(Range(1, +∞), 3), 2)` → `[[1,2,3],[4,5,6]]`,
  `Take(ChunkBy(Cycle([1,1,2]), x ↦ x), 3)` → `[[1,1],[2],[1,1]]`. `Partition`'s
  predicate form (`Partition(xs, pred)` → `[trueGroup, falseGroup]`) requires a
  finite input and is unchanged. Materializing consumers (`ListFrom`, …) behave
  as before.

## 0.85.1 _2026-07-18_

### Performance

- **Large `PointList` transposes and point-coordinate projections are now
  hybrid-lazy, and scalar arithmetic over lazy broadcasts composes lazily
  instead of grinding (or staying inert).** Numeric evaluation of
  `scalar × (PointX(P), PointY(P), …)` over a 4001-point `PointList` of
  broadcast components took ~600–1000 ms per product — each coordinate
  projection eagerly transposed the whole point list into n `Tuple`s (at ~150
  µs/element of per-element boxed re-canonicalization) only to project one slot
  back out — and could exceed the 2 s `timeLimit` on a full plot row. Three
  coordinated changes, all hybrid (collections at or below the 100-element eager
  threshold are byte-identical to the previous shapes):
  - `PointList` past the threshold transposes to the lazy `Map` form (consumable
    via `at`/`each`/`count`) instead of materializing every point-`Tuple`.
    **BREAKING (shape)**: a >100-point `PointList` now evaluates to a lazy
    `Map`, not an eager `List` — element values are unchanged.
  - `PointX`/`PointY`/`PointZ` project lazily past the threshold, and project
    straight to the source collection when the operand is the lazy transpose
    form (`PointX(PointList(a, b, c))` ≡ `a` for equal-length components; ragged
    or scalar slots keep transpose semantics).
  - `addN`/`mulN` re-dispatch their broadcast branches after numeric operand
    evaluation, so an operand that only becomes a collection through evaluation
    (`Mod(L, 11)` over a list `L`) now composes into the lazy `Map` form —
    previously the product/sum was silently left inert (`0.2 · ⟨collection⟩`
    unreduced). The eager broadcast zip also streams its operands with hoisted
    iterators instead of per-index `at()` calls (which re-instantiated a lazy
    `Map`'s mapping lambda on every access). The filed repro (a 4001-member 3D
    `vector` row) went from a 2 s timeout to ~10 ms of lazy composition, with
    materialization deferred to the consumer sweep.

### Resolved Issues

- **String `vars` values now splice into compiled JavaScript and Python as
  source, not as string literals.** The `vars` compile option is the live-path
  contract: a mapped symbol always stays a runtime input instead of having its
  assigned value folded into the emitted code — so one engine state can serve
  both a compile-once path (sliders as runtime arguments) and a fold-early
  evaluate path. The GLSL and interval targets honored it, but the JavaScript
  and Python targets JSON-stringified the mapping, so
  `compile(expr, { vars: { s: '_.s' } })` emitted `Math.sin("_.s" * _.x)` — a
  string literal yielding `NaN` at run time. A string value is now spliced
  verbatim (`Math.sin(_.s * _.x)`); a non-string value still bakes as a constant
  (`vars: { a: 7 }` → `7`), unchanged.

## 0.85.0 _2026-07-18_

### New Features

- **`DSolve` frontier round — parity with SymPy on the ODE audit (50/51, 0
  wrong).** Four new solvable classes:
  - **Nonhomogeneous Cauchy–Euler** (`x²y″ + bxy′ + cy = g(x)`): an x-power
    indicial ansatz for power forcing (`x²y″ + xy′ = x` → `c₁ + c₂·ln x + x`),
    with a variation-of-parameters fallback for resonant or non-power forcing.
  - **The Airy family** `y″ = (px + q)·y`: solutions as
    `c₁·AiryAi(t) + c₂·AiryBi(t)` with `t = ∛p·x + q/∛p²` (real cube root,
    either sign of `p`).
  - **Airy-type Riccati** `y′ = q₀(x) + q₂·y²` (constant `q₂`, linear `q₀`): the
    `y = −u′/(q₂u)` linearization yields the one-parameter
    `(Ai′ + C·Bi′)/(Ai + C·Bi)` family — `y′ = x + y²` now solves (SymPy errors
    on it).
  - **Repeated-eigenvalue first-order linear systems**: diagonal systems of any
    size and defective 2×2 systems via a generalized eigenvector, gated on an
    exact `(A−λI)² = 0` check so near-repeated numeric eigenvalues stay inert
    rather than producing an approximately-wrong solution.
- **`AiryAiPrime` / `AiryBiPrime` operators** (derivatives of the Airy
  functions), with machine-precision numerics across all three DLMF regimes and
  full derivative closure (`Ai′ → AiryAiPrime`, `AiryAiPrime′ → x·Ai(x)` — so
  repeated differentiation of Airy expressions evaluates and numericizes).

- **`NDSolveFunction` — ODE solutions as applicable functions.** Where `NDSolve`
  returns a sample `List`, the new `NDSolveFunction` (same arguments, without
  the sample count) returns the solution as a callable function — a `Function`
  literal wrapping the new `InterpolatingFunction` operator, which holds the
  adaptive solver's piecewise-quartic dense-output table. Assign it and evaluate
  anywhere in the integration interval
  (`f := NDSolveFunction(y′ = y, y, (x, 0, 1), 1)`; `f(0.5)` → `1.6487…`), at
  the integration accuracy; outside the interval the value clamps to the nearest
  endpoint, and a symbolic argument stays symbolic. The solution compiles to
  plain JavaScript — `compile(f)` yields a positional lambda (`run(0.5)`), and
  `compile(f(t))` an expression over `t` (~1 µs per evaluation) — and LaTeX
  display elides the data table
  (`\operatorname{InterpolatingFunction}_{[0, 1]}(x)`); the full table
  round-trips through MathJSON. Scalar equations (first-order and higher-order)
  are supported; the multi-dependent system form stays inert.

### Improvements

- **`NDSolve` now uses adaptive stepping (Dormand–Prince 5(4)) with dense
  output.** The output is unchanged in shape — a `List` of `steps + 1` uniform
  `[x, y]` samples — but the values are now tolerance-controlled: integration
  adapts its internal step size (embedded 4th/5th-order error control) and the
  uniform grid is emitted from the quartic dense-output interpolant. Fixed-step
  RK4 silently lost accuracy near rapid transients (`y′ = −50(y − cos x)` over
  `[0, 3]` with 100 steps erred at ~4·10⁻⁵; now ~2·10⁻¹²). Non-integrable
  problems (finite-time blow-up, tolerance failure) leave `NDSolve` inert rather
  than returning inaccurate samples.

- **Truncation dots after a repeating decimal tail now parse exactly.**
  `0.999\ldots` → `1`, `0.333\ldots` → `1/3`, `0.1212\ldots` → `4/33`: a
  truncation marker after decimal digits ending in an evident repetend (a block
  repeated at least 3 times for single digits, at least twice for longer blocks)
  is read as the exact repeating decimal. Non-repeating tails (`3.1415\ldots`)
  keep the previous behavior (the marker is display-only).

- **`nPr(n, k)` parses in lenient mode** as the k-permutation count
  `P(n, k) = C(n, k)·k!`, joining the existing `nCr(n, k)` → `Binomial`.

- **The infinite Sum/Product closed-form table grew substantially.** New
  exactly-evaluated families (each numerically verified): alternating p-series
  (`Σ (−1)^{k+1}/k → ln 2`, `Σ (−1)^{k+1}/k² → π²/12`), odd p-series
  (`Σ 1/(2k−1)² → π²/8`), Dirichlet beta (Leibniz `Σ (−1)^k/(2k+1) → π/4`;
  `β(2) →` Catalan's constant; `β(3) → π³/32`; `β(5) → 5π⁵/1536`), the
  exponential series (`Σ 1/k! → e`, `Σ xᵏ/k! → eˣ`, shifted starts adjusted
  exactly), the first-moment geometric series (`Σ k/2ᵏ → 2`; symbolic ratio →
  `x/(1−x)²` guarded on `|x| < 1`), and the logarithmic series
  (`Σ 1/(k·2ᵏ) → ln 2`; symbolic ratio → `−ln(1−x)`, same guard). New
  infinite-product entries: `Π_{k≥a} (1 − 1/k²) → (a−1)/a`,
  `Π (1 − 1/(2k+1)²) → π/4`, and `Π (1 + 1/k²) → sinh(π)/π` (previously
  numeric-only). Divergent or out-of-table shapes stay symbolic, as before.

## 0.84.2 _2026-07-18_

### Performance

- **Removed a per-call inference-snapshot tax that had slowed the whole engine
  by ~1.4× since 0.74.0.** Every top-level boxing or parsing operation eagerly
  snapshotted the set of inferred symbols by walking every binding in every
  scope — including the entire standard library — to provide provenance for the
  fresh-matrix-inference repair (`Determinant(A + B)` inferring `A`, `B` as
  matrices), a consumer that runs only when a matrix-typed parameter mismatches.
  The provenance is now computed forward: `BoxedSymbol.infer()` records a
  definition when its type first transitions unknown → concrete during a boxing
  operation, and the repair's eligibility reads that log. Matrix inference
  behavior is unchanged (pinned by a 13-case matrix in
  `matrix-operator-typing.test.ts`); eligibility is now keyed on definition
  identity rather than name, so a name whose fresh inner-scope definition was
  popped no longer masks an outer definition. Measured recovery: `π.N()` at 200
  digits 2.5 µs → 0.12 µs (21×, faster than 0.73.0); `∫ 1/(x³+1)` 5.8 ms → 1.6
  ms (3.7×); the drift vs 0.73.0 across the benchmark suite is eliminated.

### Improvements

- **The sign of integer powers of pure-imaginary bases is now determined.** For
  `z` of type `imaginary`, `z²` reports `negative`, `z⁴` `positive` (the cycle
  `(βi)^p = (-1)^{p/2}·β^p` for even `p`, including negative exponents:
  `(2i)^{-2} = -1/4`), and odd powers report `unsigned` (pure imaginary). Powers
  of a general finite non-real base report `not-zero` — a non-real value is
  necessarily nonzero. Previously all of these were indeterminate: the handler
  branch that addressed non-real bases was unreachable, and wrong as written (it
  claimed _every_ even power of a non-real base was negative — but `i⁴ = 1` and
  `(1+i)² = 2i`).

### Improvements

- **`Abs` typing now follows the operand's finiteness.** `|x|` of a provably
  finite operand (real or complex) types `finite_real` instead of the
  signature's generic `real`; a provably infinite operand types
  `non_finite_number`, and a literal `NaN` types `number`. Finiteness also
  propagates structurally (`|x|` is finite iff `x` is), which makes signs of
  products of absolute values determinate: `|x|·|y|` for finite `x`, `y` now
  reports `non-negative` (this path previously hit a latent inverted parity
  claim in `Multiply` — see the sgn audit below — and before that was masked
  entirely).

### Resolved Issues

- **Lazy broadcast over a declared-`unknown` symbol no longer throws
  `Not canonical` (Tycho item 42).** Evaluating `mod(L, N)/N` with `L` a
  declared-`unknown` symbol holding a >100-element list built the lazy
  `Map(L, …)` over the SYMBOL, whose static type is `unknown`; every lazy
  collection operator's canonical handler hard-rejected such a source and
  `boxFunction` fell back to a silently NON-canonical expression, which the
  first arithmetic composition rejected with a thrown assert. Lazy collection
  canonical handlers now admit operands whose type is merely indeterminate
  (`unknown`/`any`/`value`/`broadcastable`) — provably-scalar operands still
  reject — and `Map` over such a source keeps value-aware indexed-ness (`type`,
  `at`, `count`, and the display preview, which no longer renders with a
  misleading `Set` head). The composed lazy result is consumable and honors
  `x.N() ≡ x.evaluate().N()`.

- **A user symbol shadowing a builtin no longer breaks function application of
  that builtin.** With `N := 85` declared (ubiquitous in Desmos-style
  documents), any `["N", …]` application — including the engine's own internal
  `N(…)` wrapper that makes lazy `.N()` elements float on access — resolved to
  the user's number and produced an `incompatible-type` error (surfacing as
  `Nothing` elements in lazy maps). Operator-position binding now defers a value
  definition that provably cannot be applied (a plain number, string,
  collection…) to an outer applicable definition of the same name;
  value-position references (`N + 1`) still resolve to the user's value.
  (Consequence: after prose-style devolution of an un-applied builtin — `N + 1`
  — a later `N(3.14159, 2)` now numericizes instead of staying symbolic.)

- **The JavaScript compile target's floored-`Mod` emission is parenthesized
  (Tycho item 43).** The fragment `((a % b) + b) % b` was emitted without outer
  parentheses; composed as a `Multiply`/`Divide` factor, JS's left-associative
  same-precedence `%` reduced the whole product mod `b` (`c * ((x % 1) + 1) % 1`
  ≡ `(c·(x%1+1)) % 1`), silently value-wrong whenever the product's magnitude
  reached the divisor. The standalone form was correct, which is why it
  survived. Compiled and interpreted now agree on the Neyret-hash idiom
  `Σ cos(i)·mod(10⁴sin(10⁴i), 1)`.

- **`Sum`/`Product` over a collection-valued body type as the collection, and
  `At` extracts element types (Tycho item 44).** A big-op whose body types
  `vector<2>` (e.g. summing scaled calls of `a(t) := [cos t, sin t]`) typed
  `number`, so indexing the sum baked an `incompatible-type` error at parse
  time; it now types `vector<2>`. `At` on a `tuple`-typed operand with a literal
  index types the selected slot, and an inference widen-guard stops a loose
  parameter type from coarsening an already-precise inferred function result
  (this made `A(t)[1]` type `any`; it now types `number`).

- **`At` over a typed-collection application compiles; a collection-valued
  big-op body fails closed instead of emitting wrong code (Tycho item 45).**
  `a(x)[1]` with `a` returning `vector<2>` now compiles (the collection gate is
  type-aware, so `_SYS.at` is emitted). A compiled `Sum`/`Product` whose body is
  collection-typed previously emitted scalar accumulation over arrays — NaN or
  string concatenation, silently wrong; it now fails closed (D6) with a hint to
  distribute the element access through the big op.

- **Applying a function to a symbolic argument that mentions the parameter's own
  name no longer overflows the stack under `.N()` (Tycho item 46).** `a(t+1)`
  for `a(t) := [cos t, sin t]` with `t` unbound: symbol values resolve by name
  through the evaluation context, so `BoxedSymbol.N()` recursed through the
  call-frame binding forever (`t → t+1 → t → …`). `.N()` now substitutes a
  self-referential context value once without numericizing through it —
  mirroring plain `evaluate()` — so nested helper-call expressions (the Tycho
  item-46 `PointList(A(t)[1], A(t)[2])` repro) evaluate symbolically, verified
  against direct numeric evaluation.

- **Desmos-style range ellipsis with an elided comma parses again (Tycho item
  47, regression of the 0.76.0 "request 6" class).** `[0,...300]` →
  `Range(0,300)` (was an inert `List(0, ContinuationPlaceholder·300)`),
  `[1,...N]` and `[0,...3N^{2}-1]` likewise, and the stepped `[0,15...210]` →
  `Range(0, 210, 15)` (was the silently WRONG `List(0, Range(15,210))`).
  Fully-comma'd, bare-fused (`[1...5]`, `[-3N...3N]`), and nested-group
  (`[f(a,b)...5]`) forms are unchanged. Compound-symbolic stepped anchors
  sharing an identical additive base with numeric offsets now infer too:
  `[m+n, m+n+15, ..., m+n+60]` → `Range(m+n, m+n+60, 15)`; differing bases or
  non-numeric offsets stay a literal `List`. Stepped-range inference only
  applies to ranges the ellipsis syntax itself produced — a list literal ending
  in an explicit `\operatorname{Range}(a,b)` element stays a `List`.

- **A `Range` operand of a tighter-binding parent now serializes parenthesized
  (Tycho item 48).** `..` parses its end operand at a precedence below `Add`, so
  `Add(Range(0, L-1), 3)` serialized as `0..(L-1)+3`, which re-parses as
  `Range(0, L+2)` — wrong values on any serialize→re-parse round-trip (with
  `L = 5`, an 8-element list instead of the shifted 5-element one). A `Range`
  under `Add`/`Subtract`/`Multiply`/ `Power`/solidus-`Divide` parents now wraps
  in parentheses (`(0..(L-1))+3`); bare and stepped ranges serialize unchanged.
  Same round-trip precedence class as the 0.83.2 `Mod` fix.

- **GPU targets emit a shape-matched NaN for masked conditional branches (Tycho
  item 49).** A `When`/`Which` whose value is a tuple body — a restricted
  parametric `(x(t), y(t))` with `\{0 \le t \le 1\}` — compiles the value to a
  `vec2`, but the masked branch emitted a scalar NaN: GLSL has no implicit
  float→vecN conversion in a ternary, so the driver rejected the shader and
  every restricted parametric member lost its GPU sampling path. The NaN branch
  is now vectorized to the value's component count (`vec2(_gpu_nan())` on GLSL,
  `vec2f(bitcast<f32>(…))` on WGSL — WGSL's `select` requires matching operand
  types); scalar bodies are unchanged.

- **Sign (`sgn`) handler audit.** A mathematical-correctness pass over all ~69
  `sgn` handlers fixed a dozen wrong claims (each could mislead simplifications
  or comparisons built on `isPositive`/`isNegative`): `Gamma(0)` and `Gamma(-n)`
  reported `zero`/indeterminate instead of recognizing poles; `Log` with a
  negative base claimed a real sign (the sign only flips for a base in (0,1));
  `Truncate(1/2)` claimed `positive` (truncation of |x| < 1 is 0); `Round(-1/2)`
  claimed `zero` while `evaluate` rounds halves away from zero (−1); `GCD(0,0)`
  and `LCM(0,n)` claimed `positive` (both are 0); `Floor`/`Ceil` of a complex
  number used the sign of the raw real part instead of the rounded one
  (`⌊0.5+0.5i⌋ = 0`); `Factorial(-1/2)` claimed non-real (it is `Γ(1/2) = √π`;
  only negative _integers_ are poles, same fix for `Factorial2`); `Abs(NaN)`
  claimed `positive`; `Random(-5, 5)` claimed `non-negative`; tensor `Rank` of a
  scalar claimed `positive` (it is 0); and a latent parity inversion in
  `Multiply` swapped `non-negative`/`non-positive` for products of
  sign-indefinite factors. `Arctan` now reports the sign of its argument (it
  previously never produced one).

- **A single-letter builtin operator used as a variable now stays connected to
  later assignments.** Prose-style input like `N \equiv 1 \pmod 5` devolves the
  un-applied builtin `N` to an unknown symbol, but when every other operand
  validated cleanly the devolved symbol was discarded and the expression kept
  the original symbol, still bound to the builtin operator: a later
  `N \coloneq 11` was invisible and the expression stayed stuck symbolic. The
  substituted operand is now retained (same fix for operands re-typed by
  matrix-context inference repair), so assigning the variable evaluates as
  expected.

### Benchmarks

#### Numeric performance (200-digit precision)

Median time per call, in **microseconds — lower is better**. `—` means the tool
returned no usable result at that precision.

| Expression         | CE (current) | CE 0.84.1 | SymPy | math.js | Mathematica |
| ------------------ | -----------: | --------: | ----: | ------: | ----------: |
| $\pi^2$            |          7.0 |        12 |   184 |     110 |         4.0 |
| $\sin 1$           |           21 |        26 |   226 |     479 |         5.3 |
| $\cos 1$           |           21 |        25 |   230 |     667 |         7.1 |
| $\ln 2$            |           14 |        18 |   356 |   5,093 |         3.1 |
| $e^{\pi}$          |           12 |        17 |   215 |   4,862 |         4.5 |
| $\zeta(3)$         |        1,573 |     1,620 |   268 |       — |          49 |
| $\Gamma(\tfrac13)$ |          914 |       921 |   366 |       — |         225 |
| $\psi(\tfrac13)$   |          749 |       743 | 6,854 |       — |         188 |

#### Symbolic capability & performance

Each cell is **how many times faster than Mathematica** that engine is on the
case (`Mathematica ÷ engine`, so **higher is better**; Mathematica itself is
`1×`). `—` means the engine can't do the case; `✓` means it solves a case
Mathematica can't. Compare the **CE (current)** and **CE 0.84.1** columns to see
what is _new this release_ (a `—` under `0.84.1` next to a number under the
current build). The **CE + R/F** column is the current build with the opt-in
Rubi integrator + Fungrim identities loaded (`loadIntegrationRules` /
`loadIdentities`), on the same minified bundle.

| Operation                              | CE (current) | CE + R/F | CE 0.84.1 | SymPy  | math.js | Mathematica |
| -------------------------------------- | :----------: | :------: | :-------: | :----: | :-----: | :---------: |
| **Antiderivatives**                    |              |          |           |        |         |             |
| $\int\frac{1}{\sqrt x}\,dx$            |     6.4×     |   2.7×   |   3.4×    |  0.4×  |    —    |     1×      |
| $\int\frac{x}{\sqrt{1-x^2}}\,dx$       |     9.6×     |   1.7×   |   6.0×    | 0.09×  |    —    |     1×      |
| $\int\frac{1}{x^3+1}\,dx$              |     7.5×     |   0.9×   |   1.1×    |  0.4×  |    —    |     1×      |
| $\int\frac{\sqrt x}{1+x}\,dx$          |      —       |   1.9×   |     —     | 0.09×  |    —    |     1×      |
| $\int\frac{x}{(1+x)^{1/3}}\,dx$        |      —       |   1.3×   |     —     | 0.01×  |    —    |     1×      |
| $\int\frac{x^2}{(1+x)^{1/3}}\,dx$      |      —       |   1.1×   |     —     | 0.007× |    —    |     1×      |
| **Derivatives**                        |              |          |           |        |         |             |
| $\tfrac{d}{dx}\sqrt{1-x^2}$            |    0.03×     |  0.02×   |   0.01×   | 0.001× | 0.004×  |     1×      |
| **Simplification**                     |              |          |           |        |         |             |
| $\sqrt{3+2\sqrt2}$                     |     39×      |   29×    |    23×    |   —    |    —    |     1×      |
| $\sqrt6\,x+\sqrt2\,x$                  |     79×      |   45×    |    43×    |  2.8×  |   15×   |     1×      |
| **Evaluation**                         |              |          |           |        |         |             |
| $\lim_{x\to0}\tfrac{\sin x}{x}$        |     59×      |   29×    |    21×    |  1.2×  |    —    |     1×      |
| $\lim_{x\to\infty}(1+\tfrac1x)^x$      |     9.6×     |   5.1×   |   4.5×    |  2.3×  |    —    |     1×      |
| $\int_1^2\tfrac1x\,dx$                 |    6653×     |  6680×   |   2654×   |  63×   |    —    |     1×      |
| $\int_{-\infty}^{\infty} e^{-x^2}\,dx$ |     402×     |   104×   |   159×    |  2.5×  |    —    |     1×      |
| **Solving**                            |              |          |           |        |         |             |
| $x^4+x^2-1=0$                          |     0.2×     |   0.2×   |   0.1×    | 0.07×  |    —    |     1×      |
| $x^3-x-1=0$                            |     1.6×     |   1.8×   |   1.0×    | 0.03×  |    —    |     1×      |

Across the cases both solve, Compute Engine is a **median 7.5× faster than
Mathematica** (up to 6653×) — in the browser, not a proprietary kernel.

<sub>Measured 2026-07-18 · Compute Engine `0.84.2` @ `40cc077a` (current build)
· published `0.84.1` · SymPy `1.14.0` · math.js `15.2.0` · Mathematica
`14.3.0 for Mac OS X ARM` · Node `v22.13.1`. Correctness is verified numerically
against an independent `mpmath` reference, never another tool. Reproduce with
`npm run build production && ./venv/bin/python3 benchmarks/gen_cases.py && node benchmarks/report.mjs && node benchmarks/report_changelog.mjs`.</sub>

## 0.84.1 _2026-07-17_

### Resolved Issues

- **`Equal`/`NotEqual` over a possibly-collection operand now compile on the
  `javascript` target with an interpreter-faithful runtime dispatch.** A
  comparison like `q(2) = 9` where `q` is declared `(number) -> unknown` — so
  the call _may_ return a collection at run time — previously failed closed
  (`success: false`, interpreter fallback). The binary form now lowers to a
  runtime helper mirroring the interpreter shape by shape: scalar operands
  compare tolerantly (within `engine.tolerance`, complex via the modulus), an
  array-vs-scalar pair is element-wise (`[1,4,4] = 4` → `[false, true, true]`),
  and an array-vs-array pair is whole-collection equality — a single boolean,
  `false` on a length or shape mismatch, recursive over nested arrays. The
  chained (n-ary) form over a possibly-collection operand still fails closed:
  its pairwise `&&` conjunction is only sound over scalar booleans.

- **Equality with an operand that only becomes a collection at evaluation no
  longer produces a cartesian nest.** `L(1) = [1,2]` where `L` is declared
  `(number) -> unknown` and returns a list fanned the literal out _before_
  evaluation (the opaque call not yet being a collection), then broadcast again
  element-wise once it was — yielding a 2×2 list of lists of booleans. It now
  follows the documented, representation-independent rule that literal,
  symbol-bound and lazy collections already follow: two collections compare as a
  single boolean (`L(1) = [1,2]` → `True`), and a runtime scalar against a
  literal list still broadcasts element-wise.

- **`.N()` of an already-evaluated lazy `Map` now yields numeric elements.** The
  0.84.0 fix wrapped a lazy broadcast's elements in `N` only when the broadcast
  was _constructed_ under `.N()`; calling `.N()` on an already-evaluated lazy
  `Map` was an identity, so `Sin(Range(1, 200)).evaluate().N()` streamed exact
  elements (`sin(1)`, `sin(2)`, …) from both `each()` and `At`. Requesting a
  numeric approximation of a lazy `Map` now rewraps its mapping function so
  every element floats on access — restoring `x.evaluate().N()` ≡ `x.N()` —
  while `evaluate()` alone still keeps elements exact and the result stays lazy
  (O(1) on `Sin(Range(1, 10^8))`).

## 0.84.0 _2026-07-17_

### Resolved Issues

- **`Sort` and `Shuffle` of a non-`List` collection returned corrupt results.**
  Both rebuilt their result with the _source_ collection's operator, so the
  sorted/shuffled elements were reinterpreted as constructor arguments:
  `Sort(Range(1, 10))` produced `Range(1, 2, 3)` — the one-element list `[1]` —
  and `Shuffle(Range(1, 5))` could produce an empty collection. Both now return
  a `List`, matching every other eager collection operation. `Sort` of an
  infinite or unknown-length collection now stays inert instead of returning an
  empty collection.

- **Negative indices now work uniformly across all indexed collections.**
  Negative-index normalization (`-1` = last element) was implemented
  per-collection and most lazy collections lacked it, so `Last`, `At(xs, -1)`
  and anything built on end-relative access silently returned `Nothing` — or
  worse: `Reverse` walks its source from the end, so
  `ListFrom(Reverse(Range(1, 5)))` returned `[]` instead of `[5,4,3,2,1]`.
  Normalization is now centralized in the index dispatcher and works for
  `Range`, `Linspace`, `Zip`, `Scan`, `Differences` and every other indexed
  collection with a known finite length; infinite or unknown-length collections
  correctly return `Nothing` without enumerating.

- **A `Filter` over more than ~1000 elements crashed numeric canonicalization.**
  Determining whether a `Filter` result was finite ran its `count` handler,
  which walks the source applying the predicate — and throws
  `iteration-limit-exceeded` past the iteration limit. Boxing an expression as
  simple as `Filter(Range(1, 100000), p) + 1` threw. Finiteness and emptiness of
  a `Filter` are now answered structurally (a filter of a finite collection is
  finite) without running the predicate, and `count` of a filter of an infinite
  or unknown-length collection now correctly reports unknown instead of claiming
  `Infinity` (a filter of an infinite collection can be finite:
  `Filter(Range(1, ∞), x < 5)` has 4 elements).

- **`IsEmpty` and `Contains` no longer answer `False` when the answer is
  unknown.** Both coerced an undetermined result to a definite `False`:
  `IsEmpty(Filter(Range(1, 10^5), x ↦ False))` — a collection that _is_ empty,
  but whose emptiness can't be established within the iteration limit — returned
  `False`. Both predicates are now three-valued and stay inert (unevaluated)
  when the answer cannot be determined.

### Collections

- **A comprehension's element memo now survives unrelated evaluations.**
  Elements of a comprehension (`[x^2 for x in xs]`) are cached per instance, but
  the cache was keyed on an engine-wide counter that every scoped evaluation —
  any `\sum`, `Block` or big operator — bumps on exit, so in a live document the
  cache never survived between two reads and every re-read re-evaluated the body
  per element (the Tycho/Graph Paper team measured a document that bound
  comprehensions lazily analyzing ~3× slower than one that eagerly materialized
  them). The cache is now invalidated only by semantic mutations: reassigning a
  free variable the body reads (directly or through a helper function it calls),
  redeclaring or inferring an operator, `assume()` and `forget()` — including
  the implicit revert when a scope that assumed exits — all refresh it, and a
  comprehension nested under a `Sum`/`Product` refills for each value of the
  enclosing binder. Unrelated evaluations between reads leave the cache intact.

- **`IdentityMatrix`, `ZeroMatrix`, `OnesMatrix` and `Diagonal` of a vector are
  now safe at huge dimensions.** These constructors eagerly built the full m×n
  matrix with no size limit — `IdentityMatrix(10^6)` attempted 10¹² elements.
  Above 10,000 total elements they now produce a lazy indexed collection with
  O(1) construction and element access; at or below, the result is the same
  materialized matrix as before, compatible with `Determinant`, `Inverse` and
  the rest of the dense linear-algebra operations.

- **`.N()` of a lazily-broadcast operation yields numeric elements.** When a
  broadcast returns a lazy `Map` (see below), requesting a numeric approximation
  now wraps the mapped function so each element is computed as a float on
  access: `Sin(Range(1, 10^8)).N()` elements are numbers, while `evaluate()`
  keeps them exact (`sin(1)`, `sin(2)`, …).

- **Element-wise operations over infinite and unknown-length collections are now
  lazy instead of inert — or truncated.** Broadcasting an element-wise operator
  over an infinite collection (`Cycle`), a finite collection of unknown size
  (`Filter`), or a symbolic-length `Range` now returns a lazy `Map` supporting
  `First`, `At`, `Take` and `Length`, rather than staying an inert expression:
  `Add(Cycle([1,2]), 1)` is the lazy `[2,3,2,3,…]`, and `Add(Range(1, n), 1)`
  with `n` a declared, unassigned integer becomes `Map(Range(1, n), _ ↦ _ + 1)`,
  which picks up `n`'s value reactively when later evaluated. This also fixes a
  wrong-answer bug: the eager broadcast read a `Filter`'s unknown length as 1,
  so `Add(Filter(Range(1, 100000), x ↦ x > 2), 1)` truncated to the
  single-element list `[4]`; it now yields the full lazy sequence `[4,5,6,…]`. A
  mixed broadcast folds with shortest-input semantics:
  `Add([10,20,30], Cycle([1,2]))` → `[11,22,31]`.

- **Element-wise operations over large collections are now lazy.** Applying an
  element-wise operator (`Add`, `Multiply`, `Sin`, a user function literal, …)
  to finite indexed collections of more than 100 elements returns a lazy `Map`
  instead of materializing every element: `Add(Range(1, 10^8), 1)` and
  `Sin(Range(1, 10^8))` evaluate instantly to a lazy collection supporting `At`,
  `Take`, `First`, `Last` and `Length` without enumeration. Collections of 100
  elements or fewer are unchanged and still evaluate eagerly to a `List`.

- **`Length`, `Count`, `IsEmpty` and `Contains` see through wrappers that cannot
  change their answer.** `Sort`, `Shuffle` and `Reverse` preserve element count,
  and (together with `Unique`, for `Contains`) preserve membership, so these
  consumers now strip such wrappers at canonicalization: `Count(Sort(xs))`
  becomes `Count(xs)` and no longer sorts — `Count(Sort(Range(1, 10^5)))` went
  from ~15 s to ~1 ms.

- **Numeric argument validation no longer enumerates large lazy collections.** A
  lazy collection passed to a numeric operator was fully enumerated at
  canonicalization to check or infer its elements — even a `Map` over millions
  of elements. Validation now decides on the static element type: provably
  numeric or provably non-numeric collections are accepted or rejected without
  enumeration, and a collection whose element type is genuinely indeterminate is
  accepted structurally and fails at evaluation time if an element turns out
  non-numeric. Only eager, literal collections still have their elements
  inspected individually.

## 0.83.2 _2026-07-17_

### New Features

- **Operator definitions can now supply a custom compilation handler.** A
  `compile` handler on an operator definition emits target source for that
  operator when the expression is compiled; returning `undefined` falls back to
  the target's default lowering:

  ```ts
  ce.declare('MyGcd', {
    signature: '(number, number) -> number',
    compile: (args, compile, { language }) =>
      language === 'javascript'
        ? `_gcd(${compile(args[0])}, ${compile(args[1])})`
        : undefined,
  });
  ```

  The handler receives the canonical operands, a callback to lower
  sub-expressions, and the compilation context (branch on `context.language` —
  `javascript` or `python`). It takes precedence over the target's built-in
  operator mapping, so it can also re-map how a built-in operator compiles;
  structural and control-flow heads (`Sum`, `If`, `Block`, …) keep their bespoke
  lowering and ignore the handler.

### Resolved Issues

- **A `Mod` in a product or a power base now serializes parenthesized, fixing a
  round-trip corruption.** Juxtaposition (invisible multiply) and superscripts
  bind _tighter_ than infix `\bmod` on re-parse, so an unparenthesized `Mod`
  factor absorbed the adjacent notation into its trailing operand:
  `["Multiply", ["Mod", "A", 2], ["Mod", "B", 2]]` serialized to
  `A\bmod2B\bmod2`, which re-parses as `A mod (2B mod 2)` = `A mod 0` = NaN. It
  now serializes as `(A\bmod2)(B\bmod2)`; similarly
  `["Power", ["Mod", "A", 2], "x"]` is now `(A\bmod2)^{x}` instead of
  `A\bmod2^{x}` (which re-parsed as `A mod 2^x`). Same class as the 0.79.2
  compound-operand `Mod` parenthesization fix, on the other side of the
  operator. (Reported by the Tycho/Graph Paper team — a hex-grid Desmos state
  rendered blank because a product of two `Mod(Floor(…), 2)` factors
  round-tripped to NaN.)

## 0.83.1 _2026-07-17_

### Resolved Issues

- **Fixed a 0.82.0 regression: an operand typed `broadcastable<T>` was rejected
  by operators with a plain scalar parameter, baking an unrecoverable
  `incompatible-type` error at canonicalization.** An application of an
  _undeclared_ function symbol (`["f", "k"]`) flowing through `Add`, `Multiply`,
  or `Power` lifts the surrounding expression to `broadcastable<number>`; a
  non-threadable operator with a `number` parameter (`Binomial`, `Totient`, and
  the rest of the number-theory family, among others) then rejected it — e.g.
  `ce.box(["Binomial", ["Add", "n", ["f", "k"]], 2])` was invalid. A
  `broadcastable<T>` operand could be a plain scalar `T` at runtime, so
  validation now admits it whenever `T` matches the parameter, exactly restoring
  the pre-0.82.0 admission. LaTeX input was largely unaffected (undeclared
  `f(k)` parses as the product `f \cdot k`, and a declared function types
  precisely); expressions built directly from MathJSON were the exposed surface.

## 0.83.0 _2026-07-17_

### Breaking Changes

- **Collection indexing (`At`) now serializes with brackets by default:
  `["At", v, 1]` → `v[1]` instead of `v_1`.** The bracket form is the
  round-trip-safe notation: `v[1]` always parses back to `At`, while the
  subscript form `v_1` only does when `v` is declared as an indexed collection —
  otherwise it re-parses as the unrelated subscripted symbol `v_1`, silently
  changing the meaning on a serialize→parse cycle. The previous behavior remains
  available engine-wide via `ce.latexOptions.indexStyle = () => 'subscript'` or
  per call via `expr.toLatex({ indexStyle: () => 'subscript' })`. (Requested by
  the Tycho/Graph Paper team, whose per-call `indexStyle` opt-ins were a
  recurring source of forgotten-call-site round-trip bugs.)

### New Features

- **Five linear-algebra operators now compile to the JavaScript and Python
  targets:** `ConjugateTranspose`, `Diagonal` (rank-dispatched — a matrix gives
  its main-diagonal vector, a vector gives the diagonal matrix), `MatrixPower`
  (integer powers, with a negative power inverting first), `RowReduce` (reduced
  row echelon form), and `Rank`. Previously these threw at compile time and fell
  back to the interpreter. Note that `Rank` is the **tensor** rank — the number
  of axes (scalar `0`, vector `1`, matrix `2`) — not the linear-algebra (row)
  rank.

### Resolved Issues

- **The `javascript` compile target now lowers a reduce (`Sum`/`Product`) and
  rank-dispatched multiplication over an `unknown`- or `broadcastable`-typed
  collection operand**, where it previously failed closed and fell back to the
  interpreter. A collection `Sum`/`Product` over such an operand reduces under a
  runtime guard (a scalar at run time still matches `Sum(scalar) = scalar`),
  with an element-wise-aware combiner so a nested (matrix-valued) element
  reduces correctly rather than string-concatenating. A `Multiply` of two
  possibly-collection operands compiles to a runtime helper that dispatches on
  rank — element-wise for equal-length vectors, matrix product for matrices —
  matching the interpreter. This lets a compiled function whose evidence-derived
  signature is `(…) -> unknown` render through the compile path instead of only
  the interpreter.

- **A function declared with a fixed-length list return type (e.g.
  `(number) -> vector<11>`) now compiles.** The value assignment wraps the body
  in a `Typed` ascription, which the compile targets did not handle, so every
  compiled call threw ``Unknown operator `Typed` ``. `Typed` is a transparent,
  no-op-at-runtime ascription and now compiles to its value operand on every
  target.

- **`Multiply` of two matrices is now the matrix product regardless of how the
  operands are presented.** A matrix _literal_ or a matrix-returning function
  _application_ already contracted, but a _symbol_ whose value is a matrix
  incorrectly broadcast element-wise (Hadamard) — the dispatch keyed on the node
  kind and missed a matrix-valued symbol. All three now contract consistently,
  so 0.82.0's per-step matrix-contraction rule holds for symbol operands too.
  Vectors remain element-wise.

- **The Python compile target no longer unwraps a one-element `ElementMax` /
  `ElementMin` / `Clamp` broadcast to a scalar.** `ElementMax([1, 2], [3])` now
  compiles to a value that runs to `[3]` (zipping to the shortest operand),
  matching the interpreter and the JavaScript target, instead of the bare scalar
  `3`.

- **Broadcasting an element-wise operator over an `N×1` column matrix now
  preserves its rank-2 shape.** For example,
  `\bold{v} = \begin{pmatrix} 5 \\ -3 \end{pmatrix}` evaluates to the nested
  `[[v === 5], [v === -3]]` (a 2×1 result) instead of the flattened rank-1
  `[v === 5, v === -3]`. A column vector is a `matrix<Nx1>`, and the broadcast
  result now mirrors the operand's shape, matching how row matrices
  (`matrix<1xN>`) and plain rank-1 vectors already broadcast. Consumers reading
  the broadcast result should expect one nested list per row.

- **A `Comprehension` body containing a scoped subexpression now sees the
  iteration index correctly.** When the body contained a `Block` (e.g. a
  `with`-style local), a big operator (`Sum`, `Product`), a nested
  comprehension, or a user-function application whose evaluation was deferred by
  any of these, the subexpression evaluated blind to the index value: the index
  was bound in a runtime scope that scoped subexpressions' lexical chains never
  reached. Results could be silently wrong — an applied function literal whose
  piecewise guard could not be decided without the index escaped with its
  **parameters** permanently unbound (e.g. `[total(f(n, 4)) for n in 1..3]` with
  `f(a,b) := [{a>b: b, a}, a-b]` returned expressions still containing `a` and
  `b`) — and, because the wrongly-symbolic elements never reduced, evaluation of
  such comprehensions cascaded into orders-of-magnitude excess work. Index
  values are now installed in the comprehension's own scope for the duration of
  each element's evaluation (isolated per walk, so interleaved iterations and
  `.count` reads during a paused iteration are unaffected), and evaluating a
  canonical `Comprehension` no longer re-creates it with a detached scope.

- **Multiplying or adding a scalar to a piecewise (`Which`) no longer evaluates
  the selected branch twice.** `2 \cdot \{A=1: X, Y\}` evaluated the taken
  branch once during conditional-threading detection and again in the arithmetic
  handler, doubling the cost of every piecewise operand of `Add`/ `Multiply`
  (untaken branches were, and are, never evaluated).

- **Fixed a stack overflow when evaluating `Negate` of an indexed collection
  that cannot be materialized**, such as a `Range` with symbolic bounds reached
  inside a comprehension body (`-Range(0, m + 5)` with `m` unbound): the
  element-wise distribution retried the same non-distributable negation without
  progress.

## 0.82.0 _2026-07-17_

### Breaking Changes

- **`Multiply` of two vectors (rank-1 lists) is now element-wise, not a dot
  product.** `[1, 2, 3] \cdot [4, 5, 6]` now evaluates to `[4, 10, 18]` instead
  of the scalar `32`. This makes `Multiply` over lists consistent: element-wise
  is what `Add`, `Power` (`k^2`), scalar scaling (`2k`), and symbol-bound list
  operands (`k \cdot k` with `k := [1,3,10]`) already did — previously the
  _same_ product could zip or contract depending on whether an operand was a
  literal list, a bound symbol, or a computed expression (`\sqrt{k} \cdot k`
  silently collapsed a 3-element family to one scalar). **For the dot product,
  use the explicit `Dot` or `MatrixMultiply` operators**, which are unchanged. A
  product is folded left-to-right, one pair at a time: a step involving a matrix
  (`matrix·matrix`, `matrix·vector`, `vector·matrix`) still contracts (matrix
  product, unchanged), while a step between two vectors is element-wise. Note
  this applies per step, so in a longer chain a contraction that _produces_ a
  vector then combines element-wise with a following vector: `M·u·v` is
  `(M·u) ⊙ v`, no longer the scalar `(M·u)·v`. Vectors of differing lengths stay
  inert (no implicit zip-to-shortest). The compiled targets follow the same
  semantics: equal-length `vector·vector` compiles to the element-wise
  broadcast; statically mismatched lengths and matrix contractions fall back to
  the interpreter.

- **A bounds-less big operator over LaTeX (`\sum ⟨body⟩`, `\prod ⟨body⟩`) now
  parses to its own head (`["Sum", body]` / `["Product", body]`) instead of
  `["Reduce", body, "Add"/"Multiply"]`.** This is a (non-canonical and
  canonical) parse-shape change, called out per the pipeline-contract rules:
  consumers matching on the `Reduce` shape should match the big-op head instead.
  It makes the serialization round-trip lossless — `["Sum", body]` serializes to
  a bounds-less `\sum ⟨body⟩`, which previously re-parsed to a different
  expression. Evaluation semantics are unchanged (a collection body still
  reduces).

- **Broadcasting over a one-element collection now returns a one-element `List`
  instead of unwrapping to the scalar.** `\sin(2 \cdot [5])` now evaluates to
  `[\sin(10)]`, previously the bare scalar `\sin(10)`. Broadcasting a scalar
  function over an `n`-element indexed collection now produces an `n`-element
  `List` for every `n ≥ 1`, matching the expression's static `list<…>` type (a
  `vector<1>` operand no longer types `list<number>` while evaluating to a
  scalar) and the user-function broadcast path, which already returned a `List`
  for single-element collections. An empty broadcast still evaluates to
  `Nothing`.

### Resolved Issues

- **`Sum`/`Product` over a _computed_ list-valued body now reduces instead of
  broadcasting.** `Sum(L)` of a literal collection reduced correctly, but a body
  that only _evaluates_ to a list — e.g. a broadcast chain over a list literal,
  `\operatorname{Sum}(\operatorname{mod}(\operatorname{floor}(7/2^{[0...10]}),2))`
  — returned the broadcast list unchanged instead of its sum. The arity-1
  reducer form now reduces the evaluated value when it is a collection.

- **A symbol operand naming an operator no longer leaks into `.unknowns`.** A
  function reference held as a symbol operand (e.g. the `Add` of
  `["Reduce", L, "Add"]`) was reported as a free variable by `.unknowns` /
  `.freeVariables`, so consumers walking unknowns saw a phantom unbound name.
  Operator names now resolve as function references, not free variables.

- **`subs()` no longer corrupts a bound index named `i` (or any name that
  collides with a constant).** Substituting into a canonical big operator —
  `ce.parse("\\sum_{i=1}^{n}2^{-i}").subs({n: 9})` — re-canonicalized the held
  `Limits` index _outside_ its binding scope, re-typing `i` as the imaginary
  unit: the index slot became an `incompatible-type` error and serialization
  dropped the index (`\sum_1^9…`). Held (non-canonical) operands now stay raw
  through `subs()` and are re-bound by the parent's canonical handler, exactly
  as when the expression was first built.

- **A bare numeric bounds pair on a big operator (`\sum_1^9 ⟨body⟩`) is no
  longer silently dropped at parse.** It now parses to an index-less
  `["Limits", "Nothing", 1, 9]`: a constant body iterates (`\sum_1^9 2` → `18`),
  and a body with free variables stays symbolic rather than losing its bounds.

- **A divergent integral over `(-∞, ∞)` no longer numericizes to a clean `0`.**
  `.N()` of `\int_{-\infty}^{\infty} x\,dx` (and `x^3`, `\sin x`, any odd
  divergent integrand) returned an exact scalar `0`: the Gauss–Kronrod
  quadrature mapped the doubly-infinite domain through a symmetric transform, so
  an odd integrand cancelled to exactly 0 on the first panel with a 0 error
  estimate — indistinguishable from a genuine result downstream. The
  doubly-infinite case is now split at 0 into two half-line integrals that must
  _each_ converge (the definition of improper-integral convergence); a divergent
  half fails to converge and the result falls back to a `Measurement` with an
  honest (large) error bar that consumers can reject. Convergent integrands are
  unaffected (`\int_{-\infty}^{\infty} x e^{-x^2}\,dx` → `0`,
  `\int_{-\infty}^{\infty} e^{-x^2}\,dx` → `√π`). Note this also means no Cauchy
  principal value is implied: a symmetric divergent integral reports
  non-convergence rather than its principal value.

- **Compiled JavaScript arithmetic over a value that may be a list is now
  correct for both outcomes.** Compiling `2h(x) - 1` where `h` may return a list
  produced scalar code that yielded `NaN` on a list value at run time (behind
  `success: true`). Such operands — typed `broadcastable<…>`, see New Features —
  now compile through the runtime broadcast helper: the same compiled artifact
  returns the scalar result for a scalar value and the element-wise list for a
  list value, matching the interpreter. Cases the helper cannot lower soundly
  now **fail closed** instead of emitting silently-wrong scalar code: a product
  of two or more possibly-list operands (a run-time matrix would need the matrix
  product, not an element-wise one), `Equal`/`NotEqual` over a possibly-list
  operand, and — on the Python target, where `*`/`+` repeat or concatenate a
  plain `list` — all arithmetic over possibly-list operands. `compile()` reports
  these as compilation failures (with the interpreter fallback available),
  rather than producing code that computes the wrong value.

### New Features

- **New `broadcastable<T>` type: honest static typing for values that may
  broadcast.** The engine broadcasts element-wise at run time (`2·[1,2,3]` →
  `[2,4,6]`), and statically-visible collections have carried honest types
  (`vector<3>`, `list<number>`) for a while — but the same arithmetic over a
  value whose collection-ness is _not_ statically visible (`2h(x,y)-1` with `h`
  returning `unknown`) used to collapse to scalar `number`, even though
  evaluation broadcasts if `h` returns a list. Such expressions now type
  **`broadcastable<T>`** — "a `T`, or an indexed collection of `T`, applied
  element-wise". The type is produced by `Add`/`Multiply` and every
  broadcastable operator (`Sin`, `Sqrt`, `Power`, `Abs`, …) over an operand
  whose type is a top type (an unknown-return call) or already `broadcastable`;
  it propagates through nested arithmetic, juxtaposition (`2(2h(x)-1)` is a
  product, not a tuple), function application, and indexing (`(2h(x,y)-1)[1]` is
  valid, with element type `number`). Relatedly, a scalar function over a
  **fixed-shape-typed intermediate** no longer collapses either:
  `\sin(10^4 \cdot [1,2,3])` — whose inner product types `vector<3>` — now types
  `list<number>` through every scalar-function hop (`mod`, scaling, …), so
  indexing the end state is valid. Operators that compute their own collection
  result (`-M`, `M+N`, `matrix + scalar`) are unaffected. Subtyping:
  `number <: broadcastable<number>` and `list<number> <: broadcastable<number>`,
  but `broadcastable<number>` is _not_ a subtype of `number` (it may be a list).
  The type can be used in declarations
  (`ce.declare('b', 'broadcastable<number>')`) and signatures. Bare symbols are
  unaffected: an undeclared `x` in `2x` still types scalar (inference pending),
  and tuples/points still bind atomically.

- **Applying a scalar function to a collection-valued expression now broadcasts
  — for every function body.** Broadcasting a user function over a literal
  collection (`f([1,2,3])` → `[f(1), f(2), f(3)]`) is long-standing; it now also
  applies when the argument only _evaluates_ to a collection (`f(g(3))` where
  `g` returns a list), and for every body — previously a non-arithmetic body
  such as `x \mapsto \operatorname{If}(x > 0, 1, -1)` applied to a computed list
  stayed inert. The static type of such an application is honest as well:
  `list<R>` for a visible collection argument, `broadcastable<R>` for a
  possibly-collection argument, where `R` is the function's return type (a
  list-returning function maps to a list of lists — no flattening). Declaring a
  collection parameter type (`(list<number>) -> …`) still binds the argument
  whole, and tuple arguments still bind atomically.

## 0.81.0 _2026-07-16_

### New Features

- **New `PointList` operator (the point-list surface form).** `PointList` is the
  explicit, importer-emitted operator that zips a point-with-collection into a
  list of points: `PointList(-6, n)` with `n` a 21-element list evaluates to the
  21-element list of points, while `PointList(1, 2)` is just a plain point.
  Zip-to-shortest for multiple list components; scalars broadcast; an empty
  component yields an empty list; an infinite or unknown-length component fails
  closed (stays inert, no hang). It round-trips through LaTeX as
  `\operatorname{PointList}(…)`. A plain `Tuple` now stays inert data — it never
  transposes — so tuples used as data are genuinely unaffected;
  tuple-with-collection arithmetic still scales component-wise, with no baked
  `incompatible-type` error, so a definition such as
  `m(P) \coloneq P + s(P)\cdot(1, 0.3n)` stays valid. (An earlier, evaluate-time
  `Tuple`-transpose of this idiom was replaced by the explicit `PointList`
  operator before it ever shipped in a release.)
- **`PointList` compiles on the `javascript`, `glsl`, and `wgsl` targets.** With
  all-scalar components (including free plot variables), it emits
  byte-identically to the equivalent `Tuple` (`[x, y]` / `vec2(x, y)` /
  `vec2f(x, y)`), so point literals rewritten to `PointList` stay on the
  compiled path (GPU grids, per-pixel bodies). Provably non-scalar components
  (collection- or tuple-typed, or a union with a collection member) fail closed
  to the interpreter, as does the `interval-js` target (where `Tuple` itself has
  no lowering).
- **`At` defers a possibly-collection base to runtime instead of baking a type
  error.** Indexing an expression whose type was over-narrowed to a scalar by
  arithmetic over an unresolved operand (`(2h(x,y)-1)[1]` with `h` returning
  `unknown`), or whose type is a union with an indexable member
  (`number | list<number>`), now stays symbolic and indexes once the base
  resolves to a collection — enabling structural substitution over vector-valued
  helper functions. Provably scalar bases (`(5)[1]`, `\pi[1]`, `\sin(3)[1]`)
  still report `incompatible-type` at canonicalization, and a base that turns
  out scalar at runtime errors at evaluation.
- **Pipeline contract test suite.**
  `test/compute-engine/pipeline-contracts.test.ts` pins the guarantees for
  MathJSON-carrying pipelines: non-canonical `box(json).json` structural
  fidelity (with its documented normalizations), non-canonical `.latex`
  round-trip (with its three documented exception classes),
  transform-then-canonicalize-once equivalence, cached-boxed re-binding rules,
  compile-from-boxed parity, and the non-canonical shape vocabulary. Breaking a
  test in this suite requires a CHANGELOG callout.

### Breaking Changes

- **`Partition(xs, n)` now returns chunks of size `n`, not `n` groups.**
  `Partition([1, 2, 3, 4, 5], 2)` now evaluates to `[[1, 2], [3, 4], [5]]`
  (chunks of 2, trailing chunk short) instead of splitting the collection into 2
  nearly equal groups. **To split into a given _number_ of groups, use
  [`Chunk`](/compute-engine/reference/collections/#chunk) instead**
  (`Chunk([1, 2, 3, 4, 5], 2)` → `[[1, 2, 3], [4, 5]]`). A new sliding-window
  form `Partition(xs, size, step)` returns the complete windows of `size`
  elements whose starting positions are `step` apart
  (`Partition([1, 2, 3, 4, 5], 2, 1)` → `[[1, 2], [2, 3], [3, 4], [4, 5]]`). The
  predicate form `Partition(xs, predicate)` (split into matching / non-matching
  groups) is unchanged.

### Resolved Issues

- **The two declare forms are equivalent under declare-then-assign.** Declaring
  a function head with the object form (`ce.declare('f', {signature: …})`) and
  then assigning a function literal (`f(x) \coloneq …` or `ce.assign`) silently
  discarded the declared signature — a scalar call to a tuple-typed parameter
  stopped type-erroring — while the string form (`ce.declare('f', '(…) -> …')`)
  preserved it. The object form now runs the same reconciliation: the declared
  signature is authoritative, arity mismatches error clearly, and the stored
  definition is identical to the string form's.
- **Juxtaposition with a `value`-typed symbol is multiplication again.** A
  symbol inferred or declared with the wide `value` type (e.g. any bare symbol
  that had passed through `Max`/`Min`-style `(value*)` signatures on the same
  engine) made subsequent parses of `2x` silently produce `Tuple(2, x)` instead
  of `Multiply(2, x)` — an order-dependent wrong-parse present in released
  versions, affecting any warm engine. A wide type is not evidence of
  point-ness; the juxtaposition gate now treats `value` like `unknown` and
  multiplies. Locked by warm-engine order-independence tests in the
  pipeline-contract suite.
- **`\operatorname{sin}` (and every lowercase spelled-out native function name)
  now binds as a function call.** `\operatorname{sin}(x)^{2}` parsed as the
  unknown symbol `sin` times `x^2` with `isValid: true` — silent wrong math; it
  now parses to `\sin(x)^2` with call-binding identical to the native command
  (prefix minus after the call, postfix power on the result, `^{-1}` inverse,
  base subscripts). Covers the trig/hyperbolic/inverse families,
  `ln`/`log`/`lg`/`lb`, and `arg`; bare identifiers (`sin` without
  `\operatorname`) are unchanged.
- **A `{…}` group after a function is now its argument list.** For a
  dictionary-registered function that takes parenthesized arguments, a brace
  group is accepted exactly as if it were `(...)`: `\gcd{a}` → `GCD(a)`,
  `\gcd{2,4}` → `GCD(2, 4)`, `\operatorname{floor}{2.5}` → `Floor(2.5)`, and
  consecutive groups are successive arguments (`\mod{x}{2}` → `Mod(x, 2)`, the
  TeX multi-argument-macro habit). Previously the function parsed as a bare
  symbol and the group multiplied against it — silently wrong (`\gcd{a}` was
  `GCD · a`). Commands with implicit (unparenthesized) arguments keep the
  transparent-grouping convention — braces render invisibly, so the argument
  reads the way the rendered formula does: `\sin{x}y` is `Sin(x·y)` and
  `\sin{x}^2` is `Sin(x²)`, matching `\sin x y` and `\sin x^2`. A brace group
  after a generic declared or unknown name (`f{x}`) keeps its juxtaposition
  (multiply) reading.
- **Function-style `\operatorname{…}` aliases now bind their call like
  natively-spelled functions.** The parse-only aliases (`\operatorname{mod}`,
  `var`, `cov`, `corr`, `count`, `length`, `nCr`, `random`, `shuffle`, `repeat`,
  `join`, `range`, `histogram`, `pdf`, `cdf`) parsed as a bare symbol, so a
  prefix minus captured the function symbol itself (`-\operatorname{mod}(x,1)` →
  loud `incompatible-type` error) and a postfix power stole the argument group
  (`\operatorname{mod}(-x,1)^{2}` parsed **silently** as `Mod · ((−x,1))²`,
  evaluating to NaN). They are now function-kind dictionary entries: the call
  binds before prefix minus, and a postfix power applies to the call result
  (`\operatorname{mod}(-x,1)^{2}` → `Power(Mod(-x,1), 2)`).
- **`target.compile()` can return a failure instead of throwing.** A fail-closed
  compile error (e.g. `At` on a non-indexed base) always threw out of a
  compilation target's `compile()`; passing the new `{ fallback: true }` option
  returns the documented `{ success: false, error, run }` shape instead, with
  `run` falling back to the interpreter — matching the engine-level `compile()`
  contract. The default remains throwing, so existing callers are unaffected.
- **An over-arity function literal is rejected at registration.** Assigning
  `f(x, y) \coloneq x + y` to a name declared `(number) -> number` was silently
  accepted, and `f(3)` then silently partial-applied; it now reports a clear
  error naming the literal's arity and the declared maximum. (The declared
  signature was already authoritative for types; this closes the arity gap.)
- **Applying a function whose body stays partially symbolic no longer loses the
  argument.** When a lambda body could not fully evaluate (e.g. a `Which`/`If`
  guard over an undetermined symbol), the application returned the body inert
  with the parameter unsubstituted — so
  `Map([1,2,3], k \mapsto \operatorname{Which}(k = m, 10^9, k))` yielded three
  identical copies of the raw body with `k` leaked free and the elements gone.
  The parameter's value is now substituted into the held result, fixing `Map`,
  `Filter`, `Tabulate`, `Zip`-with-function, and direct `Apply` in one place.
- **Lazy collections serialize faithfully.** `.latex` of a canonical `Map`,
  `Filter`, `Zip`, `Tabulate`, `Range`, `Linspace`, or `Comprehension` no longer
  materializes an elided or value-baked preview (which could re-parse to a
  corrupt expression — a `Map` over a bound symbol serialized as N copies of its
  raw lambda body); each now emits its operator form, which re-parses to the
  identical expression. `toString()` still shows the materialized preview for
  display.
- **Negative indexing on a lazy `Take` was off by one.** `Take(xs, n).at(-1)`
  returned the second-to-last element of the taken prefix (`at(-1)` on
  `Take([10, 20, 30], 2)` was `10` instead of `20`).
- **The display preview of a lazy `Take` sampled the wrong tail.** Displaying
  `Take(xs, 50)` over a lazily-enumerated source showed the _source's_ last
  elements (`[1, 2, …, 98, 99]`) instead of the taken prefix's
  (`[1, 2, …, 49, 50]`): the operands were materialized to their own display
  preview — continuation placeholder included — before `Take` consumed them.
- **A boolean `Sort` comparator now orders instead of silently doing nothing.**
  A comparator returning `True`/`False` (e.g. `(a, b) -> a > b`) never reordered
  — only signed-number comparators worked. Boolean comparators are now
  interpreted Elixir-style: `True` means the first argument sorts first, so
  `(a, b) -> a > b` sorts descending.
- **A mistyped `GroupBy` key function is now reported.** `GroupBy(xs, Even)` (an
  unknown symbol auto-declared by its own use) silently placed every element in
  its own garbage group keyed `"Even(1)"`, `"Even(2)"`, …; it now throws with a
  spell-check suggestion, like `Filter` and `Partition` do for broken
  predicates. Grouping by explicitly declared symbolic functions is unaffected.
- **The optimization form of `ArgMax`/`ArgMin` canonicalizes its function
  operand again.** `ArgMin(f, RealNumbers)` (the "locations of the minimum over
  a domain" form, used by the identities library) short-circuited
  canonicalization, leaving the function literal in a non-canonical shape that
  no longer matched the identities library's stored rewrite patterns. The form
  remains inert under evaluation; the collection form (`ArgMin([3, 1, 2])` →
  `2`) is unchanged.
- **A canonical `Comprehension` now serializes to LaTeX that round-trips.** Its
  `.latex` was an elided display preview
  (`\lbrack 1, 4, 9, \dots, 62\,500\rbrack`) that silently re-parsed to a
  corrupt 11-element `List` containing a literal `\dots`; it now serializes
  through the faithful `body \operatorname{for} var = domain` form, which
  re-parses to the identical comprehension (including tuple bodies, dependent
  domains, and infinite domains).
- **Lazy `Comprehension` elements are memoized.** `.at(n)` re-walked the domain
  on every call and each `.each()` recomputed from scratch, making repeated
  indexed access quadratic (`at(100)`×100 on a 200-element comprehension: ~5 s →
  ~23 ms; a repeat `.each()` walk: ~110 ms → ~0.2 ms). The prefix cache is
  generation-stamped, so reassigning a free variable the body depends on
  invalidates it, and it is capped (100k elements) beyond which access streams
  as before.
- **A broadcast condition no longer crashes `Which`/`If`.** A condition that
  evaluates to a collection of booleans (e.g. a piecewise guard over a broadcast
  function application inside a comprehension) threw
  `Condition must evaluate to "True" or "False"`; it now stays symbolic (held),
  letting the surrounding expression evaluate.

Fixes from a review of the 0.78.0–0.80.0 changes:

- **Compiled n-ary and collection `GCD`/`LCM` returned wrong numbers.** The
  compiled form passed a third operand into the internal tolerance slot
  (`GCD(2.25, 2.1, 0.6)` compiled to `2.1`, silently consuming the `0.6` as ε;
  `GCD(12, 18, 8)` → `6` instead of `2`; `LCM(4, 6, 10)` → `12` instead of
  `60`), and a collection argument (`GCD([12, 18])`) compiled to `NaN`. All
  forms now fold pairwise and match the interpreter; collection operands whose
  elements can't be enumerated at run time fail closed to the interpreter.
- **A user-defined function sharing its name with a loop index hijacked compiled
  `Sum`/`Product`.** With `f(x) := x^2` declared, compiling `\sum_{f=1}^{3} f`
  emitted references to the function instead of the loop index (returning
  garbage with `success: true`; a null interval on `interval-js`). Bound names
  are now tracked explicitly through every binding form instead of being
  inferred from resolved code.
- **Adaptive quadrature no longer poisoned by a `NaN` sample.** A single
  integrand `NaN` at a quadrature node (e.g. the removable singularity of
  `\sin(x)/x` at the midpoint of a symmetric interval) permanently corrupted the
  convergence accumulators, silently falling back to slow, nondeterministic
  Monte Carlo. Non-finite panels are now excluded until subdivided away:
  `\int_{-1}^{1} \sin(x)/x \, dx` converges to `2\,\mathrm{Si}(1)`. Also, `.N()`
  on an integral without a closed form now uses adaptive Gauss–Kronrod before
  falling back to Monte Carlo, matching the compiled path's accuracy.
- **Comprehension iteration state was shared across traversals.** Two
  interleaved iterators over the same comprehension (or reading `.count`
  mid-iteration on a dependent comprehension) corrupted each other's index
  variables, yielding wrong elements. Each traversal now gets its own scope, and
  a function literal produced by a comprehension body now captures the
  per-iteration value of the loop variable
  (`[x \mapsto x + i \text{ for } i \in 1..3]` applied to 10 gives `11, 12, 13`,
  not `13, 13, 13`).
- **GPU `gcd` regressed on large integers.** The tolerant float loop shipped in
  0.80.0 dropped the exact-integer path on `glsl`/`wgsl`: `gcd(4000000, 2)`
  returned `4000000`. Exact Euclid is restored for integer inputs within f32
  range.
- **Tolerant `GCD`/`LCM` invariants.** Scale-mismatched inputs violated
  `gcd ≤ min` / `lcm ≥ max` (`\gcd(2.5, 10^{21})` → `10^{21}`); zero-argument
  `GCD()`/`LCM()` crashed (now the identities `0`/`1`); nested collections now
  fold in a single evaluation.
- **`Max`/`Min` absorb `NaN` found inside collections**: `Max([1, NaN, 3])` now
  returns `NaN`, consistent with `Max(NaN, 5)` and with compiled code.
- **`IndexOf` on infinite lazy collections hung indefinitely**, ignoring
  `ce.timeLimit`. The search now streams (linear instead of quadratic on lazy
  collections) with deadline checkpoints. Compiled `IndexOf` also now uses the
  interpreter's tolerance-aware comparison instead of strict `===`.
- **Sequence interpretation (`Interpret`) regressions.** The 0.79.0
  anchor-search optimization rejected legitimate non-monotonic polynomial sums
  (e.g. `100 + 164 + 198 + 208 + \dots + 308`, which is
  `\sum_{k=1}^{14} k^3-21k^2+120k`) and then ground for hours in an
  exact-rational recurrence search that ignored `ce.timeLimit`. The break
  heuristic now requires a sustained divergence streak, and the recurrence
  search honors the deadline.
- **Parse-diagnostics false positives.** `f(x) \coloneq x^2` no longer emits
  `juxtaposition-as-multiply`/`undeclared-symbol` for the definition's own head,
  and symbols declared through the `getSymbolType` handler are no longer
  reported as undeclared.
- **Custom `compile` handler contract.** The handler now genuinely takes
  precedence over built-in operator mappings (e.g. `Add`) as documented
  (control-flow heads remain non-overridable, now stated explicitly), and
  `analyzeReferences` no longer reports custom-compiled operators as
  `unsupported`.
- **Assorted**: applying a non-numeric symbol to a collection (`t(\{1,2\})` with
  `t` a string) reports an application type error again instead of a confusing
  `Multiply` error; `PointX`/`PointY` work on `Set`s of points (previously
  returned `[]`); a provider timeout inside `Integrate` is re-thrown instead of
  swallowed; `\operatorname{erf}` at a directionless complex infinity stays
  symbolic instead of saturating to `1`; compiled `Map`/`Filter` no longer leak
  JavaScript's 0-based callback index into two-parameter lambdas.
- **Long flat operator chains no longer overflow the parser stack.** A flat
  chain of a same-precedence associative operator (`1+1+\dots`,
  `a\times b\times\dots`, chained `<`) recursed one parselet frame per term and
  overflowed at ~1,300 terms; same-precedence continuations now iterate in the
  parser's infix loop (a 20,000-term sum parses; same-operator chains produce
  the same flattened n-ary trees as before). One deliberate tree change: _mixed_
  same-precedence operators now group left-to-right — the conventional reading —
  where they previously nested rightward as an artifact of the recursion
  (`a\times b\otimes c` now parses as `CircleTimes(Multiply(a,b), c)`, not
  `Multiply(a, CircleTimes(b,c))`). Right-associative chains (`a=b=c`) still
  nest by construction.
- **Only binary arithmetic operator symbols lower to first-class combiners.**
  Passing a unary or relational operator symbol where a function is expected
  (`Reduce([1,2,3], Negate, 0)`, `Map(xs, Negate)`, `Filter(xs, Less)`) compiled
  to a wrong binary infix lambda (`Negate` folded like `Subtract`, returning
  `-6` behind `success: true`). Non-arithmetic operator symbols and combiners of
  the wrong arity now fail closed to the interpreter. Also: compiled `Reduce`
  over an empty collection without an initial value returns `NaN` instead of
  throwing, and `Tabulate` with a statically non-positive dimension fails closed
  instead of returning `[]`.
- **`Flatten` with no depth now fully flattens ragged lists.**
  `Flatten([[1,x],[2]])` was a no-op (only uniform tensors flattened), which
  became visibly inconsistent once `Flatten(expr, depth)` shipped; the default
  now flattens completely, per the documented (Wolfram) semantics.
- **`Scan` no longer silently drops an invalid initial value** — the error is
  surfaced instead of computing the unseeded scan.
- **`ElementMax`/`ElementMin`/`Clamp` on the `python` target now match the
  interpreter's broadcasting.** Length-mismatched arrays previously raised a
  NumPy `ValueError` (or size-1-broadcast) where the interpreter zips to the
  shortest operand; an injected helper now aligns semantics while keeping the
  vectorized NumPy fast path.
- **`lambda.body` is canonical for every declaration route.** The public
  accessor returned a raw, non-canonical body for functions declared with a
  MathJSON `evaluate` handler (parse/assign routes were fine), tripping
  canonical-only asserts in consumers; it now returns the canonical scoped
  `Block` shape everywhere. And `analyzeReferences` now probes a custom
  `compile` handler per target language, so an operator whose handler only
  supports some targets is correctly reported in `unsupported` on the others.

### New Features

- **New collection operators.** A batch of higher-order and structural
  collection operators (see the
  [Collections reference](/compute-engine/reference/collections/)):
  - **Quantifiers** — `Any(xs, predicate?)` and `All(xs, predicate?)` test
    whether some or every element satisfies a predicate (the elements
    themselves, treated as booleans, when no predicate is given). Both
    short-circuit, so they return a definite answer even on infinite
    collections, and stay symbolic when the result depends on undetermined
    elements. `Any([])` is `False`, `All([])` is `True`.
  - **Cumulative** — `Scan(xs, f, initial?)` is the running fold (same length as
    the input: `Scan([1, 2, 3, 4], Add)` → `[1, 3, 6, 10]`), and
    `Differences(xs)` gives the successive differences (length `n − 1`, computed
    exactly).
  - **Prefix/suffix** — `TakeWhile(xs, predicate)` and
    `DropWhile(xs, predicate)` take or drop leading elements while the predicate
    holds; both are lazy and compose with infinite collections.
  - **Mapping** — `FlatMap(xs, f)` maps `f` over `xs` and splices
    collection-valued results into a single list (a scalar result is kept as a
    single element).
  - **Extrema** — `MaxBy(xs, f)` / `MinBy(xs, f)` return the _element_ with the
    largest/smallest key `f(x)`; `ArgMax(xs, f?)` / `ArgMin(xs, f?)` return its
    1-based _index_. The first occurrence wins ties, and all stay symbolic on
    empty or infinite collections.
  - **Grouping** — `ChunkBy(xs, f)` splits into maximal runs of consecutive
    elements sharing the same key `f(x)`; `Dedup(xs)` collapses consecutive
    duplicates only (contrast `Unique`, which removes all duplicates).
  - **Functional element updates** — `Insert(xs, index, value)`,
    `DeleteAt(xs, index)`, and `ReplaceAt(xs, index, value)` return a new list
    with an element inserted, removed, or replaced at a 1-based index. Negative
    indexes count from the end (Elixir-style); `Insert` at `-1` or `n + 1`
    appends.
- **`Map` is now variadic.** `Map(xs, ys, …, f)` applies `f` element-wise across
  several collections (a `zipWith`), truncating to the shortest input; the
  function is always the last argument
  (`Map([1, 2, 3], [10, 20, 30], (x, y) ↦ x + y)` → `[11, 22, 33]`).
- **`Sort` accepts a one-argument key function.** In addition to a two-argument
  comparator, `Sort(xs, f)` with a unary `f` sorts ascending by the key `f(x)`
  (stable on ties), discriminated from a comparator by arity.
- **`Flatten` accepts an optional depth.** `Flatten(xs, depth)` flattens only
  `depth` levels of nesting; without it, the collection is fully flattened
  (`Flatten([[1, [2]], [3]], 1)` → `[1, [2], 3]`).
- **More collection operators compile on the `javascript` target.**
  - **`Reduce` accepts a custom combiner**: a `Function` literal
    (`Reduce([1, 2, 3], (a, b) \mapsto a + 2b, 0)` → `12`), a user-defined
    function symbol, or an operator symbol such as `Subtract` — previously only
    the `Add`/`Multiply`/`Min`/`Max` folds compiled. A custom combiner requires
    an explicit initial value (without one the interpreter folds from `Nothing`,
    which has no numeric equivalent — that form still fails closed). **`Fold`**,
    which canonicalizes to `Reduce`, now compiles too.
  - **`Tabulate`** (1-D and 2-D) and **`Fill`** compile to native array
    construction with 1-based indexes — and therefore **`Table`**, in both its
    alias and Mathematica-style iterator forms. Compiling is the natural fast
    path for materializing these now-lazy collections.
  - **`CountIf`, `Find`, `IndexWhere`, `Position`** compile their predicate
    lambda to native array operations, with the interpreter's conventions
    preserved: 1-based indexes, `IndexWhere` → `0` and `Find` → `NaN` (`Nothing`
    projected onto a real target) when no element matches.
  - **`Append`, `Most`, `Slice`, `IsEmpty`, `Count`, `Contains`, `Unique`,
    `RotateLeft`/`RotateRight`, `Zip`, `Linspace`, `Chunk`, `Partition` (integer
    and predicate forms), `Ordering`, and `Shuffle`** compile to native array
    operations, each verified element-for-element against the interpreter
    (1-based inclusive `Slice` with negative-from-end indexes, rotation shift
    normalized modulo the length, `Chunk`/`Partition` producing `k` chunks of
    `⌈len/k⌉`, stable `Ordering` ties, `Zip` truncating to the shortest input,
    `Linspace` including both endpoints). Non-finite runtime counts/indexes fall
    back to the interpreter's defaults instead of crashing or silently
    diverging, and `Shuffle` honors the engine's `randomSeed` (a deterministic,
    reproducible permutation, like `Random`). Forms with no numeric equivalent
    on the target fail closed: a custom `Ordering` function, the explicit-seed
    `Shuffle` form, statically non-positive `Chunk`/`Partition` counts, and
    `Contains`/`Unique` over compound (nested-list, tuple, complex) elements,
    whose JS equality is referential rather than structural.
  - **`Any`, `All`, `TakeWhile`, `DropWhile`, `FlatMap`, and `Scan`** compile
    (predicate/mapping lambdas → native `some`/`every`/`flatMap` and slices;
    `Scan` is the running fold, with the initial value not emitted and the
    seedless form emitting the first element as-is, matching the interpreter).
- **Core scalar operators compile on the `javascript` target:** `Boole` (Iverson
  bracket, with the same runtime boolean guard as `Which`/`When`),
  `KroneckerDelta` (n-ary, tolerance-aware like compiled `Equal`),
  `Element(x, list)` membership, `Identity`, and `Apply` of a function literal.
- **Linear algebra compiles on the `javascript` target** (closing the parity gap
  with the `python` target): `Dot` and `MatrixMultiply` (with the interpreter's
  dimensionality dispatch — vector·vector → scalar, matrix·vector → vector,
  matrix·matrix → matrix), `Cross`, `Norm` (scalar, 2-/Frobenius, and p-norms),
  `Transpose`, `Determinant`, `Inverse` (singular → `NaN`), `Trace`, `Flatten`
  (with optional depth), `Shape`, and `Reshape` (with the interpreter's cyclic
  padding).
- **The `python` compilation target now covers collections and function
  literals.** `Function` literals compile to Python lambdas, and the collection
  operators above (list access/slicing, `Map`/`Filter` and the other
  higher-order operators, `Reduce`/`Scan`, `Tabulate`/`Fill`, `Linspace`, `Zip`,
  `Ordering`, plus `Flatten`/`Shape`/`Reshape`/`Trace` via NumPy) emit Python
  with the same interpreter-verified semantics, validated by executing the
  emitted code (venv-gated parity suite). Also fixed: **compiled `Range` was off
  by one on the Python target** — `np.arange` excludes the stop value and is
  0-based in the one-argument form, so `Range(2, 6)` compiled to `[2..5]` and
  `Range(5)` to `[0..4]`; CE `Range` is inclusive and 1-based.
- **Compiled `Range` with no explicit step now auto-descends** on both targets,
  like the interpreter: `Range(5, 1)` → `[5,4,3,2,1]` and `Range(-2)` →
  `[1,0,-1,-2]` previously compiled to `[]` on the JavaScript target (the
  implicit step was a fixed +1).

## 0.80.0 _2026-07-16_

### New Features

- **Custom per-operator compilation handler.** An `OperatorDefinition` can now
  supply a `compile` handler —
  `(args, compile, { language }) => string | undefined` — that emits
  target-language source for a call to that operator. It mirrors a built-in
  compiled-function handler (recursively `compile` operands, branch on
  `language` for `javascript`/`glsl`/`wgsl`/`python`) and **takes precedence
  over the target's built-in mapping**, so a consumer can override how even a
  built-in operator compiles (e.g. a custom-tolerance `GCD`), or add compilation
  for an operator the target doesn't know. Returning `undefined` falls back to
  the default compilation. This replaces the never-wired, interpreter-only
  `xcompile` stub. See `OperatorCompileHandler`.
- **`ElementMax`, `ElementMin` — element-wise (broadcasting) maximum and
  minimum** (the NumPy `maximum`/`minimum` primitive). Unlike `Max`/`Min`, which
  _reduce_ all operands — including a collection's elements — to a single
  scalar, these broadcast: a scalar over a collection returns a collection of
  the per-element extremum (`ElementMax(0, [1, -2, 3])` → `[1, 0, 3]`),
  collections zip, and all-scalar arguments give a scalar. They are **variadic**
  (two or more arguments): `ElementMax(0, [1, -2, 3], 2)` → `[2, 2, 3]`.
  Exactness is preserved (`ElementMax(√2, 1)` → `√2`). They compile on every
  target: `javascript` (all-scalar → a direct call, a collection operand → a
  `_SYS.bcast`), `interval-js` (interval max/min — restoring break detection),
  `glsl`/`wgsl` (native `max`/`min`), and `python` (`np.maximum`/`np.minimum`).
- **`Clamp(x, lo, hi)` — clamp a value to a range** (`min(max(x, lo), hi)`),
  also broadcasting over collection arguments (`Clamp([-1, 0.5, 2], 0, 1)` →
  `[0, 0.5, 1]`). Compiles on all targets, including the native `clamp` on
  `glsl`/`wgsl` and `np.clip` on `python`.

### Resolved Issues

- **`GCD`/`LCM` now evaluate on non-integer real arguments.** `\gcd(2.25, 2.1)`
  and `\operatorname{lcm}(2.5, 1.5)` previously stayed symbolic (and compiled to
  `NaN`, blanking any plot that used them); they now fold via a tolerant
  floating Euclidean algorithm — the standard "float GCD" that terminates when a
  remainder falls below `ε · max(|a|, |b|)` (`ε = 1e-6`). This honors the
  exactness contract: an inexact (float) argument numericizes, like `\cos(5.1)`,
  while integer and exact-rational operands keep their exact (`\gcd(4, 6) → 2`)
  and symbolic (`\gcd(9/4, 21/10)` under `evaluate()`, `0.15` under `.N()`)
  behavior. The compiled `javascript`, `glsl`, and `wgsl` targets fold reals the
  same way (the GPU `_gpu_gcd` cutoff was integer-tuned and is now
  scale-relative). The tolerance is deliberately its own constant, not the
  engine's numeric tolerance — float-commensurability is a much looser notion,
  and the value that reproduces a given renderer's output is a consumer choice.
  (Requested by the Tycho/Graph Paper team for expressions such as
  `r ≤ gcd(θ², θ + a)`.)
- **`GCD`/`LCM` reduce a finite collection argument.** `\gcd([12, 18, 24])` →
  `6` and `\operatorname{lcm}([4, 6])` → `12` (a list argument is folded over
  its elements); previously the whole call stayed symbolic even for integers.
  Mixed list-and-scalar arguments (`GCD([12, 18], 8)` → `2`) and lists of reals
  are handled; an infinite or enumeration-declined collection stays symbolic
  rather than grinding to the evaluation deadline.
- **`Max`/`Min` of a scalar and a collection no longer mis-compiles to `NaN`.**
  `Max(0, [1, -2, 3])` evaluates to `3` (the reduction folds the collection's
  elements), but on the JavaScript target it compiled to `Math.max(0, [1,-2,3])`
  — passing an array as an argument — and returned `NaN` at run time. It now
  folds the scalar and every collection's elements into a single reduction,
  matching `evaluate()`. (Surfaced by the Tycho/Graph Paper team's `max(0, …)`
  plot expressions.)
- **`Max`/`Min` compile correctly on the `python` target.** They were mapped to
  the element-wise, strictly-binary `np.maximum`/`np.minimum`, so a collection
  operand was mis-reduced (`Max(0, [1,2,3])` → `[1,2,3]` instead of `3`) and a
  single-list or n-ary call errored at run time. `Max`/`Min` now reduce a
  collection operand with `np.max`/`np.min` and combine the per-operand results
  element-wise (keeping a scalar/array operand — e.g. the plot variable —
  vectorized), matching `evaluate()` and the JavaScript target.
- **`At(base, index)` serialization now parenthesizes a compound base.**
  `At(x+1, 2)` serialized to `x+1_2` (subscript) / `x+1[2]` (bracket index
  style) — the index bound only to the trailing operand, dropping the base
  grouping so the LaTeX re-parsed to something other than element access. A base
  whose precedence falls below the postfix index operator is now wrapped:
  `(x+1)_2` / `(x+1)[2]`. Symbol and function-application bases (`v_1`,
  `H(x, y)[1]`) are unchanged. (Serialization round-trip reported by the
  Tycho/Graph Paper team. The default subscript index style is unchanged;
  programming-style bracket output — which re-parses to `At` independent of the
  base's declared type — remains available via `indexStyle: () => 'bracket'`.)

### Collections

- **A list comprehension is now a lazy collection: `evaluate()` no longer walks
  its whole domain.** A `Comprehension` (`[body \operatorname{for} i=…]`)
  behaves like `Range`/`Map` — `evaluate()` returns the comprehension itself,
  and its collection type, `.count`, and emptiness are reported from the
  iterator-clause counts without enumerating a single element. Elements are
  materialized only when actually consumed (a positive `at(n)` walks just the
  first `n`); indexing, iteration, and aggregation (`Sum`, `Length`, `At`,
  `Take`, `Map`, …) are unchanged. Binding an unread comprehension to a name is
  therefore ~O(1) instead of materializing its whole domain up front — e.g. 25
  dead 225-element weight tables dropped from ~6 s to negligible. (Reported by
  the Tycho/Graph Paper team.)
- **A bracket comprehension parses to the comprehension itself, not a
  one-element `List` wrapping it.** `[body \operatorname{for} i=…]` previously
  produced `["List", ["Comprehension", …]]`, which reported `count: 1` and
  mis-indexed (`W[3]` → `NaN`); it now returns the `Comprehension` directly,
  mirroring the `Range`/`Linspace` bracket passthrough. (Reported by the
  Tycho/Graph Paper team.)
- **`Tabulate` (and its `Table` alias) is now a lazy indexed collection.**
  `evaluate()` returns the `Tabulate` itself rather than building the whole
  array; `.count` is the outer dimension and an element is computed by applying
  the function only when indexed or iterated. A `Tabulate(f, 1_000_000)` that is
  bound but unread is now O(1) instead of hanging while it builds a
  million-element list. The Mathematica-style `Table(i^2, {i, 1, n})` inherits
  this (it canonicalizes to `Tabulate`).
- **`Permutations` and `Combinations` are now lazy collections with closed-form
  counts.** `Permutations(xs, k?)` and `Combinations(xs, k)` no longer
  materialize their factorially-many elements to be bound, counted, or indexed:
  `.count` is `P(n, k)` / `C(n, k)` computed directly (previously `Permutations`
  of a 9-element list took ~33 s just to answer `.count`), elements stream from
  the iterator, and `at(n)` walks only as far as needed.

### Compilation

- **A user-defined function passed as a higher-order operand now compiles by
  reference.** Compiling `Map(list, f)` / `Filter(list, f)` — where `f` is a
  function declared on the engine (`f(x) := …`, `x ↦ …`) rather than an inline
  lambda — previously emitted a dangling `_.f` and threw at run time. The
  function operand now resolves to the same shared local (`_fn_f`) the call-site
  path already emitted, so a user function used as a first-class value works
  wherever it is referenced, not only when it is called. Its `freeSymbols` are
  computed from the operand function's body (so a free symbol used only inside
  `f` is reported), and inline-lambda operands are unaffected. (Reported by the
  Tycho/Graph Paper team.)

### API

- **`BoxedOperatorDefinition.lambda` exposes a user-defined function's body and
  parameters.** `ce.lookupDefinition(name).operator.lambda` returns
  `{ parameters, body }` — the parameter names/types and the body as a boxed
  expression — for a definition created from a function literal (`f(x) := …`,
  `x ↦ …`, `ce.assign('f', lambda)`), or `undefined` for a built-in operator.
  This is a supported, stable accessor over the internal `_lambdaLiteral`,
  letting a consumer traverse or resolve a function reference structurally
  without re-parsing or textually inlining its source. (Requested by the
  Tycho/Graph Paper team.)

## 0.79.3 _2026-07-15_

### Parsing

- **A number-valued symbol juxtaposed with a parenthesized collection now parses
  as multiplication, not a function application.** When a symbol known to be a
  non-function value — declared with a numeric type or assigned a value — was
  juxtaposed against `(…)` whose body referenced a collection (e.g. `k(\cos(S))`
  with `k` a number and `S` a bound list), it parsed as `k` _applied_ to the
  body — an illegal application of a number, yielding `NaN` — instead of
  `k\cdot\cos(S)`. The single-argument invisible-operator rule only treated a
  scalar-numeric argument as multiplication; a collection-typed argument fell
  through to the function-call heuristic even when the leading symbol could not
  be a function. Such a symbol now scales over the argument, matching the
  scalar-argument and multi-operand cases. An undeclared or unknown-typed symbol
  stays ambiguous and keeps the `f(x)` function-application default. (Reported
  by the Tycho/Graph Paper team.)

- **Parsing a call no longer un-assigns a bare-symbol argument.** Parsing an
  application like `f(S)` runs argument-type inference on each operand. When the
  callee's parameter type was `unknown` or `any` and the argument symbol had
  been declared `unknown` and assigned a value, inference computed `unknown` (a
  no-op narrowing) and wrote it back to the symbol's type — and the
  value-definition type setter discards the held value whenever the type is set
  to `unknown`. So merely _parsing_ `f(S)` (no `evaluate`/`N`) silently cleared
  `S`'s assigned value, leaving it unbound and every dependent expression `NaN`.
  Inferring `unknown` adds no information, so it is now skipped entirely and
  never overwrites an existing binding; inference of a concrete parameter type
  still narrows an open argument as before. (Reported by the Tycho/Graph Paper
  team.)

### Collections

- **The `.x`/`.y`/`.z` point-coordinate accessors now broadcast over a list of
  points.** They previously parsed to `First`/`Second`/`Third` — the collection
  element-indexing operators. On a single point that is correct (`(3,4).x` = 3,
  since the first element of a 2-tuple is its x-coordinate), but on a _list of
  points_ the two diverge: `[(1,2),(3,4),(5,6)].x` returned the first _point_
  `(1,2)` instead of the list of x-coordinates `[1,3,5]`. The accessors now
  parse to dedicated `PointX`/`PointY`/`PointZ` operators that extract a
  coordinate and map element-wise over a list of points (matching the threadable
  `.real`/`.imag` accessors), while a single point still returns the scalar
  coordinate. `First`/`Second`/`Third` are unchanged and continue to index a
  collection. The new operators broadcast on the `javascript` compile target
  (`L.x` → `(L).map((p) => p[0])`) and, for a single point, swizzle on the GPU
  target as before. (Reported by the Tycho/Graph Paper team.)

### Compilation

- **Element-wise (scalar↔list) arithmetic now broadcasts on the `javascript`
  compile target.** An arithmetic or element-wise math operator applied to a
  list-valued operand — `x - L`, `2L`, `L^2`, `-L`, `\sin(L)`, `\sqrt{L}`, or
  two lists `L + M` — previously compiled to scalar JavaScript that returned
  garbage (`-_.L + _.x` → `NaN`) behind a `success: true`, unless an operand was
  a _concrete_ collection at compile time (which failed closed). A symbolic
  list-valued **parameter** (bound at run time — the normal compile case)
  slipped through entirely. These now compile to a `_SYS.bcast` runtime helper
  that maps the operator element-wise, matching the interpreter's broadcasting:
  scalars are reused for every element, two lists zip to the shorter length, and
  nested lists (matrices) recurse. The pure-scalar fast path is unchanged. A
  **complex**-valued list still has no coverage and now fails closed correctly
  (`success: false` → interpreter fallback) instead of silently returning
  garbage. Combined with the `.x`/`.y` point-broadcast change above, a compiled
  expression such as `\min((x - V.x)^2 + (y - V.y)^2)` over a list of points `V`
  now evaluates correctly. (Reported by the Tycho/Graph Paper team.)

## 0.79.2 _2026-07-15_

### Compilation

- **The collection form of `Sum`, `Product`, `Max`, and `Min` now compiles on
  the `javascript` target.** Applied to a collection with no indexing set — e.g.
  `[3,4,5].\operatorname{total}` (which canonicalizes to `Sum([3,4,5])`), a list
  product, or `\max(v)` for a list `v` — these previously threw
  `Sum: no indexing set` (dropping the whole expression to interpretation) or,
  for `Max`/`Min`, compiled to `Math.max([…])` and returned `NaN`. They now
  lower to a native `.reduce`, with empty-collection identities matching the
  interpreter (`Sum([]) = 0`, `Product([]) = 1`, `Max([]) = -\infty`,
  `Min([]) = +\infty`). This lets a compiled list comprehension whose body uses
  `.total`/`.count`/`\min`/`\max` compile end-to-end into a loop instead of
  falling back. The indexing-set forms (`\sum_{n=1}^{5}`) and the scalar
  variadic `\max(a,b,c)` are unchanged; a non-collection operand still fails
  closed. (Reported by the Tycho/Graph Paper team.)
- **List-shaped collection operators now compile on the `javascript` target.**
  `Last`, `Rest`, `Take`, `Drop`, `Join`, `Reverse`, `Sort`, `IndexOf`, `Map`,
  and `Filter` previously fell back to interpretation (`Unknown operator`); they
  now lower to native array operations (`.slice`, `.reverse`, `.sort`,
  `.indexOf`, `.map`, `.filter`, …). `Take`/`Drop` clamp a negative count to
  match the interpreter (`Take(xs, -2) = []`, `Drop(xs, -2) = xs`); `Reverse`
  and `Sort` copy first so the source is not mutated; `Sort` compiles the
  default ascending numeric order (a custom comparator fails closed); `IndexOf`
  is 1-based (0 when absent); `Map`/`Filter` compile their lambda operand. A
  non-indexed-collection operand fails closed. (Requested by the Tycho/Graph
  Paper team.)

### Evaluation

- **`IndexOf` / `IndexWhere` now work on a tensor-backed list.** A rectangular
  numeric list is represented as a `BoxedTensor`, which inherited the abstract
  no-op `indexWhere` and so made `IndexOf`/`IndexWhere` always return `0` (not
  found) even for a present element. The base `indexWhere` now scans any finite
  indexed collection for the 1-based index of the first match.

### Serialization

- **`Mod` (`\bmod`) parenthesizes compound operands so its LaTeX round-trips.**
  Infix `\bmod` binds tighter than `+`/`-` on re-parse, so `Mod(x+5, 2\pi)`
  serialized to `x+5\bmod2\pi` and re-parsed as `x + (5 \bmod 2\pi)`. An operand
  at addition precedence is now wrapped — `(x+5)\bmod2\pi` — while juxtaposition
  products (`3k`, `2\pi`), fractions, powers, and negation stay unwrapped since
  they already re-parse as tight units. A left-nested `Mod` is also now
  parenthesized (`\bmod` is right-associative, so `Mod(Mod(a,b),c)` →
  `(a\bmod b)\bmod c`). (Reported by the Tycho/Graph Paper team.)

### Parsing

- **Juxtaposition of a collection-typed symbol with a function call parses as
  multiplication.** A symbol declared with an abstract `indexed_collection` or
  `collection` type but not yet assigned a value — e.g. `y_r` in `y_r\sin(a)` —
  grouped into a `Tuple` instead of a `Multiply`, even though its concrete
  subtypes (`list`, `vector`, `matrix`, numeric `tuple`) and the assigned-value
  case already multiplied. The abstract collection type now scales like its
  subtypes; non-indexed `set` and heterogeneous `tuple` operands still group as
  a `Tuple`. (Reported by the Tycho/Graph Paper team.)

## 0.79.1 _2026-07-15_

### Evaluation

- **`ce.timeLimit` is now enforced during symbolic integration and rule
  matching.** Extends the 0.79.0 expression-tree-growth checkpoint to two paths
  that previously ran unbounded: the multinomial expansion of a power of a sum
  (`expandPower`) and the rule-set scan (`matchAnyRules`). The worst case is
  compiling a definite integral of a high-power integrand — e.g.
  `\int_{-15}^{15} (2 + \sin(3y) + \cos(\pi^2 y))^p\,dy`: the compiler's
  antiderivative-first attempt expands `(trinomial)^p` into a multinomial with
  `C(p+2, 2)` terms (≈ 6·10⁴ at `p=350`) and matches the integration rule set
  against it (~100 ms per rule), neither of which hit the per-node checkpoint,
  so compilation stalled for many seconds (or hung outright) instead of honoring
  the deadline. Both paths now cooperatively check the deadline, so a hard
  integrand degrades to Gauss–Kronrod quadrature at `ce.timeLimit` (default 2 s)
  as intended, and a bare `evaluate()` of such an integral throws a catchable
  `CancellationError` (`cause: 'timeout'`) rather than running unbounded.
  (Reported by the Tycho/Graph Paper team.)

## 0.79.0 _2026-07-14_

### Evaluation

- **`ce.timeLimit` is now enforced during expression-tree growth.** The
  evaluation deadline was only checked inside specific loops (collection
  enumeration, polynomial GCD, …), so an evaluation whose cost is dominated by
  expression construction never hit a checkpoint. The worst case is a nested
  user-function chain whose body references a parameter several times — e.g. a
  symbolic Newton iteration `s(y, x_p) := \frac{y - f(x_p)}{f'(x_p)} + x_p`
  applied as `s(y, s(y, … s(y, x_0)))` — which grows the result ×4 per nesting
  level: at depth 15 it previously exhausted an 8 GB heap without ever honoring
  `timeLimit`. A cooperative checkpoint on the per-node evaluation path now
  cancels such evaluations at the deadline with a catchable `CancellationError`
  (`cause: 'timeout'`), e.g. at the default 2 s limit the depth-15 chain aborts
  using < 60 MB. Numeric chains (each level folds to a number) are unaffected
  and remain fast. Note: long-running evaluations that previously _completed_
  after exceeding `timeLimit` now throw — raise `ce.timeLimit` (or set it to `0`
  for no limit) if you rely on multi-second symbolic evaluations.

### Library and Definitions

- **`ce.searchDefinitions()` now treats the query as OR-ed keywords.**
  Previously a multi-word query only matched definitions containing _every_
  word, so keyword-bag queries like `"floor quotient integer division"` returned
  nothing. Any matching word now suffices, and results are ranked by how many
  words they match and how exactly (identifier match, then trigger or curated
  keyword, then description). The query may also be an array of strings —
  `ce.searchDefinitions(['gcd', 'least common multiple'])` — with each element
  treated as an OR-ed alternative.

### Solving

- **`Solve` accepts Mathematica-style constraint systems.** The first argument
  may now bundle domain constraints together with the equation —
  `\mathrm{Solve}(\{100a+10b+c=11(a^2+b^2+c^2), a\in\{1,\dots,9\}, b\in\{0,\dots,9\}, c\in\{0,\dots,9\}\}, \{a,b,c\})`
  → `[(5, 5, 0), (8, 0, 3)]`. `Element` items inside a `Set`/`List`/`And` first
  operand are lifted into per-variable domain specs (equivalent to passing
  `a \in D` as separate spec arguments), the remaining items form the
  equation/system, and a variable list written as a set (`\{a,b,c\}`) is
  accepted alongside the existing `[a,b,c]` list form. The variable list may be
  omitted entirely when the bundled constraints name the unknowns:
  `Solve(\{eq, a\in\{1,\dots,9\}, …\})` solves for the constrained symbols in
  constraint order. A constraint for a variable that already carries a
  spec-position domain is merged conjunctively (both must hold), and a
  constraint naming a symbol absent from an explicit variable list leaves the
  expression unevaluated rather than guessing. A system of equations given as a
  `Set` (or `And`) now also solves like the equivalent `List`.
- **Trailing domain argument: `Solve(eq, x, \mathbb{Z})`.** A trailing set
  constant (`Integers`, `RealNumbers`, …) after the unknowns applies as the
  domain of every unknown, Mathematica-style:
  `\mathrm{Solve}(x^2=4, x, \mathbb{Z})` → `[2, -2]`. Unknowns that already
  carry an explicit `Element` domain keep it. Over an unbounded integer domain a
  _polynomial_ equation with no integer roots now decides `[]`
  (`\mathrm{Solve}(2x=3, x, \mathbb{Z})`,
  `\mathrm{Solve}(x^2=2, x, \mathbb{Z})`) instead of staying unevaluated;
  non-polynomial equations stay inert rather than risk over-claiming "no
  solutions" from a partial root set.
- **Inequality side conditions in constraint sets.** A relational or boolean
  predicate bundled in the first argument restricts the solution set instead of
  being mistaken for an equation: `\mathrm{Solve}(\{x^2=4, x>0\}, x)` → `[2]`
  (previously returned the incorrect `[]`), and a multi-variable condition
  filters candidate tuples —
  `\mathrm{Solve}(\{a+b=5, a\in\{0,\dots,5\}, b\in\{0,\dots,5\}, a<b\}, \{a,b\})`
  → `[(0, 5), (1, 4), (2, 3)]`. Filtering is conservative (a candidate is
  dropped only when a condition is definitely `False`) and applies across the
  symbolic, diophantine-fallback, and enumeration paths. A constraint set
  containing only predicates solves them directly by enumeration over the domain
  (`\mathrm{Solve}(\{x \equiv 2 \pmod 5, x\in\{1,\dots,20\}\}, x)` →
  `[2, 7, 12, 17]`).

### Mathematica-Style Operator Forms

- **Iterator triples: `\{i, lo, hi\}` and `\{i, lo, hi, step\}`.** The
  Mathematica iterator spec is now recognized in the iterator/bounds slot of
  `Sum`, `Product`, `Integrate` and `D`: `\mathrm{Sum}(i^2, \{i, 1, 10\})` →
  `385`, `\mathrm{Sum}(i, \{i, 0, 10, 2\})` → `30`,
  `\mathrm{Integrate}(x^2, \{x, 0, 1\})` → `1/3` (the bounds were previously
  silently dropped, yielding an indefinite integral), and
  `\mathrm{D}(f, \{x, n\})` is the n-th derivative. Symbolic bounds work
  (`\mathrm{Sum}(k, \{k, 1, n\})` ≡ `\sum_{k=1}^n k`). The interpretation is
  strictly positional — a brace set anywhere else keeps its literal set meaning
  — and operates on held (raw) operands, so the index symbol is scoped like a
  binder (an `i` index does not collapse to the imaginary unit).
- **New `Table` operator, an alias for `Tabulate`.**
  `\mathrm{Table}(i^2, \{i, 1, 5\})` → `[1, 4, 9, 16, 25]`, with general
  iterator bounds and step (`\mathrm{Table}(i, \{i, 0, 10, 2\})` →
  `[0, 2, 4, 6, 8, 10]`) and multiple iterator specs for nested dimensions
  (`\mathrm{Table}(i j, \{i, 1, 2\}, \{j, 1, 3\})` → `[[1,2,3],[2,4,6]]`, first
  spec outermost). `\{v, 1, n\}` specs canonicalize directly to `Tabulate`;
  general bounds map to `Map` over `Range`. `Tabulate` also gained the `table`
  search keyword so `ce.searchDefinitions('table')` finds it.
- **`\mathrm{D}(f, x)` differentiation.** Applied to an argument list,
  `\mathrm{D}` / `\operatorname{D}` is the derivative operator:
  `\mathrm{D}(x^3, x)` → `3x^2`, `\mathrm{D}(x^2 y, x, y)` takes sequential
  partials. The bare forms keep their previous meanings (`\mathrm{D}` is the
  upright-D glyph symbol; `\operatorname{D}` remains usable as a pipeline stage,
  `x^2 \rhd \operatorname{D}` → `2x`).
- **`Limit(f, x \to x_0)` rule-arrow form.**
  `\mathrm{Limit}(\frac{\sin x}{x}, x\to 0)` → `1`, equivalent to
  `\lim_{x\to 0}`. One-sided arrows carry the direction:
  `\mathrm{Limit}(\frac{1}{x}, x\to 0^+)` → `+∞`.
- **`Simplify(expr, assumptions)`.** An optional second argument supplies one or
  more boolean assumptions (a bare predicate, or a `List`/`And` of them) that
  hold only for the duration of the simplification:
  `\mathrm{Simplify}(\sqrt{x^2}, x>0)` → `x`, `\mathrm{Simplify}(|x|, x<0)` →
  `-x`.
- **New `ReplaceAll` operator.** `\mathrm{ReplaceAll}(x^2+x, x\to 2)` → `6`
  (Mathematica `expr /. rules`). Rules are `lhs \to rhs` (or `Rule(lhs, rhs)`),
  given as extra arguments or bundled in a set/list:
  `\mathrm{ReplaceAll}(x+y, \{x\to 1, y\to 2\})` → `3`. Symbol rules are applied
  simultaneously in a single pass; non-symbol left-hand sides use the
  pattern-rule machinery. The result is evaluated after substitution.
- **Tuple membership distributes: `(a,b) \in \mathbb{Z}`.** Membership of a
  tuple of symbols in a scalar (number-element) collection now distributes to a
  conjunction — `Element(a, Integers) ∧ Element(b, Integers)` — instead of
  evaluating to `False`. Value tuples against product sets are unaffected.

### LaTeX Parsing

- **One-sided limits: `\lim_{x\to 0^+}` and `\lim_{x\to 0^-}`.** The `^+`/`^-`
  direction marker on a limit point was previously captured by the generic
  superscript entries as `PseudoInverse(0)` / `Superminus(0)`, making every
  one-sided limit unevaluatable. The marker now maps to `Limit`'s direction
  operand (`["Limit", f, 0, 1]` / `…, -1]`), which the limit evaluator already
  supported: `\lim_{x\to 0^+} \frac{1}{x}` → `+∞`, `\lim_{x\to 0^-} \frac{1}{x}`
  → `-∞`, `\lim_{x\to 0^+} \ln x` → `-∞`. Directions serialize back as
  `^{+}`/`^{-}` (round-trip), symbolic points (`\lim_{x\to a^+}`) keep a correct
  representation, and superscript `+`/`-` everywhere else (`A^+` pseudoinverse,
  `3^-` signed value) is unaffected.
- **`\mapsto` lambda bodies extend through comparisons.** `n \mapsto n > 102`
  now parses as `n \mapsto (n > 102)` — previously the body closed at the
  comparison, mis-parsing as `(n \mapsto n) > 102`, which made unparenthesized
  predicates like `\mathrm{Filter}(\mathrm{Range}(100,105), n \mapsto n > 102)`
  fail. The body now extends through comparisons and logical connectives
  (`n \mapsto n > 2 \wedge n < 5`), stopping at the comma/sequence level, so a
  lambda in an argument list still does not swallow the following argument.
- **Ellipsis ranges in set braces.** `\{1,\dots,9\}` now parses to
  `["Range", 1, 9]`, matching the existing bracket form
  `\lbrack1,\dots,9\rbrack`; the stepped form `\{0, 2, \dots, 10\}` yields
  `["Range", 0, 10, 2]`. Previously the ellipsis was kept as a literal
  placeholder element (`["Set", 1, "ContinuationPlaceholder", 9]`), which made
  `a \in \{1,\dots,9\}` unusable as a domain. Enumerated sets of non-numeric or
  non-progression elements (`\{a, b, c\}`, `\{1, 2, 3\}`) are unaffected.

### Performance

- **`Interpret` no longer spends ~13 s rejecting a non-polynomial sequence.**
  Interpreting a continuation such as `1 + 1 + 2 + 3 + 5 + 8 + \dots + 55`
  (Fibonacci) first tries the polynomial recognizer, which fits a degree-5
  interpolant through the samples and searches for the index where it reaches
  the anchor. That interpolant has a negative leading coefficient, so it
  eventually decreases — but the search's overshoot test used the sample trend
  ("increasing"), which never fired, so it ground through all 100 000 candidate
  indices before falling through to the recurrence recognizer. The search now
  stops on the interpolant's _local_ trend (a polynomial is eventually
  monotonic, so once it is past the anchor and still diverging it cannot
  return), cutting this interpretation from ~13 s to a few milliseconds.
  Legitimate polynomial sums (triangular numbers, squares) are unaffected.

### Calculus

- **Improper integrals of `polynomial × exp-decay` no longer return `NaN`.** A
  definite integral to `±∞` whose antiderivative carries a term like
  `y^2 e^{-y}` was evaluated at the infinite bound by naive substitution,
  producing an `∞·0` indeterminate that collapsed to `NaN` — so
  `\int_0^\infty y^2 e^{-y}\,dy` (which is `\Gamma(3) = 2`) returned `NaN`. Such
  an endpoint is now resolved as the limit `\lim_{y\to\infty} F(y)` (exponential
  decay dominates polynomial growth), giving the exact closed form (`2`); with
  the Rubi integration rules loaded the same fix closes the χ²-tail
  `\int_x^\infty y^{3/2} e^{-y/2}\,dy` to `3\sqrt{2\pi} - F(x)`. An endpoint
  that still cannot be resolved keeps the integral inert (so `N()` quadrature
  applies) rather than leaking `NaN`.
- **`Limit` at infinity resolves `Erf`/`Erfc` and `\sqrt{}`/`\sqrt[n]{}`.**
  `\lim_{x\to\infty}\operatorname{erf}(x) = 1`,
  `\lim_{x\to-\infty}\operatorname{erf}(x) = -1`, `\operatorname{erfc}`
  saturating to `0`/`2`, and `\lim_{x\to\infty}\sqrt{x} = +\infty` (likewise
  `\sqrt[n]{x}`) were previously left unevaluated. Besides being correct in
  their own right, these fill the gaps behind the improper-integral endpoints
  above — an antiderivative's `\operatorname{erf}(\sqrt{y})` term needs both.
- **The upper incomplete gamma reduces at infinity: `\Gamma(s, +\infty) = 0`.**
  The tail `\int_{+\infty}^\infty t^{s-1} e^{-t}\,dt` vanishes for any finite
  `s` (the `e^{-t}` factor dominates), so `\Gamma(s, \infty)` — including
  symbolic `s` such as `\Gamma(\frac{k}{2}, \infty)` — now evaluates to `0`
  instead of staying inert (a provably infinite first argument stays symbolic).
  This closes the free-parameter χ²-tail antiderivative
  `\int_x^\infty y^{\frac{k}{2}-1} e^{-\frac{y}{2}}\,dy` to a form free of the
  leftover `\Gamma(\cdot, \infty)` term.
- **A rational integrand with fully symbolic coefficients no longer hangs.**
  `\int_0^x \frac{u-a}{b_2 u^2 + b_1 u + b_0}\,du` spun for ~109 s (ignoring a 3
  s `timeLimit`) inside the polynomial-GCD used to cancel common factors: its
  Euclidean loop divided by a symbolic constant, which produced a spurious
  nonzero constant remainder that never tested as zero, so the loop iterated
  forever building ever-larger coefficient expressions. A nonzero constant
  remainder now correctly resolves the GCD to `1` (coprime over the coefficient
  field), and the loop carries a deadline checkpoint as a backstop. The integral
  closes in ~200 ms (to an `ArcTanh`/`Ln` form with the Rubi rules loaded);
  `polynomialGCD` of coprime symbolic polynomials returns `1` instead of
  spinning.

### Compilation

- **Compiled definite integrals now resolve symbolically before falling back to
  quadrature.** `compile()` of an expression containing a definite `Integrate`
  first attempts a closed form (the same antiderivative machinery `evaluate()`
  uses); if one is found, the generated code is straight-line arithmetic rather
  than a per-call numerical integration. A plotted
  `\int_0^x 0.1\sqrt{1+t^2}\,dt` compiles to its closed form
  `0.05\,(x\sqrt{1+x^2} + \operatorname{arsinh} x)`, evaluated in microseconds
  per sample (exact and deterministic) instead of ~150 ms/sample of quadrature.
  The symbolic attempt is bounded by `ce.timeLimit` (default 2 s), so a hard
  integrand degrades to quadrature rather than stalling compilation, and it is
  skipped when the integral references a symbol supplied through the `vars`
  option (which must stay a live runtime input, not be folded to a constant).
- **The compiled quadrature fallback is now deterministic adaptive Gauss–Kronrod
  (GK15), not Monte-Carlo.** An integral that does not resolve symbolically is
  estimated with adaptive Gauss–Kronrod quadrature: near machine precision on
  smooth integrands, microseconds-to-milliseconds per call, and — unlike the
  previous 10⁷-sample Monte-Carlo estimator (~1e-4 error, a different value on
  every call, ~150 ms/call) — the **same** value on every call. Infinite bounds
  are handled by a smooth variable transform. Monte-Carlo remains an automatic
  fallback when the adaptive rule does not converge, and can be forced with the
  new `compile(expr, { quadrature: 'monte-carlo' })` option (`'adaptive'` is the
  default).

## 0.78.1 _2026-07-14_

### Parse Diagnostics

- **`juxtaposition-as-multiply` now covers unit-lexed and letter-run application
  shapes.** Two application-shaped sources that produced no diagnostic on 0.78.0
  are now reported: a symbol lexed as a **unit** applied to a group
  (`\mathrm{N}(2)`, where `N` reads as the newton unit) fires with `detail.name`
  set to the source symbol and a new additive `detail: { lexedAs: "unit" }`
  hint; and a **letter-run** applied to a group (`divisors(60)`, which segments
  into `d·i·v·i·s·o·r·s`) fires a single diagnostic whose `name` is the joined
  run (`"divisors"`) and whose span covers the full `divisors(60)` source shape.
  Run reconstruction stops at numbers and multi-character commands (`2x(3)`
  reports `x`; `\pi r(2)` reports `r`).

## 0.78.0 _2026-07-14_

### Parse Diagnostics

- **New opt-in `diagnostics` parse option.**
  `ce.parse(latex, { diagnostics: true })` attaches a `parseDiagnostics` array
  to the top-level result, flagging _charitable_ parse decisions that are
  usually errors in machine-generated LaTeX (LLM output, OCR). Four codes are
  reported: `undeclared-symbol` (a symbol reference with no declaration —
  `detail: { name, type }`), `juxtaposition-as-multiply` (a symbol immediately
  followed by a delimited group `(…)` or a matrix environment read as
  multiplication — `detail: { name, declaredAs }`), `comment-discarded` (an
  unescaped `%` dropped input — `detail: { discardedLength }`), and `recovered`
  (trailing noise silently skipped by non-strict error recovery). Each carries a
  `start`/`end` source span: `undeclared-symbol` and `juxtaposition-as-multiply`
  are offsets into CE's normalized LaTeX, while `comment-discarded` (and
  best-effort `recovered`) use original-input coordinates. The feature is purely
  additive — enabling it never changes the parse output — and works under
  `{ canonical: false }`. `parseDiagnostics` is present (a possibly-empty array)
  only on the result of a `diagnostics: true` parse, and `undefined` otherwise.

## 0.77.1 _2026-07-13_

### Breaking Changes

- **`f'(x)` now parses to `["Apply", ["Derivative", "f", 1], x]`** instead of
  `["D", ["f", x], x]`. Prime notation on an applied function denotes the
  _derivative function evaluated at the argument_ — Lagrange semantics, as in
  Mathematica and Desmos — not the derivative of the applied expression with
  respect to an inferred variable. The previous representation produced silently
  wrong results whenever the argument was not a bare variable: `f'(2)` evaluated
  to `0` instead of `f'` evaluated at 2 (e.g. `6` for `f(x) := x^2+2x+1`), and
  `f'(2x)` picked up a spurious chain-rule factor (`8x+4` instead of `4x+2`).
  Higher orders (`f''(2)` → `2`) and the `f^{(n)}(x)` superscript form follow
  the same rule. Serialization is unchanged (`f^{\prime}(x)`), so LaTeX
  round-trips are unaffected; only consumers pattern-matching the MathJSON parse
  shape need updating.

### Resolved Issues

- **Symbolic derivatives of declared-then-assigned functions** (0.77.0
  regression). Declaring a function symbol before assigning its body —
  `ce.declare("f", "function")` (or with an explicit signature such as
  `"(number) -> number"`) followed by `f(x) := x^2 + 2x + 1` — left the
  function's derivative inert: `f'(x)` evaluated to itself instead of `2x + 2`,
  and `["D", "f", "x"]` evaluated to `0`, even though direct calls like `f(2)`
  worked. The declared-signature reconciliation introduced in 0.77.0 keeps the
  assigned function literal in the symbol's _value_ definition (preserving the
  declared signature) instead of converting it to an operator definition, and
  the symbolic differentiation path only expanded function bodies from operator
  definitions. Differentiation now expands user-defined function bodies from
  both definition shapes.

## 0.77.0 _2026-07-13_

### Pattern Matching

- **New `Match` operator for structural pattern matching.**
  `["Match", subject, ["MatchCase", pattern, body], …]` selects the first case
  whose pattern matches the structure of the subject and applies its body to the
  captured values:
  `["Match", ["List", 3, 4], ["MatchCase", ["List", "_a", "_b"], ["Add", "a", "b"]]]`
  → `7`. Cases may carry a guard (`["MatchCase", pattern, guard, body]`);
  `["Pin", expr]` matches the _value_ of an expression (a constant like `Pi`, or
  the current value of a variable); `["Alternatives", p1, p2, …]` shares one
  body among several binding-free patterns. Unlike `Which`, which stays
  unevaluated while a condition is undecidable, `Match` always decides — a
  symbolic subject falls through to a wildcard case. No matching case yields an
  `["Error", "'match-no-case'"]` value.
- **Cortex: `match` expression.** The reserved `match` keyword is now a full
  pattern-matching expression:
  `match x { 0 => "zero"; 1 | 2 | == Pi => "small"; [first, ...rest] => first; n if n > 3 => n; _ => "other" }`.
  Bare identifiers in a pattern always bind (a non-final catch-all like
  `Pi => …` is a parse error suggesting `== Pi` to match the constant);
  `== expr` pins a value; `|` gives or-alternatives; `[…]`, `(…)` and
  `{key -> pat}` destructure lists, tuples and dictionaries (open matching);
  `n: integer` adds a type guard; `...rest` captures the tail.
- **Constant-time dispatch and compilation.** Matches over constant cases
  dispatch through a cached table instead of the general pattern matcher, and
  fixed-shape destructuring compiles to direct positional checks. `compile()`
  emits comparison chains or a JavaScript `switch` for constant cases and
  destructuring closures for fixed shapes; symbolic patterns (e.g. `a + b`) fail
  closed with a clear error rather than producing incorrect code.

### Typed Function Literals

- **Function literals can declare parameter and return types.** A `Function`
  parameter may be annotated — `["Typed", "x", "'integer'"]` — and the body may
  carry a return-type ascription, so
  `["Function", ["Add", "x", 1], ["Typed", "x", "'integer'"]]` now has type
  `(x: integer) -> integer` instead of `(unknown) -> number`. Annotations feed
  body type inference, and in strict mode arguments are checked at application:
  applying `2.5` to an `integer` parameter yields an `incompatible-type` error
  instead of silently computing. Partial application preserves the remaining
  annotations and the return type. Assigning an annotated literal to a symbol
  gives it the full typed signature — including the declared return type, which
  is an _ascription_ (authoritative, like a TypeScript annotation) rather than a
  check against inference. Untyped literals are unchanged.
- **New `Typed` operator for type ascription.** `["Typed", expr, type]` asserts
  the type of an expression for the type system and is transparent at
  evaluation. It accepts a type string (`"'integer'"`) or a type-name symbol
  (`integer`). LaTeX serialization drops annotations (no typed-parameter
  notation in v1); MathJSON round-trips them.
- **Cortex: typed function definitions are enforced end to end.**
  `f(x: integer) -> real = x + 1`, `function g(n: integer) -> integer {…}`, and
  the anonymous form `(x: integer) |-> x + 1` (new grammar) all parse to native
  annotated literals; mistyped calls error, declared return types are carried,
  and `serializeCortex` reconstructs the typed syntax faithfully. Recursive
  typed definitions work
  (`fact(n: integer) -> integer = if n <= 1 {1} else {n * fact(n - 1)}`).

### Programming and Collections

- **Closures capture per-call state.** A zero-parameter closure returned from a
  factory function now captures its own instance of the factory's local
  variables, so separate invocations no longer share mutable state. For example,
  two counters built from the same `makeCounter()` factory advance
  independently. Parameterized factories already behaved this way; the fix
  extends the same per-call scope instantiation to nullary functions.
- **Lazy collection operations iterate eager sources.** A lazy operation such as
  `Map` or `Filter` applied to a collection that only materializes on evaluation
  (e.g. `UnicodeScalars(s)`, `Characters(s)`) now iterates its elements instead
  of behaving as empty. For example,
  `StringFrom(Map(UnicodeScalars(s), c -> c + 1), "unicode-scalars")` now
  produces the shifted string rather than `""`.

### Symbolic Computation

- **Arithmetic and function application thread through conditional values.** A
  restricted value `When(v, cond)` and a piecewise `Which(c_1, v_1, …)` now flow
  through scalar operations instead of staying inert: `sin(When(x, x > 0))` →
  `When(sin(x), x > 0)`, guards combining by conjunction
  (`When(x, x > 0) · When(y, y < 1)` → `When(x·y, x > 0 ∧ y < 1)`), and
  `Which(x > 0, 1, x < 0, -1) + 2` → `Which(x > 0, 3, x < 0, 1)`. Logic
  operators are excluded (so `And(When(A, g), False)` still short-circuits to
  `False`), and piecewise products above 16 combined branches stay unevaluated.
- **Restriction guards survive arithmetic cancellation.** Evaluating
  `When(x, c) − When(x, c)` previously folded to plain `0`, silently discarding
  the restriction on the (fat) region where `c` fails; it now yields
  `When(0, c)`. Similarly `0 · When(x, c)` → `When(0, c)` and
  `When(x, c) / When(x, c)` → `When(1, c)`.
- **`When` respects numeric approximation.** `When(π, cond).N()` with a true
  condition now numericizes (previously the option was dropped and the value
  stayed symbolic).
- **DMS angles stay exact.** (contributed by
  [yelliver](https://github.com/yelliver)) Degrees-minutes-seconds notation now
  parses to an exact rational number of degrees instead of a float (`9°30'` is
  `19/2°`), and `Degrees` converts any rational — not just integers — to an
  exact multiple of π, so `5°37'30"` simplifies to `π/32`. Decimal components
  are recovered exactly when possible (`9°30'15.5"` → `68431/7200°`) and
  otherwise fall back to floats. `Degrees` of values beyond 2⁵³ no longer loses
  precision. In raw (non-canonical) parsing, DMS angles that previously produced
  a float now produce a `Rational`. (#321)

### Calculus

- **Exponentials with any linear exponent integrate.** `∫e^{−ax}dx` with a
  symbolic `a` previously stayed unevaluated (only `e^{a·x}`-shaped exponents
  were recognized); it now returns `−e^{−ax}/a`. Any linear exponent works:
  `∫e^{3−2x}dx`, `∫5e^{−x/2}dx`.
- **Improper integrals with symbolic parameters return convergence-guarded
  results.** `∫₀^∞ e^(−ax)dx` with a free `a` previously stayed unevaluated; it
  now returns `1/a {0 < a}`. Results that formerly leaked indeterminate endpoint
  forms are fixed: `∫₀^1 xⁿdx` returned an expression containing `0^(n+1)`, and
  `∫₁^∞ x^(−s)dx` one containing `∞^(1−s)`; they now return `1/(n+1) {0 < n+1}`
  and `1/(s−1) {1 < s}`. Integrals whose endpoint behavior cannot be classified
  stay unevaluated rather than leaking indeterminates. Numeric-parameter
  integrals are unchanged.
- **`Series` expands at algebraic branch points (Puiseux series).** A series
  expansion may now carry fractional powers: `Series(√(sin x), x)` →
  `√x − x^{5/2}/12 + x^{9/2}/1440 + O(x^{13/2})`. This covers `√x`, `1/√x`,
  `x^{3/2}·e^x`, `Root(g, r)` and rational powers `g^{p/r}` where the base
  vanishes (or has a pole), as well as compositions: `cos(√x)`, `csc(√x)` (→
  `1/√x + √x/6 + …`), and `Γ(√x)` (→ `1/√x − γ + …`).
- **`Series` expands through logarithmic singularities.** Expanding about a zero
  or pole of a logarithm's argument now yields a log-carrying series:
  `Series(ln(sin x), x)` → `ln x − x²/6 − x⁴/180 + O(x⁶)`, and `Series(x^x, x)`
  → `1 + x·ln x + x²·ln²x/2 + …`. Base-`b` logarithms (`log₂ x`, `log₁₀ x`)
  expand through the same path. Nested or reciprocal logarithms (`ln(ln x)`,
  `1/ln x`) and essential singularities (`e^{1/x}`) still stay unevaluated
  rather than returning a partial expansion.
- **`Series` of an irrational power no longer returns an invalid expansion.**
  `Series(x^π, x)` previously produced coefficients containing unresolved
  `0^{π−1}`-style indeterminates; it now stays unevaluated at 0 (the expansion
  about a regular point, e.g. `x^π` about 2, is unchanged).
- **Logarithmic asymptotic expansions at `±∞`.** A log-carrying expansion at
  `+∞` now resolves back to `x` instead of deferring: `Series(ln x, x, +∞)` →
  `ln x`, and `Series(ln(x²+x), x, +∞)` → `2 ln x + 1/x − 1/(2x²) + …`. At `−∞`
  (a logarithm of a negative quantity) such expansions still stay unevaluated.
- **Stirling asymptotics for the log-gamma.** `Series(GammaLn(x), x, +∞)` (and
  the parsed `ln Γ(x)`) now returns Stirling's series
  `x·ln x − x − ½ln x + ½ln(2π) + 1/(12x) − 1/(360x³) + O(1/x⁵)`. The series is
  asymptotic (divergent), so the `BigO` is placed at the true remainder order.
  `Series(Γ(x), x, +∞)` — the exponential of a trans-series — still defers.
- **`GammaLn` evaluates to `+∞` at the poles of `Γ`** (the non-positive
  integers, where `ln|Γ| → +∞` — as in Mathematica's `LogGamma` and SymPy's
  `loggamma`); it previously stayed inert there. Consequently
  `Series(GammaLn(x), x)` at such a pole now returns the log-aware expansion
  `−ln x − γ·x + (π²/12)·x² + …` (matching the parsed `ln Γ(x)`) instead of an
  invalid expansion with inert `GammaLn(0)`, `Digamma(0)`, … coefficients.
- **Provably-exact expansions drop the `BigO` remainder.** When the truncated
  sum is symbolically equal to the whole function, `Series` now returns it
  without a remainder term: `Series(√x, x)` → `√x`, `Series(ln x, x)` → `ln x`,
  `Series(x/(x−2)², x, 2)` → `2(x−2)⁻² + (x−2)⁻¹`. Genuinely-truncated series
  (`1/sin x`, `Γ(x)` at 0, `ζ` at 1) keep their `BigO`.

### Sums and Products

- **Geometric series closed form.** `Σ_{n=0}^∞ rⁿ` now evaluates: exactly for a
  numeric ratio (`Σ(1/2)ⁿ → 2`, `Σ(1/√2)ⁿ → 2 + √2`), and with its convergence
  condition for a symbolic ratio (`Σxⁿ → 1/(1−x) {|x| < 1}`). Constant multiples
  and integer start indices are handled (`Σ_{n=2}^∞ xⁿ → x²/(1−x) {|x| < 1}`);
  divergent numeric ratios stay unevaluated.

### Solving Equations

- **Radical equations with a symbolic right-hand side return guarded roots.**
  `Solve(√(x+3) = a, x)` previously returned `[]`; it now returns
  `a² − 3 {0 <= a}` (a square root is non-negative, so a real solution exists
  only for `a ≥ 0`). Substituting a concrete value resolves the guard: `a = 2`
  gives `1`, `a = −2` gives `Undefined`. Numeric right-hand sides are unchanged.
- **Trigonometric and hyperbolic equations with symbolic coefficients record
  their validity condition.** `Solve(sin(x) = a, x)` previously returned
  `arcsin(a)` and `π − arcsin(a)` unconditionally — wrong whenever `|a| > 1`.
  Roots now carry their domain-of-validity guard — a `When` restriction,
  displayed `arcsin(a) {|a| <= 1}` (LaTeX `\arcsin(a)\left\{|a|\le 1\right\}`).
  The guard resolves as soon as it is decidable: substituting `a = 1/2`
  collapses the root to `π/6`, while `a = 3` yields `Undefined`, and a guard
  known false at solve time prunes the root (down to `[]`). Numeric-coefficient
  equations are unchanged. The same applies to `cos` (`|ratio| ≤ 1`), `cosh`
  (`ratio ≥ 1`), and `tanh` (`|ratio| < 1`) equations.
- **`Solve` of an inequality stays inert instead of returning an empty list.**
  `Solve(x^2 < 4, x)` previously returned `[]`, which reads as "no solutions";
  univariate inequality solving is unsupported, so the expression now stays
  unevaluated. Solving equations is unchanged.

### Compilation

- **The deprecated `interval-glsl` compilation target has been removed.** GPU
  interval evaluation only pays off when the entire pipeline stays on the GPU,
  and the target could not compile relational operators, so it could not host
  restriction conditions. Use `interval-js` (CPU interval arithmetic) or the
  scalar `glsl`/`wgsl` targets instead. The `IntervalGLSLTarget` export from
  `@cortex-js/compute-engine/compile` is gone, and
  `compile(expr, { to: 'interval-glsl' })` now throws an unregistered-target
  error.

### Parsing

- **`ce.parse()` no longer blows up on repeated `\command[opt]{}` groups.**
  Adjacent bracketed groups — a garbage `\begin{tikzpicture}katu[scale=0.6]{}…`
  run, or even plain index notation `a[6]a[6]…` — triggered exponential-time
  backtracking, so a few-hundred-character string could hang the parser for tens
  of seconds (and `timeLimit`, which bounds evaluation, did not stop it). The
  reversed-bracket ISO interval notation `]a, b[` opens on `]`, the same token
  that closes an index bracket, so every stray `]` speculatively re-parsed the
  rest of the input as an interval body, nesting exponentially. Parsing is now
  polynomial; the interval (`]0, 1[`) and indexing (`a[6]`) notations are
  unchanged.
- **`\operatorname{nPr}(n, r)` now has a definition.** Matching
  `\operatorname{nCr}` (the binomial coefficient), the Desmos permutation-count
  notation lowers to `Choose(n, r)·r!` (= n!/(n−r)!), so `nPr(5, 2)` evaluates
  to `20`. Previously it parsed to an inert symbol and stayed symbolic under
  `N()`, silently producing `NaN` in a compiled function.

### Arithmetic

- **`Round` accepts an optional precision argument.** `Round(x, n)` rounds `x`
  to `n` decimal places — `Round(2.567, 2) → 2.57`, `Round(1234.5, −2) → 1200` —
  matching the Desmos/spreadsheet `round(x, n)` convention. Previously the
  second argument produced an `unexpected-argument` error. The single-argument
  round-to-integer form is unchanged, and the two-argument form compiles on the
  `javascript` and `interval-js` targets.
- **New `Rationalize` operator for rational approximation.** `Rationalize(x)`
  approximates a real number by a rational at full working precision (like
  single-argument `Rational`); with a tolerance, `Rationalize(x, tolerance)`
  returns the rational with the smallest denominator within the bound —
  `Rationalize(√3, 1/500) → 26/15`, `Rationalize(π, 1/100) → 22/7` — a
  continued-fraction convergent cut.

### Number Theory

- **New `StirlingS1` operator: signed Stirling numbers of the first kind.**
  `StirlingS1(n, m)` is the coefficient of xᵐ in the falling factorial
  x(x−1)…(x−n+1); its absolute value counts the permutations of n elements with
  m disjoint cycles — `StirlingS1(5, 2) → −50`. Complements the existing
  `Stirling` (second kind).

### Relational Operators

- **`Equal` and `NotEqual` broadcast over a named list operand.** With
  `R = [1, 2, 3]`, `x² + y² = R²` now broadcasts to a list of three element-wise
  equations — matching the literal-list form (`x² + y² = [1, 2, 3]`) and the
  inequality operators (`<`, `≤`, …), which already broadcast. Previously the
  named form collapsed to a single `False`. Whole-list equality, where two or
  more operands are collections (`[1, 2] = [1, 2]`), still returns a single
  boolean.

## 0.76.0 _2026-07-11_

### Programming and Collections

- **Dictionary lookups have the value's type.** `At(dict, key)` — and thus
  `d["a"]` in Cortex — was statically typed as the key-value _pair_
  (`tuple<string, T>`), so using a lookup directly in arithmetic (`d["a"] + 10`)
  failed with an `incompatible-type` error. It is now typed as the value; a
  record indexed by a literal string gets that field's precise type.

- **`Reduce`/`Fold` honor the exactness contract.** The compiled floating-point
  fast path no longer runs under plain `evaluate()`: exact operands fold exactly
  (`Fold((a, k) ↦ a + 1/k, 0, Range(1, 5))` → `137/60` instead of `2.2833…`).
  The fast path is reserved for `.N()` and already-inexact inputs.
  Complex-valued folds no longer silently drop imaginary parts
  (`Product(Map(Range(1, 3), k ↦ k + i))` → `10i`, previously `6`).

- **`Map` infers its element type from the mapped function.** The result of
  `Map(Range(1, 3), k ↦ k + i)` was typed with the _source_ element type
  (`integer`); it now reflects the lambda's result type, so downstream
  operations dispatch correctly.

- **Elementwise broadcasting is uniform across lazy and eager collections.** A
  finite lazy `Range` now broadcasts like an eager `List` in tuple products:
  with `R = Range(-2, 2)`, `R·(2,3)` yields a list of five scaled points instead
  of distributing the range inside the tuple components. A scalar also folds
  into a collection produced by an inner broadcast step: `L^2 - 2` evaluates to
  `[-1, 2, 7]` instead of the unevaluated `Add(-2, [1, 4, 9])`, and evaluation
  is idempotent again on these shapes. Infinite or unknown-length ranges stay
  symbolic rather than transposing.

- **Scalar operations accept lazy collection operands during validation.**
  `Mod([0,\ldots,kN], N)` with a symbolic bound produced an `incompatible-type`
  error at canonicalization even though the eager-list form broadcast fine; the
  argument validator now recognizes parametrized `indexed_collection<T>`
  wherever broadcasting applies. Declared types follow the values: broadcast
  results type `list<…>` (previously a scalar-or-list union, or a scalar type
  for symbolic-length ranges).

- **Big integers survive numeric list literals.** A list literal promoted to a
  tensor stored oversized integers in float64 and truncated them (`[100!]` lost
  digits, breaking exact iterative algorithms such as a Fibonacci pair
  accumulator). Integers beyond the float-safe range now keep their exact
  representation.

- **`StringFrom` joins collections.** With a list argument and the
  `"unicode-scalars"`, `"utf-8"`, or `"utf-16"` format,
  `StringFrom([100, 101, 102], "unicode-scalars")` returns `"def"`; it
  previously broadcast element-wise and returned a list of one-character
  strings.

- **One-step function definitions bind inside function bodies (Cortex).**
  `function outer(n) { sq(m) = m * m; sq(n) }` left `sq(n)` unevaluated because
  the call site resolved a value placeholder while the runtime created an
  operator definition; function application now falls back to the operator
  definition. The example-program suite grew by 18 programs covering control
  flow, number theory, complex numbers, linear algebra, and exact sums (mirrored
  in the Cortex documentation).

- **Cortex: `do { … }` block expressions and zero-parameter lambdas.** A
  statement block can now appear in any expression position with the explicit
  `do` prefix — its value is its final statement — so multi-statement closure
  bodies are expressible: `x |-> do { let t = x * x; t + 1 }`. Set literals are
  unchanged (`x |-> {1, 2}` still returns a set). Zero-parameter lambdas
  (`() |-> …`) now parse and apply, enabling the stateful `makeCounter`-style
  closure documented in the examples.

- **Cortex: a named inner function escapes its scope as a value.**
  `function make() { helper(x) = x + 1; helper }` now returns a callable
  first-class function — previously the returned symbol went inert once the call
  frame popped. Captured locals and parameters of the enclosing call are
  preserved; returned `|->` lambdas are unchanged.

- **Cortex: lowercase `true`/`false`, ASCII `..` ranges, and `StringJoin` over a
  list.** `true`/`false` are now input aliases for `True`/`False` (and reserved
  as binding names); `1..n` is a range (`for k in 1..5`), equivalent to the
  existing `‥`, without disturbing decimal literals like `1.5`; and `StringJoin`
  accepts a single collection of strings —
  `StringJoin(Reverse(Characters("hello")))` → `"olleh"`.

- **Cortex: "did you mean" warnings for near-miss function names.** Calling an
  undeclared function whose name is close to a library operator no longer fails
  silently symbolic: `executeCortex` emits a warning diagnostic with the
  suggestion (`Quartile(xs)` → _did you mean `Quartiles`?_). The match is
  conservative (case-insensitive, singular/plural, small edit distance, unique
  prefix) and the diagnostic only fires when a suggestion exists, so
  intentionally symbolic calls are never flagged; the returned value is
  unchanged. The matcher is also available as `ce.suggestOperatorName(name)`.
  `Arg` is now an alias for `Argument`.

### Compilation

- **Calls to user-defined functions compile.** After `f(x) := e^{-x^2/2}`,
  compiling `f(2)` — or any expression referencing `f` — emits the definition as
  a named local function instead of throwing ``Unknown operator `f` ``. Nested
  user functions are emitted in dependency order; recursive definitions fail
  closed with an explanatory error. This also removes a silent interpreted
  fallback in numeric integration: definite integrals of user-defined functions
  now run compiled quadrature (10⁷ samples instead of 10⁴ — comparable wall
  time, ~30× tighter error estimate). Applies to the `javascript` and
  `interval-js` targets.

- **Collection-valued conditions fail closed instead of compiling wrong code.**
  `Equal`/`NotEqual` over a collection-typed operand, and `If`/`Which`/`When`
  with a collection-typed condition, previously compiled with `success: true`
  and returned `null` or the wrong branch at run time; they now throw an
  explanatory compile-time error. Interpreted evaluation is unchanged:
  comparisons broadcast elementwise, conditionals require a scalar boolean.

- **`Reduce`, `Length`, and `At` compile on the `javascript` target.**
  `Reduce(xs, Add|Multiply|Min|Max, init?)` compiles to a loop; `Length` returns
  the element count; `At` follows the interpreter's 1-based, negative-from-end
  indexing (out-of-range yields `NaN`).

- **GLSL: `Length` no longer collides with the `length()` builtin.** CE's
  `Length` (element count) compiled to GLSL `length()` — the Euclidean norm —
  reporting success while computing the wrong value, or emitting invalid GLSL
  for lists longer than four. `length()` is now emitted for `Norm`; collection
  `Length` fails closed on the GPU targets.

- **GLSL/WGSL: literal integer powers are sign-correct on the GPU.** `x^3`
  compiled to `pow(x, 3.0)`, which the GLSL specification leaves undefined for
  negative bases — real GPUs returned `pow(-2, 3) = +8`, silently flipping the
  sign of odd-power terms. Small integer exponents now emit repeated
  multiplication; larger and compound-base cases use a sign-preserving
  `_gpu_powi` preamble helper; negative integer exponents wrap the reciprocal.
  Fractional exponents still emit `pow`.

- **The `interval-glsl` target is deprecated.** GPU interval evaluation only
  pays off when the entire pipeline stays on the GPU, and the target cannot
  compile relational operators, so it cannot host restriction conditions. A
  once-per-process warning now points to `interval-js` and the scalar
  `glsl`/`wgsl` targets. It will be removed in a future release.

### Numeric Evaluation

- **Numeric infinite products use tail acceleration.** `.N()` now
  Richardson-extrapolates the logarithms of positive real factors instead of
  returning a plain finite truncation. This gives accurate values for products
  such as `Product(1 + 1/k², k, 1, +∞) = sinh(π)/π`. Products with non-real,
  non-positive, or non-convergent factors decline the accelerator and retain the
  existing bounded-truncation behavior.

- **Machine `Gamma` keeps full relative accuracy through the overflow edge.**
  Positive real arguments now use a balanced recurrence from the Lanczos core
  instead of reconstructing large values from `exp(gammaln(z))`, which preserves
  about 15-16 digits up to the IEEE-754 limit near `Gamma(171.624)`.

### Linear Algebra

- **Matrix operators infer fresh symbolic operands from context.** An expression
  such as `\det(A+2B)` no longer fails because bottom-up arithmetic
  canonicalization provisionally typed `A` and `B` as numbers before
  `Determinant` required a matrix. Validation now repairs only inferences made
  while constructing the current expression and canonicalizes the argument once
  more with matrix context. Explicit declarations and inferences from earlier
  expressions are never overwritten, and ambiguous products remain unchanged
  rather than guessing which factor is the matrix.

### Symbolic Computation

- **Exact cube-root arithmetic handles more algebraic forms.** Positive
  perfect-power bases with rational exponents normalize to a common base and
  extract their integer part (`4^(2/3) → 2·2^(1/3)`), allowing compatible
  cube-root powers to combine exactly. Real nested cube roots of the form
  `∛(a+b√c)` are denested when exact integer conjugate identities prove a
  result; in particular, `∛(90+34√7) → 3+√7` (and the conjugate form with minus
  signs). The Wester-28 cube-root identity now simplifies directly to exact zero
  (no explicit `Expand` required) and numericizes without `NaN`.

- **Infinite p-series support positive-integer lower bounds beyond 1.**
  `Sum(k^(-s), k, a, +∞)` now returns `Zeta(s) − Sum(k^(-s), k, 1, a−1)` for
  exact real `s > 1`; for example, `Sum(1/k², k, 3, +∞)` evaluates to
  `π²/6 − 5/4`. The existing lower-bound-1 behavior and divergence guards are
  unchanged.

- **`e^{iθ}` stays exact for constructible angles.** `e^{i\pi/3}` now evaluates
  to `1/2 + (√3/2)i` instead of a machine float (the exact cosine/sine values
  were being recombined through float-folding arithmetic). `.N()` numericizes as
  before, and the degenerate angles (`e^{i\pi} → -1`, `e^{i\pi/2} → i`) are
  unchanged.

### Serialization

- **AsciiMath prints series in textbook order.** Taylor-series terms are
  serialized in ascending degree and asymptotic series in descending degree,
  with the `BigO` remainder last, matching LaTeX output. Canonical expression
  order and ordinary sums without a `BigO` term are unchanged.

### Parsing

- **Stepped ellipsis ranges accept negative and symbolic samples.**
  `[-9,-6,\ldots,9]` now parses to `Range(-9, 9, 3)` — previously any negative
  leading sample fell back to a literal list containing a
  `ContinuationPlaceholder` that enumerated as `NaN`. Symbolic stepped forms
  infer a symbolic step when the samples are numeric multiples of one common
  symbol (`[-3N,-2N,\ldots,3N]` → `Range(-3N, 3N, N)`, progression-validated on
  the coefficients); generic sequence notation (`[x_1,x_2,\ldots,x_n]`)
  intentionally still parses as a plain list.

### Engine Lifecycle

- **Popping a scope releases configuration listeners owned by its constants.**
  Constant definitions now retain and invoke the unsubscribe closure returned
  when they register for precision and angular-unit changes. Local constants
  from discarded scopes therefore no longer remain reachable for the lifetime of
  the compute engine.

- **Cancellation errors carry a structured cause.** A cap breach reports
  `'timeout'`, `'iteration-limit-exceeded'`, or `'recursion-depth-exceeded'` via
  the exported `CancellationCause` type. In Cortex, a final-statement breach
  carries the cause as a second operand on the `Error` value, and non-final
  statements emit a dedicated `evaluation-canceled` diagnostic. Error messages
  are unchanged, so existing string matching keeps working.

## 0.75.0 _2026-07-11_

### Numeric Evaluation

- **Inverse trigonometric and hyperbolic functions now evaluate outside their
  real domain.** `.N()` returns the complex principal value when no real value
  exists, for example `\arcsin(2)` → `1.571 − 1.317i` and
  `\operatorname{arcosh}(0.5)` → `1.047i`. Complex arguments such as
  `\operatorname{arsinh}(1+i)` are also supported. Exact arguments remain
  symbolic with `evaluate()`. Incorrect complex values from `Arcoth` on part of
  its branch cut and from `Arsech` have also been fixed.
- **Products and quotients of square roots no longer throw on large radicands.**
  Evaluating an expression such as `\sqrt{1234}\cdot\sqrt{1235}`, whose combined
  radicand (`1234·1235`) exceeds the exact-radical limit, no longer raises an
  internal "Unexpected value for radical part" error. Any perfect-square factor
  is extracted (`√(k²·r) = k·√r`), keeping the result exact when the square-free
  part is small enough and otherwise returning the numeric value.

### Solving Equations

- **`Solve` handles systems of equations.** `Solve([eq1, eq2, …], [x, y, …])`
  returns each solution as a tuple of values in the order of the variable list:
  `Solve([x + y = 3, x - y = 1], [x, y])` → `[(2, 1)]`, and a nonlinear system
  such as `[x^2 + y^2 = 25, x + y = 7]` returns both solutions
  `[(3, 4), (4, 3)]`. Linear systems solve exactly (rational values), an
  underdetermined system returns a parametric tuple (`[(5 - y, y)]` for
  `x + y = 5`), and a system the solver cannot decide stays unevaluated. This
  matches the tuple shape already used when solving over explicit domains.
- **`solve()` correctly rejects trigonometric and hyperbolic equations with no
  real solutions.** Equations such as `\sin x = 2`, `\tanh x = 2`, and
  `\cosh x = 1/2` now return no solutions. Symbolic equations and equations with
  complex polynomial roots are unaffected.

### Integration (opt-in Rubi rules)

- **More integrals containing binomial radicals are supported.** This includes
  mixed even- and odd-power numerators such as
  `\int\frac{c+dx}{\sqrt{-a-bx^4}}dx`,
  `\int\frac{x^2(c+dx+ex^2+fx^3)}{(a+bx^4)^{3/2}}dx`, and Laurent variants with
  denominators of the form `(a+b·x^n)^{3/2}`.
- **More integrals that are algebraic in a hyperbolic function are supported.**
  Half-integer powers of hyperbolic expressions such as
  `\int\coth(x)(a+b\sinh^2 x)^{3/2}\,dx`,
  `\int\coth^2 x\sqrt{a+b\tanh^2 x}\,dx`,
  `\int\frac{\operatorname{csch} x}{(a+b\sinh^2 x)^{3/2}}\,dx`, and
  `\int\sqrt{a+b\operatorname{csch}^2 x}\,dx` now close in elementary form via a
  hyperbolic substitution. This also fixes a wrong-answer case,
  `\int\frac{\sqrt{\coth(a+b\ln(cx^n))}}{x}\,dx`.
- **More integrals that are rational in a hyperbolic function are supported.**
  Ratios of hyperbolic functions with a squared-or-higher power, such as
  `\int\frac{\tanh^2 x}{a+b\tanh x}\,dx`, `\int\frac{\tanh x}{a+b\sinh x}\,dx`,
  and `\int(a+b\tanh^2 x)^3\tanh^4 x\,dx`, now close in elementary form.

### Library and Definitions

- **Definitions can be searched by concept.** `ce.searchDefinitions(query)`
  returns a ranked list of matching `{ id, kind }` entries. It searches names,
  descriptions, synonyms (for example, `average` finds `Mean`), and LaTeX
  commands (for example, `\gcd` finds `GCD`). Use the optional `limit` argument
  to control the number of results (default: 10). Custom definitions declared
  with `ce.declare()` can provide an optional `keywords` list, and returned IDs
  can be passed to `ce.lookupDefinition()`.
- **Definition descriptions filled in.** About 80 operators and constants that
  lacked a `description` now have one (the trigonometric family, logic and
  relational operators, collection primitives such as `List`, `Range`, and
  `Fold`, and constants like `Pi` and `ExponentialE`), and `Sec`'s description
  was corrected (secant is the _reciprocal_, not the inverse, of cosine). These
  surface in `ce.searchDefinitions()` and `ce.lookupDefinition()`.
- **New builtin operators are available:** `Pipe(x, f)`,
  `Append(collection, element)`, `Fold(f, init, collection)`,
  `StringJoin(s1, s2, …)`, and `RandomInteger(n)` or `RandomInteger(a, b)`.
  `Pipe` enables evaluation of `x |> f`; `StringJoin` enables the existing `<>`
  notation; and `RandomInteger` uses inclusive bounds and honors the seeded
  random-number generator.

### Calculus

- **`Limit` accepts the explicit-variable form.** `Limit(expr, var, point)` —
  e.g. `["Limit", ["Divide", ["Sin", "x"], "x"], "x", 0]` → `1` — now
  canonicalizes to the same internal form as `Limit(expr, point)`, matching the
  convention `Series` already uses. The `(function, point, direction)` reading
  is preserved when the middle operand is not a free variable of the expression.

### Linear Algebra

- **`Inverse` of an exact matrix is exact.** An integer or rational matrix now
  inverts over the rationals — `Inverse([[2,1],[1,3]])` →
  `[[3/5,-1/5],[-1/5,2/5]]` instead of floats — with `.N()` and inexact matrices
  using the numeric path as before.
- **New `LinearSolve(A, b)` operator** solves the linear system `A·x = b`,
  exactly for exact input. Composed forms like `Dot(Inverse(A), b)` also work
  now: `Inverse`'s result is typed as a matrix, so matrix operators accept it.

### Units and Quantities

- **`Quantity` accepts a string unit.** `Quantity(30, "km/h")` parses the string
  through the same unit grammar as the LaTeX path and canonicalizes identically
  to the symbolic form; a malformed unit string produces a clear error instead
  of a partially-built expression.

### Programming and Collections

- **Recursive functions can be defined without a separate declaration.** A
  function assignment that refers to itself, such as
  `ce.parse('f(n) := n \\cdot f(n-1)')`, now works directly.
- **`N()` numericizes through user-defined functions.** For `f(x) := x/3`,
  `N(f(2))` now returns `0.666…` instead of the exact `2/3`; plain `evaluate()`
  still returns the exact form, and the approximation is applied within the
  function's own scope, preserving lexical scoping.
- **`Keys(dict)` and `Values(dict)` evaluate**, returning the keys (as strings)
  and values in the dictionary's iteration order — the same order
  `for kv in dict` yields.
- **`Intersection` accepts lists** (any finite collection), deduplicating into a
  `Set`; `Union` already did.
- **A 2-element MathJSON `List` in a set operation is a collection, not an
  interval.** `["Intersection", ["List",1,2], ["List",2,3]]` (Cortex:
  `Intersection([1,2], [2,3])`) now intersects the two-element collections —
  `Set(2)` — instead of reading the lists as closed intervals. The interval
  reading of ambiguous bracket pairs is now applied where it belongs, at the
  LaTeX boundary: `x \in \lbrack 1, 5 \rbrack`, `(-\infty, 0) \cup (0, \infty)`,
  and the subset relations parse to `Interval` exactly as before, and
  `\setminus` now gets the same interval reading (previously
  `\R \setminus (0, 1)` kept a raw pair). Unambiguous interval notations
  (`[a, b)`, `]a, b[`, …) are unchanged.
- **Collection equality no longer depends on representation.** A computed
  collection — an `Intersection` or `Union` result, a lazy `Map`, `Filter`, or
  `Join` pipeline, or a symbol assigned a collection — now compares equal to a
  literal with the same elements: `Intersection({1,2,3,4}, {2,3,5}) = {2,3}` is
  `True` (it was `False` unless the operand was evaluated first). Sequences
  compare element-wise in order, sets by membership, and a set is never equal to
  a sequence. In addition, `Equal` between two collections now always returns a
  scalar boolean instead of sometimes broadcasting element-wise (`{1,2} = [1,2]`
  returned `["True","True"]`); broadcasting still applies to list-vs-scalar
  comparisons such as `L = 4`.
- **`Intersection` of two `Filter` collections no longer overflows the stack.**
  Membership tests on a lazy `Filter` recursed without bound;
  `Intersection(Filter(…), Filter(…))` now evaluates normally.
- **Indexing a matrix once returns a correctly typed row.** Expressions such as
  `At(At(m, 2), 1)` now validate and evaluate correctly for matrices and other
  rank-2-or-higher collections.
- **List elements and dictionary values are evaluated by `evaluate()` and
  `.N()`.** For example, `["List", "y", ["Add", "y", 1]]` with `y = 7` evaluates
  to `[7, 8]`, and `[1/3].N()` is numericized. Lazy collections such as `Range`,
  `Map`, and `Filter` remain lazily enumerated.
- **Ellipsis lists with symbolic bounds parse to `Range`.**
  `\left[-N,\ldots,N\right]` and `\left[-3N,\ldots,3N\right]` now parse to
  `Range(-N, N)` and `Range(-3N, 3N)`, matching the numeric-start forms
  (`[1,\ldots,N]`). Previously a symbolic start fell through to a raw `List`
  containing a literal `ContinuationPlaceholder`, which enumerated as `NaN`. In
  addition, a `Range` whose bounds bind looser than the `..` operator now
  serializes with parentheses (`(-N)..N`) so it round-trips through LaTeX (an
  unwrapped `-N..N` reads back as `-(N..N)`).
- **Using a symbol bound to a symbolic list no longer corrupts builtin
  definitions.** After `ce.assign('L_1', ce.parse('\\left[N,2N\\right]'))`,
  constructing `2 L_1` — via `subs()`, `ce.box()`, or `ce.function()` —
  permanently broke the builtin `N` operator for the lifetime of the engine:
  every subsequent parse of the token `N` returned an `unexpected-symbol` error.
  Type inference on the list elements no longer overwrites an operator
  definition with an unsatisfiable (`never`) type.

### Cortex

- **`%` and postfix `!` operators**: `a % b` is `Mod(a, b)` (multiplicative
  precedence) and `n!` is `Factorial(n)` (the `!` must directly follow its
  operand; prefix `!x` is still `Not` and `x != y` is still `NotEqual`).
- **Chained indexing**: `m[2][1]` now works alongside `m[2, 1]`.
- **String escape sequences are processed correctly.** `"a\tb\nc"` now contains
  a real tab and newline (escapes were previously double-processed in plain and
  multiline strings; interpolated strings were already correct).
- **The examples suite roughly doubled** (`src/cortex/docs/examples.md`), adding
  units and uncertainty, calculus, linear systems, dictionaries, sets, closures,
  seeded randomness, errors-as-values, and string formatting — every example
  verified by an executable test.

## 0.74.0 _2026-07-10_

This release significantly expands CE's calculus capabilities. Limits, residues,
and series now handle many poles of special functions exactly; infinite sums and
products gain more closed forms and substantially better numeric convergence;
and the optional Rubi integration rules support more integrands, any integration
variable, and reliable time limits. Step-by-step explanations now cover
integration and systems of equations or inequalities, with clearer traces for
simplification.

It also improves exact and symbolic computation throughout the engine. Linear
algebra gains exact ranks, null spaces, eigenvectors, matrix square roots, and
singular values; assumptions and simplification prove more identities; and
several correctness issues involving canonicalization, fractions, symbolic
collections, compilation, and LaTeX parsing are fixed. New special-function
support, reproducible seeded randomness, and more useful Cortex diagnostics
round out the release.

### Calculus

- **Limits and residues at special-function poles evaluate exactly.**
  Expressions at poles of `Gamma`, `Digamma`, `Trigamma`, `PolyGamma`, and
  `Zeta` that previously stayed symbolic now resolve in closed form:
  `\lim_{x\to-1}(x+1)\psi(x) = -1`, `\lim_{x\to0}(\Gamma(x)-1/x) = -\gamma`,
  `\lim_{s\to1}(s-1)\zeta(s) = 1`,
  `\operatorname{Res}_{s=1}\Gamma(s)\zeta(s) = 1`,
  `\operatorname{Res}_{x=0}\Gamma(x)^2 = -2\gamma`, and higher-order poles that
  previously deferred
  (`\operatorname{Res}_{x=-2} \Gamma(x)/(x+2) = 3/4 - \gamma/2`). Deferral
  behavior is unchanged where no exact expansion exists (branch points,
  essential singularities, two-sided pole limits).
- **The polygamma family expands, differentiates, and integrates through the
  ladder.** `Series` now produces correct Laurent expansions of `Trigamma` and
  integer-order `PolyGamma(m, x)` at their poles (previously a spurious regular
  expansion could be produced), and `D` knows `\psi_1' = \psi^{(2)}` and the
  general `d/du\,\psi^{(m)}(u) = \psi^{(m+1)}(u)`. `Series` at a `Digamma` pole
  is also about 20× faster.
- **Residues at infinity evaluate.** `Residue(f, x, \infty)` — any infinite
  point names the Riemann-sphere point at infinity — computes
  `-\operatorname{Res}_{s=0} f(1/s)/s^2` through the exact Laurent kernel:
  `\operatorname{Res}_\infty 1/x = -1`,
  `\operatorname{Res}_\infty \frac{3x^2+2}{x^3+x} = -3` (the negated sum of the
  finite residues).
- **Limits at poles resolve to signed infinities.** A _directional_ limit at a
  pole now evaluates to `\pm\infty` from the exact Laurent data:
  `\lim_{x\to0^+} 1/x = +\infty`, `\lim_{x\to0^-}\Gamma(x) = -\infty`,
  `\lim_{s\to1^\pm}\zeta(s) = \pm\infty`, `\lim_{x\to0^+}\ln x = -\infty`. A
  _two-sided_ limit resolves only when both sides agree (even pole order):
  `\lim_{x\to0} 1/x^2 = +\infty`, `\Gamma(x)^2 \to +\infty`,
  `\ln(x^2) \to -\infty`. Disagreeing two-sided limits (`\lim_{x\to0} 1/x`,
  `\Gamma`, `\ln x` at their poles) deliberately stay inert — the engine does
  not produce `ComplexInfinity` limits.
- **`Beta` joins the meromorphic pole family.** The Laurent kernel expands
  `\operatorname{B}(a,b)` through the `\Gamma`-quotient identity, so residues,
  limits and `Series` at Beta poles evaluate:
  `\operatorname{Res}_{x=0} \operatorname{B}(x,3) = 1`,
  `\lim_{x\to0} x\cdot\operatorname{B}(x,3) = 1`.
- **Numeric limits containing sums now converge instead of hanging.** `N()`
  respects evaluation limits when probing a `Limit` at `\infty` whose body
  contains a variable-length `Sum` or `Product`. Examples that now converge
  quickly include `\lim_{n\to\infty}(\sum_{k=1}^{n} 1/k - \ln n)`, which
  evaluates to the Euler–Mascheroni constant, and
  `\lim_{n\to\infty}\frac{4}{n^2}\sum_{k=1}^{n}\sqrt{n^2-k^2}`, which evaluates
  to `\pi`.
- **Numeric limits with odd-power error terms now converge correctly.** This
  fixes cases such as `H_n - \ln n - \gamma \sim 1/2n`, which previously
  returned `NaN`. Decaying oscillations such as `\operatorname{sinc}` at
  `-\infty` now resolve to `0`, while divergent oscillations such as `\sin x` at
  `\infty` still return `NaN`.

### Step-by-Step Explanations

- **`explain('Integrate')` traces symbolic integration through the Rubi rule
  chain.** With the opt-in integration rules loaded (`loadIntegrationRules(ce)`
  from `@cortex-js/compute-engine/integration-rules`),
  `ce.parse('\\int x\\sqrt{1+x}\\,dx').explain('Integrate')` replays the
  driver's derivation as whole-expression states — term-by-term splits
  (`integrate.sum`), constant factors moved out (`integrate.constant-factor`),
  each corpus rule application (a stable `rubi:…` id with a compact description
  such as _"Apply integration rule 1.1.1.2#19 (Rubi)"_), reductions to special
  functions (`integrate.si-ci`, `integrate.partial-fractions`, …), and a closing
  simplification. A **definite** integral is presented via the Fundamental
  Theorem of Calculus: the antiderivative derivation, then the bracket `F |_a^b`
  (`integrate.fundamental-theorem`), the bounds substituted unevaluated
  (`integrate.evaluate-bounds` — skipped for improper integrals, where the
  bracket is a limit), and the value. Symbolic bounds are supported. The result
  is identical to `evaluate()`. Without the rules loaded, or when the rules
  cannot close the integral, a precise error is thrown. (Also fixed: the LaTeX
  serialization of the two-bound `\left. F \right|_a^b` `EvaluateAt` form
  dropped the upper bound.)

- **`explain('solve')` traces systems of inequalities and mixed systems.** A
  `List`/`And` of linear inequalities in two variables is traced through
  constraint normalization (`solve.system.normalize-inequality`), boundary
  intersection (`solve.system.intersect-boundaries`), and the feasible vertices
  (`solve.system.vertices`); mixed equality/inequality systems show the
  elimination steps, then each candidate checked against the constraints
  (`solve.system.check-constraints`, `solve.system.reject`). Both previously
  threw "not supported" errors.

- **`explain('simplify')` surfaces the work done inside operands.**
  Simplifications applied while descending into the operands of a sum, product
  or function argument — previously summarized by an opaque bookkeeping step —
  now appear as labeled steps with their own rule ids
  (`\tan x\cot x + \frac{x^3+x^2}{x^2}` shows the $\tan x\cot x \to 1$ rewrite
  before the expansion). At default verbosity, consecutive applications of the
  same rule are coalesced into a single step; pass `verbosity: 'all'` for the
  raw chain.

### Integration (opt-in Rubi rules)

- **Integration consistently respects `timeLimitMs`.** Nested integration
  attempts now share the original time limit and recursion safeguards, avoiding
  runaway evaluation on cyclic or difficult subproblems.
- **Any integration variable works—not just `x`.** Integrals using another
  variable could previously return an expression in `x`; for example,
  `\int t^2\,dt` now correctly returns `t^3/3`.
- **Symbolic-coefficient quartic-denominator rationals close.**
  `\int \frac{d+e\,x^2}{a+b\,x^4}\,dx` — and shapes that reduce to it, such as
  `\int \frac{x^6}{(a+c\,x^4)^3}\,dx` — now reach the trinomial terminal rules
  instead of ping-ponging between integrand expansion and binomial splitting.
- **Symbolic-coefficient reciprocal hyperbolics close.**
  `\int \frac{1}{a+b\sinh x}\,dx` and the cosh/tanh/coth/sech/csch variants
  resolve via a rational-normal-form retry in the exponential-substitution
  fallback.
- **Complex special-function closures.** Rational integrands with irreducible
  quadratic denominators split over complex-conjugate roots in the Si/Ci
  fallback, reciprocal-argument integrands like `\int x^m \sin(a + b/x)\,dx`
  close, and inverse-trig antiderivatives producing complex-argument `Erfi`
  evaluate (riding the new complex error-function kernels).
- **`\int F(\ln(a\,x^n))/x\,dx` closes** via a function-of-logarithm recognizer
  (substitution `u = \ln(a\,x^n)`).
- **Products of sines and cosines reduce via product-to-sum** before
  integration, closing mixed-angle products the term-by-term rules could not
  reach.

### Arithmetic

- **Canonical expressions no longer depend on a variable's current value.** A
  mutable symbol holding `0`, `1`, or `-1` could be folded into an expression
  while it was boxed, producing stale and sometimes incorrect results after the
  symbol changed. Canonicalization now folds only literal numbers; symbol values
  are substituted during evaluation. Numeric `const` symbols follow the same
  evaluation behavior as `Pi`.
- **Huge scientific exponents no longer crash.** Parsing or serializing a number
  literal whose exponent exceeds what the bignum layer can represent
  (`1e999999999`) threw; it now overflows cleanly to `+\infty` (and `-\infty`
  for negative mantissas), matching float semantics.
- **Complex values with an infinite component type as `complex`.** A `Complex`
  whose real or imaginary part is infinite was typed `finite_complex`, so
  type-gated paths mishandled it; it now reports the non-finite `complex` type.

### Sums and Products

- **Telescoping sums and products evaluate in closed form.** A sum whose body is
  a `k \to k+1` shift pair collapses exactly, for arbitrary symbolic bounds and
  either orientation: `\sum_{k=0}^{n} \bigl(g(k+1) - g(k)\bigr)` evaluates to
  `g(n+1) - g(0)`. The product counterpart recognizes a shift-quotient body
  after combining it over a common denominator:
  `\prod_{k=1}^{n-1}\left(1 + \frac{1}{k}\right)` evaluates to `n`.
- **`\prod_{k=1}^{n} k` evaluates to `n!`.** The bare-index product with a
  symbolic upper bound returns `Factorial(n)` instead of staying inert.
- **Classic infinite series and products evaluate to their exact closed forms.**
  p-series reduce to the zeta function — `\sum_{k=1}^{\infty} \frac{1}{k^2}`
  evaluates to `\frac{\pi^2}{6}`, `\sum \frac{1}{k^2} + \frac{1}{k^3}` to
  `\frac{\pi^2}{6} + \zeta(3)` (term-wise splitting applies only when every
  summand has a closed form) — and the Wallis product
  `\prod_{k=1}^{\infty}\left(1 - \frac{1}{(2k)^2}\right)` evaluates to
  `\frac{2}{\pi}`. Series with no known closed form stay symbolic under exact
  `evaluate()`, per the infinite-domain contract.
- **`.N()` of convergent infinite sums reaches near machine precision.** The
  numeric path Richardson-extrapolates the partial sums instead of returning a
  plain 10⁴-term truncation: `\sum 1/k^2` now numericizes to ~2·10⁻¹⁶ of `π²/6`
  (previously ~10⁻⁴ off), and series without closed forms benefit equally
  (`\sum 1/(k^2+1)` to ~2·10⁻¹⁴). Divergent or non-smooth series are detected
  and fall back to the capped truncation.

### Equation Solving

- **Trigonometric equations with symbolic coefficients solve correctly.** For
  example, `x^2 - 2x\cos t + 1 = 0` solved for `t` now returns
  `\pm\arccos\left(\frac{x^2+1}{2x}\right)`.

### Assumptions

- **Transitive closure over assumed inequality chains.** Assumptions now chain:
  `a \ge b`, `b \ge c`, `c \ge d` entails `a \ge d`, strictness propagates
  (`p > q > r` entails `p > r` and `p \ne r`), and an antisymmetric cycle
  collapses to equality — Wester 21's `x \ge y, y \ge z, z \ge x` now proves
  `x = z` is `True`. A chain without a back-edge deliberately does _not_ prove
  equality.
- **Even-power monotonicity on ordered positives.** Wester 22's
  `x > y, y > 0 \vdash 2x^2 > 2y^2` now evaluates to `True` (a difference of
  equally-scaled squares factors as `k(x-y)(x+y)` with both factor signs settled
  from the assumptions). `x > y` alone deliberately does _not_ conclude
  `x^2 > y^2`, and solve()'s conservative root-filtering behavior is unchanged.

### Simplification and Exact Arithmetic

- **The Fu strategy reduces same-power sin/cos differences.**
  `simplify({ strategy: 'fu' })` now rewrites `\sin^4 x - \cos^4 x` to
  `-\cos 2x` (and the mirrored/2nd-power forms): a difference of squares whose
  Pythagorean sum factor is `1`, which the exponent-2-only TR5/TR6/TR7
  transforms could not reach. Verified numerically; the default `simplify()`
  path is deliberately unchanged (pinned by test).
- **Exact modulus of complex expressions with radical parts.** `Abs` of a
  constant `a + b\,i` with radical/rational parts computes the exact
  `\sqrt{a^2 + b^2}` when it genuinely folds: Kahan's
  `\left|3-\sqrt{7}+i\sqrt{6\sqrt{7}-15}\right|` simplifies to exactly `1` (its
  `.N()` alone carries a `1.0000000000000000315` float residue), `|5-12i| = 13`,
  `|2+\sqrt{5}\,i| = 3`, `|1+2i| = \sqrt{5}`. A split whose "imaginary part" is
  itself imaginary (a negative radicand) is rejected by a numeric cross-check,
  and symbolic `|x+iy|` never folds.
- **Matrices differentiate elementwise.** `D` over a vector/matrix `List`
  literal maps over the elements (recursively for nested lists) instead of
  producing a nonsensical scalar chain-rule expansion: the second derivative of
  the rotation matrix `[[\cos t, \sin t], [-\sin t, \cos t]]` is `-M`, as it
  should be. `Derivative` shares the fix.
- **`Together` combines fractions correctly.** It now uses a common denominator
  instead of adding numerators and denominators independently:
  `\frac{a}{b} + \frac{c}{d} \to \frac{ad + bc}{bd}`,
  `1 + \frac{1}{k} \to \frac{k+1}{k}`, reusing the denominator when terms
  already share it.

### Linear Algebra

- **Exact null spaces, ranks, and eigenvectors.** The exact bigint-fraction
  elimination introduced for `RowReduce` in 0.73.0 now backs `Kernel`
  (null-space basis vectors come out as exact rationals: `[[2,3],[0,0]]` → basis
  `[-3/2, 1]`), `MatrixRank` (rank = exact pivot count, with no float-tolerance
  ambiguity), and eigenvector computation (when the matrix and the eigenvalue
  are exact rationals, `A - \lambda I` is solved exactly — the eigenvectors of
  `[[4,1],[2,3]]` are the exact `[1, 1]` and `[-1/2, 1]`). Inexact or symbolic
  entries fall back to the numeric path unchanged.
- **`M · M^{-1}` simplifies to the identity for symbolic matrices.** Two fixes
  combine: `simplify()` now recurses into `List` elements (matrix entries were
  previously unreachable by any simplify rule), and a new rule combines a sum of
  fractions sharing an identical denominator into a single fraction so the
  diagonal entries `\frac{a^2 b}{a^2 b - b} + \frac{-b}{a^2 b - b}` cancel to
  `1`.
- **Symbolic matrix rank via the determinant.** `MatrixRank` of a small symbolic
  matrix now concludes when the simplified determinant settles the question: the
  trigonometric matrix
  `[[\sin 2t, \cos 2t], [2\sin t\cos t, \cos^2 t - \sin^2 t]]` has rank `1` (its
  determinant vanishes under `TrigReduce`). Indeterminate cases stay symbolic,
  as before.
- **Vandermonde determinants return the difference product.** The determinant of
  a symbolic Vandermonde matrix (either orientation) is produced directly in its
  factored closed form `\prod_{i<j}(x_j - x_i)` instead of an unfactored
  expansion.
- **The numeric eigensolver converges on hard spectra.** The QR iteration was
  rebuilt as Householder reduction to Hessenberg form followed by the Francis
  double-shift algorithm with deflation. The classic 8×8 Rosser stress matrix —
  double eigenvalue `1000`, a `±10\sqrt{10405}` pair, and a tiny eigenvalue
  `≈0.098` — now yields the true spectrum (the unshifted iteration returned
  wrong values), and non-symmetric matrices get proper complex-conjugate
  eigenvalue pairs (`[[0,-1],[1,0]]` → `\{i, -i\}`).
- **`MatrixPower(M, 1/2)` — principal matrix square root.** Half-integer powers
  of an exact 2×2 positive-semidefinite matrix evaluate exactly via the closed
  form
  `\sqrt{M} = (M + \sqrt{\det M}\,I)/\sqrt{\operatorname{tr} M + 2\sqrt{\det M}}`:
  `MatrixPower([[10,7],[7,17]], 1/2)` → `[[3,1],[1,4]]`, and `3/2`, `-1/2` etc.
  compose with the integer path.
- **New operator: `SingularValues`** — the singular values of a matrix,
  descending, zeros included; exact when the Gram matrix is at most 2×2 with
  rational entries (`SingularValues([[1,1],[2,2],[3,3]])` → `\{2\sqrt{7}, 0\}`),
  numeric via the SVD machinery otherwise. (Across this release's Wester rounds
  the `wester.test.ts` skip ledger drops from 21 to 3 — the remaining three are
  the radical-denesting tail.)

### Core

- **`String(…)` joins values, not serialized forms.** A string operand's quotes
  leaked into the result: `String("x = ", 3)` evaluated to a string whose
  _content_ was `"x = "3`. It now evaluates to `x = 3`. This also fixes Cortex
  string interpolation, which lowers to `String` — the documentation's headline
  example `"\(x) has type \(Type(x))"` now produces `"2047 has type integer"`.
- **`Type` reports the type of symbols and expressions.** The `Type` operator
  holds its operand unevaluated, but an unevaluated operand is not canonical and
  a non-canonical expression has no type — so `Type(y)` returned `"unknown"`
  even for a symbol bound to an integer, and `Type(1 + x)` returned `"unknown"`
  instead of `"number"`. The operand is now canonicalized (still not evaluated)
  before its type is read.

### Cortex Language (Experimental)

- **Runtime problems in non-final statements are no longer silent.** Only the
  last statement's value is returned from `executeCortex`, so an error value
  produced by an earlier statement used to vanish — an unsupported indexed
  assignment (`xs[2] = 9`) or a mid-program `const` reassignment went completely
  unreported. Each non-final statement that evaluates to an error value now
  emits a `runtime-error` diagnostic carrying the statement's source range; the
  final statement's errors stay in `value`, per the errors-are-values contract.
- **Verbatim symbols are truly literal.** The content of a backtick-quoted
  symbol (`` `while` ``) receives no escape processing and must be a valid
  MathJSON symbol name — the verbatim form exists to name reserved words.
  Previously, string escape sequences were applied inside the backticks
  (`` `\sin` `` silently cooked `\s` into a space) even though no valid symbol
  name contains an escapable character, so every such escape could only produce
  an invalid name.
- **New “Examples” documentation page.** Eighteen complete Cortex programs —
  iteration and accumulation, recursion, numeric methods, exact and symbolic
  computation, collections — from FizzBuzz-as-a-`Map` to Newton's method on
  exact rationals, the Basel problem against `\pi^2/6`, and a golden-ratio
  continued fraction checked against a `$…$` LaTeX island. Every program on the
  page is verified by an executable test suite.

### Collections

- **Symbolic-bound `Range` and `Linspace` stay inert instead of collapsing.** A
  symbolic bound was silently coerced to `1`, so `Range(1, n)` behaved as the
  one-element range `[1]` everywhere: `Count(Range(1, n))` evaluated to `1`,
  `Sum(Range(1, n))` to `1`, `Range(1, n) = Range(1, m)` to `True`, and
  materialization produced the literal `[1]`. All of these now stay
  symbolic/indeterminate, across the scalar accessors (`Count`, `At`, equality,
  `SubsetOf`, element sign), iteration, materialization, and the extrema
  (`Supremum`/`Infimum`/`Min`/`Max`). Likewise for `Linspace`: a _symbolic_
  point count is indeterminate (only a _missing_ count selects the default of
  50), and symbolic endpoints no longer materialize as `NaN` literals or fold
  `Sum(Linspace(a, 1, 3))` to `0` — a collection that reports a size but cannot
  compute its elements now keeps its lazy form rather than fold to the
  reduction's initial value. Concrete bounds are unaffected.
- **`Min`/`Max`/`Supremum`/`Infimum` keep unenumerable collections symbolic.**
  The extrema used to iterate any collection operand: an infinite one (a `Map`
  over a continuous `Interval`) ground through the interval's dense sampler
  until the evaluation deadline, and one that reports elements it cannot compute
  (a `Map` over a `Linspace` with a symbolic endpoint) silently _vanished_ from
  the result — `Min(Map(...), 5)` returned `5` even though the mapped values
  could be smaller. Both now stay in the symbolic result. A genuinely empty lazy
  collection (a `Filter` with no matches) still folds away, and finite
  collections fold as before.

### Compilation

- **New `iterationBudget` compile option.**
  `expr.compile({ iterationBudget: 1e6 })` caps the trip count of emitted
  `Sum`/`Product` loops: a loop whose iteration count would exceed the budget —
  including an _infinite_ bound, which previously compiled to a loop that never
  terminated — evaluates to `NaN` instead of running. Compilation without the
  option is unchanged (unbounded loops, zero overhead); the engine's numeric
  limit probes use it internally to stay interruptible.
- **The `interval-js` target compiles every operand of n-ary nodes.** Chained
  relations (`1<x<4`) compiled to only their first binary comparison, and n-ary
  `And`/`Or` dropped every operand past the first pair — for `Or` this was
  unsound in the exclusion direction (an interval admitted only by a dropped
  branch reported a definitive `"false"`, so a mask-driven consumer would
  wrongly cull it). Chains now emit the tri-state conjunction of all pairwise
  comparisons, and `And`/`Or` fold all operands; the `javascript`/`glsl` targets
  were always correct.
- **The `javascript` target fails closed on scalar arithmetic over a list-valued
  operand.** `L + x` with a list-valued `L` previously compiled with
  `success: true` to JS array coercion (returning a _string_). It now reports
  `success: false` with an explanatory error, and the interpretation fallback
  returns the correct broadcast list. Supported list compilation — broadcast
  (`\sin([x, 2x])`), literals, ranges, GPU vectors, custom vector operators — is
  unchanged.
- **Seeded, reproducible randomness: `ce.randomSeed`.** Assigning a `number` or
  `string` seed makes `Random()`/`Random(n)` (and `Shuffle`, `Sample`) draw from
  a per-engine deterministic PRNG stream; re-assigning the same seed resets the
  stream so identical evaluation sequences reproduce, and `null` (the default)
  restores non-deterministic behavior. With a seed set at compile time, each
  `Random` node in a `javascript`-target compilation bakes to a constant derived
  from the seed and the node's position — a compiled plot function returns the
  same value at the same call site on every invocation (one draw per
  compilation), instead of flickering per sample. The explicit per-call
  `Random(seed)` overload is unchanged.
- **GLSL masked branches emit an overridable `_gpu_nan()` helper.** The
  else-branch of a compiled `When`/`Which`/`If` was a bare `0.0 / 0.0`, whose
  NaN semantics GLSL ES 1.00 leaves implementation-defined. The literal now
  lives in a single selective-preamble helper that ES 3.00 hosts can replace
  with `intBitsToFloat(0x7FC00000)` for a guaranteed bit pattern.

### Parsing

- **Bare-command function names `\abs`, `\floor`, `\mod`, `\sign` parse as
  function calls.** `\abs\left(x\right)` → `Abs(x)`, `\floor(x)` → `Floor(x)`,
  `\mod(a, b)` → `Mod(a, b)`, `\sign(x)` → `Sign(x)` — common informal shorthand
  (and Desmos output) that previously errored with `unexpected-command`. The
  infix `a \mod b` (synonym of `\bmod`) is unchanged. Also,
  `\operatorname{sign}` now aliases to `Sign` like `sgn` (it previously parsed
  silently as a free symbol `sign` multiplied by the argument).
- **A dot-number after a closing group multiplies.**
  `\left(1-t\right).9\left(2\right)` and `t^{i}.4` parse the `.9`/`.4` as a
  decimal literal juxtaposed with the preceding operand (implicit
  multiplication), instead of erroring with `unexpected-operator`. Degenerate
  dot sequences after a _number_ (`1.2.3`) still error, and member access
  (`v.x`), ranges (`1..2`), and trailing-dot numbers (`(1., 2)`) are unaffected.
- **`\frac{d}{X}` is a division unless the denominator is a differential.**
  Leibniz-derivative parsing now requires an actual `d`-marker in the
  denominator (`\frac{d}{dx}`, `\frac{dy}{dx}`, `\frac{d^2}{dx^2}`…). A bare-`d`
  numerator over a plain denominator — `\frac{d}{L}` where `d` is an ordinary
  variable, common in pedagogy graphs — previously parsed to a malformed
  derivative `D(missing, L)`; it is now `Divide(d, L)`.
- **A matrix environment parses as a function argument.**
  `\operatorname{Trace}\left(\begin{pmatrix}1&2\\3&4\end{pmatrix}\right)` — and
  any library or user-declared function called on a `pmatrix`-family
  environment, with or without `\left`/`\right` — parsed the argument as a
  missing-argument error, so `Trace`, `Eigenvalues`, `Eigenvectors`, etc.
  appeared broken from LaTeX while working from MathJSON. The matrix (alone or
  among other arguments) now parses, evaluates, and round-trips.

### API

- **`ce.operatorInfo()` reports computability.** The returned record now carries
  `canEvaluate: boolean` — `true` when the operator's definition has an
  evaluation rule, `false` for a registered-but-inert head that only
  parses/serializes (e.g. `To`, `Tilde`). Together with an `undefined` return
  (no operator definition), integrators can gate free-form input on "can this
  actually compute" instead of hand-maintaining allowlists. Note: heads that
  reduce via canonicalization to another operator (`Exp` → `Power`, `Greater` →
  `Less`) report `false`; query the canonical form.

### Special Functions

- **New operators: `SinhIntegral` and `CoshIntegral`** — the hyperbolic sine and
  cosine integrals Shi and Chi, with numeric evaluation for real _and_ complex
  arguments (`Shi(2) ≈ 2.50157`, `Chi(2) ≈ 2.45267`; validated against mpmath)
  and derivatives (`\frac{d}{dx}\operatorname{Shi}(x) = \frac{\sinh x}{x}`,
  `\frac{d}{dx}\operatorname{Chi}(x) = \frac{\cosh x}{x}`). Exact arguments stay
  symbolic under `evaluate()`; `.N()` owns the numeric path.
- **`Erf` and `Erfi` evaluate for complex arguments.** Both error functions now
  have full complex-plane numeric kernels
  (`\operatorname{erf}(1+i) ≈ 1.31615 + 0.19045i`, validated against mpmath),
  instead of evaluating only on the real line.
- **Subscripted special-function notation parses.** `\operatorname{W}_{-1}(x)`
  now parses to the two-argument `["LambertW", x, -1]` (branch last), and
  `\operatorname{J}_{n}(x)` / `\operatorname{Y}` / `\operatorname{I}` /
  `\operatorname{K}` parse to `BesselJ(n, x)` et al. (order first) — these forms
  previously serialized but did not parse back, so LaTeX round-trips of
  non-principal Lambert branches and indexed Bessel functions now close.
- **The two-argument `LambertW(z, k)` differentiates.** Every fixed branch
  satisfies the same functional equation, so
  `d/dz W(z,k) = W(z,k)/(z·(1+W(z,k)))` now carries the branch through (chain
  rule included); the derivative with respect to the discrete branch index stays
  inert. Verified against central differences on both real branches.
- **Fungrim identities: `W₋₁(x·ln x) → ln x` fires.** The upstream entry
  `a172c7` published an _empty_ assumption interval
  (`OpenClosedInterval(0, −1/e)`); the corrected band `x ∈ (0, 1/e]` was fixed
  in the corpus fork (submitted upstream), and the recompiled identities
  artifact now carries the rule: with `loadIdentities(ce)` and
  `assume(0 < x ≤ 1/4)`, `simplify(W(x·ln x, −1))` returns `ln x`.
- **Fungrim identities: the polygamma family is live (+28 rules, artifact
  1,442).** The corpus' 2-argument `DigammaFunction(z, m)` (the order-`m`
  polygamma) now translates to CE's native `PolyGamma(m, z)` instead of a
  compat-shadowed 2-arg `Digamma`, so 28 previously skipped identities and
  special values compile and fire: `simplify(PolyGamma(1, 1)) → π²/6`,
  `PolyGamma(1, 1/4) → π² + 8·Catalan`, `PolyGamma(1, 1/2) → π²/2`, the
  digamma/polygamma recurrence and reflection identities, and more.
- **Fungrim identities: set-builder comprehensions get a real encoding (+8
  rules, artifact 1,450).** Corpus formulas of the shape
  `{f(x) : x \in S, P(x)}` used to translate to a literal `Set` that CE read as
  a two-element enumeration — producing wrong scalars where one was consulted
  (`Count` of a set-builder returned its operand count). They now translate to
  the faithful `Map(Filter(S, P), f)` form, which both fixed the miscounts and
  _recovered_ nine identities whose match side had been untranslatable — notably
  the prime-counting definition, so with `loadIdentities(ce)`, `simplify`
  rewrites `Count(\{p \in \mathrm{Primes} : p \le x\})` to
  `\operatorname{PrimePi}(x)`. Extrema over comprehensions
  (`\min\{f(x) : x \in S\}`) get the same encoding. The full 2,551-entry corpus
  now validates with **zero** numerically false entries.

## 0.73.0 _2026-07-09_

### New Operator: `Interpret`

- **`Interpret(expr)` gives formal meaning to elliptical notation.** Evaluating
  `["Interpret", expr]` turns a continuation-bearing sum or product (the inert
  notational objects produced by the ellipsis fold barrier, see below) into a
  formal `Sum`/`Product`: `Interpret(1 + 2 + \dots + n)` → `\sum_{k=1}^{n} k`,
  `Interpret(2 \cdot 4 \cdot \dots \cdot 2n)` → `\prod_{k=1}^{n} 2k`, and
  `Interpret(1 + 2 + \dots + 100)` → a `Sum` that evaluates to `5050`.
  Interpretation is an explicit opt-in — a plain `evaluate()` never guesses —
  and the gate is strict by design: at least two exact numeric sample terms in
  arithmetic progression and a single anchor whose implied upper bound is
  integral (so `1 + 3 + \dots + 2n`, whose even anchor does not belong to the
  odd progression, stays untouched). Anything the gate cannot prove is returned
  unchanged.
- **Polynomial and geometric patterns are recognized too (v2).** Successive
  finite differences identify polynomial general terms —
  `Interpret(1 + 4 + 9 + 16 + \dots + n^2)` → `\sum_{k=1}^{n} k^2`, cubes and
  triangular numbers likewise — and a constant exact ratio identifies geometric
  ones: `1 + 2 + 4 + \dots + 2^n` → `\sum_{k=1}^{n+1} 2^{k-1}`,
  `2 \cdot 4 \cdot 8 \cdot \dots \cdot 2^n` → `\prod_{k=1}^{n} 2^k`. Numeric
  anchors resolve to concrete bounds (`1 + 4 + 9 + \dots + 100` → a sum to 10
  that evaluates to `385`). An evidence discipline guards against overfitting: a
  degree-g polynomial needs its constant difference row witnessed twice, or one
  fewer sample when the anchor structurally confirms the general term — three
  samples fit _any_ quadratic, so `1 + 2 + 4 + \dots + m` stays untouched.
- **Linear recurrences are recognized (v3).** An exact-rational Berlekamp–Massey
  pass finds the minimal constant-coefficient recurrence (order ≥ 2) behind the
  samples, obtains a verified closed form through `RSolve`, and resolves numeric
  anchors by iterating the recurrence exactly:
  `Interpret(1 + 1 + 2 + 3 + 5 + 8 + \dots + 55)` →
  `\sum_{k=1}^{10} \operatorname{Fibonacci}(k)`, which evaluates exactly to
  `143`; Pell-number sums likewise (with a Binet-style body). The same evidence
  discipline applies — a recurrence of order L needs `2L+1` samples (or `2L`
  with a confirming anchor), so primes and factorials stay untouched. Closed
  forms are verified against every sample before being trusted.
- **Subtraction-spelled ellipses are protected too.**

### Number Theory

- **Modular arithmetic reaches common notation.** `Mod` and `Congruent` now
  reduce integer-valued expressions in ℤ/mℤ without materializing the
  (potentially astronomically large) intermediate value. Modular exponentiation,
  sums, products, negations and factorial reduction are all handled, so
  `2^{3^{20}} \pmod{100}` evaluates to `52` and
  `2^{3^{20}} \equiv 52 \pmod{100}` evaluates to `True`, where both used to stay
  inert. The floored-sign convention of `Mod` (the result follows the divisor)
  is preserved on the new path.
- **New `ModularInverse(a, m)`** returns the modular multiplicative inverse of
  `a` modulo `m` — the integer `x` in `[0, m)` with `a·x ≡ 1 (mod m)` — and
  stays symbolic when `a` and `m` are not coprime.
- **Linear congruences and CRT systems solve.** `solve` on a linear congruence
  returns the parametric residue family with a fresh integer parameter `t ∈ ℤ`
  (`6n \equiv 4 \pmod 7` → `7t + 3`), an empty result when there is no solution
  (`2x \equiv 1 \pmod 4`), and reduces gcd-divisible congruences
  (`4x \equiv 2 \pmod 6` → `3t + 2`). A system of simultaneous congruences in
  one unknown is combined via the Chinese Remainder Theorem — including
  non-coprime moduli — into a single family (`x \equiv 2 \pmod 3`,
  `x \equiv 3 \pmod 5`, `x \equiv 2 \pmod 7` → `105t + 23`); an inconsistent
  system reports no solution.
- **Huge exact products stay symbolic instead of overflowing.** `Multiply` now
  applies the same digit-count budget as `Power` when an exact power term
  (`base^exp`) would be folded into a product's numeric coefficient: if
  materializing that power would exceed the budget, the factor is kept as a
  symbolic `Power` term rather than computed eagerly (`2 \cdot 3^{5000000}`
  stays `2 \cdot 3^{5000000}` instead of building a multi-million-digit
  `bigint`). This also lets `Mod`/`Congruent` reduce such products —
  `2 \cdot 3^{5000000} \pmod 7` evaluates to `4` — without ever materializing
  the giant intermediate value.

### Arithmetic

- **New operator: `PolyLog` — the polylogarithm Liₛ(z).** Numeric evaluation for
  integer order s ≥ 2 over the whole complex plane (validated against mpmath to
  ≈5·10⁻¹⁵; branch cut z ∈ (1, ∞) with the below-the-cut convention), and exact
  reductions for the elementary orders and special points: `Li₁(z) → −ln(1−z)`,
  `Li₀(z) → z/(1−z)`, `Li₋₁(z) → z/(1−z)²`, `Liₙ(1) → ζ(n)`,
  `Liₙ(−1) → (2^{1−n}−1)·ζ(n)`, `Liₛ(0) → 0`. Parses and serializes as
  `\operatorname{Li}_s(z)` (the unsubscripted `\operatorname{Li}`,
  conventionally the _offset_ logarithmic integral, is deliberately not
  claimed).
- **`LogIntegral` now has its standard notation.** `\operatorname{li}(x)` parses
  to `LogIntegral` and serializes back (previously the fallback
  `\mathrm{LogIntegral}(x)`).
- **Repeating decimals box as exact rationals.** A LaTeX repeating-decimal
  literal — vinculum (`0.\overline{3}`), dots
  (`0.\overset{.}{1}4285\overset{.}{7}`), parenthetical (`1.54(2345)`), or arc
  (`0.\wideparen{142857}`) notation — and the MathJSON `{num: "0.(3)"}`
  shorthand now box directly to the exact `Rational` they represent
  (`0.\overline{3}` → `["Rational", 1, 3]`, `1.(2345)` →
  `["Rational", 12344, 9999]`) instead of a truncated decimal float carrying a
  repeating-decimal marker.
- **`Norm` accepts point-like `Tuple`s.** `\|(-3, 4)\|` now evaluates to `5`
  instead of leaving the expression inert.
- **Double-factorial symbolic reductions.** Under `simplify()`, `(2n)!!` reduces
  to `2^n \cdot n!` and `(2n+1)!!` reduces to `\frac{(2n+1)!}{2^n \cdot n!}`
  when `n` is integer-typed.

### Equation Solving

On a 40-case univariate solving benchmark derived from SymPy's own test suite
(graded by substituting the returned roots back into the equation), this release
reaches **38/40 — parity with both SymPy and Mathematica** — up from 26/40 for
the previous release (base engine, without the opt-in solve templates: 33/40, up
from 24). The two remaining cases (Dottie-style transcendental fixed points) are
unsolved by SymPy and Mathematica as well. What changed:

- **Inverse trigonometric and hyperbolic equations solve exactly.**
  `\arcsin x = c`, `\arccos x = c` and `\arctan x = c` return the exact root
  (`\arcsin x = \frac12` → `\sin\frac12`), with out-of-range constants correctly
  rejected (`\arctan x = 2` has no solution — `2` is outside arctan's range).
  `\sinh x = c` and `\tanh x = c` return their single root, and `\cosh x = c`
  returns **both** roots `\pm\operatorname{arcosh}(c)`.
- **Exponential-symmetric equations are recognized.** `e^x \pm e^{-x}`
  harmonizes to `2\cosh x` / `2\sinh x` before solving, so `e^x + e^{-x} = 4`
  returns both roots `\pm\operatorname{arcosh}(2)`.
- **Two-absolute-value equations solve.**
  `a\,\lvert f(x)\rvert = b\,\lvert g(x)\rvert` is squared into
  `a^2 f^2 - b^2 g^2` (candidates are validated against the original equation,
  so no extraneous roots): `\lvert x-1\rvert = \lvert x+3\rvert` → `-1`.
- **Rational equations cancel correctly before solving.** Clearing denominators
  no longer expands numerators past their common factor (`\frac{2x}{x+2} = 1` →
  `2`), and pure-number denominators are no longer multiplied through at all —
  rational constants stay where the solve patterns expect them.
- **`LambertW` gains the real lower branch W₋₁.** The 2-argument form
  `["LambertW", z, k]` selects the branch (`k` is `0` or `-1`; other branches
  stay symbolic): exact evaluation, machine- and arbitrary-precision numerics on
  the branch domain `[-1/e, 0)`, compilation, and `\operatorname{W}_{-1}(x)`
  LaTeX serialization. `W(-\frac1{10}, -1)` evaluates symbolically and `.N()`s
  to `-3.5771520639…`.
- **The opt-in solve templates now cover Lambert-type equations on both real
  branches.** With `loadIdentities(ce, { solve: true })` (from the `identities`
  bundle), equations reducible to `W` solve exactly and return **every real
  root**: `x e^x = -\frac1{10}` →
  `\{\operatorname{W}(-\frac1{10}), \operatorname{W}_{-1}(-\frac1{10})\}`,
  `e^x - x - 2 = 0` →
  `\{-2 - \operatorname{W}(-e^{-2}), -2 - \operatorname{W}_{-1}(-e^{-2})\}`, and
  mixed linear-exponential forms like `x + 2^x = 0` →
  `-\operatorname{W}(\ln 2)/\ln 2`. Exact rational, float, and integer
  right-hand sides are all handled. The identities library also simplifies
  `W(x e^x, -1) \to x` under `assume(x \le -1)`.

### Integration (opt-in Rubi rules)

New rule coverage in the `integration-rules` bundle (`loadIntegrationRules`):

- **Polynomial × csc²/sec² integrates by parts.** `\int x\csc^2 x\,dx` →
  `-x\cot x + \ln \sin x`, and likewise for `P(x)\sec^2(ax+b)` with any
  polynomial `P` (the recursion reduces the polynomial degree).
- **Rational × sin/cos of a linear argument reduces to Si/Ci.**
  `\int \frac{\sin x}{x+1}\,dx` returns the exact
  `\sin(-1)\operatorname{Ci}(x+1) + \cos(-1)\operatorname{Si}(x+1)` form via
  partial fractions over linear factors.
- **Secant-family binomials route through the dedicated secant rules.**
  Integrands like `\frac{1}{1+\sec x}` now resolve
  (`x - \frac{\tan x}{\sec x + 1}`) instead of returning unevaluated.
- **Cotangent integrands reflect onto the tangent rules**, closing forms like
  `\int \cot^3 x\,dx` → `-\frac{\cot^2 x}{2} - \ln \sin x`.

### Simplification and Exact Arithmetic (Wester round 1)

- **Rational radicands extract perfect-power factors.** `(1029/1000)^{1/3}` now
  canonicalizes to `\frac{7}{10}\sqrt[3]{3}` (numerator and denominator factored
  independently), extending the existing integer-radicand extraction. Also fixed
  an exactness leak where a higher root of an exact literal could evaluate to a
  float times `Root(1, n)` (e.g. Wester 28's `2^{1/3}` expressions now stay
  all-exact under `evaluate()`).
- **Pythagorean factoring in `simplify()`.**
  `\cos^3 x + \cos x\sin^2 x - \cos x` now simplifies to `0`: a sum with a
  shared factor times `\cos^2 u` and `\sin^2 u` combines
  (`g\cos^2 u + g\sin^2 u \to g`), generalizing the bare
  `\sin^2 x + \cos^2 x \to 1` case.
- **Rational-function cancellation fires in `simplify()`** (Wester 14):
  `\frac{x^2-4}{x^2+4x+4}` simplifies to `\frac{x-2}{x+2}`. The cancellation
  machinery existed but its result was destroyed by a subsequent
  expand-over-sum-denominator rewrite in the same pass; that split is now
  suppressed (it never reduces complexity).
- **`Binomial(n, k)` and `Pochhammer(a, k)` expand for small literal `k`** with
  a symbolic first argument: `Binomial(n, 3)` evaluates to
  `\frac{n(n-1)(n-2)}{6}`, `Pochhammer(a, 3)` to `a(a+1)(a+2)` (k ≤ 20).
  `Pochhammer` is a newly registered operator (it previously had no definition
  and was fully inert).
- Six Wester CAS-review tests unskipped in `wester.test.ts` (the skip ledger
  drops from 27 to 21).

### Linear Algebra

- **`RowReduce` is exact on exact input.** Reduction of an integer or rational
  matrix now uses exact bigint-fraction elimination — the RREF of an integer
  matrix has exact `-1`/`3` pivots instead of `-0.999…`/`2.999…` float
  artifacts. Float matrices use the numeric path unchanged.
  (`NullSpace`/`MatrixRank`'s float elimination is tracked in the ROADMAP for
  the same treatment.)
- **Products of declared matrices type correctly.** A product with a
  matrix/vector/list-typed operand now carries the collection type instead of
  collapsing to a numeric type: with `X` and `Y` declared `matrix`, `2Y`, `XY`,
  `X - Y` and `3X + 2Y` all type as `matrix` (previously `finite_number`, which
  made `\det(XY)` fail validation as an `incompatible-type` error). `Trace` of a
  matrix now types as `number`. All-scalar products are unchanged. Note:
  _undeclared_ symbols in a matrix-expecting argument (`\det(A+2B)` with fresh
  `A`, `B`) still infer as numbers — declare matrix/vector symbols for symbolic
  matrix algebra (see the ROADMAP "Matrix-operator typing" item for the planned
  inference-ordering fix).

### Units

- **Compound units cancel in quantity arithmetic.** Multiplying or dividing
  quantities now cancels units structurally instead of accumulating them:
  `18 \text{ in} / (12 \text{ in/ft})` evaluates to `1.5 \text{ ft}` (previously
  the inscrutable `1.5 \text{ in/in/ft}`). A repeated unit symbol cancels
  exactly — no conversion factors are introduced — while different units of the
  same dimension on opposite sides of a fraction bar are converted and folded
  into the magnitude: `\frac{10 \text{ m} \cdot 1 \text{ s}}{5 \text{ in}}` →
  `78.74 \text{ s}`. Products of same-dimension units are left as written
  (`2 \text{ in} \cdot 3 \text{ ft}` stays `6 \text{ in} \cdot \text{ft}`), and
  simplification to named derived SI units still applies afterwards
  (`2 \text{ N} \cdot 3 \text{ m}` → `6 \text{ J}`). Works with measurement
  (uncertainty-carrying) magnitudes as well.
- **New units: `yd`, `qt`, `pt`, `cup`, `wk`.** Yards, quarts, pints, cups (US
  liquid convention, consistent with the existing US `gal`) and weeks join the
  unit registry, with their English word aliases (`5 \text{ yards}` →
  `5 \text{ yd}`), and convert exactly: `1 \text{ gal} / 1 \text{ qt}` evaluates
  to `4`.
- **Currency: dollars and cents.** A new currency dimension backs the `USD` and
  `cent` units (`18 \text{ dollars}` → `18 \text{ USD}`,
  `1 \text{ dollar} + 50 \text{ cents}` → `1.5 \text{ USD}`), and currency
  participates in unit cancellation (`\$6 / (\$2/\text{lb})` → `3 \text{ lb}`).
  Other currencies are deliberately not modeled: exchange rates are not fixed
  constants, so cross-currency expressions stay inert rather than silently
  wrong.
- **Spaced unit phrases parse.** Multi-word unit text such as
  `60 \text{ miles per hour}` now parses to `60 \text{ mi/h}` — spaces inside
  `\text{...}` unit annotations are preserved and `per` reads as division —
  where previously the words ran together and the unit was not recognized.

### New Notations

- **Base-subscript numerals compute.** A numeral with an integer-literal
  subscript base, e.g. `10111_2` or `2748_{16}`, now parses to the numeric
  `BaseForm(value, base)` head (`10111_2` → `["BaseForm", 23, 2]`), so
  arithmetic on based numerals works: `1011_2 \cdot 101_2` evaluates to `55`,
  and `11_8 - 3_8 = 6_8` evaluates to `True`. The guard is strict — every digit
  must be valid for the base (`19_2` stays an inert `Subscript`), subscripted
  symbols (`x_2`) are unchanged, and values larger than 2⁵³ stay exact. The
  `BaseForm` LaTeX serializer was also fixed (it emitted an unbalanced
  parenthesis) and now round-trips: `BaseForm(23, 2)` serializes as `10111_{2}`.
  A numeral with a **symbol** subscript base, e.g. `161_b` or `161_{b}`, now
  parses to `BaseForm` of the digit polynomial in that base (`161_b` →
  `["BaseForm", ["Add", ["Power", "b", 2], ["Multiply", 6, "b"], 1], "b"]`, i.e.
  `b² + 6b + 1`), so arithmetic works symbolically (`161_b + 134_b` evaluates to
  `2b² + 9b + 5`) and the numeral round-trips back to `161_{b}`. Base equations
  solve: `161_b + 134_b = 315_b` reduces to `b² − 8b = 0` and solves to `b = 8`
  (and `b = 0`).
- **Sequence-braces notation.** `\{a_n\}_{n=1}^{\infty}` now parses to the new
  inert `IndexedSequence(term, index, lower, upper)` head instead of an
  `incompatible-type` error. The term uses the operator-call form
  (`["a_", "n"]`) so the index binding survives; `_{n\in\mathbb{N}}` subscripts
  map to the set's least element as the lower bound; the expression is inert
  under `evaluate()` and `simplify()` and round-trips through LaTeX. Bare
  `\{a_n\}` remains a `Set`, and the parenthesized form `(a_n)_{n\in\mathbb{N}}`
  is unchanged.

### Ellipsis Expressions

- **Sums and products no longer fold numeric terms across an ellipsis.** An
  `Add` or `Multiply` containing an ellipsis (`\dots`, the
  `ContinuationPlaceholder` symbol) is a notational pattern, not an arithmetic
  one: it now keeps its operands in source order with their structure intact,
  and is returned unchanged by `evaluate()`, `N()` and `simplify()`. Previously
  `1 + 2 + \dots + n` canonicalized to `n + 3 + \ldots` — folding the sample
  terms and destroying the pattern — and `2 \cdot 4 \cdot \dots \cdot 2n` folded
  to `16 \cdot \ldots \cdot n`, tearing the coefficient out of the `2n` anchor.
  Such products also round-trip through LaTeX now (an explicit `\times` is
  emitted around the ellipsis instead of juxtaposition).

### LaTeX Parsing

Recovery fixes from the Hendrycks-MATH genre sweep (`docs/mathnet/`), taking
that corpus from 97.09% to 97.38% clean parse:

- **Ordinal superscripts** devolve to the base number: `13^{\text{th}}` now
  parses as `13` (also `1^{\text{st}}`, `k^\text{th}`, `\mbox` variants). Only
  an exact ordinal suffix (`st`/`nd`/`rd`/`th`, case-insensitive) is dropped;
  other superscripts are unchanged.
- **Empty scripts are dropped:** `x^{}` and `x_{}` now parse as `x` instead of
  producing an error.
- **`{,}` thousands separator:** the LaTeX thin-separator idiom `1{,}000` now
  parses as the number `1000`. Only between digits, and a configured
  `decimalSeparator: '{,}'` (European convention) takes precedence — `3{,}14`
  still parses as `3.14` in that mode.
- **`\cancel`, `\bcancel`, `\xcancel`** unwrap to their body, and
  **`\cancelto{4}{72}`** parses to the replacement value `4` — matching the
  worked-solution usage the notation comes from.
- **`\not`-prefixed relations** compose into the negated relation: `\not=` →
  `NotEqual`, `\not\in` → `NotElement`, `\not\equiv` (incl. a trailing
  `\pmod n`) → the negated congruence, `\not\subset` → `NotSubset`, and
  relations without a dedicated negated head wrap in `Not(…)`.
- **Standalone `\pmod{7}`** now places the modulus as the second argument of
  `Mod` (previously the operands were flipped).
- **`(2n)!!` stays symbolic:** `Factorial2` accepts symbolic arguments (its
  signature was integer-only and rejected `2n` with an `incompatible-type`
  error); numeric double factorials are unchanged (`8!! = 384`).
- **Primed variables type-check as arguments:** `\sin a'` now parses to
  `Sin(Prime(a))` instead of a type error — `Prime` mirrors the type of its base
  (a primed value is a value; a primed function is a function). Derivative
  notation (`f'(x)` → `D(f(x), x)`) is unchanged.
- **Bare `N`/`D` devolve to variables in all argument positions:**
  `N \equiv 1 \pmod k` now parses as a congruence over the variable `N`
  (previously the standard-library `N` operator's function type failed the
  relation's numeric parameter check; the existing devolution fallback ran only
  for arithmetic operators). Applied uses (`N(2/3)`) still call the operator.
- **Congruence chains and fragments:**
  `3^{27}\equiv 3^7\pmod{100}\equiv 87\pmod{100}` folds into a conjunction of
  the adjacent congruence steps; a leading `\equiv b \pmod n` with an elided
  left-hand side recovers with a missing-operand placeholder.
- **Empty subscripts on multi-letter symbols are dropped:** `\alpha_{}` parses
  as `alpha` (completing the earlier `x_{}`/`13^{}` fix).
- **English unit words in `\text{…}` parse as quantities.** `18 \text{ inches}`
  → `["Quantity", 18, "in"]`: common measurement words (singular and plural —
  inches, feet, miles, gallons, pounds, minutes, hours, meters, liters, degrees,
  …) are normalized to their canonical unit symbols at the parse boundary,
  including inside compound units (`\text{ inches/foot}` → `in/ft`). An exponent
  _outside_ the text binds to the trailing unit factor:
  `7.5 \text{ gallons/ft}^3` → `Quantity(7.5, gal/ft³)` (gallons per cubic foot,
  not `(gal/ft)³`). Strictly gated: the whole text must resolve as a unit, so
  prose like `9\text{ to }80` is untouched. No `ton(s)` alias (a US short ton is
  not the metric tonne `t` — mapping it would be a silent 10% error).

### Restriction Braces

- **Comma-separated brace conditions combine as a union (`Or`).**
  `x^2\{x\ge0, x\le3\}` now parses to `["When", x², ["Or", 0≤x, x≤3]]` — each
  comma element is piecewise shorthand for `cond: 1` evaluated first-match, so
  the expression is defined where **any** condition holds. (Previously the
  condition was a `Tuple`, which is not boolean and could not compile.) Stacked
  braces (`\{c_1\}\{c_2\}`) still AND-combine, unchanged.
- **Colon groups parse as piecewise value selectors.** `x\{x>0:1, x<0:-1\}` now
  parses to `["Multiply", "x", ["Which", 0<x, 1, x<0, −1]]`: a brace group is a
  first-class piecewise _value_ (`{cond}` ≡ `{cond: 1}`) attached by
  juxtaposition — i.e. multiplication, the same convention that makes the
  bare-condition form a restriction. A trailing bare value is the else branch
  (`\{x>0:1, -1\}` → `…, "True", −1`), and a bare condition inside a colon group
  means `cond: 1`. (Previously the `cond:val` pairs were parsed as a `When`
  _gate_ for the body — inverted semantics.)
- **`When` now masks correctly on the `interval-js` compile target.** The
  interval comparisons return the tri-state string `'true' | 'false' | 'maybe'`
  — all truthy — so the previously-emitted JS ternary could never take its
  masking branch: an input interval entirely outside the restriction returned a
  normal interval result. `When` now compiles to a tri-state-aware runtime
  helper (`_IA.restrict`): `'false'` masks (`{kind: 'empty'}`), `'true'` passes
  the value through, and `'maybe'` — an input straddling the restriction
  boundary — reports the value range as domain-clipped
  (`{kind: 'partial', domainClipped: 'both'}`) so adaptive samplers see a domain
  edge rather than a clean interval. Scalar `javascript` and `glsl` `When`
  emission is unchanged.

### Pipelines and Held Operands

- **Hold operators reduce transformer heads.** `Solve`, `Integrate`, and `Limit`
  hold their expression operand (so an equation is not collapsed to a boolean
  before solving) — but a held operand whose head is an expression _transformer_
  (`Simplify`, `Expand`, `ExpandAll`, `Factor`, `Together`, `Distribute`,
  `TrigExpand`) is a computation step and is now reduced before the algorithm
  runs. `x^2+2x+1 \rhd \operatorname{Simplify} \rhd \operatorname{Solve}` now
  returns `[-1]` (previously `[]`: the solver found no roots in an expression
  whose operator was `Simplify`), and `\int \operatorname{Simplify}(x^2)\,dx` /
  `\lim` of a transformer-wrapped body compute instead of staying inert. Only
  the curated transformer set is reduced — full evaluation would collapse
  relations and substitute assigned values into the unknown.
- **Unknown-inference defers on the pipe topic placeholder.** Operators that
  infer their variable when omitted (`Solve`, `D`, `Series`, the polynomial
  operators) no longer run that inference on the pipeline topic placeholder `_`:
  `ce.box(["Solve", "_"])` stays `["Solve", "_"]` instead of canonicalizing to
  `["Solve", "_", "_"]`, which baked the placeholder into the unknown slot so a
  _prefix_ pipeline stage (`\rhd \operatorname{Solve}`) computed
  `Solve(expr, expr)` → `[0]` where the infix spelling returned `[-1]`. `Solve`
  re-infers the unknown when the applied stage evaluates; the two spellings now
  agree. Piping an _equation_ through the prefix form works too, now that an
  undecidable `Equal` survives the lambda's argument pre-evaluation (see
  "Undecidable Relations Stay Symbolic" below): `Apply(\rhd Solve, x^2 = 4)` →
  `[2, -2]`.

### Undecidable Relations Stay Symbolic

- **An equation with free variables is a condition, not a falsity.** `Equal` and
  `NotEqual` with an undecidable comparison now stay **inert** under
  `evaluate()`: `x^2 = 4` evaluates to itself instead of `False` (and `x \ne 4`
  to itself instead of `True`). This matches the inequality operators —
  `x^2 < 4` already stayed symbolic — and Mathematica's `==`. Decidable
  comparisons are unchanged (`2+2=4` → `True`, `2=3` → `False`, `x=x` → `True`),
  list/scalar elementwise comparisons are unchanged, and assumption discharge
  still applies (`assume(z > 0)` ⇒ `z \ne 0` → `True`). The previous collapse
  silently ruined stored equations: a notebook cell holding `x^2 = 4` evaluated
  to `False` at storage time, breaking every answer-referencing `Solve` pipe
  downstream.
- **`If` and `Which` stay unevaluated on an undecided condition.** A condition
  that is boolean-typed but not yet decidable (e.g. `x = 4` with a free `x`)
  leaves the conditional inert — it may become decidable once the variables are
  bound — instead of throwing `Condition must evaluate to "True" or "False"`
  (or, previously for `Equal` conditions, silently taking the else branch).
  Genuinely non-boolean conditions (a number, a misspelled symbol) still throw
  with the spell-check hint.

### Issues Resolved

- `toLatex({ digits: <number> })` no longer throws
  `RangeError: The number NaN cannot be converted to a BigInt` on a
  bignum-precision engine. A bare number — not part of the documented
  `DisplayDigits` forms, but the exact shape of a mechanical
  `fractionalDigits: n` → `digits: n` migration — is accepted with the
  deprecated numeric convention (`n ≥ 0` = fractional digits, `n < 0` =
  significant digits), and a genuinely invalid shape reports a clear validation
  error instead of crashing.
- The engine no longer trips its own
  `` `digits` and `fractionalDigits` were both specified `` deprecation warning.
  The serializer re-entered the public `toMathJson()` boundary — which always
  carries both (resolved) options — for any dictionary-_typed_ expression; for a
  symbol bound to a dictionary value this also recursed without bound (a warning
  flood followed by a stack overflow). Dictionary values now serialize inside
  the serializer proper; the warning fires only for genuine caller mistakes,
  once.
- `BoxedDictionary.toMathJson()` called without options no longer throws
  (`Cannot read properties of undefined`); it resolves the same defaults as
  every other expression kind.
- `.latex` on a dictionary-typed symbol with no value no longer overflows the
  stack; it serializes as the symbol. (`.latex` on a dictionary _value_ — which
  crashed in released builds — now returns an empty string: dictionaries have no
  LaTeX display form yet.)

### Benchmarks

#### Numeric performance (200-digit precision)

Median time per call, in **microseconds — lower is better**. `—` means the tool
returned no usable result at that precision.

| Expression         | CE (current) | CE 0.70.0 | SymPy | math.js | Mathematica |
| ------------------ | -----------: | --------: | ----: | ------: | ----------: |
| $\pi^2$            |           18 |        12 |   281 |     275 |         6.2 |
| $\sin 1$           |           34 |        36 |   353 |     945 |         7.1 |
| $\cos 1$           |           40 |        41 |   351 |   1,315 |          11 |
| $\ln 2$            |           27 |        24 |   598 |   8,195 |         5.8 |
| $e^{\pi}$          |           20 |        22 |   376 |   8,984 |         7.5 |
| $\zeta(3)$         |        2,715 |     2,787 |   494 |       — |         151 |
| $\Gamma(\tfrac13)$ |        1,452 |     1,424 | 4,843 |       — |         267 |
| $\psi(\tfrac13)$   |        1,269 |     1,241 | 3,782 |       — |         235 |

#### Symbolic capability & performance

Each cell is **how many times faster than Mathematica** that engine is on the
case (`Mathematica ÷ engine`, so **higher is better**; Mathematica itself is
`1×`). `—` means the engine can't do the case; `✓` means it solves a case
Mathematica can't. Compare the **CE (current)** and **CE 0.70.0** columns to see
what is _new this release_ (a `—` under `0.70.0` next to a number under the
current build). The **CE + R/F** column is the current build with the opt-in
Rubi integrator + Fungrim identities loaded (`loadIntegrationRules` /
`loadIdentities`), on the same minified bundle.

| Operation                              | CE (current) | CE + R/F | CE 0.70.0 |  SymPy  | math.js | Mathematica |
| -------------------------------------- | :----------: | :------: | :-------: | :-----: | :-----: | :---------: |
| **Antiderivatives**                    |              |          |           |         |         |             |
| $\int\frac{1}{\sqrt x}\,dx$            |     7.4×     |   3.0×   |   5.0×    |  0.6×   |    —    |     1×      |
| $\int\frac{x}{\sqrt{1-x^2}}\,dx$       |     8.3×     |   1.2×   |   7.7×    |  0.08×  |    —    |     1×      |
| $\int\frac{1}{x^3+1}\,dx$              |     4.3×     |   0.7×   |   3.4×    |  0.3×   |    —    |     1×      |
| $\int\frac{\sqrt x}{1+x}\,dx$          |      —       |   2.0×   |     —     |  0.1×   |    —    |     1×      |
| $\int\frac{x}{(1+x)^{1/3}}\,dx$        |      —       |   0.9×   |     —     | 0.008×  |    —    |     1×      |
| $\int\frac{x^2}{(1+x)^{1/3}}\,dx$      |      —       |   1.2×   |     —     | 0.006×  |    —    |     1×      |
| **Derivatives**                        |              |          |           |         |         |             |
| $\tfrac{d}{dx}\sqrt{1-x^2}$            |    0.01×     |  0.02×   |   0.02×   | 0.0009× | 0.002×  |     1×      |
| **Simplification**                     |              |          |           |         |         |             |
| $\sqrt{3+2\sqrt2}$                     |     29×      |   23×    |    27×    |    —    |    —    |     1×      |
| $\sqrt6\,x+\sqrt2\,x$                  |     71×      |   36×    |    54×    |  2.8×   |  9.1×   |     1×      |
| **Evaluation**                         |              |          |           |         |         |             |
| $\lim_{x\to0}\tfrac{\sin x}{x}$        |     43×      |   20×    |    38×    |  2.5×   |    —    |     1×      |
| $\lim_{x\to\infty}(1+\tfrac1x)^x$      |     6.3×     |   4.2×   |   6.0×    |  2.1×   |    —    |     1×      |
| $\int_1^2\tfrac1x\,dx$                 |    5213×     |  6049×   |   5202×   |   82×   |    —    |     1×      |
| $\int_{-\infty}^{\infty} e^{-x^2}\,dx$ |     332×     |   106×   |   279×    |  2.3×   |    —    |     1×      |
| **Solving**                            |              |          |           |         |         |             |
| $x^4+x^2-1=0$                          |     0.2×     |   0.2×   |   0.2×    |  0.06×  |    —    |     1×      |
| $x^3-x-1=0$                            |     1.2×     |   1.4×   |   1.4×    |  0.04×  |    —    |     1×      |

Across the cases both solve, Compute Engine is a **median 6.3× faster than
Mathematica** (up to 5213×).

<sub>Measured 2026-07-10 · Compute Engine `0.72.0` @ `2cf87db4` (current build)
· published `0.70.0` · SymPy `1.14.0` · math.js `15.2.0` · Mathematica
`14.3.0 for Mac OS X ARM` · Node `v22.13.1`. Correctness is verified numerically
against an independent `mpmath` reference, never another tool. Reproduce with
`npm run build production && ./venv/bin/python3 benchmarks/gen_cases.py && node benchmarks/report.mjs && node benchmarks/report_changelog.mjs`.</sub>

## 0.72.0 _2026-07-09_

### Angular Units

- **Compilation targets honor `ce.angularUnit`.** Compiled code from every
  built-in target (`javascript`, `interval-js`, `glsl`, `wgsl`, `interval-glsl`,
  `python`) now reproduces the engine's angular-unit semantics instead of always
  computing in radians: direct trigonometric arguments (`Sin`…`Csc`,
  `Haversine`) are scaled by the unit→radian factor and inverse-trigonometric
  results (`Arcsin`…`Arccsc`, `Arctan2`, `InverseHaversine`) by its reciprocal,
  for all units (`deg`, `grad`, `turn`). With `ce.angularUnit = 'deg'`,
  `compile('\\sin(x)')` emits `Math.sin(0.017453… * x)` so `run({x: 90})`
  returns 1, matching `evaluate()` — previously a degree-mode expression
  _evaluated_ in degrees but _compiled_ (and therefore plotted) as if radians.
  Radian mode emits the same code as before.

- **Hyperbolic functions are now unit-independent.** Their argument (and an
  inverse hyperbolic's result) is a dimensionless real, not an angle, so `sinh`,
  `cosh`, `tanh`, `coth`, `sech`, `csch` and `arsinh`…`artanh` no longer convert
  under a non-radian `angularUnit`. Previously in degree mode `\sinh(1)`
  evaluated to $\sinh(\pi/180) \approx 0.0175$ instead of $1.1752$.

- **Exact inverse-trigonometric values are returned in the current angular
  unit.** In degree mode `\arcsin(1)` now evaluates to the exact integer `90`
  (previously the exact _radian_ value $\pi/2$, disagreeing with `.N()`, which
  returned 90). Similarly `100` in `grad` mode and the exact rational `1/4` in
  `turn` mode; radian mode still returns $\pi/2$.

- **`Arctan2` honors `angularUnit`,** consistently with `Arctan` (it previously
  always returned radians): in degree mode `Arctan2(1, 1)` evaluates to the
  exact `45`, with the quadrant corrections applied in the current unit
  (`Arctan2(1, -1)` → `135`).

  Symbolic calculus (`D`, `Integrate`) remains radian-based regardless of
  `angularUnit` (no $\pi/180$ chain-rule factor); this is a known limitation.

### Step-by-Step Explanations

- **`explain('D')` handles higher-order and mixed partial derivatives.** A new
  `order` option requests the $n$-th derivative
  (`ce.parse('x \\sin x').explain('D', { variable: 'x', order: 2 })`), and a
  receiver that is itself a `D` expression — including mixed partials such as
  `D(f, x, y)` — is traced through its whole differentiation sequence. The
  explanation differentiates one order at a time: each stage replays the
  textbook rule applications inside the remaining derivative operators, folds to
  the simplified derivative, then differentiates again.

- **`explain('solve')` traces systems of equations and alternatives.** A `List`
  or `And` of equations is traced through the same solvers `solve()` runs:
  Gaussian elimination shows one step per eliminated variable and per
  back-substituted variable (`solve.system.eliminate`,
  `solve.system.back-substitute`, `solve.system.parametric`), and nonlinear 2×2
  systems show the product–sum or solve-and-substitute strategy
  (`solve.system.product-sum`, `solve.system.solve-for`,
  `solve.system.substitute`). An `Or` of univariate equations is solved case by
  case (`solve.case`) with the roots merged. The solutions are identical to
  `solve()` — the trace is a pure observation channel. Systems of inequalities
  and mixed systems are not traced and throw a precise error. To support
  systems, the `variable` explain option now also accepts an array of unknowns.

### Cortex Language (Experimental)

- **Cortex ships as a new entry point `@cortex-js/compute-engine/cortex`.**
  Cortex is a text-syntax programming language for scientific computing whose
  intermediate representation is MathJSON, evaluated by the Compute Engine. The
  entry point exports `parseCortex()` (Cortex text → MathJSON),
  `serializeCortex()` (MathJSON → Cortex text), and `executeCortex()` (parse and
  evaluate a program against a host-created engine):

  ```js
  import { ComputeEngine, executeCortex } from '@cortex-js/compute-engine/cortex';
  const ce = new ComputeEngine();
  const { value } = executeCortex(ce, `
    let x = 1/2
    if (x < 1) { x + 1 } else { 0 }
  `);
  // value.toString() === '3/2'
  ```

  **This is experimental**: the syntax and semantics may change between
  releases.

### Pipeline Operator

- **A pipeline operator applies the expression on its left to the function on
  its right.** `x \rhd f` (also `x \triangleright f`, `x \vartriangleright f`,
  `x ⊳ f`, or the plain-text shortcut `x |> f`) parses to `f(x)`. A `\square`
  topic marker in the right-hand side names the position the piped value fills,
  so a stage can be a multi-argument call:
  `x^2 = 4 \rhd \operatorname{Solve}(\square, x)` is `Solve(x^2 = 4, x)`. Stages
  chain left to right (`4 \rhd \sqrt \rhd \ln` is `ln(√4)`), a bare function
  command such as `\ln`, `\lb` or `\sqrt` acts as a function reference
  (`12 \rhd \ln` is `ln(12)`), and the prefix form (`\rhd f`, with no left-hand
  side) denotes the anonymous unary function `_ ↦ f(_)`.

- **The unknown/variable argument of `Solve`, `D`, `Series` and the polynomial
  operators may now be omitted.** It defaults to the input's single free
  variable, or to `x` when there are several free variables and one of them is
  `x`; with no inferable default the expression stays unevaluated. This enables
  point-free pipelines such as `x^2 = 4 \rhd \operatorname{Solve}` or
  `x^2 \rhd \operatorname{D}`. Applies to `Solve`, `D`, `Series`,
  `PolynomialDegree`, `CoefficientList`, `PolynomialRoots`, `Discriminant`,
  `PolynomialQuotient`, `PolynomialRemainder`, `PolynomialGCD`, `Resultant`,
  `Cancel`, `PartialFraction` and `Apart` (`Factor` already inferred its
  variable). For the two-input polynomial operators the default is inferred from
  both operands together.

### LaTeX Parsing

Notation coverage driven by a cross-genre corpus sweep (Hendrycks MATH, 15,546
fragments across all seven subjects including worked solutions; see
`docs/mathnet/math-genre-sweep.md`), which took the measured clean-parse rate
from 95.3% to 97.1%:

- **Text-styling commands.** `\textbf`, `\textit`, `\emph`, `\texttt`,
  `\textsf`, and `\textup` parse their argument as a text run and produce an
  `Annotated` expression with the matching style (`\textbf{Sizes}` →
  `["Annotated", "'Sizes'", {dict: {fontWeight: "bold"}}]`) that round-trips
  back to the same LaTeX. `\textrm` and `\mbox` parse like `\text`. `\bold`,
  `\boldsymbol`, and `\bm` are synonyms of `\mathbf` (`\bold{v}` → the symbol
  `v_bold`).

- **Vector-norm bars.** The `\|` command is now recognized as a norm delimiter
  everywhere `\Vert` is: `\|\mathbf{a}\|`, `\left\| b \right\|`, and `\|a\|^2`
  all parse to `Norm`.

- **TeX-primitive binomial.** The infix `{n \choose k}` form parses to
  `Binomial(n, k)`, joining the already supported `\binom`, `\dbinom` and
  `\tbinom`.

- **Bare mod annotations.** `x \pmod n` with no preceding `\equiv` parses as
  `Mod(x, n)` (`-811 \pmod{24}` → `["Mod", -811, 24]`), matching the existing
  `\bmod` behavior. Congruence chains followed by an implication now parse
  correctly: `a+1 \equiv 4 \pmod 7 \implies a \equiv 3 \pmod 7` is
  `Implies(Congruent(…), Congruent(…))` (the congruence previously disintegrated
  when `\implies` followed the modulus). `\equiv` now binds at comparison
  precedence, tighter than `\implies` (zero snapshot impact).

- **Mixed braced/unbraced fraction and binomial arguments.** `\frac1{-1}`,
  `\frac{900}7`, `\binom{n}k`, `\binom n{k+1}` parse correctly. Each argument is
  now independently a group or a single token, per TeX semantics; previously
  both arguments were forced into the style of the first, and the mixed forms
  produced a `missing` error.

### Issues Resolved

- Reading `.latex` (or `.toString()`) on the canonical, unevaluated form of a
  scalar×tuple product — `ce.parse('3(1,2)').latex`,
  `ce.box(['Multiply', 2, ['Tuple', 1, 2]]).latex` — no longer throws
  `RangeError: Maximum call stack size exceeded`. The pretty-JSON `Multiply`
  serializer round-trips through `Product.asRationalExpression()`, and the
  tuple-aware branch of `canonicalDivide` returned an inert `Divide(expr, 1)`
  instead of stripping the trivial divisor, sending the serializer into infinite
  recursion. Trivial `/1` and `/-1` divisors of tuple-typed expressions are now
  reduced.

- Juxtaposing a scalar with a tuple-**typed** symbol now means scaling, not
  tuple construction: with `z` declared `tuple<number, number>`, `3z` parses to
  `["Multiply", 3, "z"]` (previously a spurious `["Tuple", 3, "z"]`). Literal
  tuples (`3(1,2)`) were already handled; heterogeneous tuples such as
  `tuple<string, number>` still group as a `Tuple`.

- Compiled broadcasts over a list operand now compute their values. The
  generated `.map()` callback read its element variable from the vars object
  instead of the callback parameter, so a compiled `\sin([x, 2x])` returned
  `[null, null]` for every input. Compiled broadcast results now agree with
  `evaluate()`.

- `\operatorname{csch}(x)` now parses to the `Csch` function (previously a free
  symbol named `csch`, silently turning the expression into an implicit
  multiplication), joining the existing `\csch` command and matching
  `\operatorname{sech}`.

- Constructing many `ComputeEngine` instances in a synchronous loop no longer
  balloons memory (~430 KB pinned per engine until the task yielded to the event
  loop, enough to exhaust the default V8 heap after a few thousand engines).
  Every constant definition subscribed to configuration changes through a
  `new WeakRef(...)`, and the ECMAScript kept-objects rule pins each `WeakRef`
  target until the next microtask checkpoint. The tracker now holds its
  listeners directly; since it is owned by the engine, the engine and its
  listeners form a self-contained cycle that is garbage-collected as a unit.

- A bare `\ln` or `\log` — with no argument, as in the pipeline
  `12 \triangleright \ln` — now parses to the function symbol (`"Ln"`, `"Log"`),
  consistent with `\cos`, `\lg` and `\lb`. It previously parsed to an empty
  function application, so piping a value into it produced a `missing` error
  instead of applying the function:
  `ce.parse('12 \\triangleright \\ln').evaluate()` now returns $2\ln 2 + \ln 3$.
  The bare symbols also serialize back to `\ln`, `\log` and `\lg` (previously
  `\ln()`).

- A bare `\lb` (binary log) now parses to the `Lb` function symbol, so
  `12 \triangleright \lb` computes $\log_2 12$. It previously parsed to `Log`,
  silently computing the base-10 logarithm instead.

- A log with a base but no argument (`\log_2`) now parses with the pipeline
  topic marker `\square` standing in for the argument: `8 \triangleright \log_2`
  fills the hole and computes $\log_2 8 = 3$ (composing with inverse
  superscripts too: `9 \triangleright \log_3^{-1}` gives $3^9$), and a
  standalone `\log_2` displays as $\log_2(\square)$. It previously parsed as
  $\log_{10} 2$ — the base was read as the _argument_ — so piping into it
  silently discarded the piped value.

- Likewise, a function with a superscript but no argument (`\cos^2`, `\ln^{-1}`,
  `\lg^{-1}`) holds a topic-marker hole: `x \triangleright \cos^2` computes
  $\cos^2 x$, `12 \triangleright \ln^{-1}` computes $e^{12}$, and a standalone
  `\cos^2` displays as $\cos(\square)^2$. These previously produced a `Power` of
  the bare function symbol, which failed to type when piped into.

## 0.71.0 _2026-07-08_

### Differential Equations

- **First-order nonlinear equations solve.** (contributed by
  [KingArth0r](https://github.com/KingArth0r)) `DSolve` now handles four
  classical first-order classes:
  - **Separable** equations return an implicit solution when no explicit form is
    available: $y' = x/y$ gives $\frac12 y(x)^2 = \frac12 x^2 + c_1$.
  - **Bernoulli** equations $y' = p(x)\,y + q(x)\,y^n$ reduce via the
    $v = y^{1-n}$ substitution and return explicit solutions.
  - **Homogeneous** equations of the form $y' = F(y/x)$ solve by the $v = y/x$
    substitution: $y' = 1 + y/x$ gives $y(x)/x = \ln x + c_1$.
  - **Exact** equations $M(x,y) + N(x,y)\,y' = 0$ return the implicit potential:
    $2xy + y^2 + (x^2 + 2xy)\,y' = 0$ gives $x^2\,y(x) + x\,y(x)^2 = c_1$.

  Implicit solutions are expressed in terms of $y(x)$ itself. Equations outside
  the supported classes (e.g. the Riccati equation $y' = x + y^2$) stay inert.

- **Initial and boundary conditions are applied.** Scalar conditions can be
  passed in a list alongside the equation:
  `DSolve([y'' = -y, y(0) = 0, y'(0) = 1], y, x)` returns $y(x) = \sin x$.
  Derivative conditions are recognized in both the `Apply(Derivative(y, 1), x0)`
  and flat `D(y(x0), x)` forms. Conditions also apply to supported implicit
  solutions ($y' = x/y$ with $y(0) = 1$ gives
  $\frac12 y(x)^2 = \frac12 x^2 + \frac12$), and free parameters survive:
  $y' = kx/y$ with $y(0) = 2$ gives $\frac12 y(x)^2 = \frac12 k x^2 + 2$ with
  $k$ untouched. If the conditions cannot be applied to the solution class, the
  equation stays inert rather than silently dropping them.

- **Nonhomogeneous constant-coefficient equations of any order.** The
  undetermined-coefficients method now covers **polynomial, exponential, and
  sinusoidal** forcing at any order (previously polynomial forcing was
  second-order only), including **resonant** cases, which retry the ansatz with
  powers of $x$: $y'' - y = e^x$ gives $c_1 e^x + c_2 e^{-x} + \frac12 x e^x$,
  and $y''' - y = \sin x$ and resonant $y'' + y = \sin x$ both solve.

- **First-order linear homogeneous systems solve.** Pass the equations and
  dependent functions as lists: `DSolve([y' = z, z' = y], [y, z], x)` returns
  the general solution built from the eigen-decomposition of the coefficient
  matrix. Systems with repeated — or numerically indistinguishable — eigenvalues
  stay inert rather than returning a degenerate basis.

- **`NDSolve` integrates first-order systems.** Fixed-step RK4 now handles
  systems, including nonlinear ones, with the dependent functions and initial
  values given as lists:
  `NDSolve([y' = z, z' = -y], [y, z], Limits(x, 0, 1), [0, 1], 200)` produces
  samples as `[x, [y, z]]` pairs. Malformed or unsupported systems stay inert
  rather than returning partial results.

### Recurrence Equations

- **New `RSolve` operator.** (contributed by
  [KingArth0r](https://github.com/KingArth0r)) `RSolve(equation, a, n)` solves
  **linear homogeneous constant-coefficient** recurrences via the characteristic
  polynomial: geometric ($a_{n+1} = 2a_n$ gives $a(n) = c_1\,2^n$),
  Fibonacci-style, repeated roots with $n^k r^n$ modes
  ($a_{n+2} + a_n = 2a_{n+1}$ gives $a(n) = c_1 + c_2\,n$), and complex roots
  ($a_{n+2} = -a_n$ gives $a(n) = c_1\,i^n + c_2\,(-i)^n$). Initial conditions
  can be given in list form: `RSolve([a(n+1) = 2a(n), a(0) = 3], a, n)` gives
  $a(n) = 3 \cdot 2^n$. Nonhomogeneous and variable-coefficient recurrences stay
  inert.

## 0.70.0 _2026-07-08_

### Breaking Changes

- **The published `dist/` directory is reorganized into per-variant
  subdirectories.** The flat layout — where the variant was encoded in each
  filename (`compute-engine.min.esm.js`, `compute-engine.umd.cjs`, …) — is
  replaced by `esm/`, `esm-min/`, `umd/`, `umd-min/`, and the unchanged
  `types/`. The variant marker moves from the filename into the directory, so a
  bundle is now `<dir>/<name>.<ext>`. The general mapping is `<name>.esm.js` →
  `esm/<name>.js`, `<name>.min.esm.js` → `esm-min/<name>.js`, `<name>.umd.cjs` →
  `umd/<name>.cjs`, and `<name>.min.umd.cjs` → `umd-min/<name>.cjs`; for example
  `dist/compute-engine.min.esm.js` is now `dist/esm-min/compute-engine.js`.
  Consumers importing via the bare package specifier
  (`@cortex-js/compute-engine` and its sub-paths such as
  `@cortex-js/compute-engine/identities`) are **unaffected** — the package
  `exports` map absorbs the move. Only deep imports that reach into `…/dist/…`
  directly, and pinned CDN URLs, need to be updated. Each `esm*/` directory is
  now fully self-contained, with its own `chunks/` subdirectory holding only
  that variant's shared chunks, so vendoring a build is now "copy the directory
  for the variant you use."

- **The non-minified builds are no longer published to npm.** The package now
  ships `dist/esm-min/`, `dist/umd-min/`, and `dist/types/` only. The
  non-minified `esm/` and `umd/` directories — about 60% of the unpacked
  package, and never referenced by the `exports` map — are now build-only
  artifacts: `npm run build` still produces them locally for development and
  debugging, but if you need a readable (non-minified) bundle, build from
  source.

### Improvements

- **The declaration build and typecheck now run on TypeScript 7** (the native
  compiler), cutting `.d.ts` emission from ~31s to ~5s and the full production
  build from ~45s to ~29s. TS 7.0 ships no programmatic API, so it is installed
  side-by-side: the module name `typescript` stays aliased to the TS 6 API
  (`@typescript/typescript6`) for ts-jest, typedoc, typescript-eslint and madge,
  while the native compiler (`@typescript/native`) drives the CLI. No
  consumer-facing change — the published declarations are type-identical; only
  cosmetic emission differences appear (single-quoted string literals, sorted
  numeric-literal unions, literal non-ASCII property keys instead of `\uXXXX`
  escapes).

## 0.69.1 _2026-07-08_

### Issues Resolved

- **#318** Type declarations now resolve correctly in projects using
  `"module": "nodenext"`/`"node16"`. The published `.d.ts` files used
  extensionless relative imports, which produced `TS2834` errors (or collapsed
  every imported type to `any` with `skipLibCheck`). The build now rewrites the
  emitted declarations with explicit `.js` extensions and validates them against
  a `nodenext` consumer as part of every release build.

## 0.69.0 _2026-07-08_

### Breaking Changes

- **`\pm` now parses to a `Measurement`, not `PlusMinus`.** `a \pm b` parses to
  `["Measurement", a, b]` — a value with an uncertainty (see below) — replacing
  the previous `PlusMinus` head that evaluated to the two-branch tuple
  `(a−b, a+b)`. Consequences: solution sets that previously used `\pm` (e.g.
  quadratic roots) are now returned as an explicit `List` of the branches, and a
  numeric integral that reports an error estimate now returns
  `["Measurement", estimate, error]` instead of a `PlusMinus` tuple. Prefix
  `\pm b` parses to `["Measurement", 0, b]`.

- **`Loop` no longer produces a list — comprehensions moved to the new
  `Comprehension` operator.** `["Loop", body, ["Element", x, coll], …]` is now
  an imperative for-each evaluated **for effect**: its value is `Nothing` (or
  the value carried by a `Break`/`Return`), and it no longer collects the body
  values into a `List`. The trailing-`for` comprehension syntax
  (`x^2 \operatorname{for} x = [1...10]`) now parses to
  `["Comprehension", body, ["Element", …], …]`, which returns exactly what the
  collecting `Loop` used to — for consumers of the parse tree this is a head
  rename. The undocumented arity-2 form `["Loop", body, collection]` (body
  applied as a lambda to each element) has been removed: use `Map`, or an
  `Element` clause; a non-`Element` iterator argument is now an error.

- **`scalar + point` is now an error.** Adding a scalar to a numeric tuple
  (`1 + (2, 3)`) previously broadcast the scalar over the components; points are
  now proper vectors in ℝⁿ (see below) and a scalar term does not broadcast into
  them. Add a tuple explicitly (`(1,1) + (2,3)`) instead. Multiplying or
  dividing a point by a scalar still scales it.

- **Comparing a list to a scalar is now elementwise.** `[1, 4, 4] = 4`
  previously evaluated to `False` (whole-list comparison against a scalar); it
  now broadcasts and evaluates to `["List", "False", "True", "True"]`, as do
  `<`, `<=`, `>`, `>=`, and `!=`. Comparing two collections is unchanged:
  `Equal(L, M)` remains a whole-value comparison (`[1,2,3] = [1,2,3]` → `True`).

### Measurements and Uncertainty

- **New `Measurement` type — values with a propagated uncertainty.**
  `Measurement(value, error)` (written `value \pm error`) represents a measured
  quantity carrying a 1σ absolute uncertainty, and the uncertainty **propagates
  through arithmetic** using standard independent, first-order (quadrature)
  error propagation:
  - Algebraic and elementary operations propagate the error:
    `(5 \pm 0.2)(3 \pm 0.1)` → `15.00 \pm 0.78`, `\sqrt{4 \pm 0.2}` →
    `2.000 \pm 0.050`, `\sin(1 \pm 0.1)` → `0.841 \pm 0.054` (trig respects the
    engine's angular unit).
  - Measurements combine with **units**: `(5.1 \pm 0.2)\,\mathrm{cm}` is a
    measured quantity, and the error carries through quantity arithmetic and
    unit conversion (`UnitConvert` of `(5.1 \pm 0.2)\,\mathrm{cm}` to `m` →
    `(0.0510 \pm 0.0020)\,\mathrm{m}`). The bare form `5.1 \pm 0.2\,\mathrm{cm}`
    (no parentheses) parses to the same thing: a unit on only one operand of
    `\pm` scopes over the whole measurement (a dimensionless value with a
    dimensioned error is never meaningful). An error in a _different_ unit than
    the value (`5.1\,\mathrm{cm} \pm 2\,\mathrm{mm}`) stays as written.
  - **Display** follows the physics convention — the uncertainty is shown to two
    significant figures by default and the value is rounded to the same decimal
    place (`5.134 \pm 0.021`, `8.00 \pm 0.22`). Controlled by the `digits`
    serialization option (`{ significant: n }`, `{ fractional: n }`, `"max"`);
    `.toMathJson()` stays lossless.
  - **Correctness note:** propagation is _independent_ — exact when each
    measured quantity appears once (`A = L·W`) or in a single operation (`x^2`),
    but it over/under-estimates when one measured variable is reused across an
    expression (`x·x`, `x/(x+1)`), which are treated as independent. See the
    [Units guide](/compute-engine/guides/units/) for details and the `simplify`
    workaround.

### Points and Tuples

- **Numeric tuples are now points/vectors in ℝⁿ, distinct from lists.**
  Arithmetic on tuples is componentwise vector arithmetic and stays a `Tuple`:
  `(1,2) + (3,4)` → `(4,6)`, `3(1,2)` → `(3,6)`, `(4,2)/2` → `(2,1)`, `-(1,2)` →
  `(-1,-2)`. This fixes `(1,2)-(3,4)`, which previously produced a malformed
  nested list. `tuple · tuple` is an error (no implicit dot product — use
  `Dot`), and `scalar + tuple` is rejected (see Breaking Changes). Lists keep
  their existing broadcast semantics.

- **Tuple arithmetic and component access work symbolically for typed symbols.**
  A symbol declared `tuple<number, number>` participates in vector arithmetic
  without a value, and its components are accessible with the `.x`/`.y`/`.z`
  member syntax, which parses to `First`/`Second`/etc. (`P.x` →
  `["First", "P"]`). Component access on a point literal (`(1,2).x` → `1`) also
  works.

- **Color functions broadcast over lists**, so `rgb` and `hsv` applied to list
  arguments produce a list of colors, matching the other broadcastable numeric
  operators.

### Lists and Collections

- **Filtering a list with a condition in index position.** `L[L > 0]` evaluates
  to the elements of `L` where the condition holds — the Desmos list-filtering
  notation. The condition may reference the list itself (`L[L>0]`), another list
  (`L[d=4]` where `d` is a list), or compute a positional mask from a `Range`
  (`L[|[1...\operatorname{length}(L)]-i|>0]` removes the `i`-th element). A
  condition may be combined with integer indexes. The mask applies positionally
  and truncates to the shorter of list and mask.

- **Relational operators broadcast over lists.** `[-1, 2, -3] > 0` evaluates to
  `["List", "False", "True", "False"]`, typed `list<boolean>`. Scalar and
  symbolic comparisons are unchanged (`x > 0` stays symbolic). For `=` and `!=`
  the elementwise form applies only when exactly one operand is a collection —
  comparing two collections remains a whole-value equality (see Breaking
  Changes).

- **Broadcast results now report an honest `list<…>` type.** A broadcastable
  numeric operator applied to a list operand produces a list value, and its
  declared type now says so: `Sin([t, 1])` is typed `list<finite_number>`
  (previously the scalar `finite_number`, contradicting the value), and
  `[1,2] \cdot 2` / `[1,2] + x` report `vector<2>` rather than a scalar or a
  `number | vector<2>` union. Code that inspects `.type` before evaluating no
  longer needs to special-case list-broadcast expressions.

- **`When` broadcasts over a list-valued condition.** A domain restriction whose
  condition is a finite list of booleans now masks element by element — the
  Desmos restriction semantics. `x^2\{[1,2,3] > 0\}` evaluates to
  `[x^2, x^2, x^2]`, and with `x = 2`, `x\{x \le [1,2,3]\}` evaluates to
  `[Undefined, 2, 2]` (one masked branch per element: the value where the
  element condition is `True`, `Undefined` where `False`, a held `When` where
  the element is still symbolic). When the restricted expression is itself a
  list, the two are zipped elementwise, truncating to the shorter. Scalar
  restrictions (`x^2\{x > 0\}`) are unchanged, and the result type is lifted to
  `list<…>` only when the condition's type is a list of booleans.

### Parsing and Serialization

- **Fixed: bracket indexing after a symbol with `\left[` delimiters.**
  `A\left[1\right]` silently dropped the bracket group and parsed as bare `A`;
  it now parses to `["At", "A", 1]` like `A[1]` always did. Indexing also works
  on parenthesized groups and function applications: `(3,4)[1]` and `f(x)[i]`
  parse to `At` expressions.

- **Numbers with a leading or trailing decimal dot parse correctly.** `.85x`
  parses as `0.85 x`, and a trailing-dot literal inside delimiters (`(1., 2)`)
  is accepted.

- **Scaling a list/vector by juxtaposition is a `Multiply`, not a `Tuple`.** A
  scalar written next to a list- or vector-typed operand — including a scaled
  fraction whose numerator is a list or range, as in Desmos'
  `2\frac{[0,...,8]}{8}` — now canonicalizes to `Multiply` (element-wise
  scaling). Previously such juxtapositions produced a spurious `Tuple`, which
  raised an `incompatible-type` error when the result was used in further
  arithmetic. Genuine tuples (`2(3, 4)`) and plain list literals (`[1,2,3]`) are
  unaffected.

- **Restriction braces attach across visual space.** A `\{...\}`
  domain-restriction suffix now attaches to its base expression even when
  separated by spacing commands: `s(t) = (1-t)^2(1+2t)\ \{t\ge0\}\{t\le1\}`
  parses to a `When` with both conditions. The space-tolerance is specific to
  restriction braces: a space before an indexing bracket (`x\ \left[1,2\right]`)
  is still not an index access.

- **New inert `Polygon` operator.** `\operatorname{polygon}((0,0),(1,0),(0,1))`
  parses to `["Polygon", ...]`, an opaque geometric primitive like `Triangle`
  and `Segment`, for consumers that render it.

- **`histogram`, `pdf`, `cdf`, `length`, and `nCr` parse to `Histogram`, `PDF`,
  `CDF`, `Length`, and `Choose`.** The lowercase `\operatorname{...}` forms used
  by Desmos are now aliases of the existing operators. The member form `.length`
  (`S.\operatorname{length}`) also maps to `Length`, joining `.count`, `.max`,
  `.min`, `.total`, and the `.x`/`.y`/`.z` component accessors. `Histogram` and
  `BinCounts` accept any number as their bin specification (a non-integer bin
  count is left unevaluated; translate a Desmos bin _width_ to explicit bin
  edges at the import boundary).

- **New `digits` serialization option for significant-figures and decimal-place
  display control.** Available on `expr.toLatex()`, `expr.toMathJson()`, and
  honored by `expr.toString()`, `digits` controls how many digits of a number
  are _displayed_ (a formatting choice — it does not change the stored value or
  computation precision):
  - `digits: { significant: n }` rounds to `n` significant figures
    (`ce.parse("\\pi").N().toLatex({ digits: { significant: 3 } })` → `3.14`).
    Rounding is independent of notation (`1500` at two significant figures stays
    `1500` in fixed notation; use `notation: "scientific"` for
    `1.5 \cdot 10^{3}`), and exact integers, rationals, and radicals are shown
    in full — only inexact values are rounded.
  - `digits: { fractional: n }` shows `n` digits after the decimal point
    (`toFixed` semantics), and `digits: "auto"` / `"max"` behave as before.
  - The `fractionalDigits` option is **deprecated** in favor of `digits`. It
    continues to work (a numeric `n` is equivalent to
    `digits: { fractional: n }`); if both are provided, `digits` wins.

- **The pipeline operator `|>` supports a topic marker and a prefix form.** A
  `\square` in the right-hand side marks where the piped value is substituted,
  so the right-hand side may be a multi-argument call:
  `x^2 + 2x + 1 |> \operatorname{Solve}(\square, x)` parses to
  `Solve(x^2+2x+1, x)`. Without a marker the value is passed as the sole
  argument, as before (`x |> f` → `f(x)`). A prefix `|> f` (or
  `|> \operatorname{Solve}(\square, x)`) leaves the left-hand side implied and
  yields an anonymous unary function over the topic
  (`Function(Apply(f, _), _)`), which the caller applies to whatever value it
  wants to pipe in. `\rhd`, `\triangleright`, and `⊳` behave identically.

### Runtime and Scoping

- **`Declare` now accepts an optional initial value.** The three-operand form
  `["Declare", symbol, type, value]` declares the symbol with the given type,
  sets its initial value, and evaluates to that value (the previous form
  evaluated to `Nothing`). This matches the documented signature; earlier the
  value operand was silently dropped. The one- and two-operand forms are
  unchanged. A value-carrying `Declare` also compiles correctly (the initializer
  is emitted for the JavaScript and GLSL targets), not just when evaluated.

- **`Declare` can attach definition attributes via a trailing dictionary,
  including declaring constants.** An optional final `Dictionary` operand
  carries any of `type`, `value`, `constant`, and `holdUntil`, mirroring the
  JavaScript `ce.declare(name, def)` API. For example,
  `["Declare", "c", "real", 299792458, ["Dictionary", ["KeyValuePair", "constant", "True"]]]`
  declares an immutable constant (a later `Assign` to it is rejected), and
  `holdUntil` controls when the symbol's value is substituted (as for built-in
  constants such as `Pi`). A positional `type`/`value` takes precedence over the
  same key in the dictionary. This gives MathJSON a representation for constant
  declarations (e.g. the target for a `const` keyword in a surface language).

### Control Flow

- **`Loop` is now imperative control flow only** (see Breaking Changes above),
  and `["Loop", body]` is a real infinite loop: the body is evaluated repeatedly
  until it yields a `["Break", value?]` (the loop's value) or a `["Return", …]`
  (propagated), guarded by `ce.iterationLimit` and the evaluation deadline.
  Previously this form — documented as `while(true)` — evaluated the body only
  once. It compiles to `while (true) { … }` in JavaScript, and `Loop` with
  `Element` clauses compiles to plain `for` / `for…of` statement loops with no
  result array.

- **New `Comprehension` operator: value-producing list comprehensions.**
  `["Comprehension", body, ["Element", x, xs], …]` evaluates `body` for each
  combination of one or more `Element` clauses and collects the results into a
  `List`. Independent clauses produce a flat Cartesian product; a later clause's
  collection may reference an earlier binding
  (`[…, ["Element", "x", ["Range", 1, 3]], ["Element", "y", ["Range", 1, "x"]]]`
  iterates the triangle). Bound names do not leak. With a single clause it is
  equivalent to `Map(xs, x ↦ body)`; unlike `Map` (lazy) it materializes its
  result. Compiles to JavaScript as nested array-collecting loops (not available
  on the GLSL/WGSL targets, which have no dynamic arrays).

- **`Break` and `Continue` are now registered operators.** `Break(value?)` exits
  the enclosing loop immediately and its optional value becomes the loop's
  value; `Continue()` skips to the next iteration. Outside a loop both are
  inert.

- **Control flow now propagates out of `Block` statement results.** A `Break`,
  `Continue`, or `Return` produced by a statement's _result_ — e.g.
  `["If", cond, ["Break"]]` — now short-circuits the enclosing `Block` and
  propagates to the enclosing loop or function, as the documentation always
  specified. Previously only a statement that was _literally_ one of those heads
  short-circuited, so a conditional `Break` inside a block was silently
  discarded and the loop ran to the iteration limit. Consequences: the
  `while`-loop lowering `["Loop", ["Block", ["If", cond, ["Break"]], …body]]`
  now terminates correctly, and a `Block` whose value is a `Return` evaluates to
  the `["Return", value]` expression itself (unwrapped at the function
  application boundary), where it previously unwrapped eagerly.

- **`If` without an else branch is fixed.** `["If", cond, then]` — the
  documented two-operand form — failed to canonicalize (throwing
  `Cannot read properties of undefined`) and was left inert. It now
  canonicalizes and evaluates to `Nothing` when the condition is false.

- **Nested scopes now see the enclosing block's variables (lexical scoping
  fix).** A `Block`, `If` branch, or `Loop` body nested inside a `Block`
  resolved symbols against a stale canonicalization-time scope, so it could not
  read the values of the enclosing block's locals:
  `["Block", ["Declare", "k", "integer"], ["Assign", "k", 7], ["Block", "k"]]`
  evaluated to symbolic `k` instead of `7`, a `while`-style
  `["Loop", ["Block", ["If", cond, …], …]]` threw
  `Condition must evaluate to "True" or "False"`, and an `Element`-clause loop
  whose body is a `Block` left the loop variable symbolic
  (`Loop(Block(Assign(s, s + n)), Element(n, Range(1, 5)))` produced `5n`
  instead of accumulating `15`). Nested scopes now resolve enclosing block
  locals, loop variables, and — inside a function body — the function's
  parameters and locals correctly, so `while`/`for` lowerings with block bodies
  evaluate as expected.

- **Re-evaluating a program with `Declare` statements no longer throws.**
  Evaluating the same `Block` expression more than once — or a `Declare` inside
  a loop body, which re-executes every iteration — threw
  `The symbol "…" is already declared in this scope` on the second entry. A
  `Declare` statement now resets the binding it created on a previous run of the
  same scope. Genuine conflicts (redeclaring a function parameter, or
  `ce.declare()` on an explicitly declared symbol) still throw.

### Benchmarks

The numeric and symbolic state of this release is summarized below against the
last packed comparator release (`0.66.0`), SymPy, math.js, and **Mathematica** —
the reference baseline, since it is the broadest engine in the field. The tables
are generated by the harness in [`benchmarks/`](./benchmarks/)
(`node benchmarks/report_changelog.mjs`); every result is verified numerically
against an independent `mpmath` reference, never another tool. "CE 0.69.0" is
this release.

#### Numeric performance (200-digit precision)

Median time per call, in **microseconds — lower is better**. `—` means the tool
returned no usable result at that precision.

| Expression         | CE 0.69.0 | CE 0.66.0 | SymPy | math.js | Mathematica |
| ------------------ | --------: | --------: | ----: | ------: | ----------: |
| $\pi^2$            |       5.9 |       7.9 |   176 |     104 |         3.9 |
| $\sin 1$           |        20 |        20 |   222 |     442 |         5.2 |
| $\cos 1$           |        20 |        20 |   224 |     455 |         7.1 |
| $\ln 2$            |        13 |        81 |   339 |   4,315 |         3.8 |
| $e^{\pi}$          |        12 |        23 |   213 |   4,787 |         4.0 |
| $\zeta(3)$         |     1,542 |     3,395 |   268 |       — |          49 |
| $\Gamma(\tfrac13)$ |       830 |         — |   354 |       — |         214 |
| $\psi(\tfrac13)$   |       725 |         — | 2,810 |       — |         172 |

Biggest gains over `0.66.0`: $\ln 2$ **6.1× faster**, $\zeta(3)$ **2.2×
faster**.

#### Symbolic capability & performance

Each cell is **how many times faster than Mathematica** that engine is on the
case (`Mathematica ÷ engine`, so **higher is better**; Mathematica itself is
`1×`). `—` means the engine can't do the case; `✓` means it solves a case
Mathematica can't. Compare the **CE 0.69.0** and **CE 0.66.0** columns to see
what is _new this release_ (a `—` under `0.66.0` next to a number under the
current build). The **CE + R/F** column is the current build with the opt-in
Rubi integrator + Fungrim identities loaded (`loadIntegrationRules` /
`loadIdentities`), on the same minified bundle.

| Operation                              | CE 0.69.0 | CE + R/F | CE 0.66.0 | SymPy  | math.js | Mathematica |
| -------------------------------------- | :-------: | :------: | :-------: | :----: | :-----: | :---------: |
| **Antiderivatives**                    |           |          |           |        |         |             |
| $\int\frac{1}{\sqrt x}\,dx$            |   6.8×    |   3.1×   |   7.5×    |  0.5×  |    —    |     1×      |
| $\int\frac{x}{\sqrt{1-x^2}}\,dx$       |    11×    |   1.7×   |   10.0×   | 0.08×  |    —    |     1×      |
| $\int\frac{1}{x^3+1}\,dx$              |   6.3×    |   0.9×   |   6.7×    |  0.3×  |    —    |     1×      |
| $\int\frac{\sqrt x}{1+x}\,dx$          |     —     |   2.1×   |     —     |  0.1×  |    —    |     1×      |
| $\int\frac{x}{(1+x)^{1/3}}\,dx$        |     —     |   1.4×   |     —     | 0.01×  |    —    |     1×      |
| $\int\frac{x^2}{(1+x)^{1/3}}\,dx$      |     —     |   1.3×   |     —     | 0.007× |    —    |     1×      |
| **Derivatives**                        |           |          |           |        |         |             |
| $\tfrac{d}{dx}\sqrt{1-x^2}$            |   0.03×   |  0.03×   |   0.03×   | 0.001× | 0.004×  |     1×      |
| **Simplification**                     |           |          |           |        |         |             |
| $\sqrt{3+2\sqrt2}$                     |    46×    |   30×    |    41×    |   —    |    —    |     1×      |
| $\sqrt6\,x+\sqrt2\,x$                  |    98×    |   58×    |    97×    |  3.1×  |   19×   |     1×      |
| **Evaluation**                         |           |          |           |        |         |             |
| $\lim_{x\to0}\tfrac{\sin x}{x}$        |    55×    |   25×    |    56×    |  3.1×  |    —    |     1×      |
| $\lim_{x\to\infty}(1+\tfrac1x)^x$      |   9.7×    |   6.0×   |   5.1×    |  2.1×  |    —    |     1×      |
| $\int_1^2\tfrac1x\,dx$                 |   7429×   |  7782×   |   7935×   |  90×   |    —    |     1×      |
| $\int_{-\infty}^{\infty} e^{-x^2}\,dx$ |   459×    |   153×   |   586×    |  2.5×  |    —    |     1×      |
| **Solving**                            |           |          |           |        |         |             |
| $x^4+x^2-1=0$                          |   0.3×    |   0.2×   |   0.1×    | 0.06×  |    —    |     1×      |
| $x^3-x-1=0$                            |   1.9×    |   2.0×   |   0.2×    | 0.04×  |    —    |     1×      |

Across the cases both solve, Compute Engine is a **median 6.8× faster than
Mathematica** (up to 7429×).

<sub>Measured 2026-07-08 · Compute Engine `0.68.0` @ `5a2abce1` (current build)
· published `0.66.0` · SymPy `1.14.0` · math.js `15.2.0` · Mathematica
`14.3.0 for Mac OS X ARM` · Node `v22.13.1`. Correctness is verified numerically
against an independent `mpmath` reference, never another tool. Reproduce with
`npm run build production && ./venv/bin/python3 benchmarks/gen_cases.py && node benchmarks/report.mjs && node benchmarks/report_changelog.mjs`.</sub>

## 0.68.0 _2026-07-05_

### Breaking Changes

- **The ESM builds are no longer single-file: they load a shared chunk from
  `dist/chunks/`.** `compute-engine.esm.js`, `compute-engine.min.esm.js` and the
  corresponding `integration-rules` bundles are now built with code splitting,
  so the engine core is emitted once into a `chunks/chunk-*.js` file that both
  entry points import (this fixes `instanceof` failures when the
  integration-rules plugin is loaded alongside the main library, which
  previously carried its own duplicate copy of the engine). If you copy
  `compute-engine.min.esm.js` out of the package as a standalone file — for
  example to vendor it or serve it from your own static assets — you must now
  copy the `chunks/` directory alongside it, preserving the relative layout.
  Installing the package from npm, importing it from a bundler, or loading it
  from a CDN that serves the whole package (jsDelivr, unpkg, esm.sh) is
  unaffected. The `.umd.cjs` builds remain self-contained single files if you
  need a copyable artifact.

  Note that the chunk is required even if you don't use the integration-rules
  plugin: it contains the shared engine core, not the rule data. To vendor the
  Compute Engine _without_ the optional rule corpora, copy
  `compute-engine.min.esm.js` plus the `chunks/` directory and omit
  `integration-rules.*` (the Rubi corpus) and `identities.*` (the Fungrim
  corpus) — neither is loaded unless you import it explicitly. Each entry point
  imports exactly one chunk, so if you only ship the minified build you only
  need one of the two chunk files — the smaller one (the minified chunk), or
  definitively the one named in the entry file's first `import` statement. Just
  remember the names contain a content hash that changes between releases. All
  other sub-path bundles (`core`, `latex-syntax`, `numerics`, `compile`,
  `interval`, `identities`) remain self-contained. `Or`.\*\* `AB \parallel CD`
  is the parallelism relation, consistent with `\perp` → `Perpendicular`. Use
  `\lor` or `\vee` for disjunction (unchanged).

- **`\rightarrow` now parses to the mapping arrow `To`, not `Implies`.**
  `f: \mathbb{R} \rightarrow \mathbb{R}` now parses as a function signature,
  matching `\to`. This reverses the mapping introduced for issue #156:
  `\rightarrow`-as-implication was far rarer in practice than
  `\rightarrow`-as-mapping. Use `\Rightarrow`, `\implies`, or `\Longrightarrow`
  for implication (unchanged).

### New Operators

- **`Series`, `BigO`, and `Normal` provide symbolic series expansion.**
  `Series(f, x, x0, n)` returns the Taylor expansion of `f` in `x` about `x0`
  (default `x0 = 0`) up to and including the power `n` (default `n = 5`), plus
  an explicit remainder term. `x0` may be `±∞` for an asymptotic expansion in
  powers of `1/x`.
  - `Series(\sin x, x)` → $x - \tfrac{x^3}{6} + \tfrac{x^5}{120} + O(x^7)$;
    `Series(\ln(\cos x), x)` → $-\tfrac{x^2}{2} - \tfrac{x^4}{12} + O(x^6)$;
    `Series(\arctan x, x, +\infty)` →
    $\tfrac{\pi}{2} - \tfrac{1}{x} + \tfrac{1}{3x^3} - \dots$. Coefficients are
    exact (`Series(\sin x, x, \frac{\pi}{6})` gives $\tfrac12$,
    $\tfrac{\sqrt 3}{2}$, …), and an undeclared `f` yields the textbook form
    $f(0) + f'(0)x + \dots$.
  - At a **pole** the result is a Laurent expansion with a finite principal
    part: `Series(\frac{1}{\sin x}, x)` →
    $\tfrac{1}{x} + \tfrac{x}{6} + \tfrac{7x^3}{360} + O(x^7)$,
    `Series(\cot x, x)` →
    $\tfrac{1}{x} - \tfrac{x}{3} - \tfrac{x^3}{45} + \dots$, and the special
    functions expand at their poles with exact coefficients —
    `Series(\Gamma(x), x)` →
    $\tfrac{1}{x} - \gamma + (\tfrac{\gamma^2}{2} + \tfrac{\pi^2}{12})x + \dots$,
    `Series(\zeta(x), x, 1)` → $\tfrac{1}{x-1} + \gamma + O(x-1)$. Poles at `±∞`
    are handled too (`Series(\frac{x^2}{x-1}, x, +\infty)` →
    $x + 1 + \tfrac1x + \tfrac1{x^2} + \dots$). An essential singularity or
    branch point (e.g. `Series(e^{1/x}, x)`, `Series(\ln x, x)`) is still left
    unevaluated rather than expanded incorrectly.
  - `BigO(u)` is the inert Landau remainder, serialized `O\left(u\right)` and
    parsed from `\mathcal{O}(u)` and `\operatorname{O}(u)`. It is inert under
    `evaluate`/`simplify`; a numeric approximation (`.N()`) of any expression
    containing it is `NaN`.
  - `Normal(expr)` strips the `BigO` terms, yielding the compilable/plottable
    truncated polynomial: `Normal(Series(\sin x, x))` →
    $x - \tfrac{x^3}{6} + \tfrac{x^5}{120}$.

- **`TrigExpand`, `TrigToExp`, and `TrigReduce` rewrite trigonometric and
  hyperbolic expressions.** These are transformation verbs in the spirit of
  `Expand`/`Factor` and preserve exactness.
  - `TrigExpand` expands functions of sums and integer multiples of angles:
    `TrigExpand(\sin(a+b))` → $\sin a\cos b + \cos a\sin b$ and
    `TrigExpand(\cos(2x))` → $\cos^2 x - \sin^2 x$ (hyperbolic analogs, and
    `\sec`/`\csc`/`\cot` as reciprocals of the expanded `\cos`/`\sin`, are also
    handled).
  - `TrigToExp` rewrites trigonometric and hyperbolic functions in terms of the
    complex exponential, exactly: `TrigToExp(\sin x)` →
    $-\tfrac{i}{2}e^{ix} + \tfrac{i}{2}e^{-ix}$.
  - `TrigReduce` is the inverse of `TrigExpand`, rewriting products and integer
    powers as functions of multiple angles: `TrigReduce(\sin^2 x)` →
    $\tfrac{1 - \cos 2x}{2}$ and `TrigReduce(\sin x\cos x)` →
    $\tfrac{\sin 2x}{2}$.

- **Probability distributions: `NormalDistribution`, `BinomialDistribution`,
  `PoissonDistribution`, `UniformDistribution`, `ExponentialDistribution`,
  consumed by the generic `PDF`, `CDF`, and `Quantile` operators.** A
  distribution is a first-class value — assign it, pass it around, query it:
  - `PDF(dist, x)`, `CDF(dist, x)` and `Quantile(dist, p)` evaluate to **exact
    closed forms**: `CDF(NormalDistribution(0, 1), x)` →
    $\tfrac12\left(1 + \operatorname{erf}\tfrac{x}{\sqrt2}\right)$, an ordinary
    expression that can be simplified, differentiated, compiled and plotted.
    Exact arguments give exact results —
    `PDF(BinomialDistribution(4, \tfrac12), 2)` → $\tfrac38$ — and `.N()`
    numericizes at machine or arbitrary precision.
  - For discrete distributions `PDF` is the probability mass function, and
    `Quantile` (the least $k$ with $\operatorname{CDF}(k) \ge p$) is computed by
    exact search: `Quantile(PoissonDistribution(9), 0.95)` → `14`.
  - `Mean`, `Variance`, and `StandardDeviation` now also accept a distribution:
    `Mean(NormalDistribution(\mu, \sigma))` → $\mu$,
    `Variance(BinomialDistribution(n, p))` → $np(1-p)$.
  - `NormalDistribution(\mu, \sigma)` takes the standard deviation (not the
    variance), and `ExponentialDistribution(\lambda)` the rate — the Mathematica
    and scipy conventions.

- **`GammaRegularized` and `BetaRegularized` — the regularized incomplete gamma
  and beta functions.** `GammaRegularized(a, z)` is
  $Q(a, z) = \Gamma(a, z)/\Gamma(a)$ and `BetaRegularized(x, a, b)` is
  $I_x(a, b)$. They follow the exactness contract (special values fold —
  `GammaRegularized(1, z)` → $e^{-z}$ — and exact arguments stay symbolic),
  evaluate numerically at machine and arbitrary precision, and compile to
  JavaScript and Python (`scipy.special.gammaincc`/`betainc`). The discrete
  distribution CDFs evaluate to closed forms in these functions, e.g.
  `CDF(PoissonDistribution(\lambda), k)` →
  $\operatorname{GammaRegularized}(k+1, \lambda)$.

- **`Covariance`, `PopulationCovariance` and `Correlation` measure the
  relationship between two data sets.** Each accepts either two equal-length
  collections or a single collection of $(x, y)$ pairs (a scatter of points).
  Exact data gives exact results — `Covariance([1,2,3,4,5], [2,4,5,4,5])` →
  $\tfrac32$ and the Pearson correlation of the same data is
  $\tfrac{\sqrt{15}}{5}$, exactly. `Covariance` uses the sample ($n-1$)
  convention, `PopulationCovariance` the population ($n$) convention, matching
  `Variance`/`PopulationVariance`. Parse aliases: `\operatorname{cov}` and
  `\operatorname{corr}`. Both compile to JavaScript and Python
  (`np.cov`/`np.corrcoef`).

- **`LinearRegression` and `PolynomialFit` compute least-squares fits.**
  `LinearRegression(xs, ys)` (or a collection of points) evaluates to
  `(intercept, slope)`, and `PolynomialFit(data, degree)` to the list of
  coefficients, constant term first. **Exact data yields exact coefficients**:
  points lying on $1 + x^2$ fit at degree 2 to exactly `[1, 0, 1]`, and rational
  data produces exact rational coefficients rather than floats. With a trailing
  variable argument the fitted **expression** is returned directly, ready to
  plot: `PolynomialFit([(0,1), (1,2), (2,5), (3,10)], 2, x)` → $x^2 + 1$.

- **`Quantile` computes empirical quantiles of data.** `Quantile(collection, p)`
  interpolates the sorted data so that `Quantile(xs, 1/4)`, `Quantile(xs, 1/2)`
  and `Quantile(xs, 3/4)` agree exactly with `Quartiles` and `Median`
  (Moore–McCabe convention), with general `p` interpolated through the order
  statistics in rank space. (Combined with the distribution form above,
  `Quantile` covers both the theoretical and the empirical case.)

- **`Divides` and `NotDivides` express divisibility.** `a \mid b` parses to
  `Divides(a, b)` and `p \nmid ab` to `NotDivides(...)`; both evaluate for
  concrete integers (`Divides(3, 12)` → `True`) and stay symbolic otherwise.

- **Geometry notation is transcribed as inert heads.** `\angle ABC` →
  `Angle(A, B, C)` (also `\varangle`, `∠`), `\triangle ABC` →
  `Triangle(A, B, C)`, `\square ABCD` → `Quadrilateral(A, B, C, D)`, `A \perp B`
  → `Perpendicular`, `AB \parallel CD` → `Parallel`, `\widehat{ABC}` → `Arc`,
  `\overparen{BC}` → `OverParen`, and `\langle a, b \rangle` → `AngleBracket`.
  These heads have no evaluation semantics — the Compute Engine does not model
  geometry — but they parse and serialize faithfully so downstream consumers
  (e.g. graphical clients) get the structure instead of an error. Angle and arc
  measures are typed as numbers, so `\angle A + \angle B + \angle C = 180^\circ`
  composes in arithmetic.

- **`\sim` parses to the generic similarity relation `Tilde`.** It covers
  triangle similarity (`\triangle ABC \sim \triangle DEF`), asymptotic
  equivalence, and "is distributed as" (`X \sim N(0, 1)`); `\nsim` negates it,
  and `\simeq` now maps to the existing `TildeEqual` head (it previously had no
  LaTeX trigger).

### Step-by-Step Explanations

- **`expr.explain()` returns a structured, step-by-step explanation of a
  simplification** — the textbook chain _expression → step (with a reason) → … →
  result_. Each step carries the expression state after the step, a stable
  machine `id` (the localization key for consumers), and a default English
  description; `explain().result` is always the same value `simplify()` returns.
  `ce.parse('\\frac{x^2-1}{x-1}').explain()` yields one step, _"Cancel the
  common factors"_, ending at `x + 1`.
  - The step chain is curated by default (driver bookkeeping is filtered out);
    pass `{verbosity: 'all'}` for the raw trace (rule authoring, debugging).
    `simplify()` options (`rules`, `costFunction`, `strategy`) are honored.
  - The most frequently fired simplification rules ship with curated
    descriptions ("Apply the Pythagorean identity: sin²x + cos²x = 1", "Combine
    powers with the same base: xⁿ·xᵐ = xⁿ⁺ᵐ", …); other rules get a readable
    fallback derived from the rule id. `registerStepLabels()` lets a host
    application override or extend the descriptions.
  - **`expr.explain('solve')` traces equation solving.** Step values are
    _equations_ — the state after each phase — so the chain reads like textbook
    working: `2x+1=5` → _Move all terms to one side_ `2x-4=0` → _Isolate the
    unknown_ `x=2`. The trace covers the solver's algorithmic phases (clearing
    denominators, squaring both sides, substitutions like `u = eˣ` with
    back-substitution, zero-product factoring, the quadratic formula, checking
    candidates and rejecting extraneous roots) and the root-template rules,
    which now carry stable `solve.*` ids. `explain('solve').result` is a `List`
    of the same roots `solve()` returns; the unknown is inferred or passed via
    `options.variable`. Systems of equations are not traced yet.
  - **`expr.explain('D')` traces differentiation.** Steps are whole-expression
    states in traversal order — each textbook rule (sum, product, quotient,
    power, chain, exponential, logarithmic differentiation, table lookups) first
    appears with its unresolved sub-derivatives as inert `D(…)` terms, which
    resolve step by step: `D(x·sin x, x)` → _Apply the product rule_
    `x·D(sin x, x) + sin x` → _Differentiate using a known derivative_
    `x·cos x + sin x`. The variable is inferred when unambiguous (or passed via
    `options.variable`), and the result always matches evaluating
    `D(expr, variable)`.

### Solving

- **`Solve` accepts a domain for the unknown.**
  `Solve(x^2-5x+6=0,\; x \in 1..1000)` restricts solutions to a collection: the
  equation is solved symbolically and the roots are filtered to the domain (an
  integer domain also discards non-integer roots up front). When the symbolic
  solver finds nothing and the domain is finite and reasonably sized, `Solve`
  falls back to enumeration with a compiled predicate, confirming every
  candidate exactly so float rounding never produces a wrong answer (budgeted,
  interruptible; an unaffordable search returns the expression unevaluated
  rather than a partial answer). The predicate is not limited to equations — any
  boolean condition works: `Solve(2^n \equiv 1 \pmod{7},\; n \in 1..20)` →
  `[3, 6, 9, 12, 15, 18]`, and an extra condition can ride on the domain
  (`n \in 1..100, n > 5`-style, as in `Sum` indexing sets). The two-argument
  form is unchanged.

- **Multiple unknowns enumerate over the product of their domains.**
  `Solve(x^3+y^3=1729,\; x \in 1..12,\; y \in 1..12)` →
  `[(1,12), (9,10), (10,9), (12,1)]` — a `List` of `Tuple`s in unknown order,
  with the same budget, exact-confirmation, and interruption guarantees as the
  univariate case.

- **Integer equations are solved symbolically (diophantine solving).** When
  every unknown ranges over integers, `Solve` recognizes **linear** equations in
  any number of unknowns and **Pell-family** equations $x^2 - Dy^2 = N$
  (including the elliptic case $x^2 + |D|y^2 = N$) and solves them in closed
  form — ported from SymPy's diophantine module and validated against its test
  suite. Over a bounded domain this reaches answers enumeration cannot:
  `Solve(x^2-29y^2=1,\; x \in 1..10^5,\; y \in 1..10^5)` → `[(9801, 1820)]` via
  continued fractions, where the $10^{10}$-candidate sweep would be refused; an
  unsolvable equation is decided instantly
  (`Solve(6x+9y=4,\; x \in \pm10^6,\; y \in \pm10^6)` → `[]`). With
  integer-typed unknowns and no domain — previously inert — `Solve` returns the
  **parametric family**: `Solve(3n+4m=7, n, m)` → `[(4t-7,\; -3t+7)]` with the
  fresh parameter `t` ranging over ℤ, and Pell equations yield their exact
  closed forms $\bigl(\tfrac{(3+2\sqrt2)^t + (3-2\sqrt2)^t}{2}, \dots\bigr)$,
  and **Pythagorean triples** return the complete classical parametrization:
  `Solve(x^2+y^2=z^2, x, y, z)` →
  $\bigl(t(t_1^2-t_2^2),\; 2t\,t_1 t_2,\; t(t_1^2+t_2^2)\bigr)$ and its leg-swap
  — every integer triple, including all signs, lies in one of the two families.
  Every concrete solution is exact-confirmed by substitution; half-bounded
  domains (e.g. $n \ge 1$ alone) are left unevaluated, and forms whose textbook
  parametrizations are provably incomplete (weighted coefficients, four or more
  squares) are declined rather than answered partially.

- **Periodic equations expand their root families over a bounded domain.**
  `Solve(\sin x = \tfrac12,\; x \in [0, 4\pi])` returns all four exact solutions
  $\tfrac{\pi}{6}, \tfrac{5\pi}{6}, \tfrac{13\pi}{6}, \tfrac{17\pi}{6}$ — not
  just the principal values. Scaled arguments work too (`\sin 2x = 1` over
  $[0, 2\pi]$ → $\tfrac{\pi}{4}, \tfrac{5\pi}{4}$). Expansion applies when the
  unknown appears only inside trigonometric functions of linear arguments; each
  family member is verified by exact substitution, and unreasonably large
  expansions degrade gracefully to the principal roots.

- **`assume()` bounds now filter solutions.** After `assume(n > 0)`,
  `Solve(n^2 = 16, n)` (and `expr.solve("n")`) returns `[4]` instead of
  `[4, -4]`; `assume(n \in 1..10)`, inequality, and `\ne` assumptions are
  honored the same way, conjunctively with any explicit domain. Roots are
  dropped only when an assumption definitely excludes them — symbolic roots that
  cannot be decided are kept.

### Parsing Resilience

The parser was hardened against a corpus of ~2,300 math fragments extracted from
real olympiad problems (the MathNet dataset); the clean-parse rate on that
corpus went from 85% to ~96%, and the one crash it exposed is fixed. See
`docs/mathnet/` for the corpus, the regression checker, and the work plan.

- **Ellipsis in a numeric context no longer throws.**
  `(1!)^2 + (2!)^2 + \dots + (2018!)^2` crashed with
  `The type of the constant "ContinuationPlaceholder" cannot be changed` (type
  inference attempted to narrow a constant). Inference is now a no-op on
  constants.

- **`\cdots`, `\dotsb`, `\dotsc`, `\dotsm`, and Unicode `…` parse as ellipsis.**
  Previously only `\dots`/`\ldots`/`...` did; `(2!+2)(3!+3) \cdots (2019!+3)`
  now parses with the placeholder as an inert operand instead of erroring.

- **A trailing sentence period no longer breaks an equation.** Input copied from
  prose often ends in `.`, `;` or `,` (e.g. `... = z^2.`). When — and only when
  — the parse would otherwise contain an error, the trailing punctuation is
  dropped and the input re-parsed. Valid input is unaffected: `5.` still parses
  as the decimal $5$.

- **Congruences parse and evaluate.** `a \equiv b \pmod{n}` (also `\bmod`, the
  parenthesized `(\bmod n)`, and the ASCII form `n ≡ 1 (mod 3)`) parse to
  `Congruent(a, b, n)`, which evaluates for concrete integers
  (`7 \equiv 1 \pmod{3}` → `True`) and now accepts symbolic moduli
  (`2^n \equiv 1 \pmod{p^{k+1}}` stays symbolic instead of erroring).

- **Common Unicode math symbols are accepted:** `≡` (congruence), `∈`, `∉`, `∪`,
  `∩`, `≈`, `∠`, and `…` — useful when input comes from plain-text sources
  rather than LaTeX.

- **Alignment environments parse as systems.**
  `\begin{aligned} a^2+ab+c=0 \\ b^2+bc+a=0 \end{aligned}` (also `align`,
  `gather`, `split`, `multline`, `eqnarray` and their starred variants) parses
  to a `List` of the row expressions — the same convention as `\begin{cases}`,
  accepted by `solve()`. Alignment markers are transparent: `x &= y` is `x = y`.

- **Qualified number sets parse.** `\mathbb{R}_{>0}` → `PositiveNumbers`,
  `\mathbb{Z}_{\ge0}` → `NonNegativeIntegers`, `\mathbb{N}^*` →
  `PositiveIntegers`, etc., and they round-trip to canonical LaTeX. A
  qualification with no named set (`\mathbb{N}_{>1}`) falls back to a faithful
  set-builder.

- **Structural odds and ends:** `A \backslash B` parses as `SetMinus` (the
  common alternative spelling of `\setminus`); a standalone quantified condition
  `\forall n \ge 1` parses instead of erroring; `\underbrace` mirrors
  `\overbrace`.

- **A symbol's inferred type narrows instead of erroring.** When a free symbol's
  type was inferred from one use and a later use requires a more specific type,
  argument validation now narrows the inference (when sound) instead of
  producing an `incompatible-type` error. This fixes
  `(A \setminus B) \cup (B \setminus A)` — where `B` was inferred as a value and
  then rejected as a set — as well as `-n!!` (double factorial of an undeclared
  symbol) and a family of similar mixed-use expressions. Declared types are
  unaffected: passing a declared string where a set is required is still an
  error.

### Packaging

- **The `integration-rules` plugin shares code with the main library.** The ESM
  builds of `compute-engine` and the opt-in
  `@cortex-js/compute-engine/integration-rules` entry point are now emitted with
  code splitting: the engine core lives in a shared chunk imported by both,
  instead of being bundled twice. This shrinks the combined download and fixes
  cross-bundle `instanceof` failures when a host mixed objects from the two
  bundles. The UMD builds remain self-contained single files.

### Lenient parsing and string helpers

- **The string helpers take a `strict` option.** `simplify()`, `evaluate()`,
  `N()`, `expand()`, `expandAll()`, `factor()`, `solve()`, and `compile()` parse
  string input in lenient (non-strict) mode by default. Note that lenient mode
  is not a pure superset of strict LaTeX: unbraced multi-digit scripts change
  meaning — `x^23` is $x^{23}$ (not $3x^2$), and `x_23`/`a_12` are single
  multi-digit subscripts. Pass `{ strict: true }` (e.g.
  `N('x^23', { strict: true })`) to restore the strict LaTeX grammar.

- **Lenient inverse functions, `atan2`, and letter runs parse correctly.**
  `sin^-1 x` now means $\arcsin x$ (the inverse function), not $1/\sin x$
  (matching strict `\sin^{-1}`); `sin^-2 x` stays $1/\sin^2 x$. `atan2(1, 2)`
  parses as `Arctan2(1, 2)`, and `acot`/`asec`/`acsc` are recognized. A
  multi-letter run with an embedded Greek constant is segmented (`2pix` →
  $2\pi x$, `xpi` → $x\pi$) instead of injecting a spurious imaginary unit, and
  an implicit subscript is accepted on a constant base (`alpha2` → $\alpha_2$).

### Differential Equations

- **Repeated roots produce correct general solutions.** `DSolve` now clusters
  numeric characteristic roots by multiplicity: $y'''' + 2y'' + y = 0$ gives
  $(c_1 + c_2 x)\cos x + (c_3 + c_4 x)\sin x$ instead of a degenerate basis with
  spurious $e^{\varepsilon x}$ factors, and repeated real roots keep their
  $x e^{x}$ modes. A structural self-check returns the equation unevaluated
  rather than emit a basis with fewer independent solutions than the order.

- **No more corrupted solutions.** Equations with variable coefficients on
  higher-order derivatives (e.g. $x^2 y'' + x y' = x$) previously returned a
  "solution" containing an internal `Error` node; they now stay unevaluated when
  the class is unsupported. Equations whose right-hand side references the
  dependent function with a transformed argument (e.g. $y'(x) = y(2x)$) stay
  unevaluated instead of returning an unevaluated integral as "solved".

- **Exponential forcing terms solve.** Variation of parameters was silently
  disabled for exponential bases (an internal Wronskian stayed unsimplified):
  $y'' - y = e^x$ now returns
  $c_1 e^x + c_2 e^{-x} + \frac12 x e^x - \frac14 e^x$, and $y'' + y = e^x$
  returns $c_1 \cos x + c_2 \sin x + \frac12 e^x$, instead of the equation
  unevaluated. Solutions are returned in collected form (no $e^a \cdot e^b$
  products or $A\sin^2 u + A\cos^2 u$ pairs).

- **Parsed LaTeX input works end-to-end.** `ce.parse("y''(x)+y(x)=0")` no longer
  canonicalizes the derivative of an undeclared function into an `Error` node: a
  derivative now reports a numeric result type, so prime/dot-notation equations
  flow from `parse()` through `DSolve` ($\dot x + \ddot x$ expressions are
  likewise no longer corrupted). The implicit first-order form
  `Apply(Derivative(y), x)` is also recognized (order defaults to 1).

### Evaluation

- **`Beta` is exact and pole-aware.** $\mathrm{B}(a, m)$ with a positive integer
  argument reduces exactly ($\mathrm{B}(2,3) = \frac{1}{12}$,
  $\mathrm{B}(-2,2) = \frac12$), and arguments at gamma-function poles return
  $\tilde\infty$ instead of a silently wrong finite value ($\mathrm{B}(-1,2)$
  previously returned $-2.97\times10^{49}$).

- **Multiplication by infinity respects sign information.** $x \cdot \infty$
  stays symbolic when the sign of $x$ is unknown, evaluates to $-\infty$ when
  $x$ is known negative, and to `NaN` when $x$ is zero — it no longer collapses
  to $+\infty$ unconditionally.

- **Inverse hyperbolic functions have values at their poles.**
  $\operatorname{artanh}(\pm 1)$ and $\operatorname{arcoth}(\pm 1)$ evaluate to
  $\pm\infty$, $\operatorname{arsech}(0)$ to $+\infty$, and
  $\operatorname{arcsch}(0)$ to $\tilde\infty$, with result types that no longer
  claim a finite value at a pole.

- **`Sum` reports incompatible elements.** Summing a collection containing a
  string returns a typed error instead of a silent `NaN`.

- **Sums and products over an infinite domain stay symbolic under
  `evaluate()`.** An infinite domain has no exact value by truncation, so
  $\sum_{n=1}^{\infty} \frac{1}{n^2}$ now evaluates to itself; `.N()` returns
  the truncated numeric approximation, as before. Previously `evaluate()`
  returned a silently truncated partial sum (off by $\sim 10^{-4}$ for this
  example) — a float where the exactness contract promises an exact value.

- **Sums and products with symbolic bounds no longer evaluate to a number.**
  `\sum_{k=1}^{n} k` with an unbound $n$ evaluated to $50\,015\,001$ — the sum
  truncated at an internal iteration cap of $10\,001$ — under both `evaluate()`
  and `.N()`. It now stays symbolic (`simplify()` still produces the closed form
  $\tfrac{n^2+n}{2}$).

- **`Expand` computes constant powers.** `Expand((2+3i)^{1000})` returns the
  exact 557-digit Gaussian integer (matching SymPy's `expand()`), and
  `Expand(2^{1000})` the exact integer; both previously returned unevaluated.
  Structural expansion of symbolic powers is unchanged, and powers too large for
  exact computation still stay symbolic.

- **Huge exact complex numbers are finite and print in full.** An exact Gaussian
  integer with components beyond float64 range (e.g. $(2+3i)^{1000}$) reported
  `isInfinity` `true`, serialized as $\tilde\infty$ in plain text, and had a
  `NaN` `bignumIm` — all artifacts of routing through the machine-float
  projection. It now types as `finite_complex`, prints its full digits, and
  `bignumIm` is exact.

- **Perfect-power radicands reduce.** $(997^3)^{1/6} = \sqrt{997}$,
  $8^{1/6} = \sqrt2$, $8^{1/4} = 2^{3/4}$: when canonicalization folds a power
  into an opaque integer, the root now recovers the structure by perfect-power
  decomposition. In particular the zero-equivalence test
  $\sqrt{997} - (997^3)^{1/6}$ evaluates to exact $0$ (it previously leaked a
  float residue).

- **Logarithms reduce when the argument and base are powers of a common base.**
  $\log_8 32768 = 5$, $\log_8 2 = \tfrac13$, $\log_4 8 = \tfrac32$ — exactly,
  honoring the exactness contract ($\ln 2$ and $\log_8 10$ stay symbolic).

- **`Xor` cancels repeated operands.** $a \oplus a = \mathrm{False}$, so
  `Xor(x, y, y)` evaluates to $x$; cancellation composes with the existing
  `True`/`False` folding.

### Linear Algebra

- **3×3 `Eigenvalues` returned wrong values — fixed.** The analytic solver used
  a sign-flipped term in its depressed cubic, mirroring every eigenvalue about
  $\operatorname{tr}/3$: e.g. $[[5,-3,-7],[-2,1,2],[2,-3,-4]]$ returned
  $\{\tfrac{10}{3}, -\tfrac53, \tfrac13\}$ instead of $\{1, -2, 3\}$. (Spectra
  symmetric about their mean — like $\{1,2,3\}$ — were unaffected, which is how
  it escaped notice.) Additionally, a complex-conjugate eigenvalue pair was
  returned as its real part twice ($\{2, \pm i\}$ came back $\{2, 0, 0\}$);
  complex eigenvalues are now returned as complex numbers.

### Rules and Pattern Matching

- **Rule conditions must return a boolean.** A `condition` function returning a
  non-boolean (e.g. the boxed symbol `False`, which is a truthy JavaScript
  object) no longer fires the rule; a one-time console warning identifies the
  malformed condition. Returning the boxed symbol `True` is accepted.

- **`e` and `i` work in string rules.** Rules such as `'e^2 -> 7'` now match:
  the constants are resolved to `ExponentialE` and the imaginary unit when the
  rule is parsed, instead of remaining inert symbols that could never match.

- **Explicit wildcards work in LaTeX match patterns.** An object-form rule such
  as `{match: '_a + 1', replace: '_a'}` now parses `_a`/`__a` as wildcards
  instead of an implicit product.

- **A throwing condition no longer discards subexpression rewrites.** If a rule
  condition throws, the rule is skipped at that node but successful rewrites of
  the operands are kept.

## 0.67.0 _2026-07-03_

This release improves correctness and predictability across the public Compute
Engine API: exact complex and integer arithmetic stays exact more often, partial
derivatives and assumptions are more capable, LaTeX and lenient parsing
round-trip more reliably, compiled output agrees more closely with interpreted
evaluation, and arbitrary-precision arithmetic is substantially faster. It also
fixes many cases where `evaluate()`, `N()`, `simplify()`, `isEqual()`,
`assume()`, `verify()`, serialization, or compilation could return a wrong
answer, lose exactness, hang, or silently accept invalid input.

### Exact and Numeric Evaluation

- **Exact complex arithmetic preserves exact values.** Gaussian integer and
  rational complex values now stay exact through arithmetic: $(1+i)^3$ evaluates
  to $-2+2i$, $(1+i)^{-2}$ to $-\frac{i}{2}$, $\frac{1}{1+i}$ to
  $\frac{1-i}{2}$, $\sqrt{3+4i}$ to $2+i$, and $\sqrt{-4}$ to $2i$. Exact
  complex numbers also round-trip through MathJSON as `["Complex", re, im]` with
  exact components.

- **Integer powers and large integers stay exact.** Integer powers such as
  $2^{127}$ now evaluate to exact integers, negative integer powers produce
  exact rationals such as $2^{-2} = \frac14$, and powers of Gaussian integers
  such as $(1+i)^2$ evaluate exactly. Very large exact integers are no longer
  rounded when used by `IsPrime`, `IsOdd`, `IsEven`, `FactorInteger`, `Mod`, or
  `DigitSum`.

- **Exact results are preserved more consistently.** `evaluate()` no longer
  turns exact arguments into floats in cases such as $\sqrt{-2}$,
  $\operatorname{Fract}(\frac12)$, $\Re(\frac12)$, $|1+i|$, $\log_2(\pi)$,
  `Distance`, and statistics functions. For example,
  $\operatorname{Mean}([1,2,3,4])$ now returns $\frac52$, and
  $\operatorname{StandardDeviation}([1,2,3,4])$ returns $\frac{\sqrt{15}}{3}$.

- **Special functions are more accurate.** `PolyGamma`, `Zeta`, `BesselI`,
  `BesselK`, Airy functions, logarithms, roots, trigonometric functions near
  zeros and poles, `LambertW`, `acos`, `erfInv`, `Hypergeometric2F1`, `Gamma`,
  `Beta`, Fresnel integrals, and complex elementary functions have improved
  numeric accuracy, including at high precision.

- **Negative logarithms and complex logarithms are consistent.** Inexact
  negative arguments now produce the principal complex value under both
  `evaluate()` and `N()`. Exact negative arguments stay symbolic under
  `evaluate()` and produce the principal complex value under `N()`. Logarithms
  with a complex argument and explicit base now agree between `evaluate()` and
  `N()`.

- **Roots and radicals are more reliable.** Exact perfect powers such as
  $64^{1/3}$ and $(27/8)^{1/3}$ evaluate exactly, `Root(64, 3).N()` returns
  exactly $4$, odd roots of negative numbers keep the real-root convention, and
  `N(\sqrt{4y})` now returns $2\sqrt{y}$ instead of dropping the radical.

- **Sums and infinite sums behave better.** Exact sums such as
  $\sum_{k=1}^{5}\sqrt{k}$ now remain exact, while sums over infinite index sets
  such as $\sum_{n \in \mathbb{Z}^+}\frac{1}{n^2}$ evaluate numerically when
  appropriate, remain symbolic when parameters prevent evaluation, and respect
  `timeLimit`.

### Differentiation, Integration, and Simplification

- **Partial derivatives of multivariate functions now work symbolically.**
  `D(f(x, y), x)` now represents the partial derivative with respect to the
  first argument, mixed partials accumulate correctly, and multivariate chain,
  product, power, and quotient rules compose as expected. For example,
  `D(f(x^2, y), x)` returns a symbolic chain-rule result proportional to $2x$.

- **Partial-derivative notation parses and evaluates.** Forms such as
  $\partial_x f(x,y)$, $\frac{\partial}{\partial x} f(x,y)$,
  $\frac{\partial^2}{\partial x \partial y} f(x,y)$, and
  $\frac{\partial^2}{\partial x^2} f(x,y)$ now parse to `D` and evaluate
  correctly.

- **Derivative notation is more robust.** Compact derivatives such as
  `d/dx(f(g(x)))` preserve unknown-function chain rules, higher-order
  derivatives round-trip through LaTeX, and $\frac{d}{dx}[\sin x]$ treats the
  square brackets as grouping rather than a one-element list.

- **Several derivative rules are corrected.** Variable-degree radicals such as
  `Root(x, x)` differentiate as $x^{1/x}$, $\frac{d}{dx}\operatorname{Mod}(x,5)$
  gives $1$ almost everywhere, and $D(\operatorname{arcoth}(x), x)$ returns
  $\frac{1}{1-x^2}$.

- **Definite integrals no longer return fabricated closed forms.** If no
  closed-form antiderivative is found, `evaluate()` keeps the definite integral
  symbolic instead of substituting bounds into the integrand. `N()` still
  computes a numeric value.

- **Default simplification covers more identities.** The sine addition identity
  $\sin(x)\cos(y)+\cos(x)\sin(y)=\sin(x+y)$ now applies in the default
  `simplify()` path. Pythagorean identities such as $\sin^2 x+\cos^2 x$ also
  simplify inside larger sums.

- **Simplification is more exact and branch-aware.** Combining powers keeps
  exact exponents, for example $x \cdot x^{\sqrt2}$ becomes $x^{1+\sqrt2}$. The
  simplification of $\ln(x^2)$ now produces $2\ln(|x|)$ for real $x$, and
  identities that require real arguments no longer apply to symbols declared as
  complex.

- **Some unsafe rewrites were removed.** `simplify()` no longer rewrites
  $|\sin x|$ as $\sin|x|$, `Arctan2` preserves the correct quadrant, rule
  conditions such as $x \ne 0$ require proof rather than assuming unknown
  symbols satisfy them, and alternating-binomial sum simplifications now check
  their validity bounds.

- **Differential equation solvers handle higher-order equations.** (contributed
  by [KingArth0r](https://github.com/KingArth0r)) `DSolve` now solves **linear
  constant-coefficient homogeneous** equations of any order via the
  characteristic polynomial — distinct real, repeated, and complex roots — for
  example `y''(x) = y(x)` → `[y(x) = c_1·e^x + c_2·e^{-x}]` and
  `y''(x) + y(x) = 0` → `[y(x) = c_1·cos(x) + c_2·sin(x)]`. Roots are kept exact
  when the characteristic polynomial factors and fall back to numeric roots
  otherwise. It also solves **second-order constant-coefficient nonhomogeneous**
  equations (undetermined coefficients for polynomial forcing, variation of
  parameters otherwise) and **second-order Cauchy–Euler** equations. Integration
  constants are now named `c_1`, `c_2`, … (fresh names are chosen if those are
  already in use). Correspondingly, `NDSolve` now solves explicit
  **higher-order** initial value problems `y⁽ⁿ⁾(x) = f(x, y, y', …, y⁽ⁿ⁻¹⁾)` by
  reducing them to a first-order RK4 system, with the initial condition given as
  a list `[y(x0), y'(x0), …]`. Equations outside these classes remain inert.

### Parsing and Serialization

- **`\binom` is supported.** `\binom{n}{k}`, `\dbinom{n}{k}`, and
  `\tbinom{n}{k}` parse to `Binomial(n, k)`, and `Binomial` serializes back to
  `\binom`.

- **LaTeX parsing accepts more common notation.** `N(...)` and `D(...)` parse as
  numeric evaluation and differentiation outside quantifier scopes, superscripts
  on `\log`, `\ln`, `\lg`, and `\exp` bind to the applied function, and `==`,
  `!=`, chained `\ne`, mixed-direction inequality chains, parenthesized
  relations, and double negation such as `x--y` now parse with the expected
  meaning.

- **Lenient parsing is more useful.** In lenient mode, digit suffixes parse as
  subscripts (`x2` as $x_2$), bare known function names apply to the following
  factor (`sin x`), `log2(8)` means $\log_2 8$, `[1,...,10]` parses as a range,
  and $\mathbb{Z}^+$ parses as `PositiveIntegers`.

- **The public string helpers accept their documented syntax.** `simplify()`,
  `evaluate()`, `N()`, `expand()`, `expandAll()`, `factor()`, `solve()`, and
  `compile()` now parse string input in non-strict mode, so expressions such as
  `sqrt(5)`, `sin(alpha)`, and `x**2` work as documented.

- **`verify()` and `assume()` accept strings.** `ce.verify('x > 0')` and
  `ce.assume('$x > 0$')` parse the predicate and report clear errors for
  unparseable input.

- **MathJSON `.json` serialization is lossless for more numbers.** Exact large
  integers, 16- and 17-digit values, high-precision complex numbers, exact
  rational/radical values such as $\frac{\sqrt3}{2}$, and repeating decimals now
  round-trip without silently changing value.

- **LaTeX round-trips are improved.** Repeating decimals serialize with an
  overline, sequence expressions no longer serialize as ambiguous adjacent
  numbers, set-builder notation attaches conditions to the comprehension, and
  `toMathJson({exclude: ...})` honors exclusions for number literals.

### Assumptions, Types, and Equality

- **The numeric type hierarchy is more natural.** `real` is now a subtype of
  `complex`, so real-typed symbols satisfy complex-typed signatures and guards.
  Union types flatten and canonicalize their member order, and type negation now
  distinguishes `never` from `nothing`.

- **Complex and non-finite type inference is more precise.** Expressions such as
  $\sqrt2 i$, $i^2$, $i/2$, $i^3$, $e^i$, and $\ln(-1)$ infer more accurate
  types. Non-finite values such as $\tan(\frac\pi2)$, $\Gamma(0)$, $\zeta(1)$,
  $\ln(0)$, $0 \cdot \infty$, and $k/0$ no longer claim finite types when that
  is unsound.

- **Assumptions can prove more facts.** Assumptions over signed integer and real
  sets refine both type and sign. Inequality bounds now affect equality checks,
  comparisons between bounded symbols, and `verify()`. Chained inequalities and
  equations with multiple roots are recorded correctly, contradictory
  assumptions are rejected atomically, and `forget()` clears values introduced
  by `assume('x = 5')` while preserving values set with `assign()`.

- **Assumptions respect scope.** Assumptions made inside a pushed scope no
  longer leak into parent scopes or continue to affect expression results after
  `popScope()`.

- **Equality and ordering are more coherent.** `isSame` is now an equivalence
  relation, `isEqual` returns `undefined` for indeterminate equality with free
  variables, equality and ordering share one tolerance, collection equality uses
  scalar tolerance semantics, and complex numbers are no longer ordered against
  real numbers.

- **Set, collection, and statistics behavior is corrected.** `Intersection`,
  `SymmetricDifference`, `Union`, and `SetMinus` produce correct finite-set
  results, `Reverse([1,2,3])` returns `[3,2,1]`, `Quartiles` consistently uses
  the Moore-McCabe convention, and single-argument
  $\operatorname{KroneckerDelta}(0)$ returns $1$.

### Compilation

- **Compiled JavaScript, Python, GLSL, WGSL, and interval output now match the
  interpreter more closely.** Equality uses the engine tolerance, `Mod` and
  `Remainder` use consistent conventions, chained relations evaluate middle
  operands once, dynamic $0^0$ returns `NaN`, non-boolean `Which` and `When`
  conditions throw, and interval arithmetic matches interpreter conventions for
  branches, rounding, modulus, and odd roots of negative numbers.

- **Compilation fails closed when a target cannot represent an expression
  correctly.** Unsupported or unsafe cases such as invalid shader constructs,
  reserved shader variable names, non-real values in real-only target helpers,
  multi-index sums or products that a target cannot express, and invalid
  constant folds now fail at compile time instead of emitting wrong code.

- **The Python target emits valid Python for more expressions.** Conditional
  expressions, `NaN`, logical operators, chained relations, assigned symbols,
  `vars`, and target options now compile consistently with the JavaScript target
  and the interpreter.

### Additional Resolved Issues

- **Evaluation limits are honored more reliably.** Hard limits such as
  nested-exponential limits, divergent infinite sums, and very large special
  function inputs now return promptly, remain symbolic, or throw a
  `CancellationError` when `timeLimit` or the recursion limit is exceeded.
  Examples include $\lim_{x\to\infty} e^{e^{e^x}}/e^{e^{e^{x-1}}}$,
  $\Gamma(10^{300})$, $\zeta(\pm 10^{300})$, $\operatorname{Fib}(10^9)$,
  $\binom{2 \times 10^9}{10^9}$, and $\operatorname{Subfactorial}(10^6)$.

- **`simplify()` honors more of its public contract.** `simplify({rules: null})`
  now applies no rewrite rules, as documented, and logarithmic simplifications
  such as $\ln(a)/\ln(b)$ no longer reduce to an integer unless the identity can
  be verified exactly. `simplify()` also preserves exact exponents when
  combining powers, so $x \cdot x^{\sqrt2}$ becomes $x^{1+\sqrt2}$ rather than a
  decimal exponent.

- **Numeric comparison and formatting edge cases are fixed.** Two large 15-digit
  values that previously compared in the wrong order now compare correctly,
  `toPrecision(15)` no longer corrupts `999999999999999`, NaN has a
  deterministic place in canonical ordering, and high-precision `toString()`,
  `.json`, and `toFixed()` avoid long stalls on enormous exponents.

- **Substitution and collection operations are more complete.** `subs()` now
  reaches into lists and tensors, for example `Median([a,b,c]).subs({a: 1})`.
  Finite set operations such as `Intersection({1,2}, {2})` and
  `SymmetricDifference` now evaluate correctly, and `Reverse([1,2,3])` returns
  `[3,2,1]` instead of throwing.

- **Strict and non-strict validation are more predictable.** In strict mode,
  user-declared function signatures are enforced for closed arguments, numeric
  operators reject provably non-numeric operands such as `Sin("hello")`, big-op
  bounds are type-checked, and `Map([1,2,3], "nf")` is rejected. In non-strict
  mode, missing required arguments such as `Sqrt()` or `Power(2)` no longer
  crash.

- **Rule replacement is safer.** Rule guards such as $x \ne 0$ must now be
  provable before they match, wildcard conditions such as `:notzero` no longer
  assume unknowns satisfy the condition, and failed sequence-wildcard matches no
  longer drop operands from the expression being transformed.

- **Special values and combinatorics are corrected.** `Choose` and `Binomial`
  now share standard conventions, including `Choose(2,3) = 0` and negative upper
  indices such as `Binomial(-2,3) = -4`. `Argument(1+i)` evaluates to $\pi/4$,
  several `Digamma` special values simplify when the Fungrim pack is loaded, and
  integer-domain functions such as `Fibonacci(+Infinity)` and
  `MoebiusMu(Infinity)` stay symbolic instead of throwing.

- **Modular arithmetic is consistent.** `Mod` is floored everywhere, so
  `Mod(-7, 3)` returns $2$, while `Remainder` uses round-to-nearest semantics.
  Exact rational inputs stay exact, for example `Mod(\frac12, \frac13)` returns
  $\frac16$.

- **Complex and matrix products no longer lose meaning.** Multiplying a scalar
  by a complex literal such as `["Complex", 1, 1]` preserves both real and
  imaginary parts, and symbolic matrix products preserve their written order, so
  a commutator such as $MP - PM$ no longer collapses to $0$ for declared matrix
  symbols.

- **Parsing rejects or preserves ambiguous forms more reliably.** `x^2^3` is now
  a parse error instead of an unintended list power, `Sequence(1,2)` no longer
  serializes as `1 2`, parenthesized relations are treated as atomic operands
  inside larger chains, and a scalar or matrix next to a function- or
  matrix-valued symbol is parsed as multiplication rather than a tuple.

- **`0^0` and non-finite values are consistent across paths.** `evaluate()`,
  `N()`, and compiled JavaScript now agree that $0^0$ is `NaN`. Trigonometric
  poles such as `N(\cot \pi)` and `N(\csc \pi)` now return complex infinity
  rather than huge finite artifacts.

### Performance

- **LaTeX parsing is 15-28% faster.** Parsing is faster on derivative,
  polynomial, matrix, and definite-integral inputs, with the same parse results
  as before.

- **Arbitrary-precision arithmetic is substantially faster.** At 100 significant
  digits, addition, subtraction, multiplication, division, and comparison are
  now much faster than in 0.66.0, and high-precision `ln`, `exp`, `Gamma`, and
  related operations also benefit. The improvements are visible in both direct
  numeric work and symbolic operations that depend on arbitrary-precision
  arithmetic.

  **Arbitrary-precision arithmetic at 100 significant digits** (ns per
  operation, lower is better; warm median, distinct operands per call):

  | op     | **CE 0.67.0** | CE 0.66.0 |  math.js¹ | Mathematica² |
  | ------ | ------------: | --------: | --------: | -----------: |
  | `add`  |        **75** |       152 |       278 |        1,023 |
  | `sub`  |        **91** |       167 |       329 |        1,212 |
  | `mul`  |       **202** |       319 |     7,984 |        1,025 |
  | `div`  |       **501** |     1,748 |    11,890 |        1,366 |
  | `cmp`  |        **29** |       198 |        61 |          984 |
  | `sqrt` |         3,163 |     4,018 |    54,696 |    **1,055** |
  | `exp`  |         4,795 |     8,698 |   728,876 |    **1,682** |
  | `ln`   |         5,887 |    32,398 |   670,206 |    **1,353** |
  | `cos`  |         6,914 |     7,467 | 1,666,292 |    **2,059** |

  <small>¹ math.js `BigNumber` (decimal.js) at precision 100. ² Mathematica 14.3
  timed inside the kernel with result caches disabled; its ~1 µs per-call
  dispatch floor dominates its small-op rows. CE and 0.66.0 from
  `benchmarks/big-decimal/ops-results.json`; reproduce with
  `node benchmarks/big-decimal/run-ops.mjs` and
  `wolframscript -file benchmarks/big-decimal/ops-bench.wls`.</small>

  **Symbolic operations** (ms per call, lower is better; warm median, from the
  cross-library suite in `benchmarks/REPORT.md`):

  | case                    | **CE 0.67.0** | CE 0.66.0 | math.js |   SymPy | Mathematica |
  | ----------------------- | ------------: | --------: | ------: | ------: | ----------: |
  | simplify `√(3+2√2)`     |      **0.07** |      0.09 | 🟡 0.92 | 🟡 3.56 |        3.28 |
  | simplify `√6·x + √2·x`  |      **0.16** |      0.19 |    1.13 |    5.69 |        18.0 |
  | simplify `(x²−1)/(x−1)` |      **0.10** |      0.15 | 🟡 0.99 |    8.53 |        0.17 |
  | `d/dx √(1−x²)`          |          0.22 |      0.21 |    2.13 |    5.70 |   **0.008** |
  | `d/dx xˣ`               |          0.04 |      0.04 |    1.83 |    1.80 |   **0.005** |
  | `∫ x eˣ dx`             |      **0.08** |      0.09 |       — |    6.53 |        0.57 |
  | `∫ x/(x²+1) dx`         |      **0.16** |      0.18 |       — |    7.23 |        0.60 |
  | `lim sin(x)/x`          |      **0.03** |      0.04 |       — |    0.62 |        1.93 |
  | `lim (1+1/x)ˣ`          |      **0.55** |      1.13 |       — |    2.76 |        5.81 |
  | solve `x⁴+x²−1 = 0`     |          1.88 |      4.65 |       — |    8.56 |    **0.55** |
  | solve `x³−x−1 = 0`      |      **0.11** |      1.18 |       — |    5.73 |        0.23 |

  <small>🟡 = value-correct but not fully simplified. — = not supported. SymPy
  1.14 via `sympify`/`evalf` (per-call parse included, as for every string-based
  tool). All engines measured warm, per-call from source, same protocol
  (`benchmarks/REPORT.md`, "Methodology").</small>

- **Integration, assumptions, polynomial solving, and factoring are faster.**
  Rubi-backed integration spends less time on integrals it cannot solve,
  sign-related assumption queries respond faster, polynomial equations solve
  faster, and `Factor` handles common square-pattern cases more efficiently.

## 0.66.0 _2026-06-28_

### New Features

- **`Multiply` now operates on vectors and matrices.** Previously a product with
  any list/matrix operand was left unevaluated — even `2 * [1, 2, 3]`.
  `Multiply` (i.e. `*`, `\cdot`, `\times`, and implicit products) now follows
  matrix-product / scalar-scaling semantics, matching `Add`'s existing
  element-wise threading:

  - **Scalar × tensor** scales every element: `2 * [1, 2, 3]` → `[2, 4, 6]`,
    `2 * \begin{pmatrix}1&2\\3&4\end{pmatrix}` →
    `\begin{pmatrix}2&4\\6&8\end{pmatrix}` (exact values are preserved, e.g.
    `\frac12 [2, 4, 6]` → `[1, 2, 3]`).
  - **Two or more matrices/vectors** form the **matrix product**, folded
    left-to-right in the written order:
    `\begin{pmatrix}1&2\\3&4\end{pmatrix}\begin{pmatrix}5&6\\7&8\end{pmatrix}` →
    `\begin{pmatrix}19&22\\43&50\end{pmatrix}`. The product is **not**
    commutative — operand order is preserved (including for `matrix·vector` vs
    `vector·matrix`), and `vector·vector` reduces to the dot product. This
    reuses the existing `MatrixMultiply` implementation.

  Element-wise (Hadamard) multiplication of two same-shape tensors is therefore
  **not** what `*` does; tensors of incompatible dimensions are left
  unevaluated, and symbolic operands of unknown shape are unaffected.

- **Hadamard (element-wise) product `\odot`.** A new `HadamardProduct` operator,
  written `\odot`, multiplies two vectors or matrices of the same shape entry by
  entry: `[1,2,3] \odot [4,5,6]` → `[4,10,18]` and
  `\begin{pmatrix}1&2\\3&4\end{pmatrix} \odot \begin{pmatrix}5&6\\7&8\end{pmatrix}`
  → `\begin{pmatrix}5&12\\21&32\end{pmatrix}` (compare the matrix product `*`,
  which gives `\begin{pmatrix}19&22\\43&50\end{pmatrix}`). Operands of
  incompatible shape report an `incompatible-dimensions` error. It binds like
  multiplication and round-trips through LaTeX as `\odot`.

### Resolved Issues

- **Mixed chained inequalities keep their middle term.** A chain combining
  different operators — e.g. `5 \le b \lt 7` — canonicalized to
  `And(5 \le 7, b \lt 7)`, dropping `b` from the first link (so `3 \le 2 \lt 7`
  wrongly evaluated to `True`). It now canonicalizes to `And(5 \le b, b \lt 7)`.
  Uniform chains (`5 \le b \le 7`) and the already-correct `a \lt b \le c` form
  are unchanged.

- **A transcendental of an exact _constant expression_ stays symbolic.** Per the
  exactness contract, `evaluate()` of a transcendental of an exact argument
  returns a symbolic result and only `.N()` numericizes. This held for number
  literals (`sin(2)` → `sin(2)`) but not for exact constant _expressions_:
  `sin(\pi^2)` numericized to `-0.4303…` instead of staying `sin(π²)` (and
  likewise `cos(√2)`, etc.). These now stay symbolic under `evaluate()`; an
  inexact (float) argument such as `sin(2.5)` still numericizes.

- **An exact real added to the imaginary unit keeps its exact real part.**
  `\frac12 + i` evaluated to `0.5 + i`, and `\frac34\sqrt3 + i` to `1.299… + i`
  — the exact real part was floatified when folded with `i`. Exact reals
  (rationals, radicals) are now preserved alongside the imaginary unit
  (`1/2 + i`, `3/4·√3 + i`); `.N()` still numericizes, and inexact reals
  (`1.5 + i`) are unchanged.

- **Matrix/vector arithmetic preserves exact entries.** A tensor with exact
  rational or radical entries was stored with a `float64` element type, so
  element-wise operations silently produced floats — e.g.
  `\begin{pmatrix}½&⅓\end{pmatrix} + \begin{pmatrix}½&⅓\end{pmatrix}` returned
  `[1, 0.666…]` instead of `[1, ⅔]`, and a matrix of `√2` entries decayed to
  decimals. Exact entries now use the `expression` element type and stay exact;
  inexact (machine/decimal) values continue to use `float64`.

- **`A^n` is now the matrix power for an integer exponent.** A power of a matrix
  was element-wise for non-negative exponents (`A^2` squared each entry, `A^0`
  gave a matrix of ones) yet `A^{-1}` already returned the inverse, and
  `\begin{pmatrix}…\end{pmatrix}^2` did not evaluate at all. `A^n` is now the
  matrix power — repeated matrix multiplication — consistent with `*` being the
  matrix product: `A^2 = A·A`, `A^0` is the identity, `A^{-1}` the inverse, and
  `A^{-n} = (A^n)^{-1}`. A non-square base reports `expected-square-matrix`.
  (Also fixes `MatrixPower(A, n)` for `n < -1`, which previously collapsed to
  `A^{-1}`.)

- **Element-wise functions now distribute over matrix/vector-valued
  sub-expressions.** A broadcastable unary function applied to an operand that
  only becomes a collection _after_ evaluation — e.g. `\sqrt{AB}`, `\sin(AB)`,
  `|AB|` where `AB` is a matrix product — was left unevaluated, because
  broadcasting was decided from the raw (un-evaluated) operand. It now also
  broadcasts over the evaluated operand, so these distribute element-wise like
  `\sqrt{M}` on a literal matrix already did. (`Add`/`Multiply` keep their
  dedicated tensor handling.)

- **Juxtaposed matrices now form the matrix product.** Writing two matrices next
  to each other (`\begin{pmatrix}…\end{pmatrix}\begin{pmatrix}…\end{pmatrix}`),
  or a scalar next to a matrix (`2\begin{pmatrix}…\end{pmatrix}`), previously
  produced a `Tuple` instead of a product, because the `Matrix(…)` wrapper is
  not reported as an indexed collection. The invisible (implicit) operator now
  treats matrix operands as multiplication, consistent with
  `*`/`\cdot`/`\times`.

- **`Negate` (and hence `Subtract`) of a matrix-valued product is distributed
  correctly.** A negation whose operand only became a vector/matrix after
  evaluation — e.g. `Negate(Multiply(A, B))` from `A B - A B` — was left
  undistributed, so the following `Add`/`Subtract` misclassified it as a scalar
  and broadcast it over the other matrix, yielding a bogus higher-rank result.
  Matrix subtraction (e.g. the commutator `AB - BA`) now evaluates correctly.

- **A `\textcolor` wrapping a bare operator now parses as that operator.** Input
  such as `x \textcolor{red}{=} y` previously failed — the `=` could not be
  parsed as a standalone group, producing a `Tuple` around an
  `expected-closing-delimiter` error. The color command is now transparent in
  operator position, so `x \textcolor{red}{=} y` parses as `Equal(x, y)` (and
  likewise for `+`, `<`, `\le`, `\times`, …). Because MathJSON has no way to
  annotate a lone operator glyph, the operator's color is dropped; coloring an
  operand (`\textcolor{red}{y}`, `\textcolor{red}{x+1}`) is unchanged and still
  yields an `Annotated`.

- **One-sided `\left( … \right.` enclosures now parse.** `\right.` (and the
  `\bigr.`/`\Bigr.`/… variants) is a TeX _null delimiter_: a fence with no
  visible closing glyph. Previously a one-sided group such as
  `\sin\left(x\right.` was rejected, leaking the `\left` out as an
  `unexpected-command` error; it now parses the same as `\sin\left(x\right)` (→
  `Sin(x)`). The null _open_ form (`\left.…\right|`, used by `EvaluateAt`) and
  ordinary two-sided delimiters are unchanged.

- **Summation/product indices written as a `\le` range are now recognized.** An
  index set of the form `\sum_{1 \le i \le 10} i^2` (and the one-sided
  `\sum_{i \le 10}`) is now turned into the expected `Limits`, so the index `i`
  is bound by the sum instead of falling through to the imaginary unit. The
  example above now evaluates to `385` rather than staying symbolic with
  `i → Complex(0, 1)`. This mirrors the existing handling of `i \ge 1` and
  `i = 1`; strict `<` chains are not yet treated as index sets.

## 0.65.0 _2026-06-28_

### New Features

- **Differential equation solvers.** (contributed by
  [KingArth0r](https://github.com/KingArth0r)) Two new functions in the calculus
  library provide an initial slice of ordinary differential equation (ODE)
  support:

  - **`DSolve(eq, y, x)`** — symbolic solver for **first-order linear scalar**
    equations of the form `y'(x) + p(x)·y(x) = q(x)`. It returns a `List` of
    solutions, each an `Equal` expression for `y(x)`, introducing an integration
    constant `C` (a fresh name is chosen if `C` is already in use). For example,
    `DSolve(y'(x) = y(x), y, x)` → `[y(x) = C·e^x]` and
    `DSolve(y'(x) + y(x) = x, y, x)` → `[y(x) = x - 1 + C·e^{-x}]`. Nonlinear or
    higher-order equations are left unevaluated (inert).

  - **`NDSolve(eq, y, limits, y0, steps?)`** — numerical solver for **explicit
    scalar first-order** initial value problems `y'(x) = f(x, y)`, `y(x0) = y0`,
    using a fixed-step fourth-order Runge–Kutta (RK4) method. It returns a
    `List` of `[x, y]` sample pairs over the interval given by `limits` (a
    `Limits` or `Tuple` of `(x, x0, x1)`); the number of steps defaults to 100.
    It handles integrands with no elementary antiderivative (e.g. a Gaussian IVP
    whose solution is expressed with `Erf`).

  This slice is intentionally narrow so the API and result shape can get
  feedback before broader ODE support (adaptive RK45, systems, higher-order
  reductions, stiff and implicit solvers) is added.

- **`\keyword{…}` command for control-flow and logic keywords.** Keyword
  constructs — `if`/`then`/`else`, `for`/`from`/`to`/`do`, `where`, `such that`,
  `and`, `or`, `iff`, `for all`, `there exists`, `break`, `continue`, `return` —
  can now be written with a dedicated `\keyword{…}` command, for example:

  ```latex
  \keyword{if} x > 0 \keyword{then} 1 \keyword{else} 0
  ```

  Unlike `\text{…}`, `\keyword{…}` keeps the input in math mode, and unlike
  `\operatorname{…}` it is rendered with symmetric keyword spacing. The existing
  `\text{…}` and `\operatorname{…}` spellings continue to work, and all three
  parse to the same expression. Multi-word keywords are written as a single
  token (e.g. `\keyword{for all}`). `\keyword{otherwise}` / `\keyword{else}`
  also serve as the default-branch marker inside a `cases` environment.

  A new `keywordStyle` serialization option — `"text"` (default), `"keyword"`,
  or `"operatorname"` — selects which spelling is emitted when serializing `If`,
  `Loop`, `Break`, `Continue`, and `Return` back to LaTeX. The default preserves
  the previous `\text{…}` output.

## 0.64.0 _2026-06-27_

### New Features

- **Expanded number-theory library.** A set of standard number-theoretic
  functions has been added to the `number-theory` library. Integer arguments use
  arbitrary-precision (bigint) arithmetic, and long-running cases honor the
  evaluation deadline.

  _Factorization & divisors:_

  - **`FactorInteger(n)`** — prime factorization as a list of
    `[prime, exponent]` tuples ordered by ascending prime: `FactorInteger(360)`
    → `[(2, 3), (3, 2), (5, 1)]`. Following Mathematica's conventions,
    `FactorInteger(0)` → `[(0, 1)]`, `FactorInteger(1)` → `[(1, 1)]`, and a
    negative integer carries its sign in a leading `[-1, 1]` tuple.
  - **`PrimeFactors(n)`** — the sorted distinct prime factors:
    `PrimeFactors(360)` → `[2, 3, 5]`.
  - **`Divisors(n)`** — the sorted positive divisors: `Divisors(12)` →
    `[1, 2, 3, 4, 6, 12]`. `Divisors(0)` is left unevaluated.
  - **`Radical(n)`** — the square-free kernel (product of distinct primes):
    `Radical(360)` → `30`.
  - **`PrimeNu(n)`** / **`PrimeOmega(n)`** — the number of prime factors without
    / with multiplicity (ω and Ω).
  - **`MoebiusMu(n)`** — the Möbius function μ(n).
  - **`DivisorSigma(k, n)`** — the divisor function σ_k(n) (generalizes the
    existing `Sigma0`/`Sigma1`).
  - **`IsSquareFree(n)`** — whether `n` is square-free.
  - **`IsPerfectPower(n)`** — whether `n = a^b` for integers `a`, `b ≥ 2`.

  _Primes:_

  - **`NthPrime(n)`** — the nth prime (1-based): `NthPrime(10)` → 29.
    (Mathematica names this `Prime`, but in the Compute Engine `Prime` denotes
    derivative notation, so the prime-number function is `NthPrime`.)
  - **`NextPrime(n)`** / **`NextPrime(n, k)`** — the smallest prime greater than
    `n`; with `k`, the kth prime after `n` (or the |k|th before it when
    `k < 0`).
  - **`PrimePi(n)`** — the prime-counting function π(n): `PrimePi(10)` → 4.
  - **`RandomPrime(n)`** / **`RandomPrime(m, n)`** — a random prime in the
    range.

    Primality for these uses exact 6k±1 trial division for small `n` and
    switches to Miller–Rabin above 2³² (deterministic for the supported range),
    so `NextPrime` and `RandomPrime` are fast even for very large arguments.

  _Modular arithmetic & GCD:_

  - **`PowerMod(a, b, m)`** — modular exponentiation `a^b mod m`; a negative `b`
    uses the modular inverse (undefined when `a` and `m` are not coprime).
  - **`ExtendedGCD(a, b)`** — the GCD with Bézout coefficients, as `(g, x, y)`.
  - **`ChineseRemainder(residues, moduli)`** — solves a system of simultaneous
    congruences (moduli need not be coprime).
  - **`MultiplicativeOrder(a, n)`** — the order of `a` modulo `n`;
    **`PrimitiveRoot(n)`** — the smallest primitive root mod `n`.
  - **`JacobiSymbol(a, n)`** / **`LegendreSymbol(a, p)`** — the Jacobi and
    Legendre symbols.

  _Other primitives:_

  - **`IntegerSqrt(n)`** — the integer (floor) square root.
  - **`CarmichaelLambda(n)`** — the reduced totient λ(n).
  - **`LucasL(n)`** — the nth Lucas number; **`CatalanNumber(n)`** — the nth
    Catalan number.
  - **`BernoulliB(n)`** — the nth Bernoulli number as an exact rational, with
    the convention B₁ = -1/2.
  - **`ContinuedFraction(x, n?)`** / **`FromContinuedFraction(list)`** — the
    continued-fraction expansion of a number (exact for rationals) and its
    inverse.
  - **`IntegerDigits(n, base?, length?)`** / **`FromDigits(list, base?)`** — the
    digits of `n` in a given base, and its inverse.
    **`DigitCount(n, base?, digit?)`** — digit-occurrence counts;
    **`DigitSum(n, base?)`** — the digit sum.

- **`IsPrime` is now reliable for large integers.** Primality was previously
  left unevaluated above ~10¹⁵ and could silently round integers beyond 2⁵³ to a
  wrong machine value. `IsPrime` (and `IsComposite`) now route through a single
  deterministic Miller–Rabin implementation shared with the number-theory
  library, so e.g. `IsPrime(2^61 - 1)` correctly returns `True`. (The previous
  duplicate Miller–Rabin code, which used random bases and overflowed for large
  inputs, has been removed.) Relatedly, the internal `toInteger` helper now
  returns `null` instead of a precision-lost value for integers beyond the
  safe-integer range, so this class of silent-rounding bug cannot recur in the
  operators that use it for counts and indices.

- **`Factorial2`, `Subfactorial`, and `BellNumber` no longer round a non-integer
  argument.** These are defined only on integers; in non-strict mode they
  previously rounded a non-integer (e.g. `Factorial2(5.5)` returned `6!!`). They
  now stay symbolic for non-integer arguments. (In strict mode the `(integer)`
  signature already rejected such inputs.)

- **`N(expr, precision)` evaluates to a requested number of significant
  digits.** The `N` function (and the `["N", expr]` MathJSON form) now accepts
  an optional precision argument: `["N", "Pi", 50]` returns π to 50 significant
  digits. When the requested precision exceeds the engine's working precision,
  the working precision is raised to match — and kept, since display precision
  is a global setting. When it is at or below the working precision, the result
  is rounded to that many significant digits without changing the global
  precision (`N(1/3, 4)` → `0.3333`).

- **New linear-algebra operators.**
  - **`Dot(a, b)`** — vector inner product / matrix product (Mathematica's `.`):
    `Dot([1,2,3], [4,5,6])` → `32`.
  - **`Cross(a, b)`** — cross product of two 3-vectors.
  - **`MatrixRank(m)`** — the rank (number of linearly independent rows/columns)
    via the rank–nullity theorem.
  - **`MatrixPower(m, n)`** — a square matrix raised to an integer power (the
    repeated matrix product `A·A·…`, with negative powers using the inverse).
    Distinct from `["Power", m, n]`, which threads element-wise.
  - **`CharacteristicPolynomial(m, x?)`** — the monic characteristic polynomial
    `det(x·I − A)` (variable defaults to `x`): `[[1,2],[3,4]]` → `x² − 5x − 2`.
  - **`RowReduce(m)`** — the reduced row echelon form (RREF) of a matrix.
  - **`IsSymmetric(m)`** / **`IsDiagonal(m)`** / **`IsSquareMatrix(m)`** —
    matrix-shape predicates returning `True`/`False`.

### Resolved Issues

- **`["N", expr]` now numerically evaluates its operand.** The `N` operator
  holds its argument unevaluated and previously called `.N()` on the still
  unbound operand — a no-op for symbolic constants — so `["N", "Pi"]` returned
  `Pi` unchanged (and `["N", ["Sqrt", 2]]` returned `Sqrt(2)`) instead of a
  numeric value. The operand is now bound before evaluation, making
  `["N", expr]` equivalent to `expr.N()`.

## 0.63.0 _2026-06-26_

### New Features

- **LaTeX parse errors carry their source location.** (contributed by
  [zojize](https://github.com/zojize)) The `Error` expressions produced by the
  LaTeX parser now include a `sourceOffsets: [start, end]` character range
  identifying where in the input the error occurred, so a consumer can map a
  parse error back to the offending span — e.g. to highlight an invalid token in
  a mathfield. Offsets are zero-based and end-exclusive into the serialized
  LaTeX (`tokensToString`); for input that round-trips through the tokenizer
  unchanged — editor-generated LaTeX, with no comments, Unicode normalization,
  or macro expansion — they match the original input string. Missing-operand
  errors (an empty `\sqrt{}` or `\frac{}{}`) use a zero-width range at the
  position where the token was expected. The new
  `Parser.sourceOffsets(startToken, endToken?)` helper lets custom dictionary
  entries attach a range to errors they raise. The raw parser output
  (`LatexSyntax().parse()`) always carries these offsets, so an `Error` node is
  now emitted in object form (`{ fn: ["Error", …], sourceOffsets }`) rather than
  the bare `["Error", …]` array whenever a range is available — a consumer
  matching `expr[0] === "Error"` should also handle `expr.fn?.[0] === "Error"`.
  Through the boxed path (`ce.parse(latex).toMathJson()`), source offsets are
  opt-in metadata like `latex` and `wikidata`: included with
  `metadata: ['sourceOffsets']` or `metadata: 'all'`, and omitted from the
  default serialization.

- **Long numerators over a single power serialize with an inline solidus.** When
  prettifying, a large numerator divided by a single power of a small base now
  serializes as `(3x^4+2x^3+x+5)/x^{23}` instead of the tall, lopsided fraction
  `\frac{3x^4+2x^3+x+5}{x^{23}}`. This rounds out the existing prettify
  heuristics, which already factor a small denominator out of a large numerator
  (`\frac{1}{x}(…)`) and write a small numerator over a large denominator with a
  negative exponent (`(a)(…)^{-1}`). The new form applies when the numerator is
  large and the denominator is a single power of a small base — `base^{k}` with
  an integer exponent `k ≥ 2` (`/x^{23}`), a square (`/x^2`), or a square root
  (`/\sqrt{x}`). Lone powers (`\frac{1}{x^{23}}`), products in the denominator
  (`a·x^n`), compound bases (`(x+1)^{23}`), and all other shapes are unchanged.
  As with the other rewrites, it is disabled by `prettify: false`.

- **Double-quoted string literals in LaTeX.** `"hello"` now parses to a string
  (previously `"` was an `unexpected-token`). Content is read verbatim up to the
  closing quote, with LaTeX commands normalized to Unicode like `\text{…}`
  (`"\alpha"` → `α`); there is no escaping (use `\text{…}` for a string that
  must contain a `"`). Strings still serialize back to `\text{…}`. A `"` inside
  `\unicode{…}`/`\char` remains a hex prefix and is unaffected.

- **Dictionary values can be read by key with `At`.** `["At", dict, "key"]`
  (string key) now returns the value of that entry in a dictionary — e.g.
  `["At", { dict: { height: 42 } }, "height"]` → `42`. A missing key yields
  `Nothing`. Previously `At` was restricted to _indexed_ (positional)
  collections and rejected dictionaries with an `incompatible-type` error; its
  value type is now `indexed_collection | dictionary`. In LaTeX, the postfix
  bracket form accepts a string key, so `\mathrm{data}["height"]` (or
  `\mathrm{data}[\text{height}]`) parses to `["At", "data", "height"]`.
  Dot-notation also works when the base is a symbol declared as a dictionary:
  `\mathrm{data}.height` → `["At", "data", "height"]` (the key is an alphabetic,
  space-free name; for a dictionary base, `.x` / `.real` are key lookups, not
  `First` / `Real` component access). Positional indexing of indexed collections
  is unchanged.

- **`BoxedExpression.referencedFunctions` and `BoxedExpression.references`.**
  Two accessors aimed at dependency graphs (e.g. notebooks). The operator head
  of a function application — the `f` in `f(x)` or `g(x) := f(x) + 1` — is not a
  symbol of the expression, so it appears in neither `symbols` nor
  `freeVariables`; `referencedFunctions` recovers those applied user-function
  names (excluding built-in operators, constants, and names bound by an
  enclosing scope, using the same predicate `freeVariables` applies to ordinary
  symbols). `references` is the complete in-edge set — `freeVariables` ∪
  `referencedFunctions`, minus `defines` — so it pairs with `defines` (the
  out-edges) to build a use/def graph in one call. Subtracting `defines` drops
  self-references, so a recursive `g(x) := g(x - 1)` reports no dependency on
  itself.

- **`ce.declare()` refines an auto-declared binding instead of throwing.**
  Parsing auto-declares the names it encounters (a free variable `a` in `a + 1`,
  a called function `f` in `f(x)`), recording an _inferred_ binding. Calling
  `ce.declare(name, …)` for such a name now refines that inferred binding rather
  than throwing `"… already declared in this scope"` — which is exactly what the
  `inferred` flag is for. This lets a declare-first workflow parse cells to
  discover names and then declare them on the **same** engine. Re-declaring an
  _explicit_ binding still throws, and a name bound to a value (e.g. a function
  argument) is still a genuine conflict.

### Resolved Issues

- **`canonical` and `structural` options are now honored by `parse()`, `expr()`,
  and `function()`.** These methods only consulted the `form` option when
  deciding how to box their result, so the documented `canonical` / `structural`
  shortcuts were silently ignored: `ce.parse(latex, { canonical: false })`
  returned a _canonical_ expression (and, as a side effect of canonicalization,
  auto-declared its symbols), and
  `ce.function('Power', ops, { structural: true })` returned canonical `Root`
  instead of a structural `Power`. The keys now resolve the same way `form`
  does, with an explicit `form` taking precedence. As part of this,
  `ce.assume()` now canonicalizes its predicate so the assumption machinery
  always sees a normalized form (e.g. `Negate(ImaginaryUnit)` folded to the
  complex literal `-i`) regardless of how the caller boxed it.

## 0.62.1 _2026-06-22_

### New Features

- **`indexStyle` serialization option for collection indexing.** The `At`
  operator (e.g. `["At", v, 1]`) can now be serialized either as a subscript
  (`v_1`, `M_{i,j}`) or with programming-style brackets (`v[1]`, `M[i,j]`). Like
  the other style options (`fractionStyle`, `rootStyle`, …) it is a callback
  `(expr, level) => 'subscript' | 'bracket'`, settable engine-wide via
  `ce.latexOptions.indexStyle` or per-call via `expr.toLatex({ indexStyle })`.
  The default is `'subscript'`.

### Resolved Issues

- **Collection indexing (`At`) now serializes to valid, round-tripping LaTeX.**
  `["At", v, 1]` previously serialized to `\lbrack v, 1\rbrack` — i.e. the
  _list_ `[v, 1]`, which re-parsed as `["List", v, 1]`, silently changing the
  meaning on a serialize→parse cycle. It now serializes as `v_1` (or `v[1]` with
  `indexStyle: 'bracket'`), both of which parse back to `At`.

- **Accents and decorations serialize with brace notation and round-trip.**
  `OverHat`, `OverVector`, `OverTilde`, `OverBar`, `UnderBar`, the over-arrows,
  `OverBrace`, etc. had no serializer and fell back to function-call notation —
  `\hat{x}` came back out as `\hat(x)`, which re-parsed to
  `["Multiply", x, ["OverHat"]]` instead of `["OverHat", x]`. They now serialize
  as `\hat{x}`, `\vec{v}`, `\overline{x}`, … and round-trip correctly, including
  when subscripted (`\hat{x}_0`).

- **Subscripted single-letter symbols serialize with an italic base instead of
  an upright one.** When a symbol name carried a subscript (e.g. `a_1`, `x_n`,
  `S_t`), the serializer chose its font style from the _decorated_ string rather
  than the base: the subscript inflated the token count, so the multi-character
  rule wrapped the whole thing in `\mathrm{…}` and rendered the base letter
  upright (`\mathrm{a_1}`). A single-letter variable with a subscript is now
  rendered italic, as a variable should be — `a_1` serializes to `a_1`, not
  `\mathrm{a_1}`. The font style is now decided from the base alone:
  multi-letter bases are still upright with the wrapper enclosing the whole
  symbol, so descriptive subscripts stay roman
  (`speed_max → \mathrm{speed_{max}}`), and explicit style modifiers (`\mathbf`,
  `\mathbb`, …) are unchanged. Greek single-letter bases are likewise rendered
  with their default (italic) style.

## 0.62.0 _2026-06-20_

### Resolved Issues

- **Arbitrary-precision sums of three or more terms no longer collapse to
  machine precision.** `BigNumericValue.add` had a fast path that, when adding
  to a zero value, cloned the other operand through a constructor that reads its
  **machine** real part (`decimal.toNumber()`), silently truncating a
  full-precision bignum to ~16 significant digits. The exact (rational/radical)
  arithmetic path was unaffected, and two-term sums were unaffected, so this
  only surfaced when summing **three or more inexact values** at a precision
  above machine: `ExactNumericValue.sum` folds those starting from a zero
  accumulator, and the very first `0 + xᵢ` step lost all extra precision. The
  degradation was invisible when the terms were of similar magnitude (the result
  was merely capped at ~16 digits), but became a wrong answer under cancellation
  — e.g. numerically evaluating a high-order symbolic derivative at a point
  (large factorial-scale terms cancelling to a small value) returned garbage at
  any working precision. The zero-accumulator path now reads the full-precision
  real part, matching the non-zero path. Coefficients were always computed
  exactly; only the final numeric summation was affected.

- **High-order derivatives are reduced instead of blowing up.** The `Derivative`
  operator applies the differentiation rules iteratively, and the quotient and
  product rules square the denominator at each step, so the r-th derivative of a
  quotient carried an `x^(2ʳ)`-scale denominator — e.g. the 75th derivative of
  `sin(x)/x` came back over `x^(2⁷⁵)`. The result was mathematically exact (the
  integer coefficients are computed exactly), but the enormous exponent made it
  unusable and overflowed to `NaN` when evaluated at a point. `Derivative` of
  order ≥ 2 now runs a single simplification at the end, cancelling the common
  factors back to a linear-degree denominator (`x^(2⁷⁵) → x⁷⁶`). It is applied
  once, not per step, so it is cheap (~30 ms at order 75) and leaves first
  derivatives and the existing low-order results unchanged.

- **`interval-glsl` is now outward-rounded, making it a sound standalone
  exclusion oracle in `float32` (preview).** As shipped in 0.61.0 the `_iv_*`
  ops clamped to the sentinel range but rounded to nearest, so an operation — or
  the cell box itself — could come back slightly **narrower** than the true
  range. At a boundary that is enough to flip the exclusion verdict for a box
  the curve only grazes (e.g. the unit circle's tangent corner at `(1, 0)`),
  violating the containment contract that the GLSL interval must _contain_ the
  `interval-js` (float64) result — a spuriously narrow interval can exclude a
  box the curve actually passes through. Every inexact operation now widens its
  result outward (`lo` toward −∞, `hi` toward +∞) before the clamp: by ~1 ulp
  for the correctly-rounded ops (`+ − ×`, `Square`), and by a larger relative
  margin for the GLSL ES built-ins that are not correctly rounded — 8 ulp for
  `/`, `Sqrt`, `Exp`/`Ln`/`Log`, and inverse trigonometry, and 32 ulp for
  `Power` (`x^n` with `n ≥ 3`, and fractional powers such as the astroid
  `x^{2/3}`). Crucially, the cell box that `compileExclusionShader`'s `main()`
  builds is itself outward-rounded (via the new `_iv_widen_box`): the float32
  `mix` that constructs it rounds to nearest and is the actual source of the
  grazing miss, which per-op widening alone cannot fix (with exact endpoints the
  op chain is exact). That box pad is scaled to the **domain extent**, not the
  edge value, since that is what bounds the `mix` error — a value-relative pad
  would vanish for a box edge near 0 in a wide domain. Widening only ever moves
  a bound outward, so it cannot break soundness; the `empty` (`lo > hi`) /
  `entire` (`±IV_INF`) encodings, the finite `IV_INF` sentinel, the per-op
  clamp, and exact empty-propagation are all preserved. `Sin`/`Cos` remain
  best-effort (see below).

- **`freeVariables` / `unknowns` no longer report the bound variables of
  `Function` literals and integrals.** A function literal leaked its own
  parameters, and `Integrate` / `Limit` leaked their variable — e.g.
  `freeVariables` of `f(x) := x^2 + b` wrongly included the parameter `x`, and a
  definite integral leaked its integration variable. They now return only
  genuinely free symbols (`[b, f]` for that definition, `[]` for `∫ sin(x) dx`),
  while a free coefficient is still reported (`∫ a·sin(x) dx → [a]`). `Sum` /
  `Product` were already correct, and `symbols` is unchanged (it still includes
  bound variables). This is a behavior change for code that relied on the
  previous, over-inclusive result.

- **Runaway user-function recursion now throws a catchable `CancellationError`
  instead of a native `RangeError`.** A recursive definition with no reachable
  base case (e.g. `f(x) := f(x-1) + 1`) previously overflowed the JavaScript
  call stack with an uninformative `RangeError`. `recursionLimit` — previously
  defined but never enforced — is now applied to user-function application:
  exceeding it throws a `CancellationError` with
  `cause: 'recursion-depth-exceeded'`, consistent with how `timeLimit` and
  `iterationLimit` are surfaced. The default `recursionLimit` is now **256**
  (was a nominal, unenforced 1024), chosen to fire below the native stack limit
  on typical engines; raise `ce.recursionLimit` for legitimately deep recursion.
  Iterating a user function (e.g. `\sum f(i)`) is **not** counted as recursion.
  (A sufficiently complex single call can still exceed the native stack before
  the limit is reached, so a robust caller catches `RangeError` as a backstop.)

- **`Integrate` binds only the integration variable in its canonical
  integrand.** `∫ a·sin(x) dx` previously canonicalized to
  `Integrate(Function(body, a, x), …)`, listing the free coefficient `a` as a
  spurious integrand parameter; it is now `Integrate(Function(body, x), …)`.
  Introspecting the integrand (`expr.op1`) therefore reports `a` as free, and
  the integrand is a proper single-variable function. Evaluation is unchanged.

- **Nested (multivariate) integrals now parse and evaluate correctly.**
  `\int_1^2\int_3^4 x y \, dx \, dy` previously attached _all_ the trailing
  differentials to the innermost integral, leaving the outer integrals with a
  `Nothing` integration variable — so the expression could not evaluate. Each
  `\int` now consumes only its own differential (the innermost `dx` pairs with
  the innermost `\int`, the next `dy` with the next), producing a properly
  nested `Integrate` where every level carries its own variable and limits
  (`\iint` / `\iiint` still bind 2 / 3 variables at one level). Combined with
  the definite-integral evaluator now applying the limits to a _parametric_
  antiderivative (e.g. `∫_3^4 k·x dx → 7/2·k`; the symbolic `f(b) - f(a)` was
  previously left as an unevaluated `EvaluateAt`), nested definite integrals
  evaluate to a value: `∫_1^2∫_3^4 x·y dx dy → 21/4`.

- **Multiple-integral and contour-integral serialization round-trips.** `\iint`
  / `\iiint` (and `\oiint` / `\oiiint`) now serialize back to the compact sign
  with a single region subscript (`\iint_{D}\!…`) instead of a stack of `\int`s,
  so a flat multiple integral round-trips to the same structure. A separate
  long-standing bug that emitted the literal text `\ointundefined` for any
  `\oint` with a region (its limit is a 3-element `Tuple`, serialized to
  MathJSON as `Triple`, which the serializer did not recognize) is also fixed:
  `\oint_V f(s)\,ds` now serializes as `\oint_{V}\!f(s)\, \mathrm{d}s`.

- **`1^x` simplifies to `1` for any finite exponent.** A symbolic or function
  exponent (e.g. `1^{n+1}`, `1^{\sin x}`) previously left `Power(1, x)`
  un-reduced because the canonicalizer bailed before its base-1 rule. `1^x → 1`
  now (matching SymPy / Mathematica); only a genuinely infinite or NaN exponent
  stays indeterminate (`1^∞ → NaN`, unchanged).

### New Features

- **`interval-glsl`: public outward-rounding helpers and an opt-in absolute trig
  pad (preview).** The widen helpers `_iv_widen` / `_iv_widen_t` /
  `_iv_widen_pow` / `_iv_widen_sc` / `_iv_widen_box`, and their epsilons
  `IV_EPS` / `IV_EPS_FN` / `IV_EPS_POW` / `IV_BOX_EPS`, are a stable, public
  part of the emitted preamble: a renderer that builds its own cell box (instead
  of using `compileExclusionShader`) outward-rounds it by calling
  `_iv_widen_box(vec2(lo, hi), extent)` per axis, where `extent` is the domain
  extent for that axis (the box pad is domain-scaled, not value-relative). The
  preamble is now emitted for any expression with free variables (not only ones
  that reference an `_iv_*` op), so those helpers are always available — e.g.
  for an axis line `f = x`. GLSL ES `Sin`/`Cos` carry an _absolute_,
  implementation-defined error (≈2⁻¹¹ in the worst case; macOS ANGLE→Metal
  differs) that no relative pad can cover. A new `trigAbsPad` option (default
  `0`, off) on `compile()`, `IntervalGLSLTarget.compileExclusionShader()`, and
  the new `IntervalGLSLTarget.getPreamble()` adds an absolute `Sin`/`Cos` pad,
  so a trigonometric implicit curve can be a strictly-sound standalone oracle at
  the cost of fatter trig intervals.

- **`BoxedExpression.defines`.** A new accessor returning the symbols an
  expression _defines_: the target of a top-level `Assign` / `Declare` (`a` in
  `a := 3`, `f` in `f(x) := …`), recursing through `Block`. It complements
  `freeVariables` (the symbols an expression _references_) — together they let
  tooling build a definition/use dependency graph, with
  `references = freeVariables` minus `defines`.

- **`ComputeEngine.appliedNonFunctions(latex)`.** Returns the symbols written in
  function-application syntax `f(…)` in `latex` that are **not** functions in
  the current scope, and so parse as implicit multiplication (`f·x`) or are left
  unresolved. The check is scope-aware (a symbol declared as a function is not
  reported) and has no side effects. Useful for flagging a likely call to an
  undefined function — e.g. warning that `f(x)` was read as `f·x`.

## 0.61.0 _2026-06-17_

### New Features

- **`interval-glsl` compilation target (preview).** A GPU compilation target
  that evaluates an expression with **interval arithmetic** in GLSL — each value
  is a `vec2 (lo, hi)` — so a robust implicit-curve renderer can run its
  per-cell exclusion test (`lo > 0 || hi < 0`) on the GPU instead of CPU-side
  via `interval-js`. (Reinstates the `interval-glsl` target removed in 0.52,
  with a simpler `vec2`-only representation — the GPU acts as an exclusion
  oracle and the CPU keeps curve extraction — instead of the former status-flag
  struct.) `compile(expr, { to: 'interval-glsl' })` emits `_iv_*` helper calls
  plus a preamble library. Coverage: arithmetic, integer and positive rational
  powers, `Abs`, `Sqrt`, `Exp`, `Ln`/`Log`/`Lb`, trigonometry / inverse
  trigonometry (`Sin`, `Cos`, `Tan`, `Arcsin`, `Arccos`, `Arctan`, with interval
  range reduction), and the step / rounding family (`Floor`, `Ceil`, `Round`,
  `Truncate`, `Fract`, `Sign`, `Heaviside`, `Mod`, `Min`, `Max`) — covering
  polynomial, rational, algebraic, trigonometric, and lattice/periodic implicit
  curves (conics, lemniscate, astroid, superellipse, trig lattices, floor/mod
  grids, …). Jump-discontinuity functions return a tight, sound value-range
  enclosure (so cells can still be excluded), with discontinuity classification
  left to the CPU; only genuine poles widen to the full range. A head that is
  not yet supported (e.g. hyperbolic functions) is reported in the result's
  `unsupported` field, so a caller can fall back to another target
  per-expression. Values use a finite ±∞ sentinel and a `lo > hi` encoding for
  the empty (domain-undefined) interval, propagated through every operation;
  domain-restricted functions (`sqrt`/`ln`/`asin`/rational `pow` of an
  out-of-domain argument) yield `empty`, and a pole (zero-spanning denominator,
  `tan` asymptote) yields the full range. Parity with the `interval-js` target
  is verified against a shared corpus.
  `IntervalGLSLTarget.compileExclusionShader()` emits a complete, self-contained
  fragment shader (preamble + an `_implicit` interval evaluator + a reference
  `main` that derives each fragment's cell box and applies the exclusion test)
  ready to drop into a WebGL2 renderer.

### Resolved Issues

- **A function parameter now shadows a same-named constant.** A parameter named
  like a constant (`i`, `e`, `Pi`/`\pi`, …) was rewritten to the constant while
  the function body was canonicalized, so the binding was lost — `λi. 2i`
  applied to `5` returned `2i` (the imaginary unit doubled) instead of `10`.
  Parameters now shadow whatever their name means in the enclosing scope — a
  constant, an assigned variable, or nothing — which is standard lexical
  scoping. A free symbol that is _not_ a parameter is unchanged (`i` outside a
  parameter is still the imaginary unit), and closure capture is preserved
  (`λi. λz. (z + i)` captures `i` correctly).

- **`compile()` no longer emits a dangling reference to a symbol that has an
  assigned value (GLSL, WGSL, JavaScript, and interval-JS targets).** When an
  expression referenced a symbol with an assigned value in the engine
  (`ce.assign("a", 1.5)`), `compile()` emitted a bare `a` — an undeclared GLSL
  identifier (a shader that silently fails to compile) or a bare JS global (a
  `ReferenceError` when the compiled function is called) — even though the
  symbol is omitted from `expr.unknowns` and folded by `evaluate()`. The value
  is now folded into the generated code (`sin(a·x)` → `sin(1.5 * x)`), making
  `compile()`, `evaluate()`, and `unknowns` consistent. This also folds
  user-declared constants (`ce.declare("c", { value: 3 })`), and applies on the
  direct-target `compile(expr, { target })` path as well. A symbol supplied
  through the `compile()` `vars` option is never folded — the mapping always
  wins, so a per-frame GLSL uniform / JS argument keeps updating the result
  without recompiling — and a genuinely free symbol is unchanged.

- **`compile()` folds a _symbolic_ assigned value correctly, parenthesizing it
  and resolving the free symbols it references.** When a symbol was assigned an
  expression rather than a number (`ce.assign("b", ce.parse("c + 1"))`), folding
  `b` into a larger expression had two bugs: the compound value was spliced in
  without parentheses, so `b · x` compiled to `c + 1 * x` (i.e. `c + x`) instead
  of `(c + 1) * x` — a **silently wrong result** (`2·b` → `2 * c + 1`, `b²` →
  `(c + 1 * c + 1)`); and the inner free symbol `c`, hidden behind `b`'s value
  and therefore absent from `expr.unknowns`, was emitted as a bare global
  (`ReferenceError` on the JS target). The folded value is now parenthesized for
  its context, and a free symbol reachable only through a folded value routes
  through the normal free-symbol plumbing (`_.c` on the JS / interval-JS
  targets; a uniform on GPU) and is reported in the result's `freeSymbols`.

- **GPU compilation rejects non-finite numbers instead of emitting a
  non-compilable shader.** GLSL and WGSL have no infinity or NaN literals, but
  `compile()` emitted `Infinity.0` / `NaN.0` for a `±∞` or `NaN` value (e.g.
  from a literal `\infty` or a constant-folded `1/0`) and reported
  `success: true` — a shader that silently fails to compile on the GPU. Such
  values now throw a clear error from the GLSL/WGSL targets (so the free
  `compile()` falls back to `success: false` with a diagnostic), consistent with
  how other GPU-unsupported constructs are handled. The JavaScript target is
  unchanged (`Infinity` / `NaN` are valid there).

- **The JavaScript compilation target now lowers the exponential, trigonometric,
  and logarithmic integrals.** `SinIntegral` (Si), `CosIntegral` (Ci),
  `ExpIntegralEi` (Ei), and `LogIntegral` (li) compile to `_SYS` runtime
  helpers, matching the existing support for `Erf`, `FresnelS`, `Gamma`,
  `BesselJ`, etc. These are the closed forms the antiderivative engine emits
  (e.g. `∫ sin x / x dx = SinIntegral(x)`), so an "evaluate then compile"
  pipeline — such as plotting `∫ f dx` from its closed form — no longer throws
  `Unknown operator` and falls back to numeric sampling. (GLSL/WGSL shader
  approximations of these are not yet provided.)

- **The JavaScript compilation target now lowers the elliptic, AGM, and
  hypergeometric kernels.** `AGM`, `EllipticK`, `EllipticE`, `EllipticF`,
  `EllipticPi`, `Hypergeometric2F1`, `Hypergeometric1F1`, `Erfi`, and `Choose`
  compile to `_SYS` runtime helpers. Like the integral functions above, these
  are closed forms `evaluate()`/`.N()` produces (e.g. a pendulum period or an
  arc length reduces to an elliptic integral), so they can now be plotted from
  the closed form rather than re-sampled numerically. `EllipticE` and
  `EllipticPi` keep their arity-overloaded complete/incomplete forms, and `AGM`
  accepts the one-argument `AGM(z) = AGM(1, z)` shorthand. (Real-valued like the
  other special functions on this target; GLSL/WGSL not provided.)

### Improvements

- **`compile()` results now report their external references.** A
  `CompilationResult` carries two new fields so a caller can check that a result
  is self-contained _declaratively_, instead of executing or GPU-compiling the
  code to discover a dangling reference:

  - `freeSymbols` — the identifiers the generated code references that the
    caller must supply at run time (JS vars-object keys / GLSL uniforms). These
    are the free symbols _as codegen sees them_: assigned values and constants
    are folded out, bound variables (lambda parameters, `Sum`/`Product`/
    `Integrate`/`Loop` indices, `Block` locals) are excluded, and `vars`-mapped
    symbols are always included. Unlike `expr.unknowns`, it also surfaces a free
    symbol reachable only through a folded value (e.g. `b` assigned `c + 1`
    exposes `c`). Use it to build a uniforms / vars mapping that is guaranteed
    consistent with the emitted code.
  - `unsupported` — operator heads the target cannot lower (no operator/function
    mapping, not a structural form). On a failed `compile()` this is populated
    alongside a human-readable `error`, so an unlowerable operator (e.g.
    `SinIntegral` on the GLSL target) surfaces as `success: false` with a
    machine-readable list rather than only a thrown exception.

  Built-in targets populate `freeSymbols` (and an empty `unsupported`) on every
  successful compile. The direct `getCompilationTarget(name).compile(expr)` path
  still throws on a genuinely unsupported operator (so the engine-level
  `compile()` can fall back to interpretation); the `unsupported` / `error`
  fields are how the engine-level `compile()` reports that condition without a
  throw.

## 0.60.0 _2026-06-16_

### Behavior Changes

- **`isFinite` is now known for finite symbolic constants.** Expressions such as
  `√π`, `1/π`, and `π^π` report `expr.isFinite === true` (previously
  `undefined`), because finiteness is propagated through `Sqrt`, `Root`,
  `Power`, and `Divide` of finite operands. Cases that are genuinely
  indeterminate (e.g. `1/x` for an unconstrained `x`) still report `undefined`.

- **Exact transcendental expressions now remain symbolic under `evaluate()`.**
  For example, `ln(2)` remains `ln(2)` instead of becoming `0.693…`. Use `.N()`
  or `{ numericApproximation: true }` when a numeric approximation is wanted.
  Inexact inputs still evaluate numerically, and known exact values such as
  `cos(π) = -1` and `arctan(1) = π/4` still simplify. As a result, definite
  integrals also preserve exact results, such as `∫₁² 1/x dx = ln(2)` and
  `∫₀¹ 1/(1+x²) dx = π/4`.

- **`(aⁿ)ᵐ` no longer folds to `aⁿᵐ` based solely on an odd inner exponent.**
  This combine was unsound on the principal branch: when `a < 0` and `m` is not
  an integer, the two sides differ by a phase. For example `(x³)^{1/2}` now
  stays `√(x³)` (which is `8i` at `x = -4`) instead of becoming the inequivalent
  `x^{3/2}` (`-8i`), and it is again confluent with the `√(x³)` form. The fold
  still applies when the base is non-negative or the outer exponent is an
  integer. (Roots are unaffected: `(x³)^{1/3} = x` still holds, since odd-index
  roots use the real-root convention.)

- **Logarithms are no longer combined across a branch cut.**
  `ln(a) + ln(b) → ln(ab)` (and the `log` and subtraction variants) is only
  valid on the principal branch; for arguments on the negative real axis the two
  sides differ by a multiple of `2πi`. For example `ln(-2) + ln(-3)` no longer
  simplifies to the inequivalent `ln(6)` (its true value is `ln(6) + 2πi`). The
  combine still applies to positive and unconstrained-symbolic arguments. The
  guard consults the analytic-property store's branch-cut records (see Special
  Functions).

- **`e^{iθ}` stays in exponential form under `evaluate()` for a symbolic
  angle.** Euler's formula `e^{iθ} → cos θ + i·sin θ` is now applied only when
  `θ` is a constant that reduces to a closed form (`e^{iπ/2} = i`,
  `e^{iπ} = -1`, `e^{ln y} = y` are unchanged); for a symbolic angle, `e^{ix}`
  stays `e^{ix}` — a basis change is not an evaluation, and it no longer differs
  from the previous inconsistency where `(e^{ix})²` expanded while `e^{ix}` did
  not. Convert to trigonometric form on demand with the new strategy
  `expr.simplify({ strategy: 'trig' })`.

- **`N()` at a known pole now returns `ComplexInfinity` instead of `NaN`.** When
  a function is evaluated numerically at a pole recorded in the new
  analytic-property metadata store (see Special Functions), the result is
  `ComplexInfinity` rather than `NaN` or an unevaluated expression — for example
  `Digamma(0).N()` and `Digamma(-2).N()`. Functions whose kernels already
  returned an infinity at their poles (such as `Gamma`) are unchanged.

### Benchmarks

The numeric and symbolic gains in this release are summarized below against the
last release (`0.59.0`), SymPy, math.js, and **Mathematica** — the reference
baseline, since it is the broadest engine in the field. The tables are generated
by the harness in [`benchmarks/`](./benchmarks/)
(`node benchmarks/report_changelog.mjs`); every result is verified numerically
against an independent `mpmath` reference, never another tool. "CE 0.60.0" is
this release.

#### Numeric performance (200-digit precision)

Median time per call, in **microseconds — lower is better**. `—` means the tool
returned no usable result at that precision.

| Expression         | CE 0.60.0 | CE 0.59.0 | SymPy | math.js | Mathematica |
| ------------------ | --------: | --------: | ----: | ------: | ----------: |
| $\pi^2$            |        15 |        20 |   174 |     107 |         3.9 |
| $\sin 1$           |        25 |        61 |   220 |     429 |         5.2 |
| $\cos 1$           |        24 |        60 |   222 |     455 |         7.1 |
| $\ln 2$            |        87 |       302 |   339 |   4,374 |         3.7 |
| $e^{\pi}$          |        31 |       398 |   214 |   4,771 |         4.6 |
| $\zeta(3)$         |     3,419 |         — |   264 |       — |          49 |
| $\Gamma(\tfrac13)$ |     1,867 |   427,938 |   341 |       — |         212 |
| $\psi(\tfrac13)$   |     1,689 |   404,300 | 2,831 |       — |         169 |

Biggest gains over `0.59.0`: $\psi(\tfrac13)$ **239× faster**,
$\Gamma(\tfrac13)$ **229× faster**, $e^{\pi}$ **13× faster** (it no longer
recomputes $\ln e$ on every call), $\ln 2$ **3.5× faster**, $\sin 1$ / $\cos 1$
**~2.5× faster**. The elementary functions widen further at 1000+ digits (e.g.
$\ln 2$ ≈ 21× faster, where it now also leads SymPy and mpmath). `0.59.0` could
not reach 200 digits for $\zeta(3)$ (it was capped near machine precision);
math.js has no arbitrary-precision ζ/Γ/ψ. Mathematica's native bignum kernel is
faster still on these constants.

#### Symbolic capability & performance

Each cell is **how many times faster than Mathematica** that engine is on the
case (`Mathematica ÷ engine`, so **higher is better**; Mathematica itself is
`1×`). `—` means the engine can't do the case. Compare the **CE 0.60.0** and
**CE 0.59.0** columns to see what is _new this release_ (a `—` under `0.59.0`
next to a number under **CE 0.60.0**). The **CE + R/F** column is CE 0.60.0 with
the opt-in Rubi integrator and Fungrim identities loaded (`loadIntegrationRules`
/ `loadIdentities`), on the same minified bundle: sometimes it improves
performance, sometimes it hurts it, but the overall effect is improved coverage.

| Operation                              | CE 0.60.0 | CE + R/F | CE 0.59.0 | SymPy  | math.js | Mathematica |
| -------------------------------------- | :-------: | :------: | :-------: | :----: | :-----: | :---------: |
| **Antiderivatives**                    |           |          |           |        |         |             |
| $\int\frac{1}{\sqrt x}\,dx$            |   1.5×    |   3.7×   |     —     |  0.5×  |    —    |     1×      |
| $\int\frac{x}{\sqrt{1-x^2}}\,dx$       |   2.5×    |   2.6×   |     —     | 0.09×  |    —    |     1×      |
| $\int\frac{1}{x^3+1}\,dx$              |   2.2×    |   11×    |     —     |  0.3×  |    —    |     1×      |
| $\int\frac{\sqrt x}{1+x}\,dx$          |     —     |   3.7×   |     —     |  0.1×  |    —    |     1×      |
| $\int\frac{x}{(1+x)^{1/3}}\,dx$        |     —     |   3.9×   |     —     | 0.01×  |    —    |     1×      |
| $\int\frac{x^2}{(1+x)^{1/3}}\,dx$      |     —     |   4.1×   |     —     | 0.007× |    —    |     1×      |
| **Derivatives**                        |           |          |           |        |         |             |
| $\tfrac{d}{dx}\sqrt{1-x^2}$            |   0.01×   |  0.03×   |   0.01×   | 0.001× | 0.004×  |     1×      |
| **Simplification**                     |           |          |           |        |         |             |
| $\sqrt{3+2\sqrt2}$                     |    11×    |   20×    |     —     |   —    |    —    |     1×      |
| $\sqrt6\,x+\sqrt2\,x$                  |    28×    |   65×    |    30×    |  3.3×  |   18×   |     1×      |
| **Evaluation**                         |           |          |           |        |         |             |
| $\lim_{x\to0}\tfrac{\sin x}{x}$        |   9.2×    |   23×    |     —     |  3.1×  |    —    |     1×      |
| $\lim_{x\to\infty}(1+\tfrac1x)^x$      |   1.6×    |   1.6×   |     —     |  2.1×  |    —    |     1×      |
| $\int_1^2\tfrac1x\,dx$                 |   1996×   |  1907×   |     —     |  92×   |    —    |     1×      |
| $\int_{-\infty}^{\infty} e^{-x^2}\,dx$ |   106×    |   428×   |     —     |  2.5×  |    —    |     1×      |
| **Solving**                            |           |          |           |        |         |             |
| $x^4+x^2-1=0$                          |   0.07×   |  0.08×   |     —     | 0.06×  |    —    |     1×      |
| $x^3-x-1=0$                            |   0.08×   |   0.1×   |     —     | 0.04×  |    —    |     1×      |

Across the cases both solve, Compute Engine is a **median 3.7× faster than
Mathematica** (up to 1996×). The `—` entries under `0.59.0` show what is new
this release: limits, exact definite/improper integrals, and polynomial solving.
The bottom three antiderivative rows are integrals the base engine still leaves
unevaluated but the opt-in **Rubi** rules solve. Mathematica still leads on raw
derivative and root-finding latency (the `<1×` rows), where its native kernel is
hard to beat.

<sub>Measured 2026-06-16 · SymPy 1.14.0 · math.js 15.2.0 · Mathematica 14.3.0 ·
Node 22 · verified against `mpmath`. Reproduce:
`npm run build production && ./venv/bin/python3 benchmarks/gen_cases.py && node benchmarks/report.mjs && node benchmarks/report_changelog.mjs`.</sub>

### Calculus

- **`Limit` can now return exact symbolic results.** This includes direct
  substitution, indeterminate quotients, rational functions at infinity,
  dominant-term analysis, and exponential forms. Examples include
  `lim(x→0) sin(x)/x = 1`, `lim(x→∞) (1+1/x)^x = e`, and
  `lim(x→∞) arctan(x) = π/2`. Limits that cannot be determined reliably fall
  back to numeric evaluation or remain unevaluated. `NLimit` remains numeric.

- **Limits no longer return a wrong value at a special-function pole.** A limit
  whose expression contains a special function (`Gamma`, `Digamma`, `PolyGamma`,
  `Zeta`, …) evaluated at one of its poles — e.g. `lim(x→-1) (x+1)·Digamma(x)` —
  previously substituted the pole as a finite value and returned a confident
  wrong result (`0`). Such limits now stay unevaluated (or are recovered
  numerically where sampling allows) rather than reporting a false value.

- **Symbolic integration supports many more integrands**, including:
  - Gaussian integrals and quadratic exponentials using `Erf` and `Erfi`
  - Fresnel integrals
  - Sine, cosine, exponential, and logarithmic integrals
  - Products of polynomials, exponentials, and trigonometric functions
  - More radical and quadratic-root integrands
  - Powers of secant, cosecant, tangent, and cotangent
  - Reverse power-chain forms such as `∫ln(x)/x dx = ½ln²(x)`
  - Products with symbolic exponents that previously failed or timed out
  - Powers and radicals of a linear function, e.g. `∫√(1+x) dx`, `∫x√(1+2x) dx`,
    and `∫(a+bx)^p dx`
  - Radical powers of a polynomial via the reverse chain rule, e.g.
    `∫x√(1−x²) dx = −⅓(1−x²)^{3/2}`
  - Quotients by a sum of two square roots, e.g. `∫1/(√(a+bx)+√(c+bx)) dx`, by
    conjugate rationalization
  - Absolute value of a linear argument, e.g. `∫|x| dx = x|x|/2` and
    `∫|ax+b| dx = (ax+b)|ax+b|/(2a)` (valid for all `x`)

- **Rational-function integration is more exact and complete.** Partial
  fractions now preserve rational and radical coefficients for a wider range of
  denominators, including `x³+1`, `x⁴+1`, `x⁴-1`, and biquadratic polynomials.
  Several cases that previously returned incomplete results, floating-point
  coefficients, or no result now return exact antiderivatives.

- **More improper integrals evaluate correctly.** Exact results now include
  Gaussian, rational, and Fresnel integrals over infinite intervals. Numeric
  integration of convergent oscillatory integrals is also more reliable, while
  divergent or low-confidence cases remain unevaluated instead of returning a
  misleading finite value.

- Fixed incorrect or missing antiderivatives for `sin²(ax+b)`, `cos²(ax+b)`,
  `√x`, `1/√x`, `1/√(1-x²)`, and related forms.

- **New `Residue(f, x, a)` operator** computes the residue of `f` at `x = a`
  (the coefficient of `(x-a)⁻¹` in its Laurent expansion). It detects the pole
  order and evaluates exactly via the symbolic limit engine, e.g.
  `Residue(1/(x²-1), x, 1) → 1/2`, `Residue(eˣ/(x-1)², x, 1) → e`, and
  `Residue(cot(x), x, 0) → 1`. Residues of `Gamma`, `Digamma`, and `Zeta` at
  their poles use closed forms gated by the analytic-property store, e.g.
  `Residue(Gamma(x), x, -2) → 1/2` and `Residue(Zeta(s), s, 1) → 1` — including
  in a product or quotient with an analytic cofactor, such as
  `Residue(Gamma(x)/(x-5), x, -2) → -1/14`.

### Algebra and Solving

- **`solve` handles equations between two different inverse-trigonometric
  functions** by applying `tan` to both sides to clear them, then solving the
  resulting algebraic equation. For example `arcsin(x) = arctan(x) → 0` and
  `arccos(x) = arctan(x) → √((√5−1)/2)`. As part of this, `√(f(x)) = g(x)` with
  a non-linear right-hand side now solves too (e.g. `√(1−x²) = x²`).

- **New `Solve` operator.** `Solve(equation, unknown)` returns the list of
  solutions of an equation for an unknown, using the same solver as the
  `expr.solve()` method — for example `["Solve", ["Equal", "x^2", 1], "x"]`
  returns `["List", 1, -1]`. The equation may be an `Equal` expression or a bare
  expression read as `= 0`; the arguments are held, so the equation is no longer
  prematurely reduced to a boolean.

- **`solve` now handles general cubic, quartic, and higher-degree polynomials.**
  Exact roots are still preferred; when no supported exact form is available,
  real roots are returned as numeric approximations.

- **Absolute-value equations solve more reliably.** This includes equations such
  as `|x| = 2`, `|x-1| = 2`, non-linear arguments such as `|x²-3| = 1`, and
  equations with an absolute value on both sides.

- **`solve` handles more transcendental and substitution equations.** Equations
  with equal exponential bases reduce by their exponents
  (`e^{2-x²} = e^{-x} → -1, 2`; `2^x = 2^3 → 3`); `a·sin(x) + b·cos(x) = 0`
  solves via the tangent (`sin x = cos x → π/4`); equations that are polynomials
  in a root of the unknown solve by substitution (`2√x + 3·⁴√x = 2 → 1/16`); and
  a single square root with a non-constant coefficient is eliminated by squaring
  (`x = 1/√(x²+1)`).

- **Biquadratic and sparse-power equations return exact roots.** Polynomials
  whose exponents share a common factor — such as `x⁴ + x² − 1` — are solved by
  substituting `u = x²` (or `x³`, …), so the roots are exact radicals
  (`±√((√5−1)/2)`) instead of numeric approximations.

- **`solve` handles equations that are polynomials in a single nonlinear
  generator**, by substituting `u = g(x)` for a logarithmic, exponential,
  trigonometric, or radical generator `g`, solving for `u`, and inverting. For
  example `(ln x)² = 4 → e², e⁻²`, `e^{2x} − 3eˣ + 2 = 0 → 0, ln 2`, and
  `√(ln x) = ln√x → 1, e⁴`.

- **`solve` factors a zero product.** When an equation is a product whose
  factors each involve the unknown — such as `ln(x)·(x − 1) = 0`, or an
  already-factored `(x + 1)·cos³(3x) = 0` — its roots are the union of the roots
  of each factor.

- **`GCD` now finds common polynomial factors** for univariate and multivariate
  polynomials. Integer operands retain their existing behavior; use
  `PolynomialGCD()` when an explicit polynomial result of `1` is needed for
  coprime inputs.

- **New `Resultant(a, b, x)` operator** computes the resultant of two
  polynomials with respect to a variable (the Sylvester-matrix determinant). It
  is zero exactly when the polynomials share a common factor, e.g.
  `Resultant(x² - 1, x - 1, x) → 0` and `Resultant(x² + 1, x² - 1, x) → 4`.
  Symbolic coefficients are supported: `Resultant(x² + a, x + b, x) → a + b²`.

- **Polynomial factorization is more complete and reliable.** In particular,
  `Factor(xⁿ-1)` now returns polynomial factors without introducing
  branch-dependent radicals, and the public `factor()` function once again
  factors expressions such as `x²+5x+6`.

- **Nested radicals are simplified when possible**, for example
  `√(3+2√2) = 1+√2`.

### Special Functions

- Added numeric evaluation for:
  - Complete and incomplete elliptic integrals: `EllipticK`, `EllipticE`,
    `EllipticF`, and `EllipticPi`
  - The arithmetic-geometric mean `AGM`
  - `Hypergeometric2F1`, `Hypergeometric1F1`, and `AppellF1`
  - Jacobi theta functions and the Dedekind eta function
  - `Erfi`, `SinIntegral`, `CosIntegral`, `ExpIntegralEi`, and `LogIntegral`

- **`Gamma` now accepts a second argument, the upper incomplete gamma function**
  `Γ(s, z) = ∫_z^∞ tˢ⁻¹ e⁻ᵗ dt` (e.g. `["Gamma", s, z]`). It is evaluated
  numerically for real and complex arguments, including negative and fractional
  orders `s` (`Gamma(-4, 2)`, `Gamma(1/2, -1)`), and honors the exactness
  contract: it stays symbolic under `evaluate()` and reduces `Γ(s, 0)` to the
  ordinary `Γ(s)`. Use `.N()` for a numeric value. The one-argument `Γ(z)` is
  unchanged.

- **`Hypergeometric2F1` now supports analytic continuation across most of the
  complex plane**, rather than being limited to its defining power series.

- **`Zeta` and `Gamma` now honor the requested precision.** At high
  `ce.precision`, numeric evaluation of `Zeta`, `Gamma`, `GammaLn`, `Beta`,
  `Digamma`, `Trigamma`, and `PolyGamma` previously stalled near machine
  precision (e.g. `Zeta(3)` was correct to only ~16 digits regardless of
  precision). They now return the full requested precision — `Zeta` uses the
  Cohen–Villegas–Zagier acceleration, and all of these kernels compute with
  guard digits.

- **`EulerGamma` (γ) now honors the requested precision.** It was previously a
  fixed ~858-digit constant, so at higher `ce.precision` it silently stopped at
  ~858 correct digits (making identities such as `Digamma(1) = -γ` appear wrong
  past that point). It is now computed on demand to the full working precision.

- **`Gamma` and the polygamma family are dramatically faster at high precision**
  (~340× at 300 digits — `Gamma(1/3)` ≈1.9 s → ≈5 ms; ~130× at 1000 digits). The
  Stirling-series kernels (`Gamma`, `GammaLn`, `Digamma`, `Trigamma`,
  `PolyGamma`) were both shifting their argument just short of where the series
  converges (running far more terms than needed) and letting intermediate
  products grow in size without bound; the shift, term count, and per-step
  rounding are now chosen so the series converges quickly with bounded-size
  arithmetic. Results are unchanged to full precision.

- The Identities Library has been updated from 1,350 to 1,376 verified rules,
  including corrected Jacobi theta identities.

- **Modular and theta-function identities now discharge under `Im(τ) > 0`.** The
  upper-half-plane condition guarding these identities is expressed as the part
  inequality `Im(τ) > 0`, so they apply once you `assume(Im(τ) > 0)` (previously
  an opaque `τ ∈ HH` set membership was required). A new LaTeX shorthand,
  `\mathbb{C}^+` (also `\C^+`), denotes the open upper half-plane:
  `z \in \mathbb{C}^+` canonicalizes to `Im(z) > 0`. As a side effect three
  further identities became available — the derivative of the modular j-function
  and the θ₁/θ₂ logarithmic derivatives — recovered because the inequality form
  is verifiable where the opaque set was not.

- **`EisensteinE(s, τ)` now evaluates numerically.** The normalized Eisenstein
  series of even weight `s ≥ 2` gets a numeric kernel (Lambert-series
  q-expansion in the upper half-plane), joining `JacobiTheta`/`DedekindEta`. For
  example `EisensteinE(4, i).N()` is `1.45576…`, `EisensteinE(2, i).N()` is
  `3/π`, and `EisensteinE(6, i).N()` is `0` (an elliptic fixed point). Exact
  arguments stay symbolic under `evaluate()`; the kernel requires `Im(τ) > 0`.

- **New analytic-property metadata store.** `ce.functionProperties(name)`
  exposes per-operator analytic properties drawn from the Fungrim corpus —
  poles, zeros, branch points and cuts, residues, and holomorphic/meromorphic
  domains. For example `ce.functionProperties('Gamma')?.poles` is the set
  `NonPositiveIntegers`. Convenience accessors (`poles`, `zeros`, `branchCuts`,
  `holomorphicDomain`, …) return the unconditional record of each kind;
  parametric records (such as residues that depend on parameters) are available
  via `entries`. This also powers pole-aware `N()` (see Behavior Changes).

### Numeric Evaluation

- **Arbitrary-precision elementary and transcendental functions are
  substantially faster**, especially at hundreds or thousands of digits.
  High-precision `π` and trigonometric functions are no longer limited to about
  2,350 digits. Square root is roughly twice as fast at 1,000+ digits (a
  giant-steps integer square root), the natural logarithm switches to the faster
  arithmetic–geometric-mean method from around 700 digits (previously ~1,250),
  and a power no longer recomputes the logarithm of its base on every call — at
  1,000 digits `Exp(x).N()` is about three times faster, and a repeated base
  such as `2^x` or `10^x` about 2.8 times faster. Results are unchanged.

- Odd roots of negative real numbers now use the real-root convention, so
  `Root(-8, 3)` and `(-8)^(1/3)` evaluate to `-2`.

- **`N()` of a non-unit rational power of a negative base no longer returns
  `NaN`.** Previously only unit fractions worked (they route through
  `Sqrt`/`Root`); `(-4)^{3/2}`, `(-8)^{2/3}`, and similar fell through to
  `Math.pow(negative, non-integer) = NaN`. They now follow the same branch
  conventions as the roots above: an even denominator takes the principal
  complex value (`(-4)^{3/2} = -8i`, consistent with `Sqrt(-4) = 2i`), and an
  odd denominator the real root (`(-8)^{2/3} = 4`, `(-8)^{5/3} = -32`,
  consistent with `(-8)^{1/3} = -2`).

- **Exact `evaluate()` of a non-unit rational power of a perfect power now
  reduces.** When `x^{p/q}` has a real base and its `q`-th root is an exact
  perfect power, it reduces to an exact value (`8^{2/3} = 4`, `27^{2/3} = 9`,
  `(-8)^{5/3} = -32`), extending the unit-fraction behavior (`8^{1/3} = 2`) to
  non-unit numerators and matching `N()`. Non-perfect powers (`2^{2/3}`) and the
  negative even-root branch (`(-4)^{3/2}`, complex) stay symbolic under
  `evaluate()`.

- `N()` now fully evaluates applied functions and constants such as `e`, `i`,
  and expressions in Euler form.

- Complex equality and arbitrary-precision complex square roots are more robust
  in the presence of small rounding errors.

### Collections and Matrices

- `Take`, `Drop`, `Slice`, and `Count` now operate on matrix rows consistently.
  For example, `Count(matrix)` returns the number of rows.

- `Join` now preserves list order, duplicates, and all elements when joining
  lists. Joining sets continues to produce a deduplicated set.

- Sums and products over ranges from `-∞` to a finite bound, or from `-∞` to
  `∞`, now iterate over an appropriate finite approximation instead of an empty
  range.

### Resolved Issues

- Significant performance boost when many boxed expressions are involved in
  computations, due to improved handling of configuration changes and listener
  management.

- **Long-running evaluation is interruptible.** Collection operations,
  number-theory functions, limits, differentiation, simplification, and
  integration now respect `ce.timeLimit` more consistently. Operations that
  cannot finish in time either throw `CancellationError` or return the best
  numeric estimate available, as appropriate.

- **Fractional powers and radicals now preserve the correct principal complex
  branch.** This fixes several unsafe transformations involving negative or
  unknown-sign values, including `x/√(x²)`, negative factors under roots,
  products and quotients raised to fractional powers, and `1/√u`.

- Infinity arithmetic is more reliable for finite symbolic denominators, while
  indeterminate forms such as `∞/∞` remain indeterminate.

- Numeric limits now reject overflow, catastrophic cancellation, oscillation,
  and other low-confidence results instead of returning spurious values.

- Fixed hangs and crashes when factoring certain sums, simplifying expressions
  with radical coefficients, or mixing non-finite rational values with
  arbitrary-precision integers.

- `ce.number()` now throws a helpful error when passed a MathJSON expression
  array; use `ce.expr()` for expressions.

- Fixed incorrect simplification or evaluation of `2^i`, division by a
  floating-point zero coefficient, and several exact expressions involving
  negative radicals.

- Fixed a rational function such as `1/(x(x²+x))` wrongly simplifying (and
  integrating) to `0` when its factored denominator contained factors sharing a
  common root. The partial-fraction solver now detects the inconsistent system
  instead of returning a spurious all-zero decomposition.

- `Factor` is more complete: it now extracts a common monomial factor (e.g.
  `x³+x² → x²(x+1)`, `3x⁴+2x³ → x³(3x+2)`) and fully factors already-factored
  products and powers, so partial-fraction decomposition sees irreducible
  factors with correct multiplicities.

- Partial-fraction decomposition now uses exact arbitrary-precision integer
  arithmetic, so decompositions of higher-degree denominators no longer lose
  precision (the previous machine-integer solver overflowed past 2⁵³ and could
  return wrong coefficients).

- Rational functions with **repeated** linear or irreducible-quadratic factors
  now integrate to a closed form via full partial-fraction decomposition — e.g.
  `∫1/(x²(x+1)) dx` and `∫1/(x(1+x²)²) dx`, which previously returned an
  unevaluated integral.

- **Nested powers serialize to LaTeX and round-trip correctly.** A `Power` whose
  base is itself a `Power` — i.e. `(aᵇ)ᶜ` — was serialized as `a^{bᶜ}`, which
  re-parses as `a^(bᶜ)`, a different expression. It now serializes as `{aᵇ}^ᶜ`,
  so e.g. `(x³)^{2/5}` round-trips instead of becoming `x^{3^{2/5}}`.

- **GLSL/WGSL compilation no longer declares `int`/`i32` for a `Block`'s local
  bindings.** An integer-valued local (e.g. `["Assign", "r", 3]`) was declared
  as `int r;` while its value was emitted as a float literal (`r = 3.0;`),
  producing non-compilable shader code that also poisoned downstream float
  arithmetic. Scalar locals are now declared as `float`/`f32` — consistent with
  the always-float number literals and scalar shader math — and an explicit
  `["Declare", "r", "complex"]` type is honored. Complex locals still declare as
  `vec2`/`vec2f`.

- **`Loop` now compiles to JavaScript that returns its collected values.** A
  value loop such as `Loop(i², Element(i, Range(1, 5)))` compiled to a
  `for`-loop IIFE with no `return`, so it evaluated to `undefined` at runtime
  instead of the `[1, 4, 9, 16, 25]` the interpreter produces. The compiled loop
  now collects each iteration's value and returns the array. Imperative loops
  that mutate an outer accumulator or use `Break`/`Continue`/`Return` are
  unchanged.

- **`Integrate` now compiles to JavaScript that returns a numeric estimate.**
  For the common `\int x^2 dx` parse shape (where the integrand is a `Function`
  expression), the integrand was wrapped in a double lambda
  (`(x) => ((x) => x*x)`), so the Monte-Carlo estimator never called the inner
  function and returned `NaN`; it now compiles to a single lambda and returns
  the estimate (e.g. `∫₀¹ x² dx ≈ 0.333`). Integration bounds are also no longer
  floored, so non-integer limits such as `∫₀^0.5` integrate over the correct
  interval.

## 0.59.0 _2026-06-10_

This is a significant update to the Compute Engine.

The headline feature of this release is a large collection of curated
mathematical identities, the Identities Library:

```js
// When the Identities Library is loaded, CE can prove that...
console.log(parse("\\arctan(2-\\sqrt{3})").simplify().latex);
// ➔ "\frac{\pi}{12}"

// Declare that n is a positive integer...
ce.declare("n", "integer");
ce.assume(parse("n > 0"));

// ...and the parity identity applies:
console.log(parse("\\sin(\\pi n + \\frac{\\pi}{2})").simplify().latex);
// ➔ "(-1)^n"
```

Read more about the Identities Library in the
[dedicated guide](https://mathlive.io/compute-engine/guides/identities/).

This release also includes a large collection of performance improvements and
bug fixes across the library.

This release includes some breaking changes.

### Breaking Changes

- **`replace()` no longer eagerly canonicalizes the complete result.** The
  requested `form`, or the form produced by the rule, applies to replaced
  subexpressions. Call `.canonical` on the result to restore the previous
  behavior.

- **Fixed-size numeric collections now infer dimensioned types.** For example,
  `[1, 2, 3]` is now `vector<3>` instead of `list<number>`, and a 3×3 numeric
  collection is `matrix<3x3>`.

### Features

- **Curated mathematical identities**: the new opt-in `loadIdentities()` API
  loads over 1,300 guarded simplification rules and special values derived from
  [Fungrim](https://fungrim.org). Identities can be selected by topic, class, or
  purpose, and rules apply only when their side conditions can be proven.

  ```ts
  import { ComputeEngine } from '@cortex-js/compute-engine';
  import { loadIdentities } from '@cortex-js/compute-engine/identities';

  const ce = new ComputeEngine();
  loadIdentities(ce); // Or: loadIdentities(ce, { topics: ['gamma'] })
  ce.parse('\\Gamma(\\frac12)').simplify(); // → √π
  ```

  The loader is synchronous and idempotent per engine. Importing the identities
  subpath is required, so applications that do not use it incur no bundle cost.

  Simplifying with the full Identities Library loaded is now substantially
  faster: `simplify()` runs at roughly 1.2–1.3× the unloaded baseline
  (previously ~1.6×). The many guarded rules that share a common arithmetic head
  — `Multiply`, `Add`, `Divide`, … — are dispatched together per head instead of
  one at a time, so the per-rule overhead on every arithmetic node is paid once
  per head rather than once per rule. Results are unchanged.

- **More control over replacements**:
  - `ReplaceOptions.form` controls the form of replacement expressions:
    `'canonical'`, `'structural'`, `'raw'`, or a specific canonical transform.
    The previous `canonical` option is deprecated and remains available as an
    alias for this release.
  - `ReplaceOptions.direction` selects left-to-right or right-to-left traversal
    for order-sensitive rules.
  - Custom rules can now match user-defined function operators in `replace()`
    and `simplify({ rules })`.

- **Improved algebra**:
  - `solve()` now handles quadratics with symbolic coefficients, including
    `x^2 - a x + 1 = 0` and the general `a x^2 + b x + c = 0`. (#300)
  - `Factor` infers the variable of a univariate polynomial and preserves
    extracted numeric content: `Factor(x^2 + 5x + 6)` returns `(x+2)(x+3)`, and
    `Factor(6x + 9)` returns `3(2x + 3)`. (#309)

- **Parsing improvements**:
  - Two-argument `\arctan(y, x)` and `\tan^{-1}(y, x)` now parse as `Arctan2`.
  - LaTeX input is normalized to Unicode NFC, so decomposed identifiers parse
    like their precomposed equivalents.
  - A trailing bare `\` and trailing visual spacing commands are tolerated.
  - Multi-character subscripted identifiers such as `D_{etectsize}` no longer
    collide with Euler derivative notation.

### Resolved Issues

- **Numeric evaluation and arithmetic**:
  - Corrected complex powers, reciprocals, roots, and logarithms, including
    `i^2`, `i^i`, negative complex exponents, and even roots of negative reals.
  - Restored arbitrary-precision accuracy for roots, `exp()`, `ln()`, `mod()`,
    `gammaln()`, and large integer conversion. Very small real results such as
    `Power(10, -100).N()` are no longer rounded to zero.
  - Exact `floor()`, `ceil()`, and `round()` no longer lose digits beyond 2^53.
    Large decimal powers no longer report false overflow.
  - Division by zero, `NaN * 0`, infinity comparisons, and signed infinities now
    behave consistently across numeric representations.
  - Corrected `Arctan2` quadrants, `ln(Root(a, b))`, non-integer logarithm
    bases, exact radicals such as `sqrt(8)`, and division of Gaussian integers.

- **Special functions and statistics**:
  - Added complex `Gamma` and `GammaLn` evaluation. Gamma and factorial poles at
    non-positive integers now return `ComplexInfinity`, while factorials of
    positive non-integers evaluate through `Gamma(x + 1)`.
  - Improved `Erf`/`Erfc` to machine precision and corrected small-argument
    `gammaln()`.
  - Corrected `GCD`, `LCM`, `Congruent`, `Subfactorial`, negative-index
    `Fibonacci`, `IsOctahedral`, `Multinomial`, and `BellNumber`.
  - Corrected skewness, kurtosis, interquartile range, histogram/bin endpoints,
    and exact combinatorial calculations.

- **Simplification, comparison, and assumptions**:
  - Indeterminate comparisons now remain unknown instead of becoming `false`;
    this also improves sign inference, `Boole`, and `KroneckerDelta`.
  - Fixed equality handling for unordered expressions and multi-variable
    equation equivalence.
  - Prevented invalid simplification of rational powers such as `(-x)^(3/4)`.
  - Set membership now remains undecided when a symbol's type is unknown, and
    `Subset`, `SubsetEqual`, `Superset`, and empty-set relations use the correct
    direction.
  - Symbolic common factors are now recognized, and unresolved derivatives
    remain symbolic instead of recursing indefinitely.

- **Collections, matrices, and tensors**:
  - Corrected `Rest`, `Slice`, `Drop`, `Cycle`, `Position`, `SetFrom`,
    `TupleFrom`, `Filter`, `Zip`, and compiled `Reduce` behavior.
  - Determinants now work for matrices of any supported size, with exact integer
    results; inverses work beyond 2×2.
  - Corrected matrix row access and the `isUpperTriangular`, `isDiagonal`, and
    `isTriangular` predicates.
  - Incompatible tensor broadcasts now throw instead of producing invalid data;
    `diagonal()` respects its axis arguments, and mixed real/complex dtype joins
    preserve precision.

- **Types and serialization**:
  - Dimensioned list and matrix type strings now parse and round-trip, including
    unknown dimensions, spaces, parenthesized element types, and single `^N`
    dimensions.
  - Corrected union reduction, `never` subtyping, narrowing of disjoint types,
    bare `matrix` handling, numeric literal subtyping, and invalid range
    validation.
  - String literals now remain strings after MathJSON round-trips, dictionary
    conversion retains every entry, and function literals can be applied
    directly.
  - Plain symbols no longer report themselves as empty finite collections.

- **LaTeX parsing and serialization**:
  - Corrected scaled/big delimiters, nested `\text{...}`, repeating decimals
    beginning with `.`, digit-like symbol names, prefixed-symbol errors, and
    unbalanced environment names.
  - Multiplication signs are now emitted where juxtaposition would merge numeric
    factors, for example `3 \times 2^2` instead of `32^2`. (#302)
  - Re-declaring a parser symbol with the same type no longer reports a
    conflict.

- **Compilation**:
  - Corrected JavaScript compilation of symbolic `Range`, compound-bounded
    interval `Sum`/`Product`, and interpreted fallback for multi-argument
    lambdas.
  - Corrected Python parentheses for `(a^b)^c`.
  - Corrected GLSL/WGSL output for `Degrees`, complex multiplication,
    `Gamma`/`Factorial`/`Beta`/`Erf`, and `If`/`Which`/`When`.
  - Corrected symbolic derivatives of `Arcsec` and `Arccsc`.

- **Interval arithmetic**:
  - Restored conservative enclosures for multiplication involving zero and
    infinity, negative-modulus `mod`, `clamp`, `binomial`, `gcd`, `lcm`,
    `gamma`, `gammaln`, `sinc`, and Fresnel integrals.

## 0.58.0 _2026-05-12_

### Added

- **`\operatorname{count}(L)` lowercase alias** — function-call form now parses
  to `["Length", L]`, matching the existing dot-notation form
  (`L.\operatorname{count}`) and the other lowercase aliases (`mod`, `var`,
  `shuffle`, `repeat`, `join`).

- **`Repeat(value, count)` 2-arg form** — `Repeat` now accepts an optional
  integer `count` and evaluates to a finite list of `count` copies of `value`.
  The 1-arg `Repeat(value)` keeps its existing infinite-sequence semantics.
  Materialization is gated by `ce.maxCollectionSize`; larger values stay lazy
  (still accessible via `.at()` / iterator).

- **`ce.maxCollectionSize`** — new configurable cap (default `10_000`) on the
  number of elements a collection may have when materialized into a concrete
  `List`. Assigning `<= 0` or `Infinity` disables the cap (matching
  `iterationLimit` and `recursionLimit`).

- **`Sum(L)` collection-reducer form** — `Sum` now accepts a single collection
  argument and reduces to the sum of its elements:
  `["Sum", ["List", 1, 2, 3, 4, 5]] // ➔ 15`. The big-op form
  `Sum(body, [i, a, b], …)` is unchanged. The `Sum` head is now preserved
  through canonicalization (previously rewritten to `Reduce(L, "Add", 0)`), so
  `L.\operatorname{total}` round-trips cleanly with
  `latexOptions.dotNotation = true`. The async path throws `CancellationError`
  on signal abort.

- **`At` extended with boolean-mask and integer-list indices** — `At(L, mask)`
  where `mask` is a finite collection of `True`/`False` returns the elements of
  `L` where the mask is `True`. `At(L, indices)` where `indices` is a finite
  collection of integers returns a sublist picked at those positions;
  out-of-range positions are filtered. Integer indices (`At(L, 2)`) and string
  keys (`At(d, "key")`) work as before.

- **Function-application broadcasting for user-defined lambdas** — when a user
  function with scalar-typed parameters is applied to a finite indexed
  collection, CE now broadcasts the call elementwise. For
  `ce.assign('f', ce.parse('x \\mapsto x^2 + 1'))`, the expression
  `["f", ["List", 1, 2, 3]]` evaluates to `["List", 2, 5, 10]`. Multi-arg
  functions broadcast with zip semantics, mixing scalars and lists naturally.
  The inferred default for `\mapsto` lambdas is scalar parameters, so most user
  functions broadcast by default. To opt out, declare an explicit list parameter
  type via `ce.declare(name, '(list<X>) -> Y')`.

- **List type for mixed-kind and mixed-dimension elements** — `widen()` now
  builds a structural union when the common supertype would otherwise collapse
  to a lossy generic category (`scalar`, `value`, `list`, `tuple`, `dictionary`,
  …). Consumers can detect heterogeneous lists by inspecting
  `expr.type.toString()`:
  - `[1, 2, 3]` → `list<number>` (precise)
  - `[1, "hello", 3]` → `list<finite_integer | string>` (union)
  - `[(1,2), (1,2,3)]` →
    `list<tuple<finite_integer, finite_integer> | tuple<finite_integer, finite_integer, finite_integer>>`
    (mixed dimension)
  - `[]` → `list<nothing>` (empty)

- **`ce.expr(true)` / `ce.expr(false)`** — JS boolean primitives now box to the
  `True` / `False` symbols (previously fell through to `Undefined`).

- **`Length` operator definition** — `ce.operatorInfo('Length')` now returns a
  valid entry. The evaluator returns an integer count for finite collections and
  leaves the expression unevaluated for non-collection or infinite inputs.

- **Library entries for `Complex`, `Colon`, `Prime`** — `ce.operatorInfo()` now
  returns introspection data for these heads (previously `undefined`). `Complex`
  boxing is unchanged — `["Complex", re, im]` still produces a `BoxedNumber`.

- **`ce.symbolInfo(name)`** — new public API parallel to `ce.operatorInfo()`,
  for introspecting constants and declared variables. Returns
  `{ kind: 'constant' | 'variable', type: BoxedType }` for symbols like `Pi`,
  `True`, `ExponentialE`, `ImaginaryUnit`. Returns `undefined` for unknown names
  and for operator heads. Added `SymbolInfo` type to the public type surface.
  - Note: `Infinity` is registered as `PositiveInfinity` / `NegativeInfinity`;
    `Undefined` has no value definition.

- **`ce.normalizeIdentifier(latex)`** — new public helper that converts a LaTeX
  identifier string to its canonical MathJSON name without side effects.
  Examples: `R_{3}` → `R_3`, `f_{Bm}` → `f_Bm`, `\theta_x` → `theta_x`. Inputs
  that aren't identifiers (`'1 + 2'`, empty string) return `''`. Useful in
  importer pipelines that need to call `ce.declare()` with normalized names
  before parsing referencing rows.

- **`First`/`Second`/`Third` compile entries** — component access (`p.x`, `p.y`,
  `p.z`) now compiles cleanly. JS uses `[0]`/`[1]`/`[2]` index access; GLSL/WGSL
  use `.x`/`.y`/`.z` swizzles, assuming the argument compiles to a
  `vec2`/`vec3`/`vec4`. 5+-element tuples (which compile to `float[N]` arrays)
  aren't supported.

- **`Range` GPU compile entry** — `Range(lo, hi[, step])` with
  compile-time-constant bounds emits an inline `float[N](...)` (GLSL) or
  `array<f32, N>(...)` (WGSL) literal. Non-constant bounds throw a clear error
  directing the caller to materialize on the JS host and upload as a uniform.
  Sequence count is capped at 256 elements per call site.

- **`Variance`/`GCD`/`Median` GPU compile entries** — GLSL+WGSL parity with
  their JS counterparts.
  - `Variance` is inlined (no size limit).
  - `GCD` uses a preamble function implementing the Euclidean algorithm.
  - `Median` is supported for list sizes 2–8; lists with 9+ elements throw.

- **`Random` GPU compile with deterministic seed** — `Random(seed)` in GLSL/WGSL
  compiles to a hash-based pseudorandom. `Random()` (no args) in GLSL falls back
  to a `gl_FragCoord`-derived seed (fragment-shader only); in WGSL it throws —
  callers must provide an explicit seed.
  - The fract-sin hash exhibits banding near `seed ≈ kπ`. For high-quality
    shader random, use a more robust hash (e.g. PCG or xxHash).
  - JS-side `Random` is unchanged (still `Math.random`, non-seeded). A seeded JS
    form will land in a future release.

- **`toSignedFunction()`** — new method on `BoxedExpression` for
  implicit-surface rendering and region classification:
  - `Equal(a, b)` → `a - b` (zero on the surface)
  - `Less(a, b)` / `LessEqual(a, b)` → `a - b` (negative when relation holds)
  - `Greater(a, b)` / `GreaterEqual(a, b)` → `b - a` (negative when relation
    holds)
  - `NotEqual(a, b)` → `a - b`
  - Non-relation expressions return `undefined`.

  Strictness and direction are encoded in `expr.operator`. Note that CE
  canonical form normalizes `GreaterEqual` to `LessEqual(b, a)` (and similarly
  `Greater` to `Less`), so callers will typically see the `Less`/`LessEqual`
  operator on parsed expressions — the signed-function semantics are preserved.

- **`BoxedExpression.getInterval(symbol)`** — new method for extracting domain
  bounds from restriction expressions. Returns `IntervalBounds` with
  `lower`/`upper`/`lowerStrict`/`upperStrict` for `When(e, cond)`,
  `And(c1, c2, …)`, and bare comparison expressions; returns `undefined` for
  unsupported shapes. Useful for 2D-plot domain derivation (e.g. clipping
  `y = f(x)\{0 < x < 5\}` to `[0, 5]`). Added `IntervalBounds` type to the
  public type surface.

- **Compact piecewise `\{cond_1 : val_1, …, default\}`** — now parses to
  `Which(c_1, v_1, …, True, default)`, the same head CE produces for
  `\begin{cases}…\end{cases}`. Disambiguated from set-builder `\{x : type\}` by
  inspecting the LHS of the top-level `Colon`: comparison/boolean heads (`Less`,
  `Greater`, `Equal`, `And`, `Or`, `Not`, …) → piecewise branch; otherwise →
  set-builder. Normal set literals (`\{1, 2, 3\}`) and set-builder via `\mid`
  are unchanged.

#### Fixed

- **`Linspace` endpoint inclusion** — `Linspace(a, b, n)` now produces `n`
  points evenly spanning `[a, b]` inclusive of both endpoints (matching NumPy,
  Julia, and MATLAB). Previously the last sample fell short of `b` (e.g.
  `Linspace(0, 1, 5)` yielded `0, 0.2, 0.4, 0.6, 0.8` instead of
  `0, 0.25, 0.5, 0.75, 1`). `Linspace(a, b, 1)` is the degenerate case and
  returns just `a`. The `contains` check is now tolerance-based (was an exact
  `%` test that failed for typical floating-point values).

- **Heterogeneous-list type rendering** — lists containing mixed kinds or
  mixed-dimension tuples previously rendered their type as `"[object Object]"`
  in some paths (`BoxedDictionary.type`, `collectionElementType`). Types are now
  constructed programmatically. Lists containing tuples, sets, dictionaries,
  records, or strings are no longer misclassified as numeric `BoxedTensor`s.

#### Known issues

- **JS `Loop` compile produces `undefined`** — the imperative `for`-loop IIFE
  generated for `Loop(body, Element(i, Range(lo, hi)))` has no `return`
  statement, so the compiled function returns `undefined` rather than the list
  of body values. Tracked for a future release.

- **JS `Integrate` compile produces `NaN`** — when `args[0]` is a `Function`
  expression (the common `\int x^2 dx` parse shape), `compileIntegrate` produces
  a double-lambda, so `_SYS.integrate` receives a function-returning function.
  Tracked for a future release.

## 0.57.0 _2026-05-10_

### Added

- **`verbatim` opt-in for `toLatex()`** — `expr.toLatex({ verbatim: true })`
  returns the original LaTeX source captured at parse time when the expression
  was parsed with `preserveLatex: true`. Falls back to normal re-serialization
  if no verbatim is available (e.g. for synthetic or transformed expressions).
  The default behavior of `expr.latex` and `expr.toLatex()` is unchanged —
  verbatim is strictly opt-in. Useful for round-tripping authored LaTeX (e.g.
  `p.x`, `\sin(x)`) without rewriting it to canonical form.
  - Verbatim is set only on the top-level boxed expression produced directly by
    `ce.parse(..., { preserveLatex: true })`. Canonicalization, `simplify()`,
    `evaluate()`, `subs()`, and `ce._fn()` produce fresh expressions with
    `verbatimLatex === undefined`.
  - Function expressions whose operator has a custom canonical handler (e.g.
    `Sin`, `Add`) currently do not preserve top-level verbatim through
    canonicalization — the handler reconstructs the result without threading
    metadata. Atoms (symbols, numbers) and functions without custom canonical
    handlers (e.g. `First`) do preserve it. Use `form: 'structural'` to skip
    canonical handlers when verbatim preservation matters.

- **`dotNotation` serialization option** — when enabled (default off),
  member-access heads serialize to dot notation rather than function-call form:
  `First(p)` → `p.x`, `Length(L)` → `L.\operatorname{count}`, etc. Useful for
  round-tripping editor-authored dot-notation back to its source form. Set via
  `ce.latexOptions.dotNotation = true` or per-call
  `expr.toLatex({ dotNotation: true })`. Only applies to arity-1 forms;
  multi-operand forms (e.g. `Sum` with an index range) keep their standard
  serialization.
  - **Serializer-only.** The flag lives in `SerializeLatexOptions` and has no
    effect on parsing. All input forms continue to parse as before regardless of
    the flag: `|L|`, `\operatorname{count}(L)`, `L.\operatorname{count}`,
    `\operatorname{length}(L)` all still parse to `["Length", L]` whether
    `dotNotation` is on or off. The flag only decides which form the serializer
    emits.

- **Component access** (`p.x`, `L.\operatorname{count}`, `z.\operatorname{re}`)
  — dot notation now parses to existing semantic heads at parse time. No generic
  accessor head was introduced.
  - Recognized members and their AST mapping: `x`/`y`/`z` →
    `First`/`Second`/`Third`; `real`/`re` → `Real`; `imag`/`im` → `Imaginary`;
    `count` → `Length`; `total` → `Sum`; `max` → `Max`; `min` → `Min`.
  - Disambiguation: after a terminated integer or decimal, `.` followed by a
    letter or `\operatorname{...}` is component access, not a decimal point.
    Examples: `1.x` parses as `["First", 1]` (not a malformed decimal); `1.5.x`
    parses as `["First", 1.5]`.
  - Only `\operatorname{...}` and bare-letter identifiers are recognized after
    `.`. `\mathrm{...}` is not accepted (deliberately tight).
  - `Third` is a new operator (parallels `First`/`Second`) with signature
    `(any) -> any`. `First`/`Second` were widened from `(collection) -> any` to
    `(any) -> any` so component access on a non-collection (e.g. `1.x`) defers
    type-checking to evaluation; evaluation returns an `Error` expression for
    incompatible types.

- **Restriction braces** (`expr\{cond\}`) — trailing brace predicates parse to a
  new `When` head.
  - `f(x)\{0 < x < 2\}` → `["When", ["f", "x"], ["Less", 0, "x", 2]]`.
  - **Stacked restrictions canonicalize**: `expr\{c_1\}\{c_2\}` →
    `["When", expr, ["And", c_1, c_2]]`. Downstream simplification, evaluation,
    interval intersection, and compilation see a single canonical shape
    regardless of source form.
  - Disambiguation from set literals is positional: standalone `\{1, 2, 3\}`
    continues to parse as a `Set`; `<expr>\{cond\}` parses as a `When`
    restriction. Allowed left operands include function calls, tuples, list/set
    literals, bare symbols, subscripted symbols, member access, power
    expressions, and chained restrictions.
  - Evaluator semantics: `When(e, True)` evaluates `e`; `When(e, False)` returns
    `Undefined`; indeterminate `cond` holds the form.
  - Serializer round-trips to the stacked-brace form (not `\wedge` inside one
    set of braces) so authored source and re-serialized output stay visually
    consistent.
  - JS and GLSL compilation: ternary `(cond ? e : NaN)`.

- **List-range ellipsis** (`[1...9]`, `[0, 0.1, ..., 1]`) — ranges inside list
  literals parse to the existing `Range` head.
  - Endpoint-only form: `[a...b]` → `["Range", a, b]`. Triggers `...`, `\ldots`,
    and `\dots` are all accepted.
  - Inferred-step form: `[a_0, a_1, ..., a_n]` → `["Range", a_0, a_n, step]`
    where `step = a_1 - a_0` is inferred from the first sample pair.
    Intermediate samples are validated against `a_0 + k·step` within
    `ce.tolerance`; inconsistent samples produce a parse error.
  - The float idiom `[0, 0.1, 0.2, ..., 1]` is supported (tolerance-aware
    comparison; `0.1 + 0.1 ≠ 0.2` exactly but is accepted within tolerance).
  - Outside `[...]` brackets, `\ldots`/`\dots`/`...` continue to parse as the
    `ContinuationPlaceholder` symbol. The trigger is bracket context.

- **For-comprehensions** (`(x, y) \operatorname{for} x=L_1, y=L_2`) — the `Loop`
  head now accepts multiple `Element` clauses, evaluated as nested loops with
  later bindings seeing earlier ones in scope.
  - `Loop(body, Element(x, L_1), Element(y, L_2), ...)` produces an
    `indexed_collection<T>` of body evaluations, in row-major order.
  - For independent bindings this is the Cartesian product:
    `(x, y) \operatorname{for} x = [1...2], y = [1...2]` → 4 tuples.
  - For dependent bindings later clauses see earlier:
    `(x, y) \operatorname{for} x = [1...3], y = [1...x]` → 6 tuples (triangle,
    not Cartesian).
  - Precedence: `\operatorname{for}` binds looser than `,` and `=`, tighter than
    `;`. So `(x + y) \operatorname{for} x = L_1, y = L_2` parses with body
    `x + y` and two bindings.
  - Bound names do not leak into the enclosing scope (uses
    `Scope.noAutoDeclare`).
  - Legacy single-Element form continues to round-trip via the existing
    `\text{for } i \text{ from } a \text{ to } b \text{ do } body` syntax.
    Multi-Element comprehensions serialize to the `\operatorname{for}` form.

- **`Range` type is now dynamic** — element type narrows based on the step
  argument: integer step (or no step) yields `indexed_collection<integer>`;
  non-integer step yields `indexed_collection<number>`. Previously the type was
  always `indexed_collection<integer>`, which was incorrect for float-step
  ranges.

- **`When` head** — new conditional-value operator. `When(expr, cond)` returns
  `expr` when `cond` is true, `Undefined` when `cond` is false, and holds when
  `cond` is indeterminate. Used by restriction-brace parsing (see above) but
  also usable directly.

- **`ce.operatorInfo(head)`** — new method on `ComputeEngine` for introspecting
  registered operator heads. Returns
  `{ kind: 'function' | 'opaque', signature?: BoxedType }` or `undefined`.
  - `'function'` — head has an `evaluate` handler or a `collection` handler
    (lazy producers like `Range`, `Linspace`, `Tuple` work via the latter).
  - `'opaque'` — head is declared with a signature but has neither (e.g.,
    `Triangle`, `Sphere`, `GeometricVector`).
  - `undefined` — no operator definition (constants like `Pi` and unknown
    heads).
  - Lets external tooling classify heads by capability without maintaining a
    parallel list of supported operators.

- **`tolerance` in `ParseLatexOptions`** — populated automatically from
  `ce.tolerance` when parsing through `ce.parse()`. Used by list-range sample
  validation; available to other parse handlers that need tolerance-aware
  comparison.

#### Fixed

- **`Loop` with `Element` clause** — single-Element
  `Loop(body, Element(i, range))` previously did not produce a list of body
  evaluations (the iteration path for `Element` form had a bug). The new
  variadic evaluator correctly yields a `List` of body values for each
  iteration.

## 0.56.0 _2026-03-10_

### Added

- **First-class color values** — colors are now typed values with a dedicated
  `color` primitive type and per-colorspace constructor heads, rather than
  anonymous tuples.
  - **Constructor heads**: `Rgb`, `Hsv`, `Hsl`, `Oklab`, `Oklch`. Each takes 3
    components plus an optional alpha. Channels follow each colorspace's own
    conventions (RGB: 0–1 sRGB; HSV/HSL: hue in degrees, S/V/L 0–1; Oklab/Oklch:
    standard ranges).
  - **LaTeX**: `\operatorname{rgb}(...)`, `\operatorname{hsv}(...)`,
    `\operatorname{hsl}(...)`, `\operatorname{oklab}(...)`,
    `\operatorname{oklch}(...)`, parsing and serialization both directions.
  - **Conversions**: `AsRgb`, `AsHsv`, `AsHsl`, `AsOklab`, `AsOklch` convert any
    color to the named space (identity if already there).
  - **`ColorDelta(a, b)`** — perceptual color difference (ΔE_OK, Euclidean
    distance in OKLab). Wide-gamut inputs are not clipped before measurement.

- **JavaScript compile-target support for color values** — all color
  constructors, the `As*` converters, `ColorDelta`, and `Distance` are
  supported. At runtime a color is a 3- or 4-element OKLCh array (`[L, C, H]` or
  `[L, C, H, alpha]`), matching the GPU target's `vec3`/`vec4` representation,
  so values move between JS, GLSL, and WGSL without conversion.

- **`Distance(p1, p2)`** — Euclidean distance between two points represented as
  tuples. Accepts any positive dimension; mismatched dimensions return a typed
  error. LaTeX trigger `\operatorname{distance}(p1, p2)`.

- **Geometric primitive heads** — `Triangle`, `Sphere`, `Segment`, and
  `GeometricVector` are now recognized as typed function heads (no evaluator,
  preserved structurally for downstream consumers). LaTeX triggers
  `\operatorname{triangle}`, `\operatorname{sphere}`, `\operatorname{segment}`,
  `\operatorname{vector}(p1, p2)`. `GeometricVector` is distinct from the
  existing `Vector` (column-vector construction).

- **`To` head registered** — `\to` already parsed to `["To", a, b]` but was
  classified as `unsupported-operator`; it is now a known typed head.

- **Function-style aliases** — lowercase `\operatorname{...}` forms common in
  Desmos-style notation now parse to their existing capitalized operators:
  `\operatorname{mod}` → `Mod`, `\operatorname{var}` → `Variance`,
  `\operatorname{shuffle}` → `Shuffle`, `\operatorname{random}` → `Random`,
  `\operatorname{repeat}` → `Repeat`, `\operatorname{join}` → `Join`.

- **`ce.latexOptions`** — new mutable, engine-wide bag of LaTeX parse/serialize
  options (e.g. `decimalSeparator`, `digitGroupSeparator`). Available as a
  constructor option and as a read/write property:
  ```ts
  const ce = new ComputeEngine({ latexOptions: { decimalSeparator: '{,}' } });
  // or post-construction:
  ce.latexOptions = { decimalSeparator: '{,}' };
  ```
  These options are merged into every `ce.parse()` and `expr.toLatex()` call.
  Precedence (most-specific wins): `LatexSyntax` instance defaults <
  `ce.latexOptions` < per-call options. Previously, options like
  `decimalSeparator` could only be changed per call post-construction (and
  `expr.latex` could not be customized at all).

#### Changed

- **`Color('...')`** now returns an `Oklch` head instead of a 0–1 sRGB `Tuple`.
  The string parser still accepts the same set of CSS-style inputs.
- **`ColorMix`** now returns an `Oklch` head and mixes in OKLCh directly,
  preserving out-of-gamut chroma. Hue interpolation takes the shortest path
  around the wheel; mixing with an achromatic endpoint carries the other
  endpoint's hue (matches CSS Color 4 `color-mix`).
- **`ContrastingColor`** now returns an `Rgb` head (was: 0–1 sRGB `Tuple`).
- **`Colormap`** now returns `Oklch` heads — either a `List(Oklch, ...)` or a
  single `Oklch` for position-sampling.
- **`ColorToString`** with `'oklch'` format serializes typed color inputs
  without an sRGB round-trip; out-of-gamut chroma serializes losslessly.
  `'hex'`/`'rgb'`/`'hsl'` paths are unchanged.
- **Color-consuming signatures tightened** — `(any, any)` →
  `(color | string | tuple, color | string | tuple)` for `ColorDelta`,
  `ColorContrast`, `ColorMix`, `ContrastingColor`, `ColorToString`,
  `ColorToColorspace`. The `As*` converters take `(color) -> color`.

#### Migration notes

Code that consumed the tuple output of `Color('...')`, `ColorMix`,
`ContrastingColor`, or `Colormap` now sees a typed color head. To get the
previous 0–1 sRGB shape, wrap with `AsRgb`:

```ts
// Before: const tuple = ce.expr(['Color', "'red'"]).evaluate();  // [r, g, b] in 0-1
// Now (equivalent 0-1 sRGB):
const rgb = ce.expr(['AsRgb', ['Color', "'red'"]]).evaluate();
// rgb is ['Rgb', r, g, b] with channels 0-1
```

`Rgb` head components are **0–1 sRGB** across all layers (engine, JS compile,
GPU compile).

#### Fixed

- **Super-linear parse time on deeply-nested parametric expressions** —
  `ce.parse()` could exhibit exponential blowup on inputs like nested rotation
  matrices `\left(\cos(\theta)\cdot S+\sin(\theta)\right)` (depth 6 took ~44s).
  Two underlying causes were addressed: the type/sign cache on `BoxedFunction`
  was effectively disabled (causing every `.type` access to recurse through all
  operands), and `parseEnclosure` was speculatively trying matchfix definitions
  whose close-delimiter token wasn't even present in the input. Parse time on
  the affected inputs is now linear.

- **`ce.parse()` ignored the injected `LatexSyntax` instance's
  `decimalSeparator`** — `ce.parse()` hardcoded `decimalSeparator: '.'`,
  silently overriding any value configured on a `LatexSyntax` passed via the
  constructor's `latexSyntax` option. The injected instance's configured
  separator now takes effect end-to-end.

- **`expr.toMathJson({ metadata: ['latex'] })` was silently dropped** — passing
  a metadata array of specific fields (e.g. `['latex']` or `['wikidata']`) was
  ignored; only `metadata: 'all'` worked. The array form now correctly populates
  the requested fields.

- **`expr.toMathJson({ shorthands: ['all'] })` disabled all shorthands** — the
  `['all']` array form had the opposite of its intended effect. The string form
  `'all'` and explicit lists like `['function']` were unaffected.

## 0.55.6 _2026-03-08_

### Resolved Issues

- **LaTeX parsing: `\lim` with postfix operators** —
  `\lim_{x\to 0}\left(x\right)^x` now correctly parses as `Limit(x^x)` instead
  of `Power(Limit(x), x)`. The `\lim` parser was using
  `parseArguments('implicit')` which stripped the delimiters and left the `^x`
  unconsumed; it now uses `parseExpression` so postfix operators are included in
  the limit body.

- **LaTeX parsing: style, size, and color switch commands** — `\displaystyle`,
  `\textstyle`, `\scriptstyle`, `\scriptscriptstyle`, `\tiny`..`\Huge` (10 size
  commands), and `\color{...}` were silently discarded during parsing. They now
  produce `Annotated` expressions that preserve the styling information and
  round-trip correctly through serialization. Added `\scriptstyle` /
  `\scriptscriptstyle` serialization support (previously only `\displaystyle`
  and `\textstyle` were handled).

- **LaTeX parsing: set-builder notation** — `\{x \in \R \mid x > 0\}` now parses
  to `["Set", expr, ["Condition", cond]]`. Registered `\mid` as an infix
  operator (`Divides`, precedence 160). The serializer round-trips set-builder
  notation correctly.

- **LaTeX serialization: `Complement`** — `["Complement", "A"]` now serializes
  to `A^\complement` instead of falling back to the generic function form.
  Removed stale `@todo` comments about a non-existent multi-argument case.

- **LaTeX parsing: spacing commands** — `\hspace{dim}`, `\hspace*{dim}`,
  `\hskip`, and `\kern` are now consumed during parsing (previously caused
  "unexpected token" errors). These are treated as visual spacing and skipped.

- **LaTeX serialization: `HorizontalSpacing` math classes** — the 2-argument
  form `["HorizontalSpacing", expr, "'bin'"]` now serializes to `\mathbin{expr}`
  (and similarly for `rel`, `op`, `ord`, `open`, `close`, `punct`, `inner`).
  Previously the second argument was silently dropped.

- **LaTeX serialization: redundant parens on matchfix operators** — `wrap()` no
  longer adds parentheses around `Abs`, `Floor`, `Ceil`, `Norm`, and other
  matchfix expressions that already have visible delimiters.

- **LaTeX serialization: tabular environments** — default environment serializer
  now renders matrix bodies (List of Lists) with `&` column separators and `\\`
  row separators instead of nested function calls.

- **LaTeX serialization: matchfix delimiter scaling** — default matchfix
  serializer now respects `groupStyle` to choose between bare delimiters,
  `\left..\right`, or `\bigl..\bigr` scaling.

- **LaTeX parsing: Greek symbols in string groups** — `\alpha`, `\beta`, etc. in
  `parseStringGroupContent()` (used by `\begin`/`\end`, color arguments) are now
  interpreted as their Unicode equivalents instead of passing through as raw
  LaTeX commands.

## 0.55.5 _2026-03-06_

### Resolved Issues

- **Deep-zoom fractal precision** — emulated-double (dp) and perturbation (pt)
  shaders now compute per-pixel coordinates from `v_uv` and viewport uniforms
  instead of the shader template's single-precision `mix()`, which lost
  distinguishability at high zoom levels.
- **Perturbation theory: absolute vs delta coordinates** — the perturbation
  Mandelbrot/Julia handlers were passing absolute single-precision coordinates
  to the shader instead of the small delta from the reference center. Fixed by
  introducing `_pt_delta()` which computes the per-pixel offset from viewport
  uniforms.
- **`compile()` free function dropped `hints`** — the `hints` option (viewport
  center/radius) was accepted but silently not forwarded to the language target.
  Fixed in `compile-expression.ts`.

### New Features

- **`BigDecimal` export** — the arbitrary-precision decimal class is now
  exported from the public API for use by plot engines and other consumers that
  need precision beyond float64.
- **`HighPrecisionCoord` type** — new union type
  (`number | string | { hi: number; lo: number }`) for passing
  extended-precision viewport coordinates through the compile API. The
  `viewport.center` option now accepts this type instead of plain
  `[number, number]`.

## 0.55.4 _2026-03-06_

### Resolved Issues

- **[#254](https://github.com/cortex-js/compute-engine/issues/254) LaTeX
  parsing: interval notation with `\lbrack`/`\lparen`** — parsing `\lbrack5,7)`
  or `\left\lbrack5,7\right)` now correctly produces an `Interval` expression.
  Previously, when the open delimiter was a LaTeX command (e.g., `\lbrack`), the
  parser incorrectly required the close delimiter to also be a LaTeX command
  (e.g., `\rparen` instead of `)`), causing mismatched-delimiter intervals to
  fail.
- **LaTeX parsing: invalid symbols in `\mathrm{}` and related prefixes** —
  invalid content inside `\mathrm{}`, `\operatorname{}`, etc. (e.g.,
  `\mathrm{=}` or `\mathrm{DavidBowie👨🏻‍🎤}`) now produces the correct
  `invalid-symbol` error instead of cascading parse errors. Also fixed
  `matchPrefixedSymbol` leaking parser state on failure, and emoji sequences are
  now properly recognized inside symbol prefixes (e.g.,
  `\operatorname{😎🤏😳🕶🤏}`).

### New Features

- **High-precision Mandelbrot/Julia compilation** — the GPU compilation targets
  (GLSL, WGSL) now support three precision tiers for fractal rendering, selected
  automatically based on viewport hints:
  - **Single float** (zoom < 10^6x): existing implementation, no overhead
  - **Emulated double** (zoom 10^6x–10^14x): double-single (float-float)
    arithmetic using Dekker/Knuth algorithms, ~48-bit mantissa from two 32-bit
    floats
  - **Perturbation theory** (zoom > 10^14x): reference orbit computed on CPU at
    arbitrary precision via `BigDecimal`, GPU iterates only the small delta from
    the reference, with glitch detection and single-float rebase fallback
- **Viewport-aware compile API** — `compile()` accepts optional
  `hints: { viewport: { center, radius } }`. The compiler auto-selects the
  precision strategy and returns `staleWhen` thresholds for cheap staleness
  checking by the plot engine.
- **`CompilationResult` extensions** — new optional fields: `staleWhen` (plain
  data staleness predicate), `uniforms` (scalar shader uniforms), `textures`
  (typed texture data with format/dimensions for GPU upload).

## 0.55.3 _2026-03-05_

### Improved

- **Compilation: constant folding** — `Add`, `Multiply`, `Subtract`, `Negate`,
  `Divide`, `Power`, `Sqrt`, and `Root` handlers now fold numeric literals at
  compile time and eliminate identity values.
  - `x + yi` compiles to `vec2(x, y)` instead of
    `vec2(x, 0.0) + (y * vec2(0.0, 1.0))`
  - `2 + 3` → `5.0`, `x + 0` → `x`, `x * 1` → `x`, `x * 0` → `0.0`
  - `Power(x, 2)` → `(x * x)` for simple operands, `pow(f(x), 2.0)` for complex
    expressions to avoid duplicate computation
  - `Power(x, 0.5)` → `sqrt(x)`, `Power(x, 0)` → `1.0`, `Power(x, -1)` →
    `(1.0 / x)`
  - `Sqrt(4)` → `2.0`, `Root(x, 2)` → `sqrt(x)`
- **`isComplexValued`** uses expression type system instead of hard-coded
  operator list.
- **Integer arguments** in GPU fractal functions emit as `200` instead of
  `int(200.0)`.
- **Type-based optimizations** — compilation handlers now use expression type
  information for better code generation:
  - `Floor`/`Ceil`/`Round`/`Truncate` are no-ops when the operand is
    integer-typed
  - `Abs` is a no-op when the operand is provably non-negative
  - `Power(x, 2)` only expands to `(x * x)` for simple operands (symbols,
    literals) — function calls like `Power(Sin(x), 2)` use `pow`/`Math.pow` to
    avoid duplicate evaluation
  - Integer `Mod` with non-negative dividend uses plain `%` instead of the
    Euclidean double-mod formula
  - GPU variable declarations infer `i32`/`int` type for integer-typed locals

### Resolved Issues

- **`Abs` signature**: return type is now `real` instead of propagating the
  input type (which incorrectly returned `complex` for complex inputs).
- **Compilation fallback**: uses `pushScope`/`assign` pattern instead of
  crashing when receiving a vars object.

### New Features

- **`Mandelbrot` and `Julia`** operators in JavaScript and GPU compilation
  targets.

## 0.55.2 _2026-03-04_

### Resolved Issues

- **`\text{}` flush bug**: `\text{a$x$b}` now correctly produces
  `["Text", "'a'", "x", "'b'"]`. Previously the text before and after inline
  math were merged due to a missing `flush()` call in `parseTextRun`.
- **`#` / `*` parsed as valid symbols**: Bare `#` and `*` tokens were
  incorrectly accepted as valid symbol names because they match the Unicode
  `Emoji` property (keycap base characters). They now produce `unexpected-token`
  errors as expected. The fix excludes ASCII characters from the emoji regex in
  symbol validation.
- **`Text` operator type**: The `Text` operator now has return type `string`
  instead of `expression`.
- **`\textcolor` inside `\text{}`**: `\textcolor{red}{RED}` inside `\text{}` now
  correctly parses the body as text (`'RED'`) instead of switching to math mode
  and treating each letter as a separate symbol.
- **`parseSyntaxError` token consumption**: Non-command tokens (like `#`, `&`)
  are now consumed when producing errors, preventing potential parser loops.
- **`parseSymbolToken` hardening**: Raw tokens are pre-validated against
  `\p{XIDC}` before being consumed as symbols, providing defense-in-depth
  against future `isValidSymbol` regressions.

### New Features

- **Text promotion**: When `InvisibleOperator` canonicalization encounters a
  `Text` expression or a string operand, it now absorbs all operands into a
  single `Text` expression. For example, `a\text{ in $x$ }b` canonicalizes to
  `["Text", "a", " in ", "x", " ", "b"]` instead of producing a `Tuple`.
- **Text infix keywords**: `\text{and}`, `\text{or}`, `\text{iff}`, and
  `\text{if and only if}` are now recognized as infix operators that produce
  `And`, `Or`, and `Equivalent` expressions respectively, following the existing
  `\text{where}` pattern.
- **Additional text keywords**: `\text{such that}` (maps to `Colon`),
  `\text{for all}` (maps to `ForAll`), and `\text{there exists}` (maps to
  `Exists`) are now recognized as operators.
- **`Text` serializer**: `Text` expressions now round-trip back to proper
  `\text{...}` LaTeX with inline `$...$` for math sub-expressions, instead of
  falling through to the default `\mathrm{Text}(...)` output.
- **`Text` evaluate handler**: Evaluating a `Text` expression now concatenates
  all operands into a single string.

## 0.55.1 _2026-03-04_

### Resolved Issues

- After `parse('f(x):=\\sin(x)')`, the symbol `f` is now immediately recognized
  as having type `function`. Previously its type remained `unknown` until the
  `Assign` expression was explicitly evaluated.
- `2f(x)` and `2f \left(x\right)` now both correctly parse as
  `["Multiply", 2, ["f", "x"]]` when `f` is a known function symbol. Previously,
  a space before `\left` caused the parser to produce a `Tuple` instead of
  `Multiply`, and expressions whose return type was `any` (e.g., calls to
  generically-typed functions) were also misclassified as `Tuple`.
- Expressions involving operators that return `expression` type (such as `D`,
  `Simplify`, `Annotated`) are now correctly treated as multiplicable in
  juxtaposition contexts. For example, `2f'(x)` now produces
  `["Multiply", 2, ["D", ...]]` instead of `Tuple`.
- The `D` (derivative) operator now returns a numeric type when its body is
  numeric, instead of always returning the generic `expression` type.
- Undeclared symbols followed by parenthesized multi-argument expressions (e.g.,
  `2g(x,y)`) are now auto-declared as functions in all invisible operator paths,
  not just the two-operand path.

## 0.55.0 _2026-03-04_

### Breaking Changes

- `ce.box()`/`box()` renamed to `ce.expr()`/`expr()` (`ce.box()` remains as a
  deprecated wrapper).
- Removed `ce.latexDictionary` getter/setter; configure dictionaries through
  `new LatexSyntax({ dictionary: [...] })`.
- Removed `ComputeEngine.getLatexDictionary()`; import dictionary constants from
  package exports.
- Removed deprecated type guard aliases: `isBoxedExpression`, `isBoxedNumber`,
  `isBoxedSymbol`, `isBoxedFunction`, `isBoxedString`, `isBoxedTensor` (use
  `isExpression`, `isNumber`, `isSymbol`, `isFunction`, `isString`, `isTensor`).
- Removed `LibraryDefinition.latexDictionary`; LaTeX dictionaries now live in
  the `latex-syntax` module.

### Resolved Issues

- **#295** The `parse()` free function now accepts the form options object, so
  `parse("\\frac{10}{2}", { form: "raw" })` return `["Divide", "10", "2"]`.
- Undeclared symbols followed by parenthesized numeric expressions are now
  interpreted as multiplication, not implicit function calls (for example,
  `q(2q)` -> `2q^2`). Function-call behavior remains for explicitly declared
  function symbols and non-numeric argument forms.

### New Features

- Modular package exports for smaller bundles: `@cortex-js/compute-engine/core`,
  `@cortex-js/compute-engine/compile`, `@cortex-js/compute-engine/latex-syntax`,
  `@cortex-js/compute-engine/numerics`, and `@cortex-js/compute-engine/interval`
  (with existing sub-paths still available, including `math-json`).
- New standalone `LatexSyntax` API (class + `parse()`/`serialize()` helpers) for
  LaTeX ↔ MathJSON without a `ComputeEngine` instance.
- New `ILatexSyntax` interface exposed via `IComputeEngine.latexSyntax` to allow
  custom LaTeX parser/serializer implementations.
- All 16 LaTeX domain dictionaries are now exported individually, plus the
  combined `LATEX_DICTIONARY`.
- `Parser` type is now exported from the main package for typed custom
  `LatexDictionaryEntry` parse handlers.

### Changed

- `ComputeEngine` now accepts an injectable `latexSyntax` dependency.
  - Full package imports still auto-create a LaTeX syntax instance.
  - Core-only imports do not bundle LaTeX support; `parse()`, `.latex`, and
    `toLatex()` require an injected `LatexSyntax`.
  - MathJSON serialization omits optional LaTeX metadata when no LaTeX syntax is
    present.
- `decimal.js` has been replaced with a native `bigint`-backed `BigDecimal`
  implementation, reducing dependency surface and bundle size.
- `BigDecimal` `add()`, `sub()`, and `mul()` are now exact; rounding is limited
  to operations that require it (`div()`, non-integer `pow()`, transcendentals).
- Numeric string/LaTeX serialization now respects precision settings:
  `.latex`/`.toString()` round to `ce.precision`, while `.json`/`toJSON()`
  remain lossless.
- High-precision special functions (`bigGamma`, `bigGammaln`, `bigDigamma`,
  `bigTrigamma`, `bigPolygamma`, `bigZeta`) now scale with
  `BigDecimal.precision`; integer Gamma values are exact.

## 0.54.0 _2026-02-26_

- **New `expr.polynomialCoefficients()` method**: Returns the coefficients of a
  polynomial expression in descending order of degree, or `undefined` if the
  expression is not a polynomial. Auto-detects the variable when the expression
  has exactly one unknown. Subsumes `isPolynomial` (check `!== undefined`) and
  degree computation (`length - 1`).

- **`polynomialCoefficients()` now accepts an array of variables**: Pass
  `['x', 'y']` to verify the expression is polynomial in all listed variables.
  Coefficients are decomposed by the first variable.

- **New `expr.polynomialRoots()` method**: Returns the roots of a polynomial
  expression, or `undefined` if not a polynomial. Handles degree 3+ polynomials
  with rational roots via the Rational Root Theorem.

- **New `Polynomial` CAS function**: Constructs a polynomial from a coefficient
  list (descending order) and a variable. Inverse of `CoefficientList`:
  `Polynomial([1, 0, 2, 1], x)` evaluates to `x³ + 2x + 1`.

- **Improved `Factor` for degree 3+ polynomials**: `Factor` now uses the
  Rational Root Theorem to factor polynomials with integer coefficients and
  rational roots. Previously only handled degree ≤ 2.

- **Improved `Factor` with content extraction**: `Factor` now extracts the GCD
  of integer coefficients before applying other strategies. For example,
  `Factor(6x² + 12x + 6, x)` now produces `6(x+1)²`.

- **New `PartialFraction` CAS function**: Decomposes rational expressions into
  partial fractions. Supports distinct and repeated linear factors, irreducible
  quadratic factors, and improper fractions (polynomial division performed
  first). Example: `PartialFraction(1/((x+1)(x+2)), x)` → `1/(x+1) - 1/(x+2)`.

- **New `Apart` CAS function**: Alias for `PartialFraction`.

- **New `PolynomialRoots` CAS function**: Returns the roots of a polynomial as a
  set. Example: `PolynomialRoots(x² - 5x + 6, x)` → `{2, 3}`.

- **New `Discriminant` CAS function**: Returns the discriminant of a polynomial
  of degree 2, 3, or 4. Supports symbolic coefficients. Example:
  `Discriminant(x² - 5x + 6, x)` → `1`.

- **`simplify()` auto-decomposes partial fractions**: When a `Divide` expression
  has a denominator already in factored form (product or power) and the
  decomposition is simpler, `simplify()` automatically applies partial fraction
  decomposition.

- **Breaking: `CoefficientList` now returns descending order**: The CAS function
  `CoefficientList` now returns coefficients from highest to lowest degree
  (e.g., `[1, 0, 2, 1]` for `x^3 + 2x + 1`), matching the new
  `polynomialCoefficients()` method and common external conventions. Previously
  it returned ascending order.

- **`expr.match()` now accepts string patterns with auto-wildcarding**: Pass a
  LaTeX string like `'ax^2+bx+c'` and single-character symbols are automatically
  treated as wildcards. Results use clean unprefixed keys (`{a: 3, b: 2, c: 5}`)
  with self-matches filtered out. `useVariations` and `matchMissingTerms`
  default to `true` for string patterns.

- **`expr.match()` now accepts MathJSON arrays directly**: Pass a raw MathJSON
  pattern like `['Add', '_a', '_b']` without calling `ce.box()` first.

- **New `matchMissingTerms` option for `match()`**: When enabled, expressions
  with fewer operands than the pattern can still match by treating missing terms
  as identity elements (0 for `Add`, 1 for `Multiply`). For example, `3x^2+5`
  matches the pattern `ax^2+bx+c` with `b = 0`. Enabled by default for string
  patterns.

- **Non-strict parsing: implicit superscript for letter+digit**: In non-strict
  mode, a single letter immediately followed by a digit 2–9 is parsed as an
  exponent: `x2 + y2` → `x^2 + y^2`. Handles common copy-paste from web pages.
  Only digits 2–9, only single ASCII letters, and only when adjacent (no space).

## 0.53.1 _2026-02-25_

- **`timeLimit` now reliably interrupts long-running evaluations**: `Factorial`,
  `Sum`, `Product`, `Loop`, and `Reduce` all respect the `timeLimit` property
  and throw `CancellationError` when the deadline is exceeded. Previously,
  generators yielded too infrequently (every 1,000–50,000 iterations), allowing
  a single `gen.next()` call to block for longer than the timeout. All
  generators now yield every iteration. The `Factorial` handler no longer
  silently swallows `CancellationError`, and `withDeadline`/`withDeadlineAsync`
  now use `try/finally` to always reset the engine deadline.

- **Fixed GPU compilation of `Sum`, `Product`, `Loop`, and `Function`**: These
  constructs no longer leak JavaScript-specific syntax (IIFEs, `let`, `while`,
  arrow functions, `{ re, im }` objects) into GLSL/WGSL output. `Sum`/`Product`
  with small constant bounds are unrolled inline; larger ranges emit native
  `for` loops. `Loop` emits a GPU `for` loop with `int`/`i32` index. `Function`
  (lambda) now throws a clear error for GPU targets. Block-level `Declare`
  statements infer `vec2`/`vec2f` type from subsequent complex-valued
  assignments.

- **Added GLSL/WGSL compilation for `Heaviside`, `Sinc`, `FresnelC`, `FresnelS`,
  `BesselJ`**: These five special functions now compile to GPU shader targets.
  `FresnelC`/`FresnelS` use a three-region rational Chebyshev approximation
  (ported from Cephes/scipy) with a shared `_gpu_polevl` helper. `BesselJ` uses
  power series, Hankel asymptotic, and Miller's backward recurrence depending on
  the argument range. Both GLSL and WGSL preambles are emitted on demand.

- **Fixed GLSL/WGSL block expression compilation**: Block expressions (produced
  by `\coloneq` / semicolon blocks) now emit valid GPU shader code instead of
  JavaScript syntax. Variable declarations use `float x` (GLSL) or `var x: f32`
  (WGSL) instead of `let x`, and blocks are emitted as plain statements instead
  of JavaScript IIFEs. `compileFunction` correctly formats multi-statement
  bodies.

- **Fixed `\;` in `\text{where}` clauses**: Visual spacing commands like `\;`,
  `\,`, `\quad`, etc. between comma-separated bindings in where-clauses are now
  correctly skipped instead of being parsed as `HorizontalSpacing` expressions
  wrapped in `InvisibleOperator`.

- **Fixed `require()` returning empty exports on Node 22+** (#292): Because the
  package sets `"type": "module"`, Node treated the UMD `.js` files as ESM,
  breaking the UMD factory pattern. The UMD builds now use a `.cjs` extension so
  Node always treats them as CommonJS.

## 0.53.0 _2026-02-21_

### Runtime and Scoping

- **True lexical scoping for `Function` expressions**: Functions now capture
  their defining scope and resolve free variables from that scope chain (not the
  call site), with a fresh child scope on each call.

- **BigOp scope pollution fixed**: `Sum`, `Product`, and other big operators now
  only declare their index variable locally. Other names are declared in the
  enclosing scope via `noAutoDeclare`.

- **Closure capture for nested functions**: Returned functions now correctly
  capture outer parameters across multiple nesting levels.

- **`EvalContext.values` removed**: Symbol values now live only in
  `BoxedValueDefinition.value`. The per-frame shadow map and `withArguments`
  option were removed.

- **`forget()` now resets values set by `assume()`**: `forget('x')` now clears
  values introduced by `assume('x = ...')` (value reset to `undefined`), in
  addition to clearing assumptions.

### Expressions and Equality

- **`expand()` now returns the input expression instead of `null`**: Both the
  free function and internal `expand()`/`expandAll()` now return the original
  expression when no expansion is possible.

- **New `.toRational()` method**: Returns `[numerator, denominator]` integers
  for rational expressions, or `null` otherwise.

- **New `.factors()` method**: Returns multiplicative factors as a flat array by
  decomposing `Multiply` and `Negate` structurally.

- **`.is()` now tries expansion**: After structural comparison, `.is()` expands
  both sides before numeric fallback, catching forms like `(x+1)^2` and
  `x^2+2x+1`.

- **`.is()` is now symmetric**: `a.is(b) === b.is(a)` now holds across all
  expression types.

### LaTeX Parsing

- **Parse `\mleft`/`\mright` delimiters**: Alternative delimiters from the
  `mleftright` package are now treated like `\left`/`\right`.

- **Parse `\color` in math mode**: `\color{...}` is now recognized in math mode;
  the color argument is consumed so the following math parses normally.

- **Parse `:` and `\colon` as infix operators**: Outside quantifier contexts, a
  bare `:`/`\colon` now parses as `Colon` (e.g. `f:[a,b]\to\R`), without
  affecting `:=` assignment or quantifier syntax.

- **Parse `\dfrac`, `\tfrac`, and `\cfrac` as fractions**: These variants now
  parse the same as `\frac`.

### Fractals

- **New `Mandelbrot` and `Julia` functions**: Added built-in escape-time fractal
  operators. `Mandelbrot(c, maxIter)` and `Julia(z, c, maxIter)` return a
  smooth, normalized value in `[0, 1]` (`1` for interior points, fractional for
  escaping points via `log₂(log₂(|z|²))` smoothing). Both evaluate in JavaScript
  and compile to GLSL/WGSL.

## 0.52.1 _2026-02-19_

### Expressions

- **Exact number literal check**: Use `isNumber(expr) && expr.isExact` to test
  for exact numeric literals.

- **`raw` form preserves subtraction**: `x-1` now parses as
  `["Subtract", "x", "1"]` (instead of `["Add", "x", -1]`) when using raw form.

### Parsing and Blocks

- **Fix `;\;` parsing in semicolon blocks**: Spacing commands after semicolons
  (`\;`, `\,`, `\quad`, etc.) no longer create spurious `Nothing` operands.

- **Fix `\text{if}` parsing with `\;` spacing**:
  `\text{if}\;...\;\text{then}\;...\;\text{else}\;...` now parses correctly as
  `If`.

- **Block serializer now uses `; `**: Serialization emits `; ` (not `;\; `) to
  avoid reintroducing spacing-related parse issues on round-trip.

- **Block compiler filters `Nothing` operands**: The Block compiler now removes
  `Nothing` symbols and empty compile results before generating code.

- **Subscripted variable names in blocks**: Names like `r_1` are treated as
  compound symbols (not `Subscript`) when the base is not a known collection.

- **Non-strict parser supports exponents on bare functions**: In `strict: false`
  mode, forms like `sin^2(x)` and `cos^{10}(x)` now parse correctly as powers.

- **Unicode superscript/subscript digits supported**: Superscript and subscript
  Unicode digits now normalize to `^{...}` / `_{...}` in parsing.

### Compilation

- **Selective GLSL interval preamble**: `interval-glsl` now emits only used
  helper functions (plus dependencies), typically reducing preamble size by
  60-80%.

- **Selective WGSL interval preamble**: `interval-wgsl` now applies the same
  used-only preamble strategy.

- **Fix recursive GLSL gamma helper**: Replaced recursive `_gpu_gamma()`
  reflection logic (illegal in GLSL) with a non-recursive implementation.

### Equality

- **`.is()` now works with assigned variables**: Numeric fallback now applies to
  expressions with no free variables, including variables with assigned values.

- **`.is()` now accepts an optional `tolerance`**: A per-call tolerance can
  override `engine.tolerance` for numeric comparison.

## 0.52.0 _2026-02-18_

### New Features

- **Smart `.is()` / exact `.isSame()` separation**: The `.is()` and `.isSame()`
  methods on expressions now have distinct roles:
  - **`.isSame(v)`** — Fast exact structural check. No evaluation, no tolerance.
    Now accepts primitives (`number`, `bigint`, `boolean`, `string`) in addition
    to `Expression`. This is the method used internally throughout the engine.

  - **`.is(v)`** — Smart check with numeric evaluation fallback. Tries
    `.isSame()` first; if that fails and the expression is constant (no free
    variables), evaluates numerically and compares within `engine.tolerance`.
    For literal numbers, behaves identically to `.isSame()` — tolerance only
    applies to expressions that require evaluation.

  This resolves a common pain point where `ce.parse('\\cos(\\pi/2)').is(0)`
  returned `false` because `.is()` was purely structural. Now it returns `true`:

  ```ts
  ce.parse('\\sin(\\pi)').is(0);            // true  (evaluates, within tolerance)
  ce.parse('\\cos(\\frac{\\pi}{2})').is(0); // true
  ce.number(1e-17).is(0);                   // false (literal number, no tolerance)
  ce.parse('x + 1').is(1);                  // false (not constant, no fallback)
  ```

- **`numericValue()` convenience helper**: New standalone function that combines
  the `isNumber()` guard with `.numericValue` access. Returns the numeric value
  if the expression is a number literal, or `undefined` otherwise. Useful for
  safely extracting numeric values without verbose ternary patterns:

  ```ts
  import { numericValue } from '@cortex-js/compute-engine';

  // Before
  const val = isNumber(expr) ? expr.numericValue : undefined;

  // After
  const val = numericValue(expr);
  ```

- **Stochastic equality check for expressions with unknowns**: `expr.isEqual()`
  now uses a stochastic fallback when symbolic methods (expand + simplify) can't
  prove equality. Both expressions are evaluated at 50 sample points (9
  well-known values + 41 random) and compared with relative+absolute tolerance.
  This detects equivalences like `sin²(x) + cos²(x) = 1`, `(x+y)² = x²+2xy+y²`,
  and `sin(2x) = 2sin(x)cos(x)` that were previously returned as `undefined`.
  Singularities (NaN at a sample point) are skipped rather than treated as
  disagreements. The check also works when the two expressions have different
  unknowns (e.g. `x - x + y` vs `y`).

- **`expr.freeVariables` property**: New property on `BoxedExpression` that
  returns the free variables of an expression — symbols that are not constants,
  not operators, not bound to a value, and not locally scoped by constructs like
  `Sum` or `Product`. Semantically identical to `expr.unknowns`.

- **New interval-js compilation functions**: Added `Binomial`, `GCD`, `LCM`,
  `Chop`, `Erf`, `Erfc`, `Exp2`, `Arctan2`, and `Hypot` to the interval-js
  compilation target, with corresponding interval arithmetic implementations.

- **GLSL/WGSL variable exponent support**: The interval GLSL and WGSL targets
  now support `Power` with variable exponents (e.g. `(-1)^k`, `x^n`). Previously
  these threw at compile time. Added `ia_pow_interval()` to both GPU library
  preambles using four-corner `exp(exp * ln(base))` evaluation with special
  cases for point-integer exponents and `(-1)^n`.

- **`Factorial`, `Gamma`, `GammaLn` for GLSL/WGSL interval targets**: Added
  `ia_factorial` (via `ia_gamma(x+1)`) to both GPU targets. Added `ia_gamma`
  (Lanczos approximation) and `ia_gammaln` (Stirling asymptotic) to the WGSL
  target, matching existing GLSL implementations.

### Resolved Issues

- **`parse()` with `form: 'structural'` ignored the structural flag**: The
  `structural` option from `formToInternal()` was dropped in
  `parseLatexEntrypoint()`, making `ce.parse(s, { form: 'structural' })` behave
  identically to `{ form: 'raw' }` (unbound, unsorted). Now correctly produces a
  bound, structural expression.

- **Partial canonicalization with `'Flatten'` form folded numerics**: Using
  `ce.parse(s, { form: ['Flatten', 'Order'] })` unexpectedly evaluated numeric
  operands (e.g. `3×2+1` became `7`) because `flattenForm()` used
  `ce.function()` which defaults to full canonical mode. Now uses `ce._fn()` to
  preserve operand structure. This enables structural comparison of expressions
  modulo commutativity and associativity without numeric evaluation — useful for
  checking the _method_ used to solve a problem rather than just the numeric
  result:

  ```ts
  const a = ce.parse('3\\times2+1', { form: ['Flatten', 'Order'] });
  const b = ce.parse('1+2\\times3', { form: ['Flatten', 'Order'] });
  a.isSame(b);  // ➔ true  (same structure, different order)

  const c = ce.parse('7', { form: ['Flatten', 'Order'] });
  a.isSame(c);  // ➔ false (different structure)
  ```

- **Sum/Product with symbolic bounds compiled incorrectly**: Expressions like
  `\sum_{k=0}^{n} f(k, x)` where the upper bound is a variable produced loops
  that iterated 10001 times instead of using the variable `n`. The compilation
  extracted bounds via `normalizeIndexingSet()` which converted symbolic bounds
  to `NaN` and fell back to a hardcoded limit. Now bounds are extracted as
  expressions and compiled to code (e.g. `Math.floor(_.n)` for JS,
  `Math.floor((_.n).hi)` for interval-js). This fixes Taylor series patterns
  like `\sum_{k=0}^{n} \frac{(-1)^k x^{2k+1}}{(2k+1)!}` for both JS and
  interval-js targets.

- **Interval `(-1)^k` returned `empty` instead of correct value**: The
  `powInterval()` function required positive bases for variable exponents,
  causing `(-1)^k` patterns in summations (e.g. Taylor series) to fail at
  runtime. Now correctly delegates to `intPow()` when the exponent is a point
  interval with an integer value, preserving even/odd parity. Also handles the
  case where base is `-1` and the exponent spans multiple integers by returning
  the conservative interval `[-1, 1]`.

- **`Factorial` missing from interval-js compilation target**: Expressions
  containing `n!` (e.g. `\frac{(-1)^k x^{2k+1}}{(2k+1)!}`) failed interval-js
  compilation with `success: false`. Added `Factorial` and `Factorial2` interval
  functions and compilation handlers.

- **`expr.unknowns` included bound variables**: Scoped constructs like `Sum`,
  `Product`, `Integrate`, and `Block` bind index variables in a local scope, but
  `expr.unknowns` was reporting them as free unknowns. For example,
  `\sum_{k=0}^{10} k \cdot x` returned `["k", "x"]` instead of `["x"]`. Now
  correctly excludes locally bound variables from the result.

- **Symbolic upper bounds missing from `expr.unknowns`**: In expressions like
  `\sum_{k=0}^{M} k \cdot x`, the symbolic upper bound `M` was incorrectly
  excluded from `unknowns` because the scope's bindings map captured all symbols
  referenced during canonicalization. Now extracts bound variables structurally
  from `Limits`/`Element`/`Assign`/`Declare` expressions, so only true bound
  variables are excluded. This also fixes `Block` expressions where locally
  assigned variables (via `Assign` or `Declare`) were reported as unknowns.

- **`Integrate` with symbolic bounds compiled incorrectly**: Same issue as
  Sum/Product — `compileIntegrate()` used `normalizeIndexingSet()` which
  converted symbolic bounds to `NaN`. Now uses `extractLimits()` and compiles
  bounds as expressions.

- **Interval `piecewise` test fix**: Fixed test that incorrectly accessed
  `result.lo` directly instead of unwrapping the `IntervalResult` envelope
  (`result.value.lo`). The `piecewise()` function correctly returns
  `IntervalResult` objects.

## 0.51.1 _2026-02-15_

### Features

- **#172 Degrees-Minutes-Seconds (DMS) notation**: Parse and serialize
  geographic angle notation such as `9°30'15"`. The LaTeX parser now recognizes
  arc-minute (`'`, `\prime`) and arc-second (`"`, `\doubleprime`) symbols when
  they follow a degree symbol, producing
  `Add(Quantity(…, deg), Quantity(…, arcmin), …)` expressions that evaluate and
  simplify through the existing unit system. Negative angles (e.g. `-45°30'`)
  are fully supported for latitude/longitude coordinates.
- **`dmsFormat` serialization option**: Set `dmsFormat: true` in
  `SerializeLatexOptions` to serialize angle quantities as DMS notation (e.g.
  `Quantity(9.5, deg)` → `9°30'`).
- **`angleNormalization` serialization option**: Normalize angles during
  serialization with `'0...360'` (useful for bearings) or `'-180...180'` (useful
  for longitude). Default is `'none'`.
- **`realOnly` compilation option**: Pass `{ realOnly: true }` to `compile()` to
  automatically convert complex `{ re, im }` results to real numbers — returns
  `re` when `im === 0`, `NaN` otherwise. Useful for plotting and other contexts
  that only need real-valued output.
- **`Sinc` function**: Unnormalized cardinal sine `sinc(x) = sin(x)/x` with
  `sinc(0) = 1`. Includes LaTeX parsing via `\operatorname{sinc}`, JavaScript
  and interval-arithmetic compilation targets.
- **Fresnel integrals (`FresnelS`, `FresnelC`)**: Numeric evaluation using
  Cephes rational Chebyshev approximation, LaTeX parsing via
  `\operatorname{FresnelS}` / `\operatorname{FresnelC}`, JavaScript and
  interval-arithmetic compilation targets.
- **`Heaviside` step function**: `H(x) = 0` for `x < 0`, `1/2` for `x = 0`, `1`
  for `x > 0`. LaTeX parsing via `\operatorname{Heaviside}`, JavaScript and
  interval-arithmetic compilation with singularity detection at zero.

### LaTeX Syntax

- **`Which` compilation**: `\begin{cases}` expressions now compile to JavaScript
  and interval-js targets as chained ternary operators with `NaN` fallback when
  no condition matches.
- **`Sum`/`Product` compilation**: `\sum_{k=a}^{b}` and `\prod_{k=a}^{b}`
  expressions with numeric bounds now compile to JavaScript loops with
  accumulator variables, including complex number support.
- **`Loop` compilation**: `Loop`, `Break`, `Continue`, and `Return` operators
  compile to JavaScript `for` loops wrapped in IIFEs with standard control flow
  keywords.
- **Inline `If` syntax**: Parse `\text{if } C \text{ then } A \text{ else } B`
  (or `\operatorname{if}`) to `["If", C, A, B]` expressions.
- **`where` syntax**: Parse `E \text{ where } x \coloneq V` to `Block`
  expressions with implicit variable declarations.
- **Semicolon block syntax**: Semicolons (`;`, `\;`) act as statement
  separators, building `Block` expressions with auto-declared variables when
  assignments are present.
- **`for` loop syntax**: Parse
  `\text{for } i \text{ from } a \text{ to } b \text{ do } body` to
  `["Loop", body, ["Element", "i", ["Range", a, b]]]`.

### Resolved Issues

- **Interval-JS compilation for Gamma functions**: Added missing `gamma` and
  `gammaln` exports and implementations in the interval-arithmetic library.
- **Interval-JS graceful fallback**: The `interval-js` target no longer throws
  when encountering unsupported functions. Unsupported operators now produce
  `{ success: false }` at compile time, and runtime errors return
  `{ kind: "entire" }` instead of propagating.
- **`CompilationResult.run` type signature**: The TypeScript type for `run` now
  correctly reflects the actual calling convention (`(...args: unknown[])`)
  instead of the previous misleading `(...args: (number | {re, im})[])`.
- **`Loop` compilation for interval-js target**: Loop counter now uses raw
  numbers (not `_IA.point()`) for the `for` statement, with loop index
  references properly wrapped in the body. Conditions in `if`/`break`/
  `continue` statements inside loops use scalar comparisons instead of interval
  comparison functions.

### Other Changes

- Updated color palettes
- Deduplicated runtime helper object (`SYS_HELPERS`) shared between
  `ComputeEngineFunction` and `ComputeEngineFunctionLiteral` in compilation
  target
- Centralized `sinc` implementation in `numerics/special-functions.ts` (shared
  by library evaluation and JS compilation runtime)
- Removed dead `args === null` checks in compilation base class

## 0.51.0 _2026-02-14_

### Colors

- **New `colors` library**: Four MathJSON operators for color manipulation and
  color space conversion, available as the `"colors"` library category.
- **`Color`**: Parse a color string (hex 3/6/8-digit, `rgb()`, `hsl()`, named
  CSS color, `transparent`) into a canonical sRGB `Tuple` with components
  normalized to 0-1. Alpha is included as a fourth component when not equal
  to 1.
- **`Colormap`**: Sample named visualization palettes. Three variants: no second
  argument returns the full palette as a `List`; integer _n_ >= 2 resamples to
  _n_ evenly spaced colors; real _t_ in [0, 1] interpolates at position _t_
  using OKLCh color space with shorter-arc hue interpolation. Includes 8
  sequential palettes (viridis, inferno, magma, plasma, cividis, turbo, rocket,
  mako), 6 categorical palettes (graph6, spectrum6, spectrum12, tableau10,
  tycho11, kelly22), and 12 diverging palettes (roma, vik, broc, rdbu, coolwarm,
  ocean-balance, plus reversed variants).
- **`ColorToColorspace`**: Convert an sRGB color (string or `Tuple`) to
  components in `"rgb"`, `"hsl"`, `"oklch"`, or `"oklab"` (alias `"lab"`).
  Preserves alpha when present.
- **`ColorFromColorspace`**: Convert color space components back to a canonical
  sRGB `Tuple`. Accepts the same color space names as `ColorToColorspace`.
- **`ColorToString`**: Convert a color (string or sRGB `Tuple`) to a formatted
  string. Supports optional format argument: `"hex"` (default), `"rgb"`,
  `"hsl"`, or `"oklch"` for CSS-style output. Alpha is included when not equal
  to 1.
- **`ColorMix`**: Blend two colors in OKLCh space with an optional ratio
  (default 0.5). Accepts color strings or sRGB `Tuple` values. Interpolates
  lightness and chroma linearly, hue with shorter-arc interpolation.
- **`ColorContrast`**: Compute the APCA contrast ratio between a background and
  foreground color. Returns a positive value for dark-on-light and negative for
  light-on-dark.
- **`ContrastingColor`**: Choose the foreground color with better APCA contrast
  against a background. With one argument, picks between white and black. With
  three arguments, picks the better of two foreground candidates.
- **LaTeX color support**: `\textcolor{color}{body}`, `\colorbox{color}{body}`,
  and `\boxed{body}` now roundtrip through `Annotated` expressions. Parsing and
  serialization are handled in the core `Annotated` infrastructure.
- **LaTeX font annotations**: `\textbf`, `\textit`, `\texttt`, `\textsf`,
  `\textup` now serialize correctly from `Annotated` expressions via
  `fontWeight`, `fontStyle`, and `fontFamily` dict keys.
- **JavaScript compilation**: All color operators (`Color`, `ColorToString`,
  `ColorMix`, `ColorContrast`, `ContrastingColor`, `ColorToColorspace`,
  `ColorFromColorspace`, `Colormap`) now compile to JavaScript.
- **`oklab()` CSS parsing**: `parseColor()` now accepts `oklab(L a b)` and
  `oklab(L a b / alpha)` syntax, matching the existing `oklch()` support.
- **GPU compilation**: `ColorMix`, `ColorContrast`, `ContrastingColor`,
  `ColorToColorspace`, and `ColorFromColorspace` now compile to GLSL and WGSL.
  Preamble functions provide sRGB ↔ OKLab ↔ OKLCh conversion, color mixing with
  shorter-arc hue interpolation, and APCA contrast on the GPU.
- Added `rgbToHsl()` conversion function. Exported `hslToRgb()` (previously
  private).

### Resolved Issues

- **(#290) Derivatives of user-defined functions**: `\frac{d}{dx} f` and `f'(x)`
  now correctly evaluate when `f` is a user-defined function (e.g.,
  `f(x) := 2x`). Previously `\frac{d}{dx} f` returned `0` and `f'(x)` returned a
  symbolic `Apply(Derivative(...))`.
- **Cleaner `D` canonical form**: `f'(x)` now canonicalizes to
  `["D", ["f", "x"], "x"]` instead of the verbose
  `["D", ["Function", ["Block", ["f", "x"]], "x"], "x"]`. Function calls are no
  longer redundantly wrapped in `Function(Block(...))`. Similarly,
  `\frac{d}{dx} f` where `f` is a known function symbol canonicalizes to
  `["D", ["f", "x"], "x"]` by applying the function to the differentiation
  variable.

### Free Functions

- Free functions (`simplify`, `evaluate`, `N`, `expand`, `expandAll`, `factor`,
  `solve`, `compile`) now accept `ExpressionInput` in addition to `LatexString`
  and `Expression`. This means you can pass numbers, MathJSON objects, or tuple
  arrays directly — e.g., `evaluate(["Add", 1, 2])` or
  `simplify(["Power", "x", 2])`.
- Added `declare()` free function to declare symbols without instantiating a
  `ComputeEngine` explicitly — e.g., `declare('x', 'integer')` or
  `declare({ x: 'integer', y: 'real' })`.

### Units and Quantities

- **New `units` library**: A comprehensive unit system for physical quantities,
  available as the `"units"` library category. Supports SI base units, 18 named
  derived units, SI prefixes (quetta through quecto), and common non-SI units
  (imperial, angles, logarithmic).
- **`Quantity` expression**: Pairs a numeric value with a unit:
  `["Quantity", 9.8, ["Divide", "m", ["Power", "s", 2]]]`. Accessors
  `QuantityMagnitude` and `QuantityUnit` extract the parts.
- **Quantity arithmetic**: `Add`, `Subtract`, `Multiply`, `Divide`, and `Power`
  are unit-aware. Addition and subtraction automatically convert compatible
  units and express the result in the unit with the largest scale factor (e.g.,
  `12 cm + 1 m` evaluates to `1.12 m`). Incompatible dimensions remain
  unevaluated.
- **Unit conversion**: `UnitConvert` converts between compatible units,
  including compound units like `m/s` to `km/h`. Supports affine temperature
  conversions (`degC`, `degF`, `K`). Returns an error for incompatible units.
  `UnitSimplify` reduces compound units to named derived units when possible
  (e.g., `kg*m/s^2` to `N`).
- **Dimensional analysis**: `IsCompatibleUnit` tests dimensional compatibility.
  `UnitDimension` returns the 7-element SI dimension vector. Both support
  compound unit expressions.
- **LaTeX parsing**: `\mathrm{...}` and `\text{...}` containing recognized units
  produce `Quantity` expressions when juxtaposed with numbers. Compound units
  with `/`, `^`, and `\cdot` are supported (e.g., `5\,\mathrm{m/s^{2}}`).
- **siunitx commands**: `\qty{value}{unit}`, `\SI{value}{unit}`, `\unit{unit}`,
  and `\si{unit}` are parsed.
- **LaTeX serialization**: `Quantity` expressions serialize to
  `value\,\mathrm{unit}` notation.
- **DSL string sugar**: Compound units can be specified as strings in MathJSON:
  `["Quantity", 9.8, "m/s^2"]` is canonicalized to the structured form.
  Parentheses are supported for grouping: `"kg/(m*s^2)"`.
- **Temperature units**: `degC` and `degF` with affine offset conversions.
- **Angular unit unification**: Trigonometric functions (`Sin`, `Cos`, `Tan`,
  etc.) accept `Quantity` arguments with angular units (`deg`, `rad`, `grad`,
  `arcmin`, `arcsec`) and convert to radians automatically.
- **Physics constants**: 11 CODATA 2018 constants defined as `Quantity`
  expressions: `SpeedOfLight`, `PlanckConstant`, `Mu0`, `StandardGravity`,
  `ElementaryCharge`, `BoltzmannConstant`, `AvogadroConstant`,
  `VacuumPermittivity`, `GravitationalConstant`, `StefanBoltzmannConstant`, and
  `GasConstant`.

### Compilation

- **Tuple and Matrix compilation**: `Tuple` and `Matrix` expressions can now be
  compiled across all targets. `compile('(\\sin(t), \\cos(t))')` produces
  `[Math.sin(t), Math.cos(t)]` in JavaScript, `vec2(sin(t), cos(t))` in GLSL,
  `vec2f(sin(t), cos(t))` in WGSL, and `(np.sin(t), np.cos(t))` in Python.
- **GPU-native matrix types**: Square matrices (2x2, 3x3, 4x4) compile to native
  GPU matrix constructors (`mat2`/`mat3`/`mat4` in GLSL,
  `mat2x2f`/`mat3x3f`/`mat4x4f` in WGSL) with proper column-major transposition.
  Column vectors are flattened to `vecN`/`vecNf` instead of nested
  single-element arrays.
- **Complex number compilation**: The JavaScript compilation target now supports
  complex-valued expressions. The compiler performs static type analysis at
  compile time to determine whether each subexpression is real or complex, and
  emits the appropriate code path. Simple arithmetic (Add, Subtract, Multiply,
  Divide, Negate) uses inline `{re, im}` field math to avoid allocation.
  Transcendental functions (Sin, Cos, Exp, Ln, Sqrt, Power, and others) delegate
  to runtime helpers backed by the `complex-esm` library. Mixed real/complex
  operands are promoted inline. `ImaginaryUnit` compiles to `{re: 0, im: 1}`.
  Symbols with unknown type are assumed real. Complex-aware `Sum` and `Product`
  loops emit `{re, im}` accumulators when the loop body is complex-valued.
  Reciprocal trig/hyperbolic functions (Cot, Sec, Csc, Coth, Sech, Csch) and
  their inverses dispatch to complex helpers when operands are complex.
- **Python complex compilation**: The Python target now supports complex-valued
  expressions using Python's native `complex()` constructor and the `cmath`
  module for transcendental functions. Real-valued expressions continue to use
  NumPy.
- **Gamma function compilation**: `Gamma` and `GammaLn` can now be compiled to
  `interval-js`, `glsl`, `wgsl`, and `interval-glsl` targets. The interval
  targets include pole detection at non-positive integers and correct
  monotonicity handling around the minimum at x ≈ 1.46.
- **Special function compilation**: 27 additional functions can now be compiled
  to JavaScript: `Erf`, `Erfc`, `ErfInv`, `Beta`, `Digamma`, `Trigamma`,
  `PolyGamma`, `Zeta`, `LambertW`, `BesselJ`, `BesselY`, `BesselI`, `BesselK`,
  `AiryAi`, `AiryBi`, `Factorial`, `Factorial2`, `Exp2`, `Log2`, `Log10`, `Lg`,
  `Arctan2`, `Hypot`, `Degrees`, `Haversine`, `InverseHaversine`, `Binomial`,
  and `Fibonacci`.
- **GPU special functions**: `Erf`, `Erfc`, `ErfInv`, `Beta`, `Factorial`,
  `Arctan2`, `Hypot`, `Haversine`, `InverseHaversine`, `Log10`, and `Lg` can now
  be compiled to GLSL and WGSL targets. `Erf`/`ErfInv` use Abramowitz & Stegun
  polynomial approximations; `Beta` and `Factorial` leverage the existing GPU
  Gamma preamble.

### Simplification

- **Factorial quotient simplification**: `n!/k!` is now simplified to a partial
  product for both concrete integers (e.g., `10!/7!` → `720`) and symbolic
  expressions with small constant difference (e.g., `n!/(n-2)!` → `n(n-1)`).
- **Binomial detection**: Expressions of the form `n!/(k!(n-k)!)` are
  automatically recognized and simplified to `Binomial(n, k)`.
- **Binomial identity simplification**: `C(n,0)` → `1`, `C(n,1)` → `n`, `C(n,n)`
  → `1`, `C(n,n-1)` → `n`.
- **Factorial sum factoring**: Sums and differences of factorials with related
  arguments are factored out, e.g., `n! - (n-1)!` → `(n-1)! * (n-1)`,
  `(n+1)! + n!` → `n! * (n+2)`.

## 0.50.2 _2026-02-12_

### Numerics

- **Centralized overflow protection**: Improved robustness of `Rational` and
  `ExactNumericValue` arithmetic by centralizing overflow checks and automatic
  promotion to `BigInt`.
- **\[#287\](https://github.com/cortex-js/compute-engine/issues/287) Improved
  precision for large integer products**: Multiplications and additions of large
  integers that would previously lose precision (exceeding
  `Number.MAX_SAFE_INTEGER`) are now automatically promoted to `BigInt` to
  maintain exact results.

### Symbols

- **[\#288](https://github.com/cortex-js/compute-engine/issues/288) Allow
  reassigning a symbol from operator to value**: `ce.assign()` no longer throws
  when assigning a plain value to a symbol that was previously declared as a
  function. Existing expressions using the symbol as a function head will
  produce a type error at evaluation time if the new value is not callable.

### Evaluation

- **Fixed scope leaks**: Ensured that evaluation contexts are correctly popped
  even when an error or timeout occurs in `BoxedFunction.evaluate()`,
  `findUnivariateRoots()`, and rule-boxing operations.
- **Improved numerical evaluation performance**: `Sum`, `Product`, `Divide`, and
  statistical operators (`Mean`, `Variance`, etc.) now correctly propagate the
  `numericApproximation` option, significantly speeding up large numerical
  calculations by avoiding expensive exact arithmetic.

## 0.50.1 _2026-02-11_

### Compilation

- **`CompilationResult.preamble` for shader targets**: `compile()` with
  `interval-wgsl` and `interval-glsl` targets now returns a `preamble` field
  containing the interval arithmetic library (struct definitions, helper
  functions). Previously, the compiled `code` referenced functions like `ia_div`
  and `ia_sin` that were not included in the output. Use `preamble + code` for a
  self-contained shader, or call `compileShaderFunction()` on the target
  directly.

## 0.50.0 _2026-02-11_

### Breaking API Changes

This release includes several breaking changes to the public API.

The most significant is the restructuring of the `Expression` type hierarchy and
the introduction of type-guarded role interfaces, which improves type safety and
API ergonomics but requires updates to code that accessed role-specific
properties directly on expression instances.

See
[`MIGRATION_GUIDE_0.50.0.md`](https://github.com/cortex-js/compute-engine/blob/main/MIGRATION_GUIDE_0.50.0.md)
for details.

#### Naming Alignment: `Expression`, `MathJsonExpression`, and `ExpressionInput`

- The compute-engine runtime type is now `Expression` (preferred name).
  `BoxedExpression` is retained as a deprecated alias for migration.
- The MathJSON type is now `MathJsonExpression` (the old MathJSON `Expression`
  name has been removed from the `math-json` entrypoint).
- `SemiBoxedExpression` is now `ExpressionInput` (with a deprecated alias for
  migration).

#### Role-Specific Properties Moved to Type-Guarded Interfaces

Properties that were previously on all `Expression` instances (returning
`undefined` when not applicable) have been moved to role interfaces. They are
now only accessible after narrowing with a type guard.

| Removed from `Expression`             | Access via                                                           |
| :------------------------------------ | :------------------------------------------------------------------- |
| `.symbol`                             | `isSymbol(expr)` or `isSymbol(expr, 'Pi')` then `expr.symbol`        |
| `.string`                             | `isString(expr)` then `expr.string`                                  |
| `.ops`, `.nops`, `.op1`/`.op2`/`.op3` | `isFunction(expr)` or `isFunction(expr, 'Add')` then `expr.ops` etc. |
| `.numericValue`, `.isNumberLiteral`   | `isNumber(expr)` then `expr.numericValue`                            |
| `.tensor`                             | `isTensor(expr)` then `expr.tensor`                                  |

```ts
// Before
if (expr.symbol !== null) console.log(expr.symbol);

// After
import { isSymbol, sym } from '@cortex-js/compute-engine';

if (isSymbol(expr)) console.log(expr.symbol);
// isSymbol() accepts an optional symbol name:
if (isSymbol(expr, 'Pi')) { /* expr is the Pi symbol */ }
// or use the convenience helper:
if (sym(expr) === 'Pi') { /* ... */ }

// isFunction() accepts an optional operator name:
if (isFunction(expr, 'Add')) {
  // expr is narrowed to a function AND has operator 'Add'
  console.log(expr.ops);
}
```

Properties that remain on `Expression`: `.operator`, `.re`/`.im`, `.shape`, all
arithmetic methods (`.add()`, `.mul()`, etc.), and all numeric predicates
(`.isPositive`, `.isInteger`, etc.).

#### Expression Creation: `form` Replaces `canonical`/`structural`

The `canonical` (boolean or array) and `structural` (boolean) options on
`ce.box()`, `ce.function()`, and `ce.parse()` have been unified into a single
`form` option.

```ts
ce.box(['Add', 1, 'x'], { form: 'canonical' }); // default
ce.box(['Add', 1, 'x'], { form: 'raw' });        // no canonicalization, no binding
ce.function('Add', [1, 'x'], { form: 'structural' }); // bound, not fully canonical
ce.box(['Add', 1, 'x'], { form: ['Number', 'Order'] }); // selective passes
```

#### New Free Functions

Top-level free functions are now available for common operations and use a
shared `ComputeEngine` instance created on first call.

| Function                               | Purpose                                                        |
| :------------------------------------- | :------------------------------------------------------------- |
| `getDefaultEngine()`                   | Return the shared default `ComputeEngine` instance.            |
| `parse(latex)`                         | Parse a LaTeX string into an `Expression`.                     |
| `simplify(exprOrLatex)`                | Simplify an expression or LaTeX input.                         |
| `evaluate(exprOrLatex)`                | Evaluate an expression or LaTeX input symbolically.            |
| `N(exprOrLatex)`                       | Numerically evaluate an expression or LaTeX input.             |
| `assign(id, value)` / `assign(record)` | Assign one symbol value or many at once.                       |
| `expand(exprOrLatex)`                  | Expand distributively at the top level (`Expression \| null`). |
| `expandAll(exprOrLatex)`               | Expand distributively recursively (`Expression \| null`).      |
| `solve(exprOrLatex, vars?)`            | Solve equations/systems (returns solve result variants).       |
| `factor(exprOrLatex)`                  | Factor an expression.                                          |
| `compile(exprOrLatex, options?)`       | Compile to a target language with `CompilationResult`.         |

```ts
import {
  getDefaultEngine,
  parse,
  simplify,
  evaluate,
  N,
  assign,
  expand,
  expandAll,
  solve,
  factor,
  compile,
} from '@cortex-js/compute-engine';

assign('x', 3);

const expr = parse('x^2 - 5x + 6');
solve(expr, 'x');           // [2, 3]
factor('(2x)(4y)');         // 8xy
compile('x^2 + 1').run({ x: 3 }); // 10
```

Except for `parse()`, `assign()`, and `getDefaultEngine()`, these free functions
accept either a LaTeX string or an existing `Expression`.

#### Free Function Notes

- `compile()` is now a top-level entry point returning `CompilationResult`.
  Custom compilation targets are managed with `ce.registerCompilationTarget()`
  and `ce.unregisterCompilationTarget()`.
- `expand()` and `expandAll()` return `null` when an expression is not
  expandable.
- `solve()` is available as a top-level wrapper over equation/system solving.
- `factor()` is the top-level factoring entry point. Specialized helpers such as
  `factorPolynomial()` and `factorQuadratic()` remain expression-only APIs.

#### `trigSimplify()` Method Removed

Use `simplify({ strategy: 'fu' })` instead, which is equivalent.

```ts
// Before
const result = expr.trigSimplify();

// After
const result = expr.simplify({ strategy: 'fu' });
```

#### Library System

The constructor now accepts a `libraries` option for controlling which libraries
are loaded. Libraries declare their dependencies and are loaded in topological
order.

```ts
// Load specific standard libraries
const ce = new ComputeEngine({
  libraries: ['core', 'arithmetic', 'trigonometry'],
});

// Add a custom library
const ce = new ComputeEngine({
  libraries: [
    ...ComputeEngine.getStandardLibrary(),
    { name: 'physics', requires: ['arithmetic'], definitions: { /* ... */ } },
  ],
});
```

#### User-Extensible Simplification Rules

`ce.simplificationRules` is now a public getter/setter. Users can push
additional rules or replace the entire rule set.

```ts
ce.simplificationRules.push({
  match: ['Power', ['Sin', '_x'], 2],
  replace: ['Subtract', 1, ['Power', ['Cos', '_x'], 2]],
});
```

### Canonicalization

- **Exact numeric folding during canonicalization**: `canonicalAdd` and
  `canonicalMultiply` now fold **exact** numeric operands at canonicalization
  time, making behavior consistent with `canonicalDivide` which already folded
  coefficients. This means expressions are reduced earlier in the pipeline
  without waiting for a `.simplify()` call.

  **What gets folded** (exact values):
  - Integers: `Add(2, x, 5)` &rarr; `Add(x, 7)`
  - Rationals: `Add(1/3, x, 2/3)` &rarr; `Add(x, 1)`
  - Radicals: `Add(√2, x, √2)` &rarr; `Add(x, 2√2)`
  - Mixed exact: `Multiply(2, x, 5)` &rarr; `Multiply(10, x)`
  - Full reduction: `Add(2, 3)` &rarr; `5`, `Multiply(2, 3)` &rarr; `6`
  - Identity elimination: `Multiply(1/2, x, 2)` &rarr; `x`
  - Complex promotion: `Add(1, Complex(0, -1))` &rarr; `Complex(1, -1)`

  **What is NOT folded** (non-exact values):
  - Machine floats: `Add(1.5, x, 0.5)` remains `Add(x, 0.5, 1.5)`
  - Infinity/NaN: `Multiply(0, ∞)` correctly returns `NaN`
  - Single numeric: `Multiply(5, Pi)` is unchanged (nothing to fold)

  The folding uses the existing `ExactNumericValue` arithmetic, which
  automatically handles radical grouping (`√2 + √2 = 2√2`) and rational
  simplification (`1/3 + 2/3 = 1`).

- **Exact numeric folding in `canonicalPower`**: Integer powers of numeric
  literals are now folded during canonicalization when the exponent is an
  integer with |e| &le; 64. For machine-number bases, the result must be a safe
  integer; for exact numeric values (rationals, radicals), `NumericValue.pow()`
  is used.
  - `Power(2, 3)` &rarr; `8`
  - `Power(3, 2)` &rarr; `9`
  - `Power(1/2, 2)` &rarr; `1/4`
  - `Power(-2, 3)` &rarr; `-8`
  - `Power(2, 100)` remains unevaluated (exponent exceeds limit)

- **Complex promotion handles non-adjacent operands**: `canonicalAdd` now
  combines a real float with imaginary terms even when they are not adjacent in
  the operand list. Previously, only a real immediately followed by an imaginary
  was promoted to a complex number.

### Type Inference

- **Type handlers for 25 operators**: Added explicit `type` handlers to
  operators that were missing them, enabling the type system to return precise
  types instead of the broad signature return type.
  - **Arithmetic**: `Factorial`, `Factorial2`, `Sign` return `finite_integer`;
    `Ceil` and `Floor` return `finite_integer` for finite inputs, `integer`
    otherwise.
  - **Trigonometry**: `Arctan` uses `numericTypeHandler` (returns `finite_real`
    for real inputs, `finite_number` for complex).
  - **Complex**: `Real`, `Imaginary`, `Argument` return `finite_real`.
  - **Number theory**: `Totient`, `Sigma0`, `Sigma1`, `Eulerian`, `Stirling`,
    `NPartition` return `finite_integer`; `SigmaMinus1` returns
    `finite_rational`.
  - **Combinatorics**: `Choose`, `Fibonacci`, `Binomial`, `Multinomial`,
    `Subfactorial`, `BellNumber` return `finite_integer`.
  - **`Truncate`, `GCD`, `LCM` type handlers**: `Truncate` returns
    `finite_integer` for finite inputs (matching `Ceil`/`Floor`); `GCD` and
    `LCM` always return `finite_integer`.

### Solving

- **`And` operator support for systems of equations**: `solve()` now accepts
  `And(Equal(...), Equal(...))` in addition to `List(Equal(...), Equal(...))`
  for representing systems of equations. Both forms route through the same
  linear, polynomial, and inequality solvers.

- **Parametric solution type filtering**: `filterSolutionByTypes` now uses
  `=== false` instead of `!== true` for type predicate checks. This allows
  underdetermined (parametric) solutions to pass through when type predicates
  return `undefined` (unknown) rather than being incorrectly rejected.

- **`Or` operator support in `solve()`**: Solving `Or(Equal(x,1), Equal(x,2))`
  returns the union of solutions from each branch, with deduplication. Works for
  both univariate (returns array of values) and multivariate (returns array of
  records) cases.

- **Mixed equality + inequality systems**: `solve()` now handles systems
  combining `Equal` and inequality operators (`Less`, `LessEqual`, `Greater`,
  `GreaterEqual`). Equalities are solved first, then solutions are filtered
  against the inequalities.

- **Parametric solutions omit free variables**: Underdetermined linear systems
  no longer include free variables (self-referential entries) in the result
  record. Only dependent variables with non-trivial expressions are returned.

### Special Functions

- **Numeric evaluation for Digamma, Trigamma, PolyGamma, Beta, Zeta, LambertW**:
  These six functions now evaluate numerically when `.N()` is called, at both
  machine precision and arbitrary precision (bignum). Returns unevaluated
  without numeric approximation.
  - `Digamma`/`Trigamma`: recurrence + asymptotic with Bernoulli numbers
  - `PolyGamma`: generalized recurrence for arbitrary order n
  - `Beta`: via gamma, with log-gamma fallback for large arguments
  - `Zeta`: Cohen-Villegas-Zagier acceleration, functional equation for
    $\operatorname{Re}(s)<0$
  - `LambertW`: Halley's method with branch-point handling

- **Arbitrary-precision (bignum) variants for special functions**: When
  `ce.precision > 15`, `Digamma`, `Trigamma`, `PolyGamma`, `Beta`, `Zeta`, and
  `LambertW` now compute results to the requested precision using bignum
  arithmetic. The asymptotic shift threshold scales with precision to maintain
  accuracy (e.g., `ce.precision = 50` produces 50-digit results for Digamma and
  Zeta).

- **Numeric evaluation for Bessel functions (`BesselJ`, `BesselY`, `BesselI`,
  `BesselK`)**: Integer-order Bessel functions now evaluate numerically.
  - `BesselJ`: power series for small $|x|$, Miller's backward recurrence for
    intermediate values, Hankel asymptotic expansion for large $|x|$
  - `BesselY`: DLMF 10.8.3 series for $Y_0$/$Y_1$, forward recurrence for higher
    orders, shared Hankel asymptotic with `BesselJ`
  - `BesselI`: power series + asymptotic expansion
  - `BesselK`: series for $K_0$, Wronskian-derived $K_1$, forward recurrence for
    higher orders, asymptotic for large $x$

- **Numeric evaluation for Airy functions (`AiryAi`, `AiryBi`)**: Power series
  using Maclaurin coefficients for $|x| \leq 5$, asymptotic expansions
  (exponential decay for Ai, exponential growth for Bi at positive $x$,
  oscillatory for negative $x$) for large arguments.

### Linear Algebra

(Fix [#285](https://github.com/cortex-js/compute-engine/issues/285))

- **`\begin{vmatrix}` now parses to `Determinant`**: The `vmatrix` LaTeX
  environment now produces `["Determinant", ["Matrix", ...]]` instead of
  `["Matrix", ..., "'||'"]`. Serialization round-trips correctly back to
  `\begin{vmatrix}...\end{vmatrix}` when the argument is a `Matrix` expression,
  and uses `\det\left(...\right)` for symbol arguments.

- **`\begin{Vmatrix}` now parses to `Norm`**: The `Vmatrix` LaTeX environment
  now produces `["Norm", ["Matrix", ...]]` instead of `["Matrix", ..., "'‖‖'"]`.
  Serialization round-trips to `\begin{Vmatrix}...\end{Vmatrix}` when the
  argument is a `Matrix`, and uses `\left\Vert...\right\Vert` for symbol
  arguments.

- **`A^{-1}` produces `Inverse` for matrix-typed symbols and matrix
  expressions**: When a symbol is declared with type `matrix`, parsing `A^{-1}`
  now returns `["Inverse", "A"]` instead of `["Power", "A", -1]`. This also
  works for inline matrix expressions, e.g.
  `\begin{pmatrix}...\end{pmatrix}^{-1}`. Undeclared symbols still fall through
  to the default `Power`/`Divide` handling, and function symbols still produce
  `InverseFunction` (e.g., `\sin^{-1}` &rarr; `Arcsin`).

- **`Inverse` serializes as `^{-1}`**: `["Inverse", "A"]` now serializes to
  `A^{-1}` instead of `\mathrm{Inverse}(A)`.

- **`Power(A, -1)` canonicalizes to `Inverse(A)` for matrices**: When `A` has a
  matrix type, `ce.box(["Power", "A", -1])` now canonicalizes to
  `["Inverse", "A"]` instead of `["Divide", 1, "A"]`.

- **`\det(A)` and `\tr(A)` now parse correctly**: Fixed `Determinant` and
  `Trace` LaTeX dictionary entries to use `latexTrigger` (`\det`, `\tr`) instead
  of `symbolTrigger`, which only matches plain identifiers. Both functions also
  accept plain text forms (`det(A)`, `tr(A)`).

- **`\det A` and `\tr A` work without parentheses**: `Determinant` and `Trace`
  now accept implicit arguments, so `\det A` parses as `["Determinant", "A"]`
  (like `\cos x` parses as `["Cos", "x"]`). Implicit arguments bind at
  multiplication precedence, so `\det 2A + 1` parses as `det(2A) + 1`.

- **`Determinant` serialization uses `\det A` for simple arguments**: Symbol
  arguments serialize as `\det A` instead of `\det\left(A\right)`. Matrix
  arguments still serialize as `\begin{vmatrix}...\end{vmatrix}`.

- **Added standard LaTeX operators `\ker`, `\dim`, `\deg`, `\hom`**: These
  commands are now in the MathJSON LaTeX dictionary as function entries with
  implicit arguments, so forms like `\ker V`, `\dim V`, `\deg p`, and
  `\hom(V, W)` parse correctly and serialize back to the corresponding standard
  operator notation. The corresponding function symbols (`Kernel`, `Dimension`,
  `Degree`, `Hom`) are also registered in the linear algebra library.

- **Implemented runtime evaluation for `Kernel`, `Dimension`, `Degree`, and
  `Hom`**:
  - `Kernel` now computes a numeric null-space basis (for scalar/vector/matrix
    real inputs) and returns it as a list of basis vectors.
  - `Dimension` now evaluates finite dimensions for concrete tensors and
    collections, and computes `dim(Hom(V, W)) = dim(V) * dim(W)` when both
    dimensions are inferable.
  - `Degree` now evaluates polynomial degree for polynomial-form expressions
    while keeping ambiguous bare symbols (for example `Degree(p)`) unevaluated.
  - `Hom` now evaluates/simplifies its arguments while preserving the symbolic
    `Hom(...)` form.

#### LaTeX Parsing

- **`arguments: 'implicit'` option for function dictionary entries**: Function
  entries in the LaTeX dictionary can now set `arguments: 'implicit'` to accept
  bare arguments without parentheses (e.g., `\det A`), matching the behavior of
  trig functions. The default remains `'enclosure'` (parentheses required).
  Applied to `\det`, `\tr`, `\Re`, `\Im`, `\arg`, `\max`, `\min`, `\sup`,
  `\inf`.

### Simplification

- **Infinity handling for 24+ functions**: `arctan(∞)`, `arccot(±∞)`,
  `tanh/coth/sech/csch(±∞)`, `arsinh(-∞)`, `arcosh(-∞)`, `arccoth(±∞)`,
  `arcsch(±∞)`, `π^∞`, `∞^n`, `(-∞)^{-n}`, `log_∞(x)`, `log_{0.5}(∞)`, `√∞`,
  `∛∞` now all return correct limits.

- **Root edge cases**: `Root(x, 0) → NaN`, `Root(0, n)`, `Root(1, n)`,
  `Root(+∞, n)`, and `Sqrt(+∞)` now handled correctly.

- **Division edge cases**: `a/a → 1` now works for compound expressions (e.g.,
  `(π+1)/(π+1)`); `2/0 → ComplexInfinity` and `1/(1/0) → 0` propagate correctly.

- **Logarithm edge cases**: Fixed infinity detection in `simplify-log.ts` (was
  using `sym()` which fails on `BoxedNumber` infinity values); added
  `log_∞(∞) → NaN`, base-aware `log_c(0)`, guards for `log_1(x)` and
  `log_c(c^x)` evaluation.

- **Absolute value of odd functions**: `|arcsin(x)|`, `|sinh(x)|`,
  `|arsinh(x)|`, `|artanh(x)|` now simplify to `f(|x|)`.

- **Even function with abs argument**: `cosh(|x+2|) → cosh(x+2)`.

- **Trig period shifts**: `cot(π+x) → cot(x)`, `csc(π+x) → -csc(x)`.

- **Ln simplification in Add/Multiply operands**: `ln(x^3) − 3·ln(x) → 0` and
  `ln(x^√2) → √2·ln(x)` now work; cost function bypassed for log rules that are
  mathematically valid but structurally more expensive.

- **Preserved function identity**: Removed unconditional expansions of
  `sinh/cosh → exp`, `arsinh/arcosh/artanh → ln`, and `arcsin → arctan2` that
  prevented abs/odd-function rules from firing.

### Compilation

- **WGSL (WebGPU Shading Language) Compilation Target**: New built-in WGSL
  target for compiling mathematical expressions to WebGPU shaders.

  ```ts
  // Via the registry
  const result = compile(expr, { to: 'wgsl' });
  ```

  WGSL-specific differences from GLSL:
  - `inverseSqrt` (camelCase) instead of `inversesqrt`
  - `%` operator for mod instead of `mod()` function
  - `vec2f`/`vec3f`/`vec4f` constructors instead of `vec2`/`vec3`/`vec4`
  - `array<f32, n>()` instead of `float[n]()`
  - `fn name(x: f32) -> f32` instead of `float name(float x)`
  - `@vertex`/`@fragment`/`@compute` entry points with struct-based I/O
  - `@group`/`@binding` uniform declarations and `@workgroup_size` for compute

- **Interval WGSL Compilation Target**: New `interval-wgsl` target for interval
  arithmetic in WebGPU shaders, mirroring the existing `interval-glsl` target.
  Since WGSL does not support function overloading, the library uses `_v`
  suffixes for internal vec2f-parameter implementations (e.g., `ia_add_v`),
  while the public API (`ia_add`, `ia_sin`, etc.) takes `IntervalResult` values.

### Resolved Issues

- **`Sequence` type inference now returns a proper tuple type**: Multi-argument
  `Sequence` expressions previously returned `'any'` as their inferred type,
  losing all type information. They now return a `tuple<...>` type with each
  element's individual type preserved (e.g., `Sequence(1, "a")` types as
  `tuple<integer, string>`), consistent with the `Tuple` operator.

- **Subscript parsing now checks for collection type**: The LaTeX subscript
  (`_`) parser now checks whether the LHS is a collection (symbol declared as
  `indexed_collection`, or a list literal) and produces `At()` directly at parse
  time, consistent with bracket indexing (`x[i]`). Multi-index subscripts on
  collections (`A_{k,j}`) are now correctly unpacked into separate `At`
  arguments instead of being wrapped in a `Tuple`.

- **`NumericValue(0).mul(Infinity)` now returns NaN**: All three `NumericValue`
  subclasses (`MachineNumericValue`, `BigNumericValue`, `ExactNumericValue`) had
  an early-return `if (this.isZero) return this` in `mul()`, which returned `0`
  without checking if the other operand was infinity. `0 × ±∞` is now correctly
  indeterminate (`NaN`), and `±∞ × 0` is handled symmetrically.

- **Power simplification `(a^n)^m -> a^{nm}` now correctly guarded**: The rule
  was applied unconditionally, which is mathematically incorrect when the base
  can be negative and exponents are non-integer. The classic counterexample:
  `((-1)^2)^{1/2} = 1`, but `(-1)^{2·1/2} = -1`. The rule is now only applied
  when: (1) the base is non-negative, (2) the outer exponent is an integer, or
  (3) the inner exponent is an odd integer. This fix applies to canonicalization
  (`canonicalPower`), the `pow()` helper, and simplification (`simplifyPower`).
  As a result, `(x^2)^{1/2}` now correctly simplifies to `|x|` instead of `x`.

- **Power distribution rules now guarded for non-integer exponents**: Three
  additional power distribution rules in `pow()` were applied unconditionally,
  producing wrong results when the exponent is non-integer and operands are
  negative. (1) `(a/b)^c -> a^c / b^c` — e.g. `((-2)(-3))^{1/2} = sqrt(6)` but
  distributing gives `(-2)^{1/2} * (-3)^{1/2} = -sqrt(6)`. (2)
  `(a*b)^c -> a^c * b^c` — same class of bug. (3) `(-x)^n` used `n % 2 === 0` to
  test parity, but for non-integer `n` (e.g. 0.5), `0.5 % 2 = 0.5` falls to the
  odd branch, giving `(-x)^{0.5} -> -(x^{0.5})` which is wrong. All three rules,
  plus the corresponding `canonicalPower()` Divide rule, now require integer
  exponents (or non-negative operands) before distributing.

- **Sqrt/Root exponent rearrangement now guarded**: Two more rules in `pow()`
  unconditionally rearranged exponents. (1) `(√a)^b -> √(a^b)` rearranges
  `(a^{1/2})^b` to `(a^b)^{1/2}`, which is wrong for negative `a` (e.g.
  `(√(-4))^3 = -8i` but `√((-4)^3) = 8i`). Now only applied when `a >= 0`. The
  even-integer branches (`(√a)^2 -> a`, `(√a)^{2k} -> a^k`) remain unconditional
  since integer outer exponents are always safe. (2) `Root(a,b)^c -> a^{c/b}`
  combined exponents unconditionally. Now guarded with `a >= 0` or `c` is
  integer. Audit of `simplify-power.ts` confirmed all rules there are already
  properly guarded.

- **Relational operators now evaluate**: Seven relational operators
  (`TildeFullEqual`, `TildeEqual`, `Approx`, `ApproxEqual`, `ApproxNotEqual`,
  `Precedes`, `Succeeds`) previously had `canonical` handlers but no `evaluate`
  handlers, so expressions like `Approx(3.14, 3.14)` returned unevaluated. The
  approximate-equality family (`TildeFullEqual`, `TildeEqual`, `Approx`,
  `ApproxEqual`) now checks whether `|a - b| <= tolerance` via `ce.chop()`, with
  support for multi-argument chains. `Precedes` and `Succeeds` evaluate as
  numeric `<` and `>` respectively. Negated variants (`NotApprox`,
  `NotTildeFullEqual`, etc.) work automatically through the `Not` operator.

- **`BoxedNumber.operator` now returns specific numeric types**: The `operator`
  property on `BoxedNumber` instances previously returned the generic `'Number'`
  for all numeric values. It now returns specific types that match the internal
  type system: `'Integer'` for integers, `'Rational'` for non-integer rationals,
  `'Real'` for floating-point numbers, `'Complex'` for complex numbers with
  non-zero imaginary part, and `'NaN'`, `'PositiveInfinity'`,
  `'NegativeInfinity'` for special values. This improves API consistency with
  the `type` property and enables more precise pattern matching and type
  discrimination in user code. **Breaking change**: Code that explicitly checks
  for `.operator === 'Number'` will need to be updated to check for specific
  numeric types or use the `isNumber()` type guard instead.

- **Non-XIDC Unicode characters in symbol names now encoded correctly**: When
  parsing LaTeX symbols containing non-identifier Unicode characters via
  `\unicode{...}`, `\char`, or `^^XX` escapes (e.g., figure dash U+2012 in
  `\operatorname{speed\unicode{"2012}of\unicode{"2012}sound}`), the characters
  are now encoded as `____XXXXXX` (4 underscores + 6 hex digits) in the symbol
  name. This encoding is valid per `isValidSymbol()` and round-trips correctly:
  the serializer decodes `____XXXXXX` back to `\unicode{"XXXX"}` in LaTeX
  output. Previously, these characters passed through raw and caused symbol
  validation to fail.

- **Assign to compound symbol names no longer misinterpreted as sequence
  definitions** (fixes
  [#286](https://github.com/cortex-js/compute-engine/issues/286)):
  `ce.box(["Assign", "t_half", 10])` previously failed because the Assign
  evaluate handler split any symbol containing `_` and treated it as a
  subscripted sequence definition. User-provided compound symbols like `t_half`
  or `half_life` are now assigned correctly. Sequence definitions via parsed
  LaTeX (e.g., `L_0 := 1`) continue to work as before.

## 0.35.6 _2026-02-07_

### Resolved Issues

- **Monte Carlo improper integrals**: Fixed two bugs in `monteCarloEstimate()`
  that produced incorrect results (typically `NaN` or `Infinity`) for improper
  integrals. The change-of-variables estimator was inverted
  ($f(x) / \mathrm{jacobian}$ instead of $f(x) * \mathrm{jacobian}$), and the
  finite-interval scale factor $b - a$ was applied to transformed domains where
  it is infinite. Affects `NIntegrate` and compiled `integrate` for any integral
  with infinite bounds.

### Compilation

- **`Truncate`, `Remainder`, and `Mod` for JS/GLSL targets**: Added `Truncate`
  (`Math.trunc` / `trunc`), `Remainder`, and `Mod` to the JavaScript and GLSL
  compilation targets, matching the Python target which already had them.

- **Interval `trunc` and `remainder`**: Added `trunc()` and `remainder()` to the
  interval arithmetic library. `trunc` has proper discontinuity detection
  (behaves like `floor` for positive, `ceil` for negative, continuous at zero).
  `remainder(a, b) = a - b * round(a/b)` composes existing interval operations
  with discontinuity detection inherited from `round`. Added corresponding
  mappings to both interval JavaScript and interval GLSL targets.

- **Interval `Lb`, `Log`, and `Root` for GLSL**: Added `ia_log2`, `ia_log10`,
  and `Root` to the interval GLSL target for consistency with the interval
  JavaScript target.

- **Reverse cross-reference test**: Added a test that verifies all core CE math
  functions have compilation support in every target. Currently all 5 targets
  have full coverage of the 47 compilable math functions.

## 0.35.5 _2026-02-06_

### Resolved Issues

- **Compilation Target Function Name Mismatches**: Fixed several function keys
  in compilation targets that did not match their canonical library operator
  names, causing silent compilation failures and runtime errors ("Unexpected
  value"). Affected mappings: `Ceiling` → `Ceil`, `Sgn` → `Sign`, `LogGamma` →
  `GammaLn`, `Arcsinh` → `Arsinh`, `Arccosh` → `Arcosh`, `Arctanh` → `Artanh`,
  `Re` → `Real`, `Im` → `Imaginary`, `Arg` → `Argument` across all five
  compilation targets.

- **Missing Library Operator Definitions**: Added library definitions for
  `Exp2`, `Fract`, `Log10`, `Log2`, `Remainder`, and `Truncate` which were
  referenced by compilation targets but had no corresponding library entries.
  `Exp2` canonicalizes to `Power(2, x)`, `Log10`/`Log2` canonicalize to `Log`
  with the appropriate base, and `Fract`, `Remainder`, `Truncate` have direct
  numeric evaluation.

- **Derivative Rule for GammaLn**: Fixed the derivative table entry that used
  the non-canonical name `LogGamma` instead of `GammaLn`, preventing the
  derivative `d/dx GammaLn(x) = Digamma(x)` from being computed.

## 0.35.4 _2026-02-06_

### Interval Arithmetic

- **Discontinuity Continuity Direction**: Singular interval results now include
  an optional `continuity` field (`'left'` or `'right'`) indicating from which
  side the function is continuous at a jump discontinuity. `Floor`, `Round`,
  `Fract`, and `Mod` report `'right'` (right-continuous), `Ceil` reports
  `'left'` (left-continuous). Pole-type singularities (e.g., `tan`, `1/x`) leave
  the field undefined. This is reflected in both the JavaScript and GLSL
  interval arithmetic targets (new `IA_SINGULAR_RIGHT` and `IA_SINGULAR_LEFT`
  status constants in GLSL).

## 0.35.3 _2026-02-06_

### Compilation

- **Expanded Function Support Across All Targets**: Added comprehensive function
  mappings to all five compilation targets (JavaScript, GLSL, Interval GLSL,
  Interval JavaScript, Python): reciprocal trig (`Cot`, `Csc`, `Sec`), inverse
  reciprocal trig (`Arccot`, `Arccsc`, `Arcsec`), hyperbolic (`Sinh`, `Cosh`,
  `Tanh`), reciprocal hyperbolic (`Coth`, `Csch`, `Sech`), inverse hyperbolic
  (`Arcosh`, `Arsinh`, `Artanh`, `Arcoth`, `Arcsch`, `Arsech`), and elementary
  functions (`Sgn`, `Lb`, `Log` with base, `Square`, `Root`, `Fract`).

- **Interval Discontinuity Detection**: `Floor`, `Ceil`, `Round`, `Sign`,
  `Fract`, and `Mod` now correctly report singularities when an interval spans a
  discontinuity point, in both the JavaScript and GLSL interval arithmetic
  targets. Previously these functions returned normal interval bounds even
  across jump discontinuities, which could cause incorrect connecting lines in
  plotted curves.

- **New Interval Functions**: Added `Round`, `Fract`, and `Mod` to the interval
  arithmetic targets (both JS and GLSL) with proper discontinuity detection.

## 0.35.2 _2026-02-05_

### Resolved Issues

- **Decimal Number Representation**: Numbers written with a decimal point (e.g.,
  `6.02e23`) are now correctly treated as approximate decimal values
  (`BigNumericValue`) rather than exact integers. Previously, `6.02e23` was
  incorrectly converted to the exact bigint `602000000000000000000000`, which
  implied false precision and caused memory inefficiency for very large
  exponents. Numbers without a decimal point (e.g., `602e21`) continue to be
  treated as exact integers when possible. This change aligns with the
  documented behavior of the `parseNumbers: 'auto'` option.

- **Scientific Notation Serialization**
  ([#284](https://github.com/cortex-js/compute-engine/issues/284)): Fixed
  `toLatex()` with `scientific` and `adaptiveScientific` notation options to
  produce properly normalized output. Previously, numbers like `6.02e23` would
  serialize as `602\cdot10^{21}` instead of the expected `6.02\cdot10^{23}`. The
  output now depends only on the numeric value and formatting options, not on
  the internal representation.

- **Numeric Sum Precision**: Fixed precision loss when summing large integers
  with rational values (e.g., `12345678^3 + 1/3`). The `ExactNumericValue.sum()`
  method now uses `bignumRe` instead of `re` to preserve full precision when
  handling large integer values from `BigNumericValue`.

- **Broadcastable Functions with Union/Any Types**
  ([#235](https://github.com/cortex-js/compute-engine/issues/235)):
  Broadcastable (threadable) functions like `Multiply` and `Add` no longer
  reject arguments whose type is a union of numeric and collection types (e.g.,
  `number | list`) or `any`. Previously, declaring a symbol as
  `ce.declare('a', 'number | list')` and using it in
  `ce.box(['Multiply', 'a', 'b'])` would produce an `incompatible-type` error.

- **Division Canonicalization Over-Simplification**
  ([#227](https://github.com/cortex-js/compute-engine/issues/227)): Fixed `A/A`
  being incorrectly simplified to `1` during canonicalization for constant
  expressions that evaluate to infinity or zero, such as `tan(π/2)/tan(π/2)`.
  This now correctly evaluates to `NaN` (since `∞/∞` is indeterminate) instead
  of `1`. Expressions with free variables (e.g., `x/x`, `sin(x)/sin(x)`)
  continue to simplify to `1` per standard algebraic convention. Also fixed
  deferred constant divisions like `0/(1-1)` and `(1-1)/(1-1)` to properly
  evaluate to `NaN` instead of remaining as unevaluated expressions.

## 0.35.1 _2026-02-03_

### Resolved Issues

- **Interval Arithmetic (JS/GLSL)**: Fixed interval evaluation of compound
  arguments (e.g. `sin(2x)`, `sin(x+x)`, `sin(x^2)`, `cos(2x)`) by propagating
  interval results through trig, elementary, and comparison functions in
  `interval-js`, and by adding `IntervalResult` overloads to the GLSL interval
  library for `interval-glsl`.

## 0.35.0 _2026-02-02_

### Parsing

- **Large Integer Precision**: Fixed precision loss when parsing integers
  exceeding `Number.MAX_SAFE_INTEGER` with `parseNumbers: 'rational'`. Large
  integers and rational numerators now use BigInt arithmetic to preserve exact
  values. Fixes #283.

### Compilation

- **Interval Arithmetic Targets**: Added two new compilation targets for
  reliable singularity detection:
  - `interval-js` - Compiles to JavaScript using interval arithmetic
  - `interval-glsl` - Compiles to GLSL for GPU-based interval evaluation

## 0.34.0 _2026-02-01_

### Parsing

- **`\mathopen` and `\mathclose`**: The LaTeX parser supports `\mathopen` and
  `\mathclose` delimiter prefixes for matchfix operators (explicit delimiter
  spacing control), e.g. `\mathopen(a, b\mathclose)` and
  `\mathopen{(}a, b\mathclose{)}`.

- **Interval Notation Parsing**: Added support for parsing mathematical interval
  notation from LaTeX, including half-open intervals. Addresses #254.

  ```javascript
  // Half-open intervals (American notation)
  ce.parse('[3, 4)').json;   // → ["Interval", 3, ["Open", 4]]
  ce.parse('(3, 4]').json;   // → ["Interval", ["Open", 3], 4]

  // Open intervals (ISO/European notation)
  ce.parse(']3, 4[').json;   // → ["Interval", ["Open", 3], ["Open", 4]]

  // LaTeX bracket commands and sizing prefixes
  ce.parse('\\lbrack 3, 4\\rparen').json;  // → ["Interval", 3, ["Open", 4]]
  ce.parse('\\left[ 3, 4 \\right)').json;  // → ["Interval", 3, ["Open", 4]]
  ce.parse('\\bigl( 3, 4 \\bigr]').json;   // → ["Interval", ["Open", 3], 4]
  ```

  **Contextual Parsing**: Lists and tuples are automatically converted to
  intervals when used in set contexts (Element, Union, Intersection, etc.):

  ```javascript
  ce.parse('x \\in [0, 1]').json;
  // → ["Element", "x", ["Interval", 0, 1]]

  ce.parse('[0, 1] \\cup [2, 3]').json;
  // → ["Union", ["Interval", 0, 1], ["Interval", 2, 3]]

  // Standalone notation remains backward compatible
  ce.parse('[0, 1]').json;  // → ["List", 0, 1]
  ce.parse('(0, 1)').json;  // → ["Tuple", 0, 1]
  ```

### Compilation

- **Custom Operator Compilation**: The `compile()` method now supports
  overriding operators to use function calls instead of native operators. This
  enables compilation of vector/matrix operations and custom domain-specific
  languages. Addresses #240.

  ```javascript
  // Override operators for vector operations
  const expr = ce.parse('v + w');
  const compiled = expr.compile({
    operators: {
      Add: ['add', 11],      // Convert + to add() function
      Multiply: ['mul', 12]  // Convert * to mul() function
    },
    functions: {
      add: (a, b) => a.map((v, i) => v + b[i]),
      mul: (a, b) => a.map((v, i) => v * b[i])
    }
  });

  const result = compiled({ v: [1, 2, 3], w: [4, 5, 6] });
  // → [5, 7, 9]
  ```

  Highlights:
  - Map operators via an object or a function
  - Function-name operators compile to calls; symbol operators compile to infix
  - Supports scalar/collection arguments and partial overrides

- **Exported Compilation Interfaces**: Advanced users can now create custom
  compilation targets by using the exported `CompileTarget` interface,
  `BaseCompiler` class, and `JavaScriptTarget` class.

  ```javascript
  import { BaseCompiler, JavaScriptTarget } from '@cortex-js/compute-engine';

  // Create a custom compilation target
  const customTarget = {
    language: 'my-dsl',
    operators: (op) => ({ Add: ['ADD', 11], Multiply: ['MUL', 12] }[op]),
    functions: (id) => id.toUpperCase(),
    var: (id) => `VAR("${id}")`,
    string: (s) => `"${s}"`,
    number: (n) => n.toString(),
    ws: () => ' ',
    preamble: '',
    indent: 0,
  };

  const expr = ce.parse('x + y * 2');
  const code = BaseCompiler.compile(expr, customTarget);
  // → "ADD(VAR("x"), MUL(VAR("y"), 2))"
  ```

  Exported building blocks include `CompileTarget`, `LanguageTarget`,
  `CompilationOptions`, `CompiledExecutable`, `BaseCompiler`,
  `JavaScriptTarget`, and `GLSLTarget` (plus helper types like
  `CompiledOperators` and `CompiledFunctions`).

- **Compilation Plugin Architecture**: The Compute Engine now supports
  registering custom compilation targets, allowing you to compile mathematical
  expressions to any target language beyond the built-in JavaScript and GLSL
  targets.

  ```javascript
  import { ComputeEngine, BaseCompiler } from '@cortex-js/compute-engine';

  const ce = new ComputeEngine();

  // Define a custom Python target
  class PythonTarget {
    // ... implementation (see documentation)
  }

  // Register the custom target
  ce.registerCompilationTarget('python', new PythonTarget());

  // Compile to Python
  const expr = ce.parse('\\sin(x) + \\cos(y)');
  const pythonCode = expr.compile({ to: 'python' });
  console.log(pythonCode.toString());
  // → math.sin(x) + math.cos(y)

  // Switch between targets
  const jsFunc = expr.compile({ to: 'javascript' });
  const glslCode = expr.compile({ to: 'glsl' });
  ```

  Notes:
  - Built-in targets: `javascript` (executable) and `glsl` (shader code)
  - Add targets via `ce.registerCompilationTarget(name, target)`
  - Switch targets with `compile({ to: ... })` (or override once with `target`)

- **Python/NumPy Compilation Target**: Added a complete Python/NumPy compilation
  target for scientific computing workflows. The `PythonTarget` class compiles
  mathematical expressions to NumPy-compatible Python code.

  ```javascript
  import { ComputeEngine, PythonTarget } from '@cortex-js/compute-engine';

  const ce = new ComputeEngine();
  const python = new PythonTarget({ includeImports: true });

  // Register the target
  ce.registerCompilationTarget('python', python);

  // Compile expressions to Python
  const expr = ce.parse('\\sin(x) + \\cos(y)');
  const code = expr.compile({ to: 'python' });
  console.log(code.toString());
  // → import numpy as np
  //
  //   np.sin(x) + np.cos(y)

  // Generate complete Python functions
  const func = python.compileFunction(
    ce.parse('\\sqrt{x^2 + y^2}'),
    'magnitude',
    ['x', 'y'],
    'Calculate vector magnitude'
  );
  // Generates:
  // import numpy as np
  //
  // def magnitude(x, y):
  //     """Calculate vector magnitude"""
  //     return np.sqrt(x ** 2 + y ** 2)
  ```

  Highlights:
  - NumPy-compatible output (including arrays)
  - Function mapping for common math + linear algebra
  - Helpers for full functions, lambdas, and vectorized code

  See the [Python/NumPy Target Guide](/compute-engine/guides/python-target/) for
  complete documentation and examples.

- **GLSL Compilation Target**: New built-in GLSL (OpenGL Shading Language)
  target for compiling mathematical expressions to WebGL shaders.

  ```javascript
  const expr = ce.parse('x^2 + y^2');
  const glslCode = expr.compile({ to: 'glsl' });
  console.log(glslCode.toString());
  // → pow(x, 2.0) + pow(y, 2.0)

  // Generate complete GLSL functions
  import { GLSLTarget } from '@cortex-js/compute-engine';
  const glsl = new GLSLTarget();

  const distExpr = ce.parse('\\sqrt{x^2 + y^2 + z^2}');
  const func = glsl.compileFunction(distExpr, 'distance3D', 'float', [
    ['x', 'float'],
    ['y', 'float'],
    ['z', 'float'],
  ]);
  console.log(func);
  // → float distance3D(float x, float y, float z) {
  //     return sqrt(pow(x, 2.0) + pow(y, 2.0) + pow(z, 2.0));
  //   }

  // Generate complete shaders
  const shader = glsl.compileShader({
    type: 'fragment',
    version: '300 es',
    outputs: [{ name: 'fragColor', type: 'vec4' }],
    body: [
      {
        variable: 'fragColor',
        expression: ce.box(['List', 1, 0, 0, 1]),
      },
    ],
  });
  ```

  Highlights:
  - Native vector/matrix operators and constructors
  - Float literal formatting (`2.0`)
  - Helpers for functions and complete shaders

### Algebra

- **Polynomial Factoring**: The `Factor` function now supports comprehensive
  polynomial factoring including perfect square trinomials, difference of
  squares, and quadratic factoring with rational roots. Addresses #180 and #33.

  ```javascript
  // Perfect square trinomials
  ce.parse('x^2 + 2x + 1').factor().latex;
  // → "(x+1)^2"

  ce.parse('4x^2 + 12x + 9').factor().latex;
  // → "(2x+3)^2"

  // Difference of squares
  ce.parse('x^2 - 4').factor().latex;
  // → "(x-2)(x+2)"

  // Quadratic with rational roots
  ce.box(['Factor', ['Add', ['Power', 'x', 2], ['Multiply', 5, 'x'], 6], 'x'])
    .evaluate().latex;
  // → "(x+2)(x+3)"
  ```

  **Automatic Factoring in sqrt Simplification**: Square roots now automatically
  factor their arguments before applying simplification rules, enabling
  expressions like `√(x²+2x+1)` to simplify to `|x+1|`.

  ```javascript
  // Issue #180 - Now works!
  ce.parse('\\sqrt{x^2 + 2x + 1}').simplify().latex;
  // → "\\vert x+1\\vert"

  ce.parse('\\sqrt{4x^2 + 12x + 9}').simplify().latex;
  // → "\\vert 2x+3\\vert"

  ce.parse('\\sqrt{a^2 + 2ab + b^2}').simplify().latex;
  // → "\\vert a+b\\vert"
  ```

  Includes perfect square trinomials, difference of squares, and quadratics with
  rational roots. Helper functions are exported for advanced usage
  (`factorPerfectSquare`, `factorDifferenceOfSquares`, `factorQuadratic`,
  `factorPolynomial`).

  **MathJSON API**:

  ```json
  ["Factor", expr]              // Auto-detect variable
  ["Factor", expr, variable]    // Explicit variable specification
  ```

  The enhanced factoring system works seamlessly with existing polynomial
  functions like `Expand`, `Together`, `Cancel`, `PolynomialGCD`, and others.

### Simplification

- **Absolute Value Power Simplification**: Fixed simplification of `|x^n|`
  expressions with even and rational exponents. Previously, expressions like
  `|x²|` and `|x^{2/3}|` were not simplified. Now they correctly simplify based
  on the parity of the exponent's numerator. Addresses #181.

  ```javascript
  ce.parse('|x^2|').simplify().latex;      // → "x^2" (even exponent)
  ce.parse('|x^3|').simplify().latex;      // → "|x|^3" (odd exponent)
  ce.parse('|x^{2/3}|').simplify().latex;  // → "x^{2/3}" (even numerator)
  ce.parse('|x^{3/2}|').simplify().latex;  // → "|x|^{3/2}" (odd numerator)
  ```

- **Assumption-Based Simplification**: Simplification rules use assumptions
  about symbol signs:

  ```javascript
  ce.assume(ce.parse('x > 0'));
  ce.parse('\\sqrt{x^2}').simplify().latex;  // → "x" (was "|x|")
  ce.parse('|x|').simplify().latex;          // → "x" (was "|x|")

  ce.assume(ce.parse('y < 0'));
  ce.parse('\\sqrt{y^2}').simplify().latex;  // → "-y"
  ce.parse('|y|').simplify().latex;          // → "-y"
  ```

- **Nested Root Simplification**: Nested roots simplify to a single root:

  ```javascript
  ce.box(['Sqrt', ['Sqrt', 'x']]).simplify()     // → root(4)(x)
  ce.box(['Root', ['Root', 'x', 3], 2]).simplify() // → root(6)(x)
  ce.box(['Sqrt', ['Root', 'x', 3]]).simplify()  // → root(6)(x)
  ```

  Applies to all combinations: `sqrt(sqrt(x))`, `root(sqrt(x), n)`,
  `sqrt(root(x, n))`, and `root(root(x, m), n)`.

- **Extended Coefficient Factoring in Power Combination**: The power combination
  rule now handles additional coefficient forms when combining same-base powers
  in products:
  - **Multi-prime coefficients**: `12·2ˣ·3ˣ` &rarr; `2^(x+2)·3^(x+1)` (since 12
    = 2²·3). All primes in the factorization must have a matching base.
    Non-matching multi-prime coefficients like `6·2ˣ` are left unchanged.
  - **Negative coefficients**: `-4·2ˣ` &rarr; `-2^(x+2)`, `-8·2ˣ` &rarr;
    `-2^(x+3)`. The absolute value is factored and the sign is preserved.
  - **Rational-radical coefficients**: `√2·2ˣ` &rarr; `2^(x+½)`, `2√2·2ˣ` &rarr;
    `2^(x+3/2)`, `(√2/2)·2ˣ` &rarr; `2^(x-½)`. Decomposes `(num/den)·√radical`
    into prime contributions from all three components (radical primes get
    half-integer exponents, numerator primes get positive exponents, denominator
    primes get negative exponents).
  - **Rational coefficients**: `2ˣ/4` &rarr; `2^(x-2)`, `3ˣ/9` &rarr; `3^(x-2)`.
    Factors both numerator (positive exponents) and denominator (negative
    exponents).

- **Improved Cost Function for Negated Powers**: `Negate(Power(...))` now costs
  `3 + cost(exponent)`, consistent with the cost of `Multiply(-1, Power(...))`.
  This makes the cost model more accurate when comparing negated power forms.

### Assumptions & Types

- **Improved `ask()` Queries**: `ce.ask()` now matches patterns with wildcards
  correctly, can answer common "bound" queries such as
  `ask(["Greater", "x", "_k"])` and `ask(["Greater", "_x", "_k"])`, normalizes
  inequality patterns for matching (e.g. `ask(["Greater", "_x", 0])`), and falls
  back to `verify()` for closed predicates when the fact is known but not stored
  as an explicit assumption.

- **Tri-state `verify()`**: Implemented `ce.verify()` as a truth query that
  returns `true`, `false` or `undefined` when a predicate cannot be determined
  from the current assumptions and declarations. `And`/`Or`/`Not` use 3-valued
  logic.

- **`Element`/`NotElement` Type Membership**: `Element(x, T)` and
  `NotElement(x, T)` now support type-style RHS (e.g. `real`, `finite_real`,
  `number`, `any`) in addition to set collections (e.g. `RealNumbers`,
  `Integers`).

- **Value Resolution from Equality Assumptions**: After
  `ce.assume(['Equal', symbol, value])`, the symbol now evaluates to the assumed
  value:

  ```javascript
  ce.assume(ce.box(['Equal', 'one', 1]));
  ce.box('one').evaluate();               // → 1 (was: 'one')
  ce.box(['Equal', 'one', 1]).evaluate(); // → True (was: ['Equal', 'one', 1])
  ce.box(['Equal', 'one', 0]).evaluate(); // → False
  ce.box('one').type.matches('integer');  // → true
  ```

  This also fixes comparison evaluation: `Equal(symbol, assumed_value)` now
  correctly evaluates to `True` instead of staying symbolic.

- **Inequality Evaluation Using Assumptions**: Inequality comparisons can use
  transitive bounds extracted from assumptions.

  ```javascript
  ce.assume(ce.box(['Greater', 'x', 4]));
  ce.box(['Greater', 'x', 0]).evaluate();  // → True (x > 4 > 0)
  ce.box(['Less', 'x', 0]).evaluate();     // → False
  ce.box('x').isGreater(0);                // → true
  ce.box('x').isPositive;                  // → true
  ```

- **Type Inference from Assumptions**: Inequalities infer `real`; equalities
  infer from the value.

  ```javascript
  ce.assume(ce.box(['Greater', 'x', 4]));
  ce.box('x').type.toString();  // → 'real' (was: 'unknown')

  ce.assume(ce.box(['Equal', 'one', 1]));
  ce.box('one').type.toString();  // → 'integer' (was: 'unknown')
  ```

- **Tautology and Contradiction Detection**: `ce.assume()` returns `'tautology'`
  for redundant assumptions and `'contradiction'` for conflicts.

  ```javascript
  ce.assume(ce.box(['Greater', 'x', 4]));

  // Redundant assumption (x > 4 implies x > 0)
  ce.assume(ce.box(['Greater', 'x', 0]));  // → 'tautology' (was: 'ok')

  // Conflicting assumption (x > 4 contradicts x < 0)
  ce.assume(ce.box(['Less', 'x', 0]));     // → 'contradiction'

  // Same assumption repeated
  ce.assume(ce.box(['Equal', 'one', 1]));
  ce.assume(ce.box(['Equal', 'one', 1]));  // → 'tautology'

  // Conflicting equality
  ce.assume(ce.box(['Less', 'one', 0]));   // → 'contradiction'
  ```

### Solving

- **Systems of Linear Equations**: The `solve()` method now handles systems of
  linear equations parsed from LaTeX `\begin{cases}...\end{cases}` environments.
  Returns an object mapping variable names to their solutions.

  ```javascript
  const e = ce.parse('\\begin{cases}x+y=70\\\\2x-4y=80\\end{cases}');
  const result = e.solve(['x', 'y']);
  console.log(result.x.json);  // 60
  console.log(result.y.json);  // 10

  // 3x3 systems work too
  const e2 = ce.parse('\\begin{cases}x+y+z=6\\\\2x+y-z=1\\\\x-y+2z=5\\end{cases}');
  const result2 = e2.solve(['x', 'y', 'z']);
  // → { x: 1, y: 2, z: 3 }
  ```

  Non-linear systems that don't match known patterns and inconsistent systems
  return `null`.

- **Non-linear Polynomial Systems**: The `solve()` method now handles certain
  non-linear polynomial systems with 2 equations and 2 variables:
  - **Product + sum pattern**: Systems like `xy = p, x + y = s` are solved by
    recognizing that x and y are roots of the quadratic `t² - st + p = 0`.

  - **Substitution method**: When one equation is linear in one variable, it
    substitutes into the other equation and solves the resulting univariate
    equation.

  Returns an array of solution objects (multiple solutions possible):

  ```javascript
  // Product + sum pattern
  const e = ce.parse('\\begin{cases}xy=6\\\\x+y=5\\end{cases}');
  const result = e.solve(['x', 'y']);
  // → [{ x: 2, y: 3 }, { x: 3, y: 2 }]

  // Substitution method
  const e2 = ce.parse('\\begin{cases}x+y=5\\\\x^2+y=7\\end{cases}');
  const result2 = e2.solve(['x', 'y']);
  // → [{ x: 2, y: 3 }, { x: -1, y: 6 }]
  ```

  Only real solutions are returned; complex solutions are filtered out.

- **Exact Rational Arithmetic in Linear Systems**: The linear system solver now
  uses exact rational arithmetic throughout the Gaussian elimination process.
  Systems with fractional coefficients produce exact fractional results rather
  than floating-point approximations.

  ```javascript
  const e = ce.parse('\\begin{cases}x+y=1\\\\x-y=1/2\\end{cases}');
  const result = e.solve(['x', 'y']);
  console.log(result.x.json);  // ["Rational", 3, 4]  (exact 3/4)
  console.log(result.y.json);  // ["Rational", 1, 4]  (exact 1/4)

  // Fractional coefficients
  const e2 = ce.parse('\\begin{cases}x/3+y/2=1\\\\x/4+y/5=1\\end{cases}');
  const result2 = e2.solve(['x', 'y']);
  // → { x: 36/7, y: -10/7 }
  ```

- **Linear Inequality Systems**: The `solve()` method now handles systems of
  linear inequalities in 2 variables, returning the vertices of the feasible
  region (convex polygon). Supports all inequality operators: `<`, `<=`, `>`,
  `>=`.

  ```javascript
  // Triangle: x >= 0, y >= 0, x + y <= 10
  const e = ce.parse('\\begin{cases}x\\geq 0\\\\y\\geq 0\\\\x+y\\leq 10\\end{cases}');
  const result = e.solve(['x', 'y']);
  // → [{ x: 0, y: 0 }, { x: 10, y: 0 }, { x: 0, y: 10 }]

  // Square: 0 <= x <= 5, 0 <= y <= 5
  const square = ce.parse('\\begin{cases}x\\geq 0\\\\x\\leq 5\\\\y\\geq 0\\\\y\\leq 5\\end{cases}');
  square.solve(['x', 'y']);
  // → [{ x: 0, y: 0 }, { x: 5, y: 0 }, { x: 5, y: 5 }, { x: 0, y: 5 }]
  ```

  Vertices are returned in counterclockwise convex hull order. Returns `null`
  for infeasible systems or non-linear constraints.

- **Under-determined Systems (Parametric Solutions)**: The `solve()` method now
  returns parametric solutions for under-determined linear systems (fewer
  equations than variables) instead of returning `null`. Free variables appear
  as themselves in the solution, with other variables expressed in terms of
  them.

  ```javascript
  // Single equation with two variables
  const e = ce.parse('\\begin{cases}x+y=5\\end{cases}');
  const result = e.solve(['x', 'y']);
  // → { x: -y + 5, y: y }  (y is a free variable)

  // Two equations with three variables
  const e2 = ce.parse('\\begin{cases}x+y+z=6\\\\x-y=2\\end{cases}');
  const result2 = e2.solve(['x', 'y', 'z']);
  // → { x: -z/2 + 4, y: -z/2 + 2, z: z }  (z is a free variable)
  ```

  Inconsistent systems still return `null`.

- **Extended Sqrt Equation Solving**: The equation solver now handles sqrt
  equations of the form `√(f(x)) = g(x)` by squaring both sides and solving the
  resulting polynomial. Extraneous roots are automatically filtered.

  ```javascript
  ce.parse('\\sqrt{x+1} = x').solve('x');      // → [1.618...] (golden ratio)
  ce.parse('\\sqrt{2x+3} = x - 1').solve('x'); // → [4.449...]
  ce.parse('\\sqrt{3x-2} = x').solve('x');     // → [1, 2]
  ce.parse('\\sqrt{x} = x').solve('x');        // → [0, 1]
  ```

- **Two Sqrt Equation Solving**: The equation solver now handles equations with
  two sqrt terms of the form `√(f(x)) + √(g(x)) = e` using double squaring. Both
  addition and subtraction forms are supported, and extraneous roots are
  automatically filtered.

  ```javascript
  ce.parse('\\sqrt{x+1} + \\sqrt{x+4} = 3').solve('x');  // → [0]
  ce.parse('\\sqrt{x} + \\sqrt{x+7} = 7').solve('x');    // → [9]
  ce.parse('\\sqrt{x+5} - \\sqrt{x-3} = 2').solve('x');  // → [4]
  ce.parse('\\sqrt{2x+1} + \\sqrt{x-1} = 4').solve('x'); // → [46 - 8√29] ≈ 2.919
  ```

- **Nested Sqrt Equation Solving**: The equation solver now handles nested sqrt
  equations of the form `√(x + √x) = a` using substitution. These patterns have
  √x inside the argument of an outer sqrt. The solver uses u = √x substitution,
  solves the resulting quadratic, and filters negative u values.

  ```javascript
  ce.parse('\\sqrt{x + 2\\sqrt{x}} = 3').solve('x');  // → [11 - 2√10] ≈ 4.675
  ce.parse('\\sqrt{x + \\sqrt{x}} = 2').solve('x');   // → [9/2 - √17/2] ≈ 2.438
  ce.parse('\\sqrt{x - \\sqrt{x}} = 1').solve('x');   // → [φ²] ≈ 2.618
  ```

- **Quadratic Equations Without Constant Term**: Added support for solving
  quadratic equations of the form `ax² + bx = 0` (missing constant term). These
  are solved by factoring: `x(ax + b) = 0` → `x = 0` or `x = -b/a`.

  ```javascript
  ce.parse('x^2 + 3x = 0').solve('x');  // → [0, -3]
  ce.parse('2x^2 - 4x = 0').solve('x'); // → [0, 2]
  ```

### Subscripts & Indexing

- **Subscript Evaluation Handler**: Define custom evaluation functions for
  subscripted symbols like mathematical sequences using `subscriptEvaluate`:

  ```javascript
  // Define a Fibonacci sequence
  ce.declare('F', {
    subscriptEvaluate: (subscript, { engine }) => {
      const n = subscript.re;
      if (!Number.isInteger(n) || n < 0) return undefined;
      // Calculate Fibonacci number...
      return engine.number(fibValue);
    },
  });

  ce.parse('F_{10}').evaluate();  // → 55
  ce.parse('F_5').evaluate();     // → 5
  ce.parse('F_n').evaluate();     // → stays symbolic (handler returns undefined)
  ```

  Both simple subscripts (`F_5`) and complex subscripts (`F_{5}`) are supported.
  When the handler returns `undefined`, the expression stays symbolic.
  Subscripted expressions with `subscriptEvaluate` have type `number` and can be
  used in arithmetic operations: `ce.parse('F_{5} + F_{3}').evaluate()` works
  correctly.

- **Type-Aware Subscript Handling**: Subscripts on symbols declared as
  collection types (list, tuple, matrix, etc.) now automatically convert to
  `At()` indexing operations:

  ```javascript
  ce.declare('v', 'list<number>');
  ce.parse('v_n');      // → At(v, n)
  ce.parse('v_{n+1}');  // → At(v, n+1)
  ce.parse('v_{i,j}');  // → At(v, Tuple(i, j))
  ```

  This works for both simple subscripts (`v_n`) and complex subscripts
  (`v_{n+1}`). The type of the `At()` expression is correctly inferred from the
  collection's element type, allowing subscripted collection elements to be used
  in arithmetic.

- **Complex Subscripts in Arithmetic** (Issue #273): Subscript expressions like
  `a_{n+1}` can now be used in arithmetic operations without type errors:

  ```javascript
  ce.parse('a_{n+1} + 1');     // → Add(Subscript(a, n+1), 1)
  ce.parse('2 * a_{n+1}');     // → Multiply(2, Subscript(a, n+1))
  ce.parse('a_{n+1}^2');       // → Power(Subscript(a, n+1), 2)
  ```

  Previously, complex subscripts would fail with "incompatible-type" errors when
  used in arithmetic contexts.

- **Multi-Index `At()` Support**: The `At` function now supports multiple
  indices for accessing nested collections (e.g., matrices):

  ```javascript
  const matrix = ce.box(['List', ['List', 2, 3, 4], ['List', 6, 7, 9]]);
  ce.box(['At', matrix, 1, 2]).evaluate();  // → 3 (row 1, column 2)
  ```

  The signature was updated from single index to variadic:
  `(value: indexed_collection, index: (number|string)+) -> unknown`

- **Text Subscripts**: Added support for `\text{}` in subscripts, allowing
  descriptive subscript names:

  ```javascript
  ce.parse('x_{\\text{max}}');  // → symbol "x_max"
  ce.parse('v_{\\text{initial}}');  // → symbol "v_initial"
  ```

### Sequences

- **Declarative Sequence Definitions**: Define mathematical sequences using
  recurrence relations with the new `declareSequence()` method:

  ```javascript
  // Fibonacci sequence
  ce.declareSequence('F', {
    base: { 0: 0, 1: 1 },
    recurrence: 'F_{n-1} + F_{n-2}',
  });
  ce.parse('F_{10}').evaluate();  // → 55
  ce.parse('F_{20}').evaluate();  // → 6765

  // Arithmetic sequence: a_n = a_{n-1} + 2, a_0 = 1
  ce.declareSequence('A', {
    base: { 0: 1 },
    recurrence: 'A_{n-1} + 2',
  });
  ce.parse('A_{5}').evaluate();  // → 11

  // Factorial via recurrence
  ce.declareSequence('H', {
    base: { 0: 1 },
    recurrence: 'n \\cdot H_{n-1}',
  });
  ce.parse('H_{5}').evaluate();  // → 120
  ```

  Features:
  - Base cases as index → value mapping
  - Recurrence relation as LaTeX string or BoxedExpression
  - Automatic memoization for efficient evaluation (configurable)
  - Custom index variable name (default: `n`)
  - Domain constraints (min/max valid indices)
  - Symbolic subscripts stay symbolic (e.g., `F_k` remains unevaluated)

  Alternatively, sequences can be defined using natural LaTeX assignment
  notation:

  ```javascript
  // Arithmetic sequence via LaTeX
  ce.parse('L_0 := 1').evaluate();
  ce.parse('L_n := L_{n-1} + 2').evaluate();
  ce.parse('L_{5}').evaluate();  // → 11

  // Fibonacci via LaTeX
  ce.parse('F_0 := 0').evaluate();
  ce.parse('F_1 := 1').evaluate();
  ce.parse('F_n := F_{n-1} + F_{n-2}').evaluate();
  ce.parse('F_{10}').evaluate();  // → 55
  ```

  Base cases and recurrence can be defined in any order. The sequence is
  finalized when both are present.

- **Sequence Status API**: Query the status of sequence definitions with
  `getSequenceStatus()`:

  ```javascript
  ce.parse('F_0 := 0').evaluate();
  ce.getSequenceStatus('F');
  // → { status: 'pending', hasBase: true, hasRecurrence: false, baseIndices: [0] }

  ce.parse('F_n := F_{n-1} + F_{n-2}').evaluate();
  ce.getSequenceStatus('F');
  // → { status: 'complete', hasBase: true, hasRecurrence: true, baseIndices: [0] }

  ce.getSequenceStatus('x');
  // → { status: 'not-a-sequence', hasBase: false, hasRecurrence: false }
  ```

- **Sequence Introspection API**: Inspect and manage defined sequences:

  ```javascript
  // Get sequence information
  ce.getSequence('F');
  // → { name: 'F', variable: 'n', baseIndices: [0, 1], memoize: true, cacheSize: 5 }

  // List all defined sequences
  ce.listSequences();  // → ['F', 'A', 'H']

  // Check if a symbol is a sequence
  ce.isSequence('F');  // → true
  ce.isSequence('x');  // → false

  // Manage memoization cache
  ce.getSequenceCache('F');  // → Map { 2 => 1, 3 => 2, ... }
  ce.clearSequenceCache('F');  // Clear cache for specific sequence
  ce.clearSequenceCache();     // Clear all sequence caches
  ```

- **Generate Sequence Terms**: Generate a list of sequence terms with
  `getSequenceTerms()`:

  ```javascript
  ce.declareSequence('F', {
    base: { 0: 0, 1: 1 },
    recurrence: 'F_{n-1} + F_{n-2}',
  });

  ce.getSequenceTerms('F', 0, 10);
  // → [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

  // With step parameter (every other term)
  ce.getSequenceTerms('F', 0, 10, 2);
  // → [0, 1, 3, 8, 21, 55]
  ```

- **Sum and Product over Sequences**: `Sum` and `Product` now work seamlessly
  with user-defined sequences:

  ```javascript
  ce.declareSequence('F', {
    base: { 0: 0, 1: 1 },
    recurrence: 'F_{n-1} + F_{n-2}',
  });

  ce.parse('\\sum_{k=0}^{10} F_k').evaluate();  // → 143
  ce.parse('\\prod_{k=1}^{5} A_k').evaluate();  // Works with any defined sequence
  ```

- **OEIS Integration**: Look up sequences in the Online Encyclopedia of Integer
  Sequences (OEIS) and verify your sequences against known mathematical
  sequences:

  ```javascript
  // Look up a sequence by its terms
  const results = await ce.lookupOEIS([0, 1, 1, 2, 3, 5, 8, 13]);
  // → [{ id: 'A000045', name: 'Fibonacci numbers', terms: [...], url: '...' }]

  // Check if your sequence matches a known OEIS sequence
  ce.declareSequence('F', {
    base: { 0: 0, 1: 1 },
    recurrence: 'F_{n-1} + F_{n-2}',
  });

  const result = await ce.checkSequenceOEIS('F', 10);
  // → { matches: [{ id: 'A000045', name: 'Fibonacci numbers', ... }], terms: [...] }
  ```

  Note: OEIS lookups require network access to oeis.org.

- **Multi-Index Sequences**: Define sequences with multiple indices like
  Pascal's triangle `P_{n,k}` or grid-based recurrences:

  ```javascript
  // Pascal's Triangle: P_{n,k} = P_{n-1,k-1} + P_{n-1,k}
  ce.declareSequence('P', {
    variables: ['n', 'k'],
    base: { 'n,0': 1, 'n,n': 1 },  // Pattern-based base cases
    recurrence: 'P_{n-1,k-1} + P_{n-1,k}',
    domain: { n: { min: 0 }, k: { min: 0 } },
    constraints: 'k <= n',  // k must not exceed n
  });

  ce.parse('P_{5,2}').evaluate();  // → 10
  ce.parse('P_{10,5}').evaluate(); // → 252
  ```

  Features:
  - Multiple index variables with `variables: ['n', 'k']`
  - Pattern-based base cases: `'n,0'` matches any (n, 0), `'n,n'` matches
    diagonal
  - Per-variable domain constraints
  - Constraint expressions (e.g., `'k <= n'`)
  - Composite key memoization (e.g., `'5,2'`)
  - Full introspection support with `isMultiIndex` flag

  Pattern matching for base cases:
  - Exact values: `'0,0'` matches only (0, 0)
  - Wildcards: `'n,0'` matches any value for n with k=0
  - Equality: `'n,n'` matches when both indices are equal
  - Priority: exact matches are checked before patterns

### Special Functions

- **Special Function Definitions**: Added type signatures for special
  mathematical functions, enabling them to be used in expressions without type
  errors:
  - `Zeta` - Riemann zeta function $\zeta(s)$
  - `Beta` - Euler beta function $B(a,b) = \Gamma(a)\Gamma(b)/\Gamma(a+b)$
  - `LambertW` - Lambert W function (product logarithm)
  - `BesselJ`, `BesselY`, `BesselI`, `BesselK` - Bessel functions of
    first/second kind
  - `AiryAi`, `AiryBi` - Airy functions

  These functions now have proper signatures and can be composed with other
  expressions: `ce.box(['Add', 1, ['LambertW', 'x']])` works correctly.

- **Special Function LaTeX Parsing**: Added LaTeX parsing support for special
  functions: `\zeta(s)`, `\Beta(a,b)`, `\operatorname{W}(x)`, Bessel functions
  via `\operatorname{J}`, `\operatorname{Y}`, etc., and Airy functions via
  `\operatorname{Ai}`, `\operatorname{Bi}`.

### Calculus

- **LambertW Derivative**: Added derivative rule for the Lambert W function:
  `d/dx W(x) = W(x)/(x·(1+W(x)))`

- **Bessel Function Derivatives**: Added derivative support for all four Bessel
  function types using order-dependent recurrence relations:

  ```javascript
  ce.box(['D', ['BesselJ', 'n', 'x'], 'x']).evaluate();
  // → 1/2 * BesselJ(n-1, x) - 1/2 * BesselJ(n+1, x)

  ce.box(['D', ['BesselI', 'n', 'x'], 'x']).evaluate();
  // → 1/2 * BesselI(n-1, x) + 1/2 * BesselI(n+1, x)

  ce.box(['D', ['BesselK', 'n', 'x'], 'x']).evaluate();
  // → -1/2 * BesselK(n-1, x) - 1/2 * BesselK(n+1, x)
  ```

  Chain rule is automatically applied for composite arguments.

- **Multi-Argument Function Derivatives**: Added derivative support for:
  - **Log(x, base)** - Logarithm with custom base:

    ```javascript
    ce.box(['D', ['Log', 'x', 2], 'x']).evaluate();  // → 1/(x·ln(2))
    ce.box(['D', ['Log', 'x', 'a'], 'x']).evaluate(); // → 1/(x·ln(a))
    ```

    Also handles cases where both x and base depend on the variable by applying
    the quotient rule to ln(x)/ln(base).

  - **Discrete functions (Mod, GCD, LCM)** - Return 0 as these are step
    functions with derivative 0 almost everywhere:
    ```javascript
    ce.box(['D', ['Mod', 'x', 5], 'x']).evaluate();  // → 0
    ce.box(['D', ['GCD', 'x', 6], 'x']).evaluate();  // → 0
    ```

- **Integration of `1/(x·ln(x))` Pattern**: Added support for integrating
  expressions where the denominator is a product and one factor is the
  derivative of another:

  ```javascript
  ce.parse('\\int \\frac{1}{x\\ln x} dx').evaluate();  // → ln(|ln(x)|)
  ce.parse('\\int \\frac{3}{x\\ln x} dx').evaluate();  // → 3·ln(|ln(x)|)
  ```

  This uses u-substitution: since `1/x = d/dx(ln(x))`, the integral becomes
  `∫ h'(x)/h(x) dx = ln|h(x)|`.

- **Cyclic Integration for e^x with Trigonometric Functions**: Added support for
  integrating products of exponentials and trigonometric functions that require
  the "solve for the integral" technique:

  ```javascript
  ce.parse('\\int e^x \\sin x dx').evaluate();
  // → -1/2·cos(x)·e^x + 1/2·sin(x)·e^x

  ce.parse('\\int e^x \\cos x dx').evaluate();
  // → 1/2·sin(x)·e^x + 1/2·cos(x)·e^x

  // Also works with linear arguments:
  ce.parse('\\int e^x \\sin(2x) dx').evaluate();
  // → -2/5·cos(2x)·e^x + 1/5·sin(2x)·e^x

  ce.parse('\\int e^x \\cos(2x) dx').evaluate();
  // → 1/5·cos(2x)·e^x + 2/5·sin(2x)·e^x
  ```

  These patterns cannot be solved by standard integration by parts (which would
  lead to infinite recursion) and instead use direct formulas:
  - `∫ e^x·sin(ax+b) dx = (e^x/(a²+1))·(sin(ax+b) - a·cos(ax+b))`
  - `∫ e^x·cos(ax+b) dx = (e^x/(a²+1))·(a·sin(ax+b) + cos(ax+b))`

- **Derivative Recursion Safety**: Added recursion protection to
  `differentiate()` with a depth limit (`MAX_DIFFERENTIATION_DEPTH`), returning
  `undefined` when the limit is exceeded.

- **Equation Equivalence in `isEqual()`** (Issue #275): Two equations are now
  recognized as equivalent if they have the same solution set:

  ```javascript
  ce.parse('2x+1=0').isEqual(ce.parse('x=-1/2'));   // → true
  ce.parse('3x+1=0').isEqual(ce.parse('6x+2=0'));   // → true
  ```

  Uses sampling to check whether (LHS₁-RHS₁)/(LHS₂-RHS₂) is a non-zero constant.

### Logic

- **Boolean Simplification Rules**: Added absorption laws and improved boolean
  expression simplification:
  - **Absorption**: `A ∧ (A ∨ B) → A` and `A ∨ (A ∧ B) → A`
  - **Idempotence**: `A ∧ A → A` and `A ∨ A → A`
  - **Complementation**: `A ∧ ¬A → False` and `A ∨ ¬A → True`
  - **Identity**: `A ∧ True → A` and `A ∨ False → A`
  - **Domination**: `A ∧ False → False` and `A ∨ True → True`
  - **Double negation**: `¬¬A → A`

  These rules are applied automatically during simplification:

  ```javascript
  ce.box(['And', 'A', ['Or', 'A', 'B']]).simplify();  // → A
  ce.box(['Or', 'A', ['And', 'A', 'B']]).simplify();  // → A
  ```

- **Prime Implicants and Minimal Normal Forms**: Added Quine-McCluskey algorithm
  for finding prime implicants/implicates and computing minimal CNF/DNF:
  - `PrimeImplicants(expr)` - Find all prime implicants (minimal product terms)
  - `PrimeImplicates(expr)` - Find all prime implicates (minimal sum clauses)
  - `MinimalDNF(expr)` - Convert to minimal DNF using prime implicant cover
  - `MinimalCNF(expr)` - Convert to minimal CNF using prime implicate cover

  ```javascript
  // Find prime implicants (terms that can't be further simplified)
  ce.box(['PrimeImplicants', ['Or', ['And', 'A', 'B'], ['And', 'A', ['Not', 'B']]]]).evaluate();
  // → [A] (AB and A¬B combine to just A)

  // Compute minimal DNF
  ce.box(['MinimalDNF', ['Or',
    ['And', 'A', 'B'],
    ['And', 'A', ['Not', 'B']],
    ['And', ['Not', 'A'], 'B']
  ]]).evaluate();
  // → A ∨ B (simplified from 3 terms to 2)
  ```

  Limited to 12 variables to prevent exponential blowup; larger expressions
  return unevaluated.

### Linear Algebra

- **Matrix Decompositions**: Added four matrix decomposition functions for
  numerical linear algebra:
  - `LUDecomposition(A)` → `[P, L, U]` - LU factorization with partial pivoting
  - `QRDecomposition(A)` → `[Q, R]` - QR factorization using Householder
    reflections
  - `CholeskyDecomposition(A)` → `L` - Cholesky factorization for positive
    definite matrices
  - `SVD(A)` → `[U, Σ, V]` - Singular Value Decomposition

  ```javascript
  ce.box(['LUDecomposition', [[4, 3], [6, 3]]]).evaluate();
  // → [P, L, U] where PA = LU

  ce.box(['QRDecomposition', [[1, 2], [3, 4]]]).evaluate();
  // → [Q, R] where A = QR, Q orthogonal, R upper triangular

  ce.box(['CholeskyDecomposition', [[4, 2], [2, 2]]]).evaluate();
  // → L where A = LL^T

  ce.box(['SVD', [[1, 2], [3, 4]]]).evaluate();
  // → [U, Σ, V] where A = UΣV^T
  ```

### Fixed

- **replace() Literal Matching in Object Rules**:
  `.replace({ match: 'a', replace: 2 })` no longer treats `'a'` as a wildcard
  (string rules like `"a*x -> 2*x"` still auto-wildcard).

  ```javascript
  const expr = ce.box(['Add', ['Multiply', 'a', 'x'], 'b']);
  expr.replace({match: 'a', replace: 2}, {recursive: true});
  // → 2x + b (was: 2 - incorrectly matched entire expression)
  ```

- **forget() Clears Assumed Values**: `ce.forget()` now clears values set by
  equality assumptions across all evaluation context frames.

  ```javascript
  ce.assume(ce.box(['Equal', 'x', 5]));
  ce.box('x').evaluate();  // → 5
  ce.forget('x');
  ce.box('x').evaluate();  // → 'x' (was: 5)
  ```

- **Scoped Assumptions Clean Up on popScope()**: Assumptions made inside a scope
  no longer leak after `popScope()`.

  ```javascript
  ce.pushScope();
  ce.assume(ce.box(['Equal', 'y', 10]));
  ce.box('y').evaluate();  // → 10
  ce.popScope();
  ce.box('y').evaluate();  // → 'y' (was: 10)
  ```

- **Extraneous Root Filtering for Sqrt Equations**: Candidate solutions are now
  validated against the original expression (before clearing denominators /
  harmonization) to filter extraneous roots.

  Examples of equations that now correctly filter extraneous roots:
  - `√x = x - 2` → returns `[4]` (filters out x=1)
  - `√x + x - 2 = 0` → returns `[1]` (filters out x=4)
  - `√x - x + 2 = 0` → returns `[4]` (filters out x=1)
  - `x - 2√x - 3 = 0` → returns `[9]` (filters out x=1)
  - `2x + 3√x - 2 = 0` → returns `[1/4]` (filters out x=4)

- **Simplification (#178)**:
  - Safer division canonicalization for denominators that may simplify to `0`
  - Implicit multiplication powers: `xx` → `x^2`
  - Targeted exp/log rewriting for `\exp(\log(x)±y)`

## 0.33.0 _2026-01-30_

### Resolved Issues

#### Arithmetic and Infinity

- **Division by Zero**: Improved handling of division by zero:
  - `0/0` returns `NaN` (indeterminate form)
  - `a/0` where `a ≠ 0` returns `ComplexInfinity` (~∞) as a "better NaN" that
    indicates an infinite result with unknown sign
  - This applies to all forms including `1/0`, `x/0`, and rational literals

- **Infinity Sign Propagation**: Fixed infinity multiplication not propagating
  signs correctly. Now `∞ * (-2) = -∞` and `-∞ * 2 = -∞` as expected.

- **Infinity Division**: Fixed `∞/∞` incorrectly returning `1`. Now correctly
  returns `NaN` (indeterminate form). The `a/a → 1` simplification rule now
  excludes infinity values.

#### Trigonometry

- **Trigonometric Period Identities**: Fixed incorrect sign handling for
  `csc(π+x)` and `cot(π+x)`:
  - `csc(π+x)` now correctly simplifies to `-csc(x)` (was incorrectly `csc(x)`)
  - `cot(π+x)` now correctly simplifies to `cot(x)` (was incorrectly `-cot(x)`,
    cotangent has period π)

- **Trigonometric Co-function Identities**: Fixed co-function identities not
  applying to canonical form expressions. Now correctly simplifies:
  - `sin(π/2 - x)` → `cos(x)`
  - `cos(π/2 - x)` → `sin(x)`
  - `tan(π/2 - x)` → `cot(x)`
  - `cot(π/2 - x)` → `tan(x)`
  - `sec(π/2 - x)` → `csc(x)`
  - `csc(π/2 - x)` → `sec(x)`

- **Double Angle with Coefficient**: Fixed `2sin(x)cos(x)` not simplifying to
  `sin(2x)`. The product-to-sum identity now handles coefficients:
  - `2sin(x)cos(x)` → `sin(2x)`
  - `c·sin(x)cos(x)` → `c·sin(2x)/2` for any coefficient `c`

- **Trigonometric Product Identities**: Improved handling of trig products in
  simplification. The Multiply rule now correctly defers to trig-specific rules
  for patterns like `sin(x)*cos(x)` and `tan(x)*cot(x)`, ensuring these are
  simplified to `sin(2x)/2` and `1` respectively.

#### Logarithms and Exponentials

- **Logarithm-Exponential Composition**: Fixed `log(exp(x))` incorrectly
  simplifying to `x`. Now correctly returns `x/ln(10)` ≈ `0.434x` since
  `log₁₀(eˣ) = x·log₁₀(e) = x/ln(10)`. The identity `log(exp(x)) = x` only holds
  for natural logarithm.

- **Logarithm of e**: Added simplification for `log(e)` → `1/ln(10)` ≈ `0.434`
  and `log_c(e)` → `1/ln(c)` for any base `c`.

- **Logarithm Combination Base Preservation**: Fixed `log(x) + log(y)` (base 10)
  incorrectly becoming `ln(xy)`. Now correctly produces `log(xy)` preserving the
  original base.

- **Logarithm Quotient Rule**: Added expansion rule for logarithm of quotients.
  `ln(x/y)` now simplifies to `ln(x) - ln(y)` when x and y are known positive.
  Similarly for any base: `log_c(x/y)` → `log_c(x) - log_c(y)`.

- **Exponential-Logarithm Composition**: Added simplification for `exp(log(x))`
  where log has a different base than e. Now `e^log(x)` → `x^{1/ln(10)}` and
  more generally `e^log_c(x)` → `x^{1/ln(c)}` for any base c.

#### Powers and Exponents

- **Zero Power with Symbolic Exponent**: Fixed `0^π` and similar expressions
  with positive symbolic exponents not simplifying. Now `0^x` → `0` when `x` is
  known to be positive (including `π`, `e`, etc.).

- **Exponent Evaluation in Products**: Fixed `(x³)² · (y²)²` not simplifying to
  `x⁶y⁴`. Numeric subexpressions in exponents (like `2×3` in `x^{2×3}`) are now
  evaluated when the expression is part of a product.

- **Negative Exponents on Fractions**: Fixed `(a/b)^{-n}` not simplifying
  properly. Now `(x³/y²)^{-2}` correctly simplifies to `y⁴/x⁶` during
  canonicalization by distributing the negative exponent.

- **Negative Base with Fractional Exponent**: Fixed `(-ax)^{p/q}` returning
  complex results when `p` and `q` are both odd. Now correctly factors out the
  negative sign: `(-2x)^{3/5}` → `-(2x)^{3/5}` = `-2^{3/5}·x^{3/5}`, giving real
  results. This affects products like `(-2x)^{3/5}·x` which now correctly
  simplify to `-2^{3/5}·x^{8/5}` instead of returning an imaginary value.

#### Radicals

- **Radical Perfect Square Factoring**: Fixed `√(x²y)` not simplifying to
  `|x|√y`. Adjusted cost function to penalize radicals containing perfect
  squares, enabling the simplification rule to apply.

- **Generalized Root Extraction**: Added comprehensive root simplification
  rules:
  - `√[n]{x^m}` → `x^{m/n}` for odd roots (always valid)
  - `√[n]{x^m}` → `|x|^{m/n}` for even roots with integer result
  - `√{x^{odd}}` → `|x|^n · √x` factoring (e.g., `√{x⁵}` → `|x|²√x`)
  - Handles all combinations: `√[4]{x⁶}` → `|x|^{3/2}`, `√[3]{x⁶}` → `x²`

- **Symbolic Radicals Preservation**: Fixed numeric radicals (`√2`, `∛5`,
  `2^{3/5}`) being evaluated to floating-point approximations during
  multiplication. Now `x * √2` stays as `√2 · x` instead of `1.414... · x`, and
  `x * 2^{1/3}` stays as `x · ∛2` instead of `1.259... · x`. This preserves
  exact irrational values and allows proper algebraic manipulation. Use `.N()`
  to get numeric approximations when needed.

#### LaTeX Parsing

- **LaTeX `\exp()` Juxtaposition**: Fixed adjacent `\exp()` calls not parsing as
  multiplication. Now `\exp(x)\exp(2)` correctly parses as `e^x · e^2` instead
  of producing a parse error. The expression then simplifies to `e^{x+2}` as
  expected.

### Features

#### Trigonometry

- **Fu Algorithm for Trigonometric Simplification**: Implemented the Fu
  algorithm based on Fu, Zhong, and Zeng's paper "Automated and readable
  simplification of trigonometric expressions" (2006). This provides systematic,
  high-quality trigonometric simplification through:
  - **Transformation Rules (TR1-TR22)**: Comprehensive set of rewrite rules
    including reciprocal conversions (sec→1/cos), ratio forms (tan→sin/cos),
    Pythagorean substitutions (sin²+cos²=1), power reductions, product-to-sum,
    sum-to-product, angle expansion/contraction, and Morrie's law for cosine
    product chains.

  - **Rule Lists (RL1, RL2)**: Organized application sequences for tan/cot
    expressions and sin/cos expressions respectively, with greedy selection of
    optimal results.

  - **Cost Function**: Minimizes trigonometric function count as primary metric,
    with leaf count as secondary, to find the most readable form.

  **Usage**:

  ```typescript
  // Option 1: Use strategy option with simplify()
  const result = expr.simplify({ strategy: 'fu' });

  // Option 2: Dedicated trigSimplify() method
  const result = expr.trigSimplify();
  ```

  **Examples**:
  - `sin(x)⁴ - cos(x)⁴` → `-cos(2x)`
  - `tan(x)·cot(x)` → `1`
  - `sin²(x) + cos²(x)` → `1`
  - `2sin(x)cos(x)` → `sin(2x)`
  - `cos(x)·cos(2x)·cos(4x)` → `sin(8x)/(8sin(x))` (Morrie's law)

  **Enhanced Transformations**:
  - **TRmorrie with Rational Coefficients**: Morrie's law now handles angles
    that are rational multiples of π, such as `cos(π/9)·cos(2π/9)·cos(4π/9)` →
    `1/8`. The algorithm detects maximal geometric sequences and handles cases
    where the sine terms cancel to produce pure fractions.

  - **TR12i Tangent Sum Identity**: Recognizes the pattern
    `tan(A) + tan(B) - k·tan(A)·tan(B)` and simplifies to `-tan(C)` when
    `A + B + C = π` and `k = tan(C)`. Works with standard angles (π/6, π/4, π/3,
    etc.) and handles sign variations.

  - **TRpythagorean for Compound Expressions**: Detects `sin²(x) + cos²(x)`
    pairs within larger Add expressions and simplifies them to 1, e.g.,
    `sin²(x) + cos²(x) + 2` → `3`.

  - **Early TR9 Sum-to-Product**: Applies sum-to-product transformation before
    angle expansion to catch patterns like `sin(x+h) + sin(x-h)` →
    `2sin(x)cos(h)` that would otherwise be expanded and lose their simplified
    form.

  - **Dual Strategy Approach**: The Fu strategy now tries both "Fu first" and
    "simplify first" approaches and picks the best result. This handles both
    Morrie-like patterns (which need Fu before evaluation) and period reduction
    patterns (which need simplification first for angle contraction).

- **Trigonometric Periodicity Reduction**: Trigonometric functions now simplify
  arguments containing integer multiples of π:
  - `sin(5π + k)` → `-sin(k)` (period 2π, with sign change for odd multiples)
  - `cos(4π + k)` → `cos(k)` (period 2π)
  - `tan(3π + k)` → `tan(k)` (period π)
  - Works for all six trig functions: sin, cos, tan, cot, sec, csc
  - Handles both positive and negative multiples of π

- **Pythagorean Trigonometric Identities**: Added simplification rules for all
  Pythagorean identities:
  - `sin²(x) + cos²(x)` → `1`
  - `1 - sin²(x)` → `cos²(x)` and `1 - cos²(x)` → `sin²(x)`
  - `sin²(x) - 1` → `-cos²(x)` and `cos²(x) - 1` → `-sin²(x)`
  - `tan²(x) + 1` → `sec²(x)` and `sec²(x) - 1` → `tan²(x)`
  - `1 + cot²(x)` → `csc²(x)` and `csc²(x) - 1` → `cot²(x)`
  - `a·sin²(x) + a·cos²(x)` → `a` (with coefficient)

- **Trigonometric Equation Solving**: The `solve()` method now handles basic
  trigonometric equations:
  - `sin(x) = a` → `x = arcsin(a)` and `x = π - arcsin(a)` (two solutions)
  - `cos(x) = a` → `x = arccos(a)` and `x = -arccos(a)` (two solutions)
  - `tan(x) = a` → `x = arctan(a)` (one solution per period)
  - `cot(x) = a` → `x = arccot(a)`
  - Supports coefficient form: `a·sin(x) + b = 0`
  - Domain validation: returns no solutions when |a| > 1 for sin/cos
  - Automatic deduplication of equivalent solutions (e.g., `cos(x) = 1` → single
    solution `0`)

#### Calculus

- **([#163](https://github.com/cortex-js/compute-engine/issues/163)) Additional
  Derivative Notations**: Added support for parsing multiple derivative
  notations beyond Leibniz notation:
  - **Newton's dot notation** for time derivatives: `\dot{x}` →
    `["D", "x", "t"]`, `\ddot{x}` for second derivative, `\dddot{x}` and
    `\ddddot{x}` for higher orders. The time variable is configurable via the
    new `timeDerivativeVariable` parser option (default: `"t"`).

  - **Lagrange prime notation with arguments**: `f'(x)` now parses to
    `["D", ["f", "x"], "x"]`, inferring the differentiation variable from the
    function argument. Works for `f''(x)`, `f'''(x)`, etc. for higher
    derivatives.

  - **Euler's subscript notation**: `D_x f` → `["D", "f", "x"]` and `D^2_x f` or
    `D_x^2 f` for second derivatives.

  - **Derivative serialization**: `D` expressions now serialize to Leibniz
    notation (`\frac{\mathrm{d}}{\mathrm{d}x}f`) for consistent round-trip
    parsing.

- **Derivative Rules for Special Functions**: Added derivative formulas for:
  - `d/dx Digamma(x) = Trigamma(x)`
  - `d/dx Erf(x)`, `d/dx Erfc(x)`, `d/dx Erfi(x)`
  - `d/dx FresnelS(x)`, `d/dx FresnelC(x)`
  - `d/dx LogGamma(x) = Digamma(x)`

#### Special Functions

- **Special Function Definitions**: Added type signatures for Digamma, Trigamma,
  and PolyGamma functions to the library:
  - `Digamma(x)` - The digamma function ψ(x), logarithmic derivative of Gamma
  - `Trigamma(x)` - The trigamma function ψ₁(x), derivative of digamma
  - `PolyGamma(n, x)` - The polygamma function ψₙ(x), nth derivative of digamma

#### Logarithms and Exponentials

- **Logarithm Combination Rules**: Added simplification rules that combine
  logarithms with the same base:
  - `ln(x) + ln(y)` → `ln(xy)` (addition combines via multiplication)
  - `ln(x) - ln(y)` → `ln(x/y)` (subtraction combines via division)
  - `log_c(x) + log_c(y)` → `log_c(xy)` (works with any base)
  - `log_c(x) - log_c(y)` → `log_c(x/y)`
  - Handles multiple terms: `ln(a) + ln(b) - ln(c)` → `ln(ab/c)`

- **Exponential e Simplification**: Added rules for combining powers of e:
  - `eˣ · eʸ` → `e^(x+y)` (same-base multiplication)
  - `eˣ / eʸ` → `e^(x-y)` (same-base division)
  - `eˣ · e` → `e^(x+1)` and `eˣ / e` → `e^(x-1)`
  - Preserves symbolic form instead of evaluating e^n numerically

#### Powers and Exponents

- **Negative Base Power Simplification**: Added rules to simplify powers with
  negated bases:
  - `(-x)^n` → `x^n` when n is even (e.g., `(-x)^4` → `x^4`)
  - `(-x)^n` → `-x^n` when n is odd (e.g., `(-x)^3` → `-x^3`)
  - `(-x)^{n/m}` → `x^{n/m}` when n is even and m is odd
  - `(-x)^{n/m}` → `-x^{n/m}` when both n and m are odd
  - `(-1)^{p/q}` → `-1` when both p and q are odd (real odd root)

- **Power Distribution**: Added rule to distribute integer exponents over
  products:
  - `(ab)^n` → `a^n · b^n` when n is an integer
  - Example: `(x³y²)²` → `x⁶y⁴`
  - Example: `(-2x)²` → `4x²`

- **Same-Base Power Combination**: Improved power combination for products with
  3+ terms:
  - `a³ · a · a²` → `a⁶` (combines all same-base terms)
  - Works with unknown symbols when sum of exponents is positive
  - Handles mixed products: `b³c²dx⁷ya⁵gb²x⁵(3b)` → `3dgyx¹²b⁶a⁵c²`

#### Sum and Product

- **([#133](https://github.com/cortex-js/compute-engine/issues/133))
  Element-based Indexing Sets for Sum/Product**: Added support for `\in`
  notation in summation and product subscripts:
  - **Parsing**: `\sum_{n \in \{1,2,3\}} n` now correctly parses to
    `["Sum", "n", ["Element", "n", ["Set", 1, 2, 3]]]` instead of silently
    dropping the constraint.

  - **Evaluation**: Sums and products over finite sets, lists, and ranges are
    now evaluated correctly:
    - `\sum_{n \in \{1,2,3\}} n` → `6`
    - `\sum_{n \in \{1,2,3\}} n^2` → `14`
    - `\prod_{k \in \{1,2,3,4\}} k` → `24`

  - **Serialization**: Element-based indexing sets serialize back to LaTeX with
    proper `\in` notation: `\sum_{n\in \{1, 2, 3\}}n`

  - **Range support**: Works with `Range` expressions via `ce.box()`:
    `["Sum", "n", ["Element", "n", ["Range", 1, 5]]]` → `15`

  - **Bracket notation as Range**: Two-element integer lists in bracket notation
    `[a,b]` are now treated as Range(a,b) when used in Element context:
    - `\sum_{n \in [1,5]} n` → `15` (iterates 1, 2, 3, 4, 5)
    - Previously returned `6` (treated as List with just elements 1 and 5)

  - **Interval support**: `Interval` expressions work with Element-based
    indexing, including support for `Open` and `Closed` boundary markers:
    - `["Interval", 1, 5]` → iterates integers 1, 2, 3, 4, 5 (closed bounds)
    - `["Interval", ["Open", 0], 5]` → iterates 1, 2, 3, 4, 5 (excludes 0)
    - `["Interval", 1, ["Open", 6]]` → iterates 1, 2, 3, 4, 5 (excludes 6)

  - **Infinite series with Element notation**: Known infinite integer sets are
    converted to their equivalent Limits form and iterated (capped at
    1,000,000):
    - `NonNegativeIntegers` (ℕ₀) → iterates from 0, like `\sum_{n=0}^{\infty}`
    - `PositiveIntegers` (ℤ⁺) → iterates from 1, like `\sum_{n=1}^{\infty}`
    - Convergent series produce numeric approximations:
      `\sum_{n \in \Z^+} \frac{1}{n^2}` → `≈1.6449` (close to π²/6)

  - **Non-enumerable domains stay symbolic**: When the domain cannot be
    enumerated (unknown symbol, non-iterable infinite set, or symbolic bounds),
    the expression stays symbolic instead of returning NaN:
    - `\sum_{n \in S} n` with unknown `S` → stays as
      `["Sum", "n", ["Element", "n", "S"]]`
    - `\sum_{n \in \Z} n` → stays symbolic (bidirectional, can't forward
      iterate)
    - `\sum_{x \in \R} f(x)` → stays symbolic (non-countable)
    - `\sum_{n \in [1,a]} n` with symbolic bound → stays symbolic
    - Previously these would all return `NaN` with no explanation

  - **Multiple Element indexing sets**: Comma-separated Element expressions now
    parse and evaluate correctly:
    - `\sum_{n \in A, m \in B} (n+m)` →
      `["Sum", ..., ["Element", "n", "A"], ["Element", "m", "B"]]`
    - Nested sums like `\sum_{i \in A}\sum_{j \in B} i \cdot j` evaluate
      correctly
    - Mixed indexing sets (Element + Limits) work together

  - **Condition/filter support in Element expressions**: Conditions can be
    attached to Element expressions to filter values from the set:
    - `\sum_{n \in S, n > 0} n` → sums only positive values from S
    - `\sum_{n \in S, n \ge 2} n` → sums values ≥ 2 from S
    - `\prod_{k \in S, k < 0} k` → multiplies only negative values from S
    - Supported operators: `>`, `>=`, `<`, `<=`, `!=`
    - Conditions are attached as the 4th operand of Element:
      `["Element", "n", "S", ["Greater", "n", 0]]`

#### Linear Algebra

- **Matrix Multiplication**: Added `MatrixMultiply` function supporting:
  - Matrix × Matrix: `A (m×n) × B (n×p) → result (m×p)`
  - Matrix × Vector: `A (m×n) × v (n) → result (m)`
  - Vector × Matrix: `v (m) × B (m×n) → result (n)`
  - Vector × Vector (dot product): `v1 (n) · v2 (n) → scalar`
  - Proper dimension validation with `incompatible-dimensions` errors
  - LaTeX serialization using `\cdot` notation

- **Matrix Addition and Scalar Broadcasting**: `Add` now supports element-wise
  operations on tensors (matrices and vectors):
  - Matrix + Matrix: Element-wise addition (shapes must match)
  - Scalar + Matrix: Broadcasts scalar to all elements
  - Vector + Vector: Element-wise addition
  - Scalar + Vector: Broadcasts scalar to all elements
  - Symbolic support: `[[a,b],[c,d]] + [[1,2],[3,4]]` evaluates correctly
  - Proper dimension validation with `incompatible-dimensions` errors

- **Matrix Construction Functions**: Added convenience functions for creating
  common matrices:
  - `IdentityMatrix(n)`: Creates an n×n identity matrix
  - `ZeroMatrix(m, n?)`: Creates an m×n matrix of zeros (square if n omitted)
  - `OnesMatrix(m, n?)`: Creates an m×n matrix of ones (square if n omitted)

- **Matrix and Vector Norms**: Added `Norm` function for computing various
  norms:
  - **Vector norms**: L1 (sum of absolute values), L2 (Euclidean, default),
    L-infinity (max absolute value), and general Lp norms
  - **Matrix norms**: Frobenius (default, sqrt of sum of squared elements), L1
    (max column sum), L-infinity (max row sum)
  - Scalar norms return the absolute value

- **Eigenvalues and Eigenvectors**: Added functions for eigenvalue
  decomposition:
  - `Eigenvalues(matrix)`: Returns list of eigenvalues (2×2: symbolic via
    characteristic polynomial; 3×3: Cardano's formula; larger: numeric QR)
  - `Eigenvectors(matrix)`: Returns list of corresponding eigenvectors using
    null space computation via Gaussian elimination
  - `Eigen(matrix)`: Returns tuple of (eigenvalues, eigenvectors)

- **Diagonal Function**: Now fully implemented with bidirectional behavior:
  - Vector → Matrix: Creates a diagonal matrix from a vector
    (`Diagonal([1,2,3])` → 3×3 diagonal matrix)
  - Matrix → Vector: Extracts the diagonal as a vector
    (`Diagonal([[1,2],[3,4]])` → `[1,4]`)

- **Higher-Rank Tensor Operations**: Extended `Transpose`, `ConjugateTranspose`,
  and `Trace` to work with rank > 2 tensors:
  - **Transpose**: Swaps last two axes by default (batch transpose), or specify
    explicit axes with `['Transpose', T, axis1, axis2]`
  - **ConjugateTranspose**: Same axis behavior as Transpose, plus element-wise
    complex conjugation
  - **Trace (batch trace)**: Returns a tensor of traces over the last two axes.
    For a `[2,2,2]` tensor, returns `[trace of T[0], trace of T[1]]`. Optional
    axis parameters: `['Trace', T, axis1, axis2]`

- **Reshape Cycling**: Implements APL-style ravel cycling. When reshaping to a
  larger shape, elements cycle from the beginning: `Reshape([1,2,3], (2,2))` →
  `[[1,2],[3,1]]`

- **Scalar Handling**: Most linear algebra functions now handle scalar inputs:
  - `Flatten(42)` → `[42]` (single-element list)
  - `Transpose(42)` → `42` (identity)
  - `Determinant(42)` → `42` (1×1 matrix determinant)
  - `Trace(42)` → `42` (1×1 matrix trace)
  - `Inverse(42)` → `1/42` (scalar reciprocal)
  - `ConjugateTranspose(42)` → `42` (conjugate of real is itself)
  - `Reshape(42, (2,2))` → `[[42,42],[42,42]]` (scalar replication)

- **Improved Error Messages**: Operations requiring square matrices
  (`Determinant`, `Trace`, `Inverse`) now return `expected-square-matrix` error
  for vectors and tensors (rank > 2).

### Performance

- **Pattern Matching Optimization**: Significantly improved performance of
  commutative pattern matching by adding early rejection guards:
  - **Arity Guard**: Patterns without sequence wildcards (`__`/`___`) now
    immediately reject expressions with mismatched operand counts instead of
    attempting factorial permutations
  - **Anchor Fingerprint**: Patterns with literal or symbolic anchors verify
    anchor presence before attempting permutation matching, eliminating
    impossible matches in O(n) time
  - **Universal Anchoring**: Extended the efficient anchor-based backtracking
    algorithm to all patterns with anchors, not just those with sequence
    wildcards
  - **Hash Bucketing**: For patterns with many anchors (4+) against large
    expressions (6+ operands), uses hash-based indexing to reduce anchor lookup
    from O(n×m) to O(n+m) average case
  - Example: Matching `a + b + c + 1` against `x + y + z` now rejects
    immediately (arity mismatch: 4 vs 3) instead of trying 24 permutations

### Resolved Issues

#### Arithmetic

- **Indeterminate Form Handling**: Fixed incorrect results for mathematical
  indeterminate forms:
  - `0 * ∞` now correctly returns `NaN` (previously returned `∞`)
  - `∞ / ∞` now correctly returns `NaN` (previously returned `1`)
  - `∞^0` now correctly returns `NaN` (was already correct)
  - All combinations (`0 * (-∞)`, `(-∞) / ∞`, etc.) are handled correctly

- **([#176](https://github.com/cortex-js/compute-engine/issues/176)) Power
  Combination Simplification**: Fixed simplification failing to combine powers
  with the same base when one factor has an implicit exponent or when there are
  3+ operands. Previously, expressions like `2 * 2^x`, `e * e^x * e^{-x}`, and
  `x^2 * x` would not simplify. Now correctly simplifies to `2^(x+1)`, `e`, and
  `x^3` respectively. The fix includes:
  - Extended power combination rules to support numeric literal bases
  - Added functional rule to handle n-ary Multiply expressions (3+ operands)
  - Adjusted simplification cost threshold from 1.2 to 1.3 to accept
    mathematically valid simplifications where exponents become slightly more
    complex (e.g., `2 * 2^x → 2^(x+1)`)

- **Symbolic Factorial**: Fixed `(n-1)!` incorrectly evaluating to `NaN` instead
  of staying symbolic. The factorial `evaluate` function was attempting numeric
  computation on symbolic arguments. Now correctly returns `undefined` (keeping
  the expression symbolic) when the argument is not a number literal.

#### Linear Algebra

- **Matrix Operations Type Validation**: Fixed matrix operations (`Shape`,
  `Rank`, `Flatten`, `Transpose`, `Determinant`, `Inverse`, `Trace`, etc.)
  returning incorrect results or failing with type errors. The root cause was a
  type mismatch: function signatures expected `matrix` type (a 2D list with
  dimensions), but `BoxedTensor.type` returned `list<number>` without
  dimensions. Now `BoxedTensor`, `BoxedFunction`, and `BoxedSymbol` correctly
  derive `shape` and `rank` from their type's dimensions. Additionally, linear
  algebra functions now properly evaluate their operands before checking if they
  are tensors.

#### Calculus

- **Numerical Integration**: Fixed `\int_0^1 \sin(x) dx` returning `NaN` when
  evaluated numerically with `.N()`. The integrand was already wrapped in a
  `Function` expression by the canonical form, but the numerical evaluation code
  was wrapping it again, creating a nested function that returned a function
  instead of a number. Now correctly checks if the integrand is already a
  `Function` before wrapping.

#### LaTeX Parsing and Serialization

- **Subscript Function Calls**: Fixed parsing of function calls with subscripted
  names like `f_\text{a}(5)`. Previously, this was incorrectly parsed as a
  `Tuple` instead of a function call because `Subscript` expressions weren't
  being canonicalized before the function call check. Now correctly recognizes
  that `f_a(5)` is a function call when the subscript canonicalizes to a symbol.

- **([#130](https://github.com/cortex-js/compute-engine/issues/130))
  Prefix/Postfix Operator LaTeX Serialization**: Fixed incorrect LaTeX output
  for prefix operators (like `Negate`) and postfix operators (like `Factorial`)
  when applied to expressions with lower precedence. Previously,
  `Negate(Add(a, b))` incorrectly serialized as `-a+b` instead of `-(a+b)`,
  causing round-trip failures where parsing the output produced a mathematically
  different expression. Similarly, `Factorial(Add(a, b))` now correctly
  serializes as `(a+b)!` instead of `a+b!`. The fix ensures operands are wrapped
  in parentheses when their precedence is lower than the operator's precedence.

- **([#156](https://github.com/cortex-js/compute-engine/issues/156)) Logical
  Operator Precedence**: Fixed parsing of logical operators `\vee` (Or) and
  `\wedge` (And) with relational operators. Previously, expressions like
  `3=4\vee 7=8` were incorrectly parsed with the wrong precedence. Now correctly
  parses as `["Or", ["Equal", 3, 4], ["Equal", 7, 8]]`. Logical operators have
  lower precedence (230-235) than comparison operators (245) and set relations
  (240), so compound propositions parse correctly without requiring parentheses.

- **([#156](https://github.com/cortex-js/compute-engine/issues/156)) Logical
  Connective Arrows**: Added support for additional arrow notation in logical
  expressions:
  - `\rightarrow` now parses as `Implies` (previously parsed as `To` for
    set/function mapping)
  - `\leftrightarrow` now parses as `Equivalent` (previously produced an
    "unexpected-command" error)
  - Long arrow variants now supported: `\Longrightarrow`, `\longrightarrow` →
    `Implies`; `\Longleftrightarrow`, `\longleftrightarrow` → `Equivalent`
  - The existing variants `\Rightarrow`, `\Leftrightarrow`, `\implies`, `\iff`
    continue to work
  - `\to` remains available for function/set mapping notation (e.g.,
    `f: A \to B`)

#### Simplification

- **Rules Cache Isolation**: Fixed rules cache building failing with "Invalid
  rule" errors when user expressions had previously polluted the global scope.
  For example, parsing `x(y+z)` would add `x` as a symbol with function type to
  the global scope. Later, when the simplification rules cache was built, rule
  parsing would fail because wildcards like `_x` in rules would be type-checked
  against the polluted scope where `x` had incompatible type. The fix ensures
  rule parsing uses a clean scope that inherits only from the system scope
  (containing built-in definitions), not from user-polluted scopes.

- **Simplification Rules**: Added and fixed several simplification rules:
  - `x + x` now correctly simplifies to `2x` (term combination)
  - `e^x * e^{-x}` now correctly simplifies to `1` (exponential inverse)
  - `sin(∞)` and `cos(∞)` now correctly evaluate to `NaN`
  - `tanh(∞)` now correctly evaluates to `1`, `tanh(-∞)` to `-1`
  - `log_b(x^n)` now correctly simplifies to `n * log_b(x)` (log power rule)
  - Improved cost function to prefer `n * ln(x)` form over `ln(x^n)`
  - Trigonometric functions now reduce arguments by their period (e.g.,
    `cos(5π + k)` simplifies using `cos(π + k) = -cos(k)`)

- **([#178](https://github.com/cortex-js/compute-engine/issues/178))
  Non-Canonical Expression Simplification**: Fixed `.simplify()` not working on
  expressions parsed with `{ canonical: false }`. Previously,
  `ce.parse('x+x', { canonical: false }).simplify()` would return `x+x` instead
  of `2x`. The bug was in the simplification loop detection: when canonicalizing
  before simplification, the non-canonical form was recorded in the "seen" set,
  and since `isSame()` considers non-canonical and canonical forms equivalent,
  the canonical form was incorrectly detected as already processed. Now the
  simplification correctly starts fresh when canonicalizing, allowing full
  simplification to proceed.

## 0.32.0 _2026-01-28_

### Resolved Issues

#### Calculus

- **([#230](https://github.com/cortex-js/compute-engine/issues/230)) Root
  Derivatives**: Fixed the `D` operator not differentiating expressions
  containing the `Root` operator (n-th roots). Previously, `D(Root(x, 3), x)`
  (derivative of ∛x) would return an unevaluated derivative expression instead
  of computing the result. Now correctly returns `1/(3x^(2/3))`, equivalent to
  the expected `(1/3)·x^(-2/3)`. The fix adds a special case in the
  `differentiate` function to handle `Root(base, n)` by applying the power rule
  with exponent `1/n`.

- **Abs Derivative**: Fixed `d/dx |x|` returning an error when evaluated with a
  variable that has an assigned value. The derivative formula now uses `Sign(x)`
  instead of a complex `Which` expression that couldn't be evaluated
  symbolically.

- **Step Function Derivatives**: Fixed `D(floor(x), x)`, `D(ceil(x), x)`, and
  `D(round(x), x)` causing infinite recursion. These step functions now
  correctly return 0 (the derivative is 0 almost everywhere). Also fixed a bug
  where derivative formulas that evaluate to 0 weren't recognized due to a falsy
  check.

- **Inverse Trig Integrals**: Fixed incorrect integration formulas for `arcsin`,
  `arccos`, and `arctan`. The previous formulas were completely wrong. Correct:
  - `∫ arcsin(x) dx = x·arcsin(x) + √(1-x²)`
  - `∫ arccos(x) dx = x·arccos(x) - √(1-x²)`
  - `∫ arctan(x) dx = x·arctan(x) - (1/2)·ln(1+x²)`

- **Erfc Derivative**: Fixed incorrect derivative formula for `erfc(x)`. Now
  correctly returns `-2/√π · e^(-x²)` (the negative of the `erf` derivative).

- **LogGamma Derivative**: Added derivative rule for `LogGamma(x)` which returns
  `Digamma(x)` (the digamma/psi function).

- **Special Function Derivatives**: Fixed derivative formulas for several
  special functions and removed incorrect ones:
  - Fixed `d/dx erfi(x) = (2/√π)·e^(x²)` (imaginary error function)
  - Fixed `d/dx S(x) = sin(πx²/2)` (Fresnel sine integral)
  - Fixed `d/dx C(x) = cos(πx²/2)` (Fresnel cosine integral)
  - Removed incorrect derivative formulas for Zeta, Digamma, PolyGamma, Beta,
    LambertW, Bessel functions, and Airy functions (these now return symbolic
    derivatives like `Digamma'(x)` instead of wrong numeric results)

- **Symbolic Derivative Evaluation**: Fixed derivatives of unknown functions
  returning `0` instead of symbolic derivatives. For example, `D(Digamma(x), x)`
  now correctly returns `Digamma'(x)` (as `Apply(Derivative(Digamma, 1), x)`)
  instead of incorrectly returning `0`.

#### LaTeX Parsing and Serialization

- **([#256](https://github.com/cortex-js/compute-engine/issues/256)) Subscript
  Symbol Parsing**: Fixed parsing of single-letter symbols with subscripts.
  Previously, `i_A` was incorrectly parsed as
  `["Subscript", ["Complex", 0, 1], "A"]` because `i` was recognized as the
  imaginary unit before the subscript was processed. Now `i_A` correctly parses
  as the symbol `i_A`. This applies to all single-letter symbols including
  constants like `e` and `i`. Complex subscripts containing operators (`n+1`),
  commas (`n,m`), or parentheses (`(n+1)`) still produce `Subscript`
  expressions.

- **LaTeX Serialization**: Fixed TypeScript error in power serialization where
  `denom` (a `number | null`) was incorrectly passed where an `Expression` was
  expected. Now correctly uses `operand(exp, 2)` to get the expression form.

- **([#168](https://github.com/cortex-js/compute-engine/issues/168)) Absolute
  Value**: Fixed parsing of nested absolute value expressions that start with a
  double bar (e.g. `||3-5|-4|`), which previously produced an invalid structure
  instead of evaluating correctly.

- **([#244](https://github.com/cortex-js/compute-engine/issues/244))
  Serialization**: Fixed LaTeX and ASCIIMath serialization ambiguity for
  negative bases and negated powers. Powers now render `(-2)^2` (instead of
  `-2^2`) when the base is negative, and negated powers now render as `-(2^2)`
  rather than `-2^2`.

- **([#243](https://github.com/cortex-js/compute-engine/issues/243)) LaTeX
  Parsing**: Fixed logic operator precedence causing expressions like
  `x = 1 \vee x = 2` to be parsed incorrectly as `x = (1 ∨ x) = 2` instead of
  `(x = 1) ∨ (x = 2)`. Comparison operators (`=`, `<`, `>`, etc.) now correctly
  bind tighter than logic operators (`\land`, `\lor`, `\veebar`, etc.).

- **([#264](https://github.com/cortex-js/compute-engine/issues/264))
  Serialization**: Fixed LaTeX serialization of quantified expressions
  (`ForAll`, `Exists`, `ExistsUnique`, `NotForAll`, `NotExists`). Previously,
  only the quantifier symbol was output (e.g., `\forall x` instead of
  `\forall x, x>y`). The body of the quantified expression is now correctly
  serialized.

- **([#257](https://github.com/cortex-js/compute-engine/issues/257)) LaTeX
  Parsing**: Fixed `\gcd` command not parsing function arguments correctly.
  Previously `\gcd\left(24,37\right)` would parse as
  `["Tuple", "GCD", ["Tuple", 24, 37]]` instead of the expected
  `["GCD", 24, 37]`. The `\operatorname{gcd}` form was unaffected. Also added
  support for `\lcm` as a LaTeX command (in addition to the existing
  `\operatorname{lcm}`).

- **([#223](https://github.com/cortex-js/compute-engine/issues/223))
  Serialization**: Fixed scientific/engineering LaTeX serialization dropping the
  leading coefficient for exact powers of ten. For example, `1000` now
  serializes to `1\cdot10^{3}` (or `1\times10^{3}` depending on
  `exponentProduct`) instead of `10^{3}`.

- **LaTeX Parsing**: Fixed `\cosh` incorrectly mapping to `Csch` instead of
  `Cosh`.

- **([#255](https://github.com/cortex-js/compute-engine/issues/255)) LaTeX
  Parsing**: Fixed multi-letter subscripts like `A_{CD}` causing
  "incompatible-type" errors in arithmetic operations. Multi-letter subscripts
  without parentheses are now interpreted as compound symbol names (e.g.,
  `A_{CD}` → `A_CD`, `x_{ij}` → `x_ij`, `T_{max}` → `T_max`). Use parentheses
  for expression subscripts: `A_{(CD)}` creates a `Subscript` expression where
  `CD` represents implicit multiplication. The `Delimiter` wrapper is now
  stripped from subscript expressions for cleaner output.

#### First-Order Logic

- **([#263](https://github.com/cortex-js/compute-engine/issues/263)) Quantifier
  Scope**: Fixed quantifier scope in First-Order Logic expressions. Previously,
  `\forall x.P(x)\rightarrow Q(x)` was parsed with the implication inside the
  quantifier scope: `["ForAll", "x", ["To", P(x), Q(x)]]`. Now it correctly
  follows standard FOL conventions where the quantifier binds only the
  immediately following formula: `["To", ["ForAll", "x", P(x)], Q(x)]`. This
  applies to all quantifiers (`ForAll`, `Exists`, `ExistsUnique`, `NotForAll`,
  `NotExists`) and all logical connectives (`\rightarrow`, `\to`, `\implies`,
  `\land`, `\lor`, `\iff`). Use explicit parentheses for wider scope:
  `\forall x.(P(x)\rightarrow Q(x))`. Also fixed quantifier type signatures to
  properly return `boolean`, enabling correct type checking when quantified
  expressions are used as arguments to logical operators.

#### Simplification

- **Sign Simplification**: Fixed `Sign(x).simplify()` returning `1` instead of
  `-1` when `x` is negative. The simplification rule incorrectly returned
  `ce.One` for both positive and negative cases.

#### Type System

- **Ceil Type Signature**: Fixed `Ceil` function signature from
  `(real) -> integer` to `(number) -> integer` to match `Floor`. This resolves
  "incompatible-type" errors when computing derivatives of ceiling expressions
  or using `Ceil` in contexts expecting a general number type.

#### Polynomials

- **Polynomial Degree Detection**: Fixed `polynomialDegree()` returning 0 for
  expressions like `e^x` or `e^(-x^2)` when it should return -1 (not a
  polynomial). When the base of a power is constant but the exponent depends on
  the variable, this is not a polynomial. This bug caused infinite recursion in
  simplification when simplifying expressions containing exponentials, such as
  the derivative of `erf(x)` which is `(2/√π)·e^(-x²)`.

#### Pattern Matching

- **([#258](https://github.com/cortex-js/compute-engine/issues/258)) Pattern
  Matching**: Fixed `BoxedExpression.match()` returning `null` when matching
  patterns against canonicalized expressions. Several cases are now handled:
  - `Rational` patterns now match expressions like `['Rational', 'x', 2]` which
    are canonicalized to `['Multiply', ['Rational', 1, 2], 'x']`
  - `Power` patterns now match `['Power', 'x', -1]` which is canonicalized to
    `['Divide', 1, 'x']`, returning `{_base: x, _exp: -1}`
  - `Power` patterns now match `['Root', 'x', 3]` (cube root), returning
    `{_base: x, _exp: ['Divide', 1, 3]}`

#### Sum and Product

- **([#252](https://github.com/cortex-js/compute-engine/issues/252))
  Sum/Product**: Fixed `Sum` and `Product` returning `NaN` when the body
  contains free variables (variables not bound by the index). For example,
  `\sum_{n=1}^{10}(x)` now correctly evaluates to `10x` instead of `NaN`, and
  `\prod_{n=1}^{5}(x)` evaluates to `x^5`. Mixed expressions like
  `\sum_{n=1}^{10}(n \cdot x)` now return `55x`. Also fixed `toString()` for
  `Sum` and `Product` expressions with non-trivial bodies (e.g., `Multiply`)
  which were incorrectly displayed as `int()`.

#### Equation Solving

- **([#242](https://github.com/cortex-js/compute-engine/issues/242)) Solve**:
  Fixed `solve()` returning an empty array for equations with variables in
  fractions. For example, `F = 3g/h` solved for `g` now correctly returns `Fh/3`
  instead of an empty array. The solver now clears denominators before applying
  solve rules, enabling it to handle expressions like `a + bx/c = 0`. Also added
  support for solving equations where the variable is in the denominator (e.g.,
  `a/x = b` now returns `x = a/b`).

- **([#220](https://github.com/cortex-js/compute-engine/issues/220)) Solve**:
  Fixed `solve()` returning an empty array for equations involving square roots
  of the unknown, e.g. `2x = \sqrt{5x}`. The solver now handles equations of the
  form `ax + b√x + c = 0` using quadratic substitution. Also added support for
  solving logarithmic equations like `a·ln(x) + b = 0` which returns
  `x = e^(-b/a)`.

### Improvements

#### First-Order Logic

- **([#263](https://github.com/cortex-js/compute-engine/issues/263)) First-Order
  Logic**: Added several improvements for working with First-Order Logic
  expressions:
  - **Configurable quantifier scope**: New `quantifierScope` parsing option
    controls how quantifier scope is determined. Use `"tight"` (default) for
    standard FOL conventions where quantifiers bind only the immediately
    following formula, or `"loose"` for scope extending to the end of the
    expression.
    ```typescript
    ce.parse('\\forall x. P(x)', { quantifierScope: 'tight' })  // default
    ce.parse('\\forall x. P(x)', { quantifierScope: 'loose' })
    ```
  - **Automatic predicate inference**: Single uppercase letters followed by
    parentheses (e.g., `P(x)`, `Q(a,b)`) are now automatically recognized as
    predicate/function applications without requiring explicit declaration. This
    enables natural FOL syntax like `\forall x. P(x) \rightarrow Q(x)` to work
    out of the box.
  - **Quantifier evaluation over finite domains**: Quantifiers (`ForAll`,
    `Exists`, `ExistsUnique`, `NotForAll`, `NotExists`) now evaluate to boolean
    values when the bound variable is constrained to a finite set. For example:
    ```typescript
    ce.box(['ForAll', ['Element', 'x', ['Set', 1, 2, 3]], ['Greater', 'x', 0]]).evaluate()
    // Returns True (all values in {1,2,3} are > 0)
    ce.box(['Exists', ['Element', 'x', ['Set', 1, 2, 3]], ['Greater', 'x', 2]]).evaluate()
    // Returns True (3 > 2)
    ce.box(['ExistsUnique', ['Element', 'x', ['Set', 1, 2, 3]], ['Equal', 'x', 2]]).evaluate()
    // Returns True (only one element equals 2)
    ```
    Supports `Set`, `List`, `Range`, and integer `Interval` domains up to 1000
    elements. Nested quantifiers are evaluated over the Cartesian product of
    their domains.
  - **Symbolic simplification for quantifiers**: Quantifiers now simplify
    automatically in special cases:
    - `∀x. True` → `True`, `∀x. False` → `False`
    - `∃x. True` → `True`, `∃x. False` → `False`
    - `∀x. P` → `P` (when P doesn't contain x)
    - `∃x. P` → `P` (when P doesn't contain x)
  - **CNF/DNF conversion**: New `ToCNF` and `ToDNF` functions convert boolean
    expressions to Conjunctive Normal Form and Disjunctive Normal Form
    respectively:
    ```typescript
    ce.box(['ToCNF', ['Or', ['And', 'A', 'B'], 'C']]).evaluate()
    // Returns (A ∨ C) ∧ (B ∨ C)
    ce.box(['ToDNF', ['And', ['Or', 'A', 'B'], 'C']]).evaluate()
    // Returns (A ∧ C) ∨ (B ∧ C)
    ```
    Handles `And`, `Or`, `Not`, `Implies`, `Equivalent`, `Xor`, `Nand`, and
    `Nor` operators using De Morgan's laws and distribution.
  - **Boolean operator evaluation**: Added evaluation support for `Xor`, `Nand`,
    and `Nor` operators with `True`/`False` arguments:
    ```typescript
    ce.box(['Xor', 'True', 'False']).evaluate()   // Returns True
    ce.box(['Nand', 'True', 'True']).evaluate()   // Returns False
    ce.box(['Nor', 'False', 'False']).evaluate()  // Returns True
    ```
  - **N-ary boolean operators**: `Xor`, `Nand`, and `Nor` now support any number
    of arguments:
    - `Xor(a, b, c, ...)` returns true when an odd number of arguments are true
    - `Nand(a, b, c, ...)` returns the negation of `And(a, b, c, ...)`
    - `Nor(a, b, c, ...)` returns the negation of `Or(a, b, c, ...)`
  - **Satisfiability checking**: New `IsSatisfiable` function checks if a
    boolean expression can be made true with some assignment of variables:
    ```typescript
    ce.box(['IsSatisfiable', ['And', 'A', ['Not', 'A']]]).evaluate()  // False
    ce.box(['IsSatisfiable', ['Or', 'A', 'B']]).evaluate()            // True
    ```
  - **Tautology checking**: New `IsTautology` function checks if a boolean
    expression is true for all possible variable assignments:
    ```typescript
    ce.box(['IsTautology', ['Or', 'A', ['Not', 'A']]]).evaluate()     // True
    ce.box(['IsTautology', ['And', 'A', 'B']]).evaluate()             // False
    ```
  - **Truth table generation**: New `TruthTable` function generates a complete
    truth table for a boolean expression:
    ```typescript
    ce.box(['TruthTable', ['And', 'A', 'B']]).evaluate()
    // Returns [["A","B","Result"],["False","False","False"],...]
    ```
  - **Explicit `Predicate` function**: Added a new `Predicate` function to
    explicitly represent predicate applications in First-Order Logic. Inside
    quantifier scopes (`\forall`, `\exists`, etc.), single uppercase letters
    followed by parentheses are now parsed as `["Predicate", "P", "x"]` instead
    of `["P", "x"]`. This distinguishes predicates from regular function
    applications and avoids naming conflicts with library functions.
    ```typescript
    ce.parse('\\forall x. P(x)').json
    // Returns ["ForAll", "x", ["Predicate", "P", "x"]]
    ```
    Outside quantifier scopes, `P(x)` is still parsed as `["P", "x"]` to
    maintain backward compatibility with function definitions like
    `Q(x) := ...`.
  - **`D(f, x)` no longer maps to derivative**: The LaTeX notation `D(f, x)` is
    not standard mathematical notation for derivatives and previously caused
    confusion with the `D` derivative function in MathJSON. Now `D(f, x)` in
    LaTeX parses as `["Predicate", "D", "f", "x"]` instead of the derivative.
    Use Leibniz notation (`\frac{d}{dx}f`) for derivatives in LaTeX, or
    construct the derivative directly in MathJSON: `["D", expr, "x"]`.
  - **`N(x)` no longer maps to numeric evaluation**: Similarly, `N(x)` in LaTeX
    is CAS-specific notation, not standard math notation. Now `N(x)` parses as
    `["Predicate", "N", "x"]` instead of the numeric evaluation function. This
    allows `N` to be used as a variable (e.g., "for all N in Naturals"). Use the
    `.N()` method for numeric evaluation, or construct it directly in MathJSON:
    `["N", expr]`.

#### Polynomials

- **Polynomial Simplification**: The `simplify()` function now automatically
  cancels common polynomial factors in univariate rational expressions. For
  example, `(x² - 1)/(x - 1)` simplifies to `x + 1`, `(x³ - x)/(x² - 1)`
  simplifies to `x`, and `(x + 1)/(x² + 3x + 2)` simplifies to `1/(x + 2)`.
  Previously, this required explicitly calling the `Cancel` function with a
  variable argument.

#### Sum and Product

- **Sum/Product Simplification**: Added simplification rules for `Sum` and
  `Product` expressions with symbolic bounds:
  - Constant body: `\sum_{n=1}^{b}(x)` simplifies to `b * x`
  - Triangular numbers (general bounds): `\sum_{n=a}^{b}(n)` simplifies to
    `(b(b+1) - a(a-1))/2`
  - Sum of squares: `\sum_{n=1}^{b}(n^2)` simplifies to `b(b+1)(2b+1)/6`
  - Sum of cubes: `\sum_{n=1}^{b}(n^3)` simplifies to `[b(b+1)/2]^2`
  - Geometric series: `\sum_{n=0}^{b}(r^n)` simplifies to `(1-r^(b+1))/(1-r)`
  - Alternating unit series: `\sum_{n=0}^{b}((-1)^n)` simplifies to
    `(1+(-1)^b)/2`
  - Alternating linear series: `\sum_{n=0}^{b}((-1)^n * n)` simplifies to
    `(-1)^b * floor((b+1)/2)`
  - Arithmetic progression: `\sum_{n=0}^{b}(a + d*n)` simplifies to
    `(b+1)(a + db/2)`
  - Sum of binomial coefficients: `\sum_{k=0}^{n}C(n,k)` simplifies to `2^n`
  - Alternating binomial sum: `\sum_{k=0}^{n}((-1)^k * C(n,k))` simplifies to
    `0`
  - Weighted binomial sum: `\sum_{k=0}^{n}(k * C(n,k))` simplifies to
    `n * 2^(n-1)`
  - Partial fractions (telescoping): `\sum_{k=1}^{n}(1/(k(k+1)))` simplifies to
    `n/(n+1)`
  - Partial fractions (telescoping): `\sum_{k=2}^{n}(1/(k(k-1)))` simplifies to
    `(n-1)/n`
  - Weighted squared binomial sum: `\sum_{k=0}^{n}(k^2 * C(n,k))` simplifies to
    `n(n+1) * 2^(n-2)`
  - Weighted cubed binomial sum: `\sum_{k=0}^{n}(k^3 * C(n,k))` simplifies to
    `n²(n+3) * 2^(n-3)`
  - Alternating weighted binomial sum: `\sum_{k=0}^{n}((-1)^k * k * C(n,k))`
    simplifies to `0` (n ≥ 2)
  - Sum of binomial squares: `\sum_{k=0}^{n}(C(n,k)^2)` simplifies to `C(2n, n)`
  - Sum of consecutive products: `\sum_{k=1}^{n}(k(k+1))` simplifies to
    `n(n+1)(n+2)/3`
  - Arithmetic progression (general bounds): `\sum_{n=m}^{b}(a + d*n)`
    simplifies to `(b-m+1)(a + d(m+b)/2)`
  - Product of constant: `\prod_{n=1}^{b}(x)` simplifies to `x^b`
  - Factorial: `\prod_{n=1}^{b}(n)` simplifies to `b!`
  - Shifted factorial: `\prod_{n=1}^{b}(n+c)` simplifies to `(b+c)!/c!`
  - Odd double factorial: `\prod_{n=1}^{b}(2n-1)` simplifies to `(2b-1)!!`
  - Even double factorial: `\prod_{n=1}^{b}(2n)` simplifies to `2^b * b!`
  - Rising factorial (Pochhammer): `\prod_{k=0}^{n-1}(x+k)` simplifies to
    `(x)_n`
  - Falling factorial: `\prod_{k=0}^{n-1}(x-k)` simplifies to `x!/(x-n)!`
  - Telescoping product: `\prod_{k=1}^{n}((k+1)/k)` simplifies to `n+1`
  - Wallis-like product: `\prod_{k=2}^{n}(1 - 1/k^2)` simplifies to `(n+1)/(2n)`
  - Factor out constants: `\sum_{n=1}^{b}(c \cdot f(n))` simplifies to
    `c \cdot \sum_{n=1}^{b}(f(n))`, and similarly for products where the
    constant is raised to the power of the iteration count
  - Nested sums/products: inner sums/products are simplified first, enabling
    cascading simplification
  - Edge cases: empty ranges (upper < lower) return identity elements (0 for
    Sum, 1 for Product), and single-iteration ranges substitute the bound value

## 0.31.0 _2026-01-27_

### Breaking Changes

- The `[Length]` function has been renamed to `[Count]`.
- The `xsize` property of collections has been renamed to `count`.
- The `xcontains()` method of collections has been renamed to `contains()`.
- Handling of dictionaries (`["Dictionary"]` expressions and `\{dict:...\}`
  shorthand) has been improved.
- **Inverse hyperbolic functions** have been renamed to follow the ISO 80000-2
  standard: `Arcsinh` → `Arsinh`, `Arccosh` → `Arcosh`, `Arctanh` → `Artanh`,
  `Arccoth` → `Arcoth`, `Arcsech` → `Arsech`, `Arccsch` → `Arcsch`. The "ar"
  prefix (for "area") is mathematically correct since these functions relate to
  areas on a hyperbola, not arc lengths. Both LaTeX spellings (`\arsinh` and
  `\arcsinh`) are accepted as input (Postel's law).

### Resolved Issues

#### LaTeX Parsing

- **Metadata Preservation**: Fixed `verbatimLatex` not being preserved when
  parsing with `preserveLatex: true`. The original LaTeX source is now correctly
  stored on parsed expressions (when using non-canonical mode). Also fixed
  metadata (`latex`, `wikidata`) being lost when boxing MathJSON objects that
  contain these attributes.

- **String Parsing**: Fixed parsing of `\text{...}` with `preserveLatex: true`
  which was incorrectly returning an "invalid-symbol" error instead of a string
  expression.

#### Calculus

- **Derivatives**: `d/dx e^x` now correctly simplifies to `e^x` instead of
  `ln(e) * e^x`. The `hasSymbolicTranscendental()` function now recognizes that
  transcendentals which simplify to exact rational values (like `ln(e) = 1`)
  should not be preserved symbolically.

- **Derivatives**: `d/dx log(x)` now returns `1 / (x * ln(10))` symbolically
  instead of evaluating to `0.434... / x`. Fixed by using substitution instead
  of function application when applying derivative formulas, which preserves
  symbolic transcendental constants.

#### Arithmetic

- **Rationals**: Fixed `reducedRational()` to properly normalize negative
  denominators before the early return check. Previously `1/-2` would not
  canonicalize to `-1/2`.

- **Arithmetic**: Fixed `.mul()` to preserve logarithms symbolically. Previously
  multiplying expressions containing `Ln` or `Log` would evaluate the logarithm
  to its numeric value.

#### Serialization

- **Serialization**: Fixed case inconsistency in `toString()` output for
  trigonometric functions. Some functions like `Cot` were being serialized with
  capital letters while others like `csc` were lowercase. All trig functions now
  consistently serialize in lowercase (e.g., `cot(x)` instead of `Cot(x)`).

- **Serialization**: Improved display of inverse trig derivatives and similar
  expressions:
  - Negative exponents like `x^(-1/2)` now display as `1/sqrt(x)` in both LaTeX
    and ASCII-math output
  - When a sum starts with a negative term and contains a positive constant, the
    constant is moved to the front (e.g., `-x^2 + 1` displays as `1 - x^2`)
    while preserving polynomial ordering (e.g., `x^2 - x + 3` stays unchanged)
  - `d/dx arcsin(x)` now displays as `1/sqrt(1-x^2)` instead of
    `(-x^2+1)^(-1/2)`

- **Scientific Notation**: Fixed normalization of scientific notation for
  fractional values (e.g., numbers less than 1).

#### Sum and Product

- **Compilation**: Fixed compilation of `Sum` and `Product` expressions.

- **Sum/Product**: Fixed `sum` and `prod` library functions to correctly handle
  substitution of index variables.

### New Features and Improvements

#### Serialization

- **Number Serialization**: Added `adaptiveScientific` notation mode. When
  serializing numbers to LaTeX, this mode uses scientific notation but avoids
  exponents within a configurable range (controlled by `avoidExponentsInRange`).
  This provides a balance between readability and precision for numbers across
  different orders of magnitude.

#### Type System

- Refactored the type parser to use a modular architecture. This allows for
  better extensibility and maintainability of the type system.

#### Pattern Matching

- **Pattern Matching**: The `validatePattern()` function is now exported from
  the public API. Use it to check patterns for invalid combinations like
  consecutive sequence wildcards before using them.

#### Polynomials

- **Polynomial Arithmetic**: Added new library functions for polynomial
  operations:
  - `PolynomialDegree(expr, var)` - Get the degree of a polynomial
  - `CoefficientList(expr, var)` - Get the list of coefficients
  - `PolynomialQuotient(dividend, divisor, var)` - Polynomial division quotient
  - `PolynomialRemainder(dividend, divisor, var)` - Polynomial division
    remainder
  - `PolynomialGCD(a, b, var)` - Greatest common divisor of polynomials
  - `Cancel(expr, var)` - Cancel common factors in rational expressions

#### Calculus

- **Integration**: Significantly expanded symbolic integration capabilities:
  - **Polynomial division**: Integrals like `∫ x²/(x²+1) dx` now correctly
    divide first, yielding `x - arctan(x)`
  - **Repeated linear roots**: `∫ 1/(x-1)² dx = -1/(x-1)` and higher powers
  - **Derivative pattern recognition**: `∫ f'(x)/f(x) dx = ln|f(x)|` is now
    recognized automatically
  - **Completing the square**: Irreducible quadratics like `∫ 1/(x²+2x+2) dx`
    now yield `arctan(x+1)`
  - **Reduction formulas**: `∫ 1/(x²+1)² dx` now works using reduction formulas
  - **Mixed partial fractions**: `∫ 1/((x-1)(x²+1)) dx` now decomposes correctly
  - **Factor cancellation**: `∫ (x+1)/(x²+3x+2) dx` simplifies before
    integrating
  - **Inverse hyperbolic**: Added `∫ 1/√(x²+1) dx = arcsinh(x)` and
    `∫ 1/√(x²-1) dx = arccosh(x)`
  - **Arcsec pattern**: Added `∫ 1/(x·√(x²-1)) dx = arcsec(x)`
  - **Trigonometric substitution**: Added support for `∫√(a²-x²) dx`,
    `∫√(x²+a²) dx`, and `∫√(x²-a²) dx` using trig/hyperbolic substitution

## 0.30.2 _2025-07-15_

### Breaking Changes

- The `expr.value` property reflects the value of the expression if it is a
  number literal or a symbol with a literal value. If you previously used the
  `expr.value` property to get the value of an expression, you should now use
  the `expr.N().valueOf()` method instead. The `valueOf()` method is suitable
  for interoperability with JavaScript, but it may result in a loss of precision
  for numbers with more than 15 digits.

- `BoxedExpr.sgn` now returns _undefined_ for complex numbers, or symbols with a
  complex-number value.

- The `ce.assign()` method previously accepted
  `ce.assign("f(x, y)", ce.parse("x+y"))`. This is now deprecated. Use
  `ce.assign("f", ce.parse("(x, y) \\mapsto x+y")` instead.

- It was previously possible to invoke `expr.evaluate()` or `expr.N()` on a
  non-canonical expression. This will now return the expression itself.

  To evaluate a non-canonical expression, use `expr.canonical.evaluate()` or
  `expr.canonical.N()`.

  That's also the case for the methods `numeratorDenominator()`, `numerator()`,
  and `denominator()`.

  In addition, invoking the methods `inv()`, `abs()`, `add()`, `mul()`, `div()`,
  `pow()`, `root()`, `ln()` will throw an error if the expression is not
  canonical.

### New Features and Improvements

- Collections now support lazy materialization. This means that the elements of
  some collection are not computed until they are needed. This can significantly
  improve performance when working with large collections, and allow working
  with infinite collections. For example:

  ```js
  ce.box(['Map', 'Integers', 'Square']).evaluate().print();
  // -> [0, 1, 4, 9, 16, ...]
  ```

  Materialization can be controlled with the `materialization` option of the
  `evaluate()` method. Lazy collections are materialized by default when
  converted to a string or LaTeX, or when assigned to a variable.

- The bindings of symbols and function expressions is now consistently done
  during canonicalization.

- It was previously not possible to change the type of an identifier from a
  function to a value or vice versa. This is now possible.

- **Antiderivatives** are now computed symbolically:

```js
ce.parse(`\\int_0^1 \\sin(\\pi x) dx`).evaluate().print();
// -> 2 / pi
ce.parse(`\\int \\sin(\\pi x) dx`).evaluate().print();
// -> -cos(pi * x) / pi
```

Requesting a numeric approximation of the integral will use a Monte Carlo
method:

```js
ce.parse(`\\int_0^1 \\sin(\\pi x) dx`).N().print();
// -> 0.6366
```

- Numeric approximations of integrals is several order of magnitude faster.

- Added **Number Theory** functions: `Totient`, `Sigma0`, `Sigma1`,
  `SigmaMinus1`, `IsPerfect`, `Eulerian`, `Stirling`, `NPartition`,
  `IsTriangular`, `IsSquare`, `IsOctahedral`, `IsCenteredSquare`, `IsHappy`,
  `IsAbundant`.

- Added **Combinatorics** functions: `Choose`, `Fibonacci`, `Binomial`,
  `CartesianProduct`, `PowerSet`, `Permutations`, `Combinations`, `Multinomial`,
  `Subfactorial` and `BellNumber`.

- The `symbol` type can be refined to match a specific symbol. For example
  `symbol<True>`. The type `expression` can be refined to match expressions with
  a specific operator, for example `expression<Add>` is a type that matches
  expressions with the `Add` operator. The numeric types can be refined with a
  lower and upper bound. For example `integer<0..10>` is a type that matches
  integers between 0 and 10. The type `real<1..>` matches real numbers greater
  than 1 and `rational<..0>` matches non-positive rational numbers.

- Numeric types can now be constrained with a lower and upper bound. For
  example, `real<0..10>` is a type that matches real numbers between 0 and 10.
  The type `integer<1..>` matches integers greater than or equal to 1.

- Collections that can be indexed (`list`, `tuple`) are now a subtype of
  `indexed_collection`.

- The `map` type has been replaced with `dictionary` for collections of
  arbitrary key-value pairs and `record` for collections of structured key-value
  pairs.

- Support for structural typing has been added. To define a structural type, use
  `ce.declareType()` with the `alias` flag, for example:

  ```js
  ce.declareType(
    "point", "tuple<x: integer, y: integer>",
    { alias: true }
  );
  ```

- Recursive types are now supported by using the `type` keyword to forward
  reference types. For example, to define a type for a binary tree:

  ```js
  ce.declareType(
    "binary_tree",
    "tuple<value: integer, left: type binary_tree?, right: type binary_tree?>",
  );
  ```

- The syntax for variadic arguments has changeed. To indicate a variadic
  argument, use a `+` or `*` after the type, for example:

  ```js
  ce.declare('f', '(number+) -> number');
  ```

  Use `+` for a non-empty list of arguments and `*` for a possibly empty list.

- Added a rule to solve the equation `a^x + b = 0`

- The LaTeX parser now supports the `\placeholder[]{}`, `\phantom{}`,
  `\hphantom{}`, `\vphantom{}`, `\mathstrut`, `\strut` and `\smash{}` commands.

- The range of recognized sign values, i.e. as returned from
  `BoxedExpression.sgn` has been simplified (e.g. '...-infinity' and 'nan' have
  been removed)

- The Power canonical-form is less aggressive - only carrying-out ops. as listed
  in doc. - is much more careful in its consideration of operand types &
  values... (for example, typically, exponents are required to be _numbers_:
  e.g. `x^1` will simplify, but `x^y` (where `y===0`), or `x^{1+0}`, will not)

### Issues Resolved

- Ensure expression LaTeX serialization is based on MathJSON generated with
  matching "pretty" formatting (or not), therefore resulting in LaTeX with less
  prettification, where `prettify === false` (#daef87f)

- Symbols declare with a `constant` flag are now not marked as "inferred"

- Some `BoxedSymbols` properties now more consistently return `undefined`,
  instead of a `boolean` (i.e. because the symbol is non-bound)

- Some `expr.root()` computations

- Canonical-forms
  - Fixes the `Number` form
  - Forms (at least, `Number`, `Power`) do not mistakenly _fully_ canonicalize
    operands
  - This (partial canonicalization) now substitutes symbols (constants) with a
    `holdUntil` value of `"never"` during/prior-to canonicalization (i.e. just
    like for full canonicalization)

## 0.29.1 _2025-03-31_

- **#231** During evaluation, some numbers, for example `10e-15` were
  incorrectly rounded to 0.

## 0.28.0 _2025-02-06_

### Issues Resolved

- **#211** More consistent canonicalization and serialization of exact numeric
  values of the form `(a√b)/c`.
- **#219** The `invisibleOperator` canonicalization previously also
  canonicalized some multiplication.
- **#218** Improved performance of parsing invisible operators, including fixing
  some cases where the parsing was incorrect.
- **#216** Correctly parse subscripts with a single character, for example
  `x_1`.
- **#216** Parse some non-standard integral signs, for example
  `\int x \cdot \differentialD x` (both the `\cdot` and the `\differentialD` are
  non-standard).
- **#210** Numeric approximation of odd nth roots of negative numbers evaluate
  correctly.
- **#153** Correctly parse integrals with `\limits`, e.g.
  `\int\limits_0^1 x^2 \mathrm{d} x`.
- Correctly serialize to ASCIIMath `Delimiter` expressions.
- When inferring the type of numeric values do not constrain them to be `real`.
  As a result:

  ```js
  ce.assign('a', ce.parse('i'));
  ce.parse('a+1').evaluate().print();
  ```

  now returns `1 + i` instead of throwing a type error.

- Correctly parse and evaluate unary and binary `\pm` and `\mp` operators.

### New Features and Improvements

- `expr.isEqual()` will now return true/false if the expressions include the
  same unknowns and are structurally equal after expansion and simplifications.
  For example:

  ```js
  console.info(ce.parse('(x+1)^2').isEqual(ce.parse('x^2+2x+1')));
  // -> true
  ```

#### Asynchronous Operations

Some computations can be time-consuming, for example, computing a very large
factorial. To prevent the browser from freezing, the Compute Engine can now
perform some operations asynchronously.

To perform an asynchronous operation, use the `expr.evaluateAsync` method. For
example:

```js
try {
  const fact = ce.parse('(70!)!');
  const factResult = await fact.evaluateAsync();
  factResult.print();
} catch (e) {
  console.error(e);
}
```

It is also possible to interrupt an operation, for example by providing a
pause/cancel button that the user can press. To do so, use an `AbortController`
object and a `signal`. For example:

```js
const abort = new AbortController();
const signal = abort.signal;
setTimeout(() => abort.abort(), 500);
try {
  const fact = ce.parse('(70!)!');
  const factResult = await fact.evaluateAsync({ signal });
  factResult.print();
} catch (e) {
  console.error(e);
}
```

In the example above, we trigger an abort after 500ms.

It is also possible to control how long an operation can run by setting the
`ce.timeLimit` property with a value in milliseconds. For example:

```js
ce.timeLimit = 1000;
try {
  const fact = ce.parse('(70!)!');
  fact.evaluate().print();
} catch (e) {
  console.error(e);
}
```

The time limit applies to either the synchronous or asynchronous evaluation.

The default time limit is 2,000ms (2 seconds).

When an operation is canceled either because of a timeout or an abort, a
`CancellationError` is thrown.

## 0.27.0 _2024-12-02_

- **#217** Correctly parse LaTeX expressions that include a command followed by
  a `*` such as `\\pi*2`.

- **#217** Correctly calculate the angle of trigonometric expressions with an
  expression containing a reference to `Pi`, for example `\\sin(\\pi^2)`.

- The `Factorial` function will now time out if the argument is too large. The
  timeout is signaled by throwing a `CancellationError`.

- When specifying `exp.toMathJSON({shorthands:[]})`, i.e., not to use shorthands
  in the MathJSON, actually avoid using shorthands.

- Correctly use custom multiply, plus, etc. for LaTeX serialization.

- When comparing two numeric values, the tolerance is now used to determine if
  the values are equal. The tolerance can be set with the `ce.tolerance`
  property.

- When comparing two expressions with `isEqual()` the values are compared
  structurally when necessary, or with a stochastic test when the expressions
  are too complex to compare structurally.

- Correctly serialize nested superscripts, e.g. `x^{y^z}`.

- The result of evaluating a `Hold` expression is now the expression itself.

- To prevent evaluation of an expression temporarily, use the `Unevaluated`
  function. The result of evaluating an `Unevaluated` expression is its
  argument.

- The type of a `Hold` expression was incorrectly returned as `string`. It now
  returns the type of its argument.

- The statistics function (`Mean`, `Median`, `Variance`, `StandardDeviation`,
  `Kurtosis`, `Skewness`, `Mode`, `Quartiles` and `InterQuartileRange`) now
  accept as argument either a collection or a sequence of values.

  ```js
  ce.parse("\\mathrm{Mean}([7, 2, 11])").evaluate().print();
  // -> 20/3
  ce.parse("\\mathrm{Mean}(7, 2, 11)").evaluate().print();
  // -> 20/3
  ```

- The `Variance` and `StandardDeviation` functions now have variants for
  population statistics, `PopulationVariance` and `PopulationStandardDeviation`.
  The default is to use sample statistics.

  ```js
  ce.parse("\\mathrm{PopulationVariance}([7, 2, 11])").evaluate().print();
  // -> 13.555
  ce.parse("\\mathrm{Variance}([7, 2, 11])").evaluate().print();
  // -> 20.333
  ```

- The statistics function can now be compiled to JavaScript:

  ```js
  const code = ce.parse("\\mathrm{Mean}(7, 2, 11)").compile();
  console.log(code());
  // -> 13.555
  ```

- The statistics function calculate either using machine numbers or bignums
  depending on the precision. The precision can be set with the `precision`
  property of the Compute Engine.

- The argument of compiled function is now optional.

- Compiled expressions can now reference external JavaScript functions. For
  example:

  ```js
  ce.defineFunction('Foo', {
    signature: 'number -> number',
    evaluate: ([x]) => ce.box(['Add', x, 1]),
  });

  const fn = ce.box(['Foo', 3]).compile({
    functions: { Foo: (x) => x + 1 },
  })!;

  console.info(fn());
  // -> 4
  ```

  ```js
  ce.defineFunction('Foo', {
    signature: 'number -> number',
    evaluate: ([x]) => ce.box(['Add', x, 1]),
  });

  function foo(x) {
    return x + 1;
  }

  const fn = ce.box(['Foo', 3]).compile({
    functions: { Foo: foo },
  })!;

  console.info(fn());
  // -> 4
  ```

  Additionally, functions can be implicitly imported (in case they are needed by
  other JavaScript functions):

  ```js
  ce.defineFunction('Foo', {
    signature: 'number -> number',
    evaluate: ([x]) => ce.box(['Add', x, 1]),
  });

  function bar(x, y) {
    return x + y;
  }

  function foo(x) {
    return bar(x, 1);
  }


  const fn = ce.box(['Foo', 3]).compile({
    functions: { Foo: 'foo' },
    imports: [foo, bar],
  })!;

  console.info(fn());
  // -> 4
  ```

- Compiled expression can now include an arbitrary preamble (JavaScript source)
  that is executed before the compiled function is executed. This can be used to
  define additional functions or constants.

  ```js
  ce.defineFunction('Foo', {
    signature: 'number -> number',
    evaluate: ([x]) => ce.box(['Add', x, 1]),
  });

  const code = ce.box(['Foo', 3]).compile({
    preamble: "function Foo(x) { return x + 1};",
  });
  ```

- The `hold` function definition flag has been renamed to `lazy`

## 0.26.4 _2024-10-17_

- **#201** Identifiers of the form `A_\text{1}` were not parsed correctly.
- **#202** Fixed serialization of integrals and bigops.

## 0.26.3 _2024-10-17_

- Correctly account for `fractionalDigits` when formatting numbers.
- **#191** Correctly handle `\\lnot\\forall` and `\\lnot\\exists`.
- **#206** The square root of 1000000 was canonicalized to 0.
- **#207** When a square root with a literal base greater than 1e6 was preceded
  by a non-integer literal number, the literal number was ignored during
  canonicalization.
- **#208** **#204** Correctly evaluate numeric approximation of roots, e.g.
  `\\sqrt[3]{125}`.
- **#205** `1/ln(0)` was incorrectly evaluated to `1`. It now returns `0`.

## 0.26.1 _2024-10-04_

### Issues Resolved

- **#194** Correctly handle the precedence of unary negate, for example in
  `-5^{\frac12}` or `-5!`.
- When using a function definition with `ce.declare()`, do not generate a
  runtime error.

### New Features and Improvements

- Added `.expand()` method to boxed expression. This method expands the
  expression, for example `ce.parse("(x+1)^2").expand()` will return
  `x^2 + 2x + 1`.

## 0.26.0 _2024-10-01_

### Breaking Changes

- The property `expr.head` has been deprecated. Use `expr.operator` instead.
  `expr.head` is still supported in this version but will be removed in a future
  update.

- The MathJSON utility functions `head()` and `op()` have been renamed to
  `operator()` and `operand()` respectively.

- The methods for algebraic operations (`add`, `div`, `mul`, etc...) have been
  moved from the Compute Engine to the Boxed Expression class. Instead of
  calling `ce.add(a, b)`, call `a.add(b)`.

  Those methods also behave more consistently: they apply some additional
  simplication rules over canonicalization. For example, while
  `ce.parse('1 + 2')` return `["Add", 1, 2]`, `ce.box(1).add(2)` will return
  `3`.

- The `ce.numericMode` option has been removed. Instead, set the `ce.precision`
  property to the desired precision. Set the precision to `"machine"` for
  machine precision calculations (about 15 digits). Set it to `"auto"` for a
  default of 21 digits. Set it to a number for a greater fixed precision.

- The MathJSON Dictionary element has been deprecated. Use a `Dictionary`
  expression instead.

- The `ExtendedRealNumbers`, `ExtendedComplexNumbers` domains have been
  deprecated. Use the `RealNumbers` and `ComplexNumbers` domains instead.

- The "Domain" expression has been deprecated. Use types instead (see below).

- Some `BoxedExpression` properties have been removed:
  - Instead of `expr.isZero`, use `expr.is(0)`.
  - Instead of `expr.isNotZero`, use `!expr.is(0)`.
  - Instead of `expr.isOne`, use `expr.is(1)`.
  - Instead of `expr.isNegativeOne`, use `expr.is(-1)`.

- The signature of `ce.declare()` has changed. In particular, the `N` handler
  has been replaced with `evaluate`.

```ts
// Before
ce.declare('Mean', {
  N: (ce: IComputeEngine): BoxedExpression => {
    return ce.number(1);
  },
});

// Now
ce.declare('Mean', { evaluate: (ops, { engine }) => ce.number(1) });
```

### New Features and Improvements

- **New Simplification Engine**

  The way expressions are simplified has been completely rewritten. The new
  engine is more powerful and more flexible.

  The core API remains the same: to simplify an expression, use
  `expr.simplify()`.

  To use a custom set of rules, pass the rules as an argument to `simplify()`:

  ```js
  expr.simplify({rules: [
    "|x:<0| -> -x",
    "|x:>=0| -> x",
  ]});
  ```

  There are a few changes to the way rules are represented. The `priority`
  property has been removed. Instead, rules are applied in the order in which
  they are defined.

  A rule can also now be a function that takes an expression and returns a new
  expression. For example:

  ```js
  expr.simplify({rules: [
    (expr) => {
      if (expr.operator !== 'Abs') return undefined;
      const x = expr.args[0];
      return x.isNegative ? x.negate() : expr;
    }
  ]});
  ```

  This can be used to perform more complex transformations at the cost of more
  verbose JavaScript code.

  The algorithm for simplification has been simplified. It attempts to apply
  each rule in the rule set in turn, then restarts the process until no more
  rules can be applied or the result of applying a rule returns a previously
  seen expression.

  Function definitions previously included a `simplify` handler that could be
  used to perform simplifications specific to this function. This has been
  removed. Instead, use a rule that matches the function and returns the
  simplified expression.

- **Types**

  Previously, an expression was associated with a domain such as `RealNumbers`
  or `ComplexNumbers`. This has been replaced with a more flexible system of
  types.

  A type is a set of values that an expression can take. For example, the type
  `real` is the set of real numbers, the type `integer` is the set of integers,

  The type of an expression can be set with the `type` property. For example:

  ```js
  const expr = ce.parse('\\sqrt{-1}');
  console.info(expr.type); // -> imaginary
  ```

  The type of a symbol can be set when declaring the symbol. For example:

  ```js
  ce.declare('x', 'imaginary');
  ```

  In addition to primitive types, the type system supports more complex types
  such union types, intersection types, and function types.

  For example, the type `real|imaginary` is the union of the real and imaginary
  numbers.

  When declaring a function, the type of the arguments and the return value can
  be specified. For example, to declare a function `f` that takes two integers
  and returns a real number:

  ```js
  ce.declare('f', '(integer, integer) -> real');
  ```

  The sets of numbers are defined as follows:
  - `number` - any number, real or complex, including NaN and infinity
  - `non_finite_number` - NaN or infinity
  - `real`
  - `finite_real` - finite real numbers (exclude NaN and infinity)
  - `imaginary` - imaginary numbers (complex numbers with a real part of 0)
  - `finite_imaginary`
  - `complex` - complex numbers with a real and imaginary part not equal to 0
  - `finite_complex`
  - `rational`
  - `finite_rational`
  - `integer`
  - `finite_integer`

  To check the type of an expression, use the `isSubtypeOf()` method. For
  example:

  ```js
  let expr = ce.parse('5');
  console.info(expr.type.isSubtypeOf('rational')); // -> true
  console.info(expr.type.isSubtypeOf('integer')); // -> true

  expr = ce.parse('\\frac{1}{2}');
  console.info(expr.type.isSubtypeOf('rational')); // -> true
  console.info(expr.type.isSubtypeOf('integer')); // -> false
  ```

  As a shortcut, the properties `isReal`, `isRational`, `isInteger` are
  available on boxed expressions. For example:

  ```js
  let expr = ce.parse('5');
  console.info(expr.isInteger); // -> true
  console.info(expr.isRational); // -> true
  ```

  They are equivalent to `expr.type.isSubtypeOf('integer')` and
  `expr.type.isSubtypeOf('rational')` respectively.

  To check if a number has a non-zero imaginary part, use:

  ```js
  let expr = ce.parse('5i');
  console.info(expr.isNumber && expr.isReal === false); // -> true
  ```

- **Collections**

  Support for collections has been improved. Collections include `List`, `Set`,
  `Tuple`, `Range`, `Interval`, `Linspace` and `Dictionary`.

  It is now possible to check if an element is contained in a collection using
  an `Element` expression. For example:

  ```js
  let expr = ce.parse('[1, 2, 3]');
  ce.box(['Element', 3, expr]).print(); // -> True
  ce.box(['Element', 5, expr]).print(); // -> False
  ```

  To check if a collection is a subset of another collection, use the `Subset`
  expression. For example:

  ```js
  ce.box(['Subset', 'Integers', 'RealNumbers']).print(); // -> True
  ```

  Collections can also be compared for equality. For example:

  ```js
  let set1 = ce.parse('\\lbrace 1, 2, 3 \\rbrace');
  let set2 = ce.parse('\\lbrace 3, 2, 1 \\rbrace');
  console.info(set1.isEqual(set2)); // -> true
  ```

  There are also additional convenience methods on boxed expressions:
  - `expr.isCollection`
  - `expr.contains(element)`
  - `expr.size`
  - `expr.isSubsetOf(other)`
  - `expr.indexOf(element)`
  - `expr.at(index)`
  - `expr.each()`
  - `expr.get(key)`

- **Exact calculations**

  The Compute Engine has a new backed for numerical calculations. The new backed
  can handle arbitrary precision calculations, including real and complex
  numbers. It can also handle exact calculations, preserving calculations with
  rationals and radicals (square root of integers). For example `1/2 + 1/3` is
  evaluated to `5/6` instead of `0.8(3)`.

  To get an approximate result, use the `N()` method, for example
  `ce.parse("\\frac12 + \\frac13").N()`.

  Previously the result of calculations was not always an exact number but
  returned a numerical approximation instead.

  This has now been improved by introducing a `NumericValue` type that
  encapsulates exact numbers and by doing all calculations in this type.
  Previously the calculations were handled manually in the various evaluation
  functions. This made the code complicated and error prone.

  A `NumericValue` is made of:
  - an imaginary part, represented as a fixed-precision number
  - a real part, represented either as a fixed or arbitrary precision number or
    as the product of a rational number and the square root of an integer.

  For example:
  - 234.567
  - 1/2
  - 3√5
  - √7/3
  - 4-3i

  While this is a significant change internally, the external API remains the
  same. The result of calculations should be more predictable and more accurate.

  One change to the public API is that the `expr.numericValue` property is now
  either a machine precision number or a `NumericValue` object.

- **Rule Wildcards**

  When defining a rule as a LaTeX expression, single character identifiers are
  interpreted as wildcards. For example, the rule `x + x -> 2x` will match any
  expression with two identical terms. The wildcard corresponding to `x` is
  `_x`.

  It is now possible to define sequence wildcards and optional sequence
  wildcards. Sequence wildcards match 1 or more expressions, while optional
  sequence wildcards match 0 or more expressions.

  They are indicated in LaTeX as `...x` and `...x?` respectively. For example:

  ```js
  expr.simplify("x + ...y -> 2x");
  ```

  If `expr` is `a + b + c` the rule will match and return `2a`

  ```js
  expr.simplify("x + ...y? -> 3x");
  ```

  If `expr` is `a + b + c` the rule will match and return `3a`. If `expr` is `a`
  the rule will match and return `3a`.

- **Conditional Rules**

  Rules can now include conditions that are evaluated at runtime. If the
  condition is not satisfied, the rules does not apply.

  For example, to simplify the expression `|x|`:

  ```js
  expr.simplify({rules: [
    "|x_{>=0}| -> x",
    "|x_{<0}| -> -x",
  ]});
  ```

  The condition is indicated as a subscript of the wildcard. The condition can
  be one of:
  - `boolean` - a boolean value, True or False
  - `string` - a string of characters
  - `number` - a number literal
  - `symbol`
  - `expression`

  - `numeric` - an expression that has a numeric value, i.e. 2√3, 1/2, 3.14
  - `integer` - an integer value, -2, -1, 0, 1, 2, 3, ...
  - `natural` - a natural number, 0, 1, 2, 3, ...
  - `real` - real numbers, including integers
  - `imaginary` - imaginary numbers, i.e. 2i, 3√-1 (not including real numbers)
  - `complex` - complex numbers, including real and imaginary
  - `rational` - rational numbers, 1/2, 3/4, 5/6, ...
  - `irrational` - irrational numbers, √2, √3, π, ...
  - `algebraic` - algebraic numbers, rational and irrational
  - `transcendental` - transcendental numbers, π, e, ...

  - `positive` - positive real numbers, \> 0
  - `negative` - negative real numbers, \< 0
  - `nonnegative` - nonnegative real numbers, \>= 0
  - `nonpositive` - nonpositive real numbers, \<= 0

  - `even` - even integers, 0, 2, 4, 6, ...
  - `odd` - odd integers, 1, 3, 5, 7, ...

  - `prime` :A000040 - prime numbers, 2, 3, 5, 7, 11, ...
  - `composite` :A002808 - composite numbers, 4, 6, 8, 9, 10, ...

  - `notzero` - a value that is not zero
  - `notone` - a value that is not one

  - `finite` - a finite value, not infinite
  - `infinite`

  - `constant`
  - `variable`

  - `function`

  - `operator`
  - `relation` - an equation or inequality
  - `equation`
  - `inequality`

  - `vector` - a tensor of rank 1
  - `matrix` - a tensor of rank 2
  - `list` - a collection of values
  - `set` - a collection of unique values
  - `tuple` - a fixed length list
  - `single` - a tuple of length 1
  - `pair` - a tuple of length 2
  - `triple` - a tuple of length 3
  - `collection` - a list, set, or tuple
  - `tensor` - a nested list of values of the same type
  - `scalar` - not a tensor or list

  or one of the following expressions:
  - `>0'` -> `positive`,
  - `\gt0'` -> `positive`,
  - `<0'` -> `negative`,
  - `\lt0'` -> `negative`,
  - `>=0'` -> `nonnegative`,
  - `\geq0'` -> `nonnegative`,
  - `<=0'` -> `nonpositive`,
  - `\leq0'` -> `nonpositive`,
  - `!=0'` -> `notzero`,
  - `\neq0'` -> `notzero`,
  - `!=1'` -> `notone`,
  - `\neq1'` -> `notone`,
  - `\in\Z'` -> `integer`,
  - `\in\mathbb{Z}'` -> `integer`,
  - `\in\N'` -> `natural`,
  - `\in\mathbb{N}'` -> `natural`,
  - `\in\R'` -> `real`,
  - `\in\mathbb{R}'` -> `real`,
  - `\in\C'` -> `complex`,
  - `\in\mathbb{C}'` -> `complex`,
  - `\in\Q'` -> `rational`,
  - `\in\mathbb{Q}'` -> `rational`,
  - `\in\Z^+'` -> `integer,positive`,
  - `\in\Z^-'` -> `intger,negative`,
  - `\in\Z^*'` -> `nonzero`,
  - `\in\R^+'` -> `positive`,
  - `\in\R^-'` -> `negative`,
  - `\in\R^*'` -> `real,nonzero`,
  - `\in\N^*'` -> `integer,positive`,
  - `\in\N_0'` -> `integer,nonnegative`,
  - `\in\R\backslash\Q'` -> `irrational`,

  More complex conditions can be specified following a semi-colon, for example:

  ```js
  expr.simplify({x -> 2x; x < 10});
  ```

  Note that this syntax complements the existing rule syntax, and can be used
  together with the existing, more verbose, rule syntax.

  ```js
  expr.simplify({rules: [
    {match: "x + x", replace: "2x", condition: "x < 10"}
  ]});
  ```

  This advanced syntax can specify more complex conditions, for example above
  the rule will only apply if `x` is less than 10.

- Improved results for `Expand`. In some cases the expression was not fully
  expanded. For example, `4x(3x+2)-5(5x-4)` now returns `12x^2 - 17x + 20`.
  Previously it returned `4x(3x+2)+25x-20`.

- **AsciiMath serialization** The `expr.toString()` method now returns a
  serialization of the expression using the [AsciiMath](https://asciimath.org/)
  format.

  The serialization to AsciiMath can be customized using the `toAsciiMath()`
  method. For example:

  ```js
  console.log(ce.box(['Sigma', 2]).toAsciiMath({functions: {Sigma: 'sigma'}}));
  // -> sigma(2)
  ```

- The tolerance can now be specified with a value of `"auto"` which will use the
  precision to determine a reasonable tolerance. The tolerance is used when
  comparing two numbers for equality. The tolerance can be specified with the
  `ce.tolerance` property or in the Compute Engine constructor.

- Boxed expressions have some additional properties:
  - `expr.isNumberLiteral` - true if the expression is a number literal.This is
    equivalent to checking if `expr.numericValue` is not `null`.
  - `expr.re` - the real part of the expression, if it is a number literal,
    `undefined` if not a number literal.
  - `expr.im` - the imaginary part of the expression, if it is a number literal,
    `undefined` if not a number literal.
  - `expr.bignumRe` - the real part of the expression as a bignum, if it is a
    number literal, `undefined` if not a number literal or a bignum
    representation is not available.
  - `expr.bignumIm` - the imaginary part of the expression as a bignum, if it is
    a number literal, `undefined` if not a number literal or if a bignum
    representation is not available.
  - `expr.root()` to get the root of the expression. For example, `expr.root(3)`
    will return the cube root of the expression.
  - Additionally, the relational operators (`expr.isLess(), expr.isEqual()`,
    etc...) now accept a number argument. For example, `expr.isGreater(1)` will
    return true if the expression is greater than 1.

- Added LaTeX syntax to index collections. If `a` is a collection:
  - `a[i]` is parsed as `["At", "a", "i"]`.
  - `a[i,j]` is parsed as `["At", "a", "i", "j"]`.
  - `a_i` is parsed as `["At", "a", "i"]`.
  - `a_{i,j}` is parsed as `["At", "a", "i", "j"]`.

- Added support for Kronecker delta notation, i.e. `\delta_{ij}`, which is
  parsed as `["KroneckerDelta", "i", "j"]` and is equal to 1 if `i = j` and 0
  otherwise.

  When a single index is provided the value of the function is 1 if the index is
  0 and 0 otherwise

  When multiple index are provided, the value of the function is 1 if all the
  indexes are equal and 0 otherwise.

- Added support for Iverson Bracket notation, i.e. `[a = b]`, which is parsed as
  `["Boole", ["Equal", "a", "b"]]` and is equal to 1 if its argument is true and
  0 otherwise. The argument is expected to be a relational expression.

- Implemented `Unique` and `Tally` on collections. `Unique` returns a collection
  with only the unique elements of the input collection, and `Tally` returns a
  collection with the count of each unique element.

  ```js
  console.log(ce.box(['Unique', ['List', 1, 2, 3, 1, 2, 3, 4, 5]]).value);
  // -> [1, 2, 3, 4, 5]

  console.log(ce.box(['Tally', ['List', 1, 2, 3, 1, 2, 3, 4, 5]]).value);
  // -> [['List', 1, 2, 3, 4, 5], ['List', 2, 2, 2, 1, 1]]
  ```

- Implemented the `Map`, `Filter` and `Tabulate` functions. These functions can
  be used to transform collections, for example:

  ```js
  // Using LaTeX
  console.log(ce.parse('\\mathrm{Map}([3, 5, 7], x \\mapsto x^2)').toString());
  // -> [9, 25, 49]

  // Using boxed expressions
  console.log(
    ce.box(['Map', ['List', 3, 5, 7], ['Square', '_']]).value
  );
  // -> [9, 25, 49]

  console.log(ce.box(['Tabulate',['Square', '_'], 5]).value);
  // -> [1, 4, 9, 16, 25]
  ```

  `Tabulate` can be used with multiple indexes. For example, to generate a 4x4
  unit matrix:

  ```js
  console.log(ce.box(['Tabulate', ['If', ['Equal', '_1', '_2'], 1, 0]], 4, 4).value);
  // -> [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]

  // Using the Kronecker delta notation:
  console.log(ce.parse('\\mathrm{Tabulate}(i, j \\mapsto \\delta_{ij}, 4, 4)').value);
  // -> [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]

  ```

- Added `Random` function. `["Random"]` returns a real pseudo-random number
  betwen 0 and 1. `["Random", 10]` returns an integer between 0 and 9,
  `["Random", 5, 10]` returns an integer between 5 and 10.

- Extended the definition of `expr.isConstant`. Previously, it only applied to
  symbols, e.g. `Pi`. Now it apply to all expressions. `expr.isConstant` is true
  if the expression is a number literal, a symbol with a constant value, or a
  pure function with constant arguments.

- The boxed expression properties `isPositive`, `isNegative`, `isNonNegative`,
  `isNonPositive`, `isZero`, `isNotZero` now return a useful value for most
  function expressions. For example, `ce.parse('|x + 1|').isPositive` is true.

  If the value cannot be determined, the property will return `undefined`. For
  example, `ce.parse('|x + 1|').isZero` is `undefined`.

  If the expression is not a real number, the property will return `NaN`. For
  example, `ce.parse('i').isPositive` is `NaN`.

- Added `Choose` function to compute binomial coefficients, i.e. `Choose(5, 2)`
  is equal to 10.

- The fallback for non-constructible complex values of trigonometric functions
  is now implemented via rules.

- The canonical order of the arguments has changed and should be more consistent
  and predictable. In particular, for polynomials, the
  [monomial order](https://en.wikipedia.org/wiki/Monomial_order) is now
  **degrevlex**.

- Canonical expressions can now include a `Root` expression. For example, the
  canonical form of `\\sqrt[3]{5}` is `["Root", 5, 3]`. Previously, these were
  represented as `["Power", 5, ["Divide", 1, 3]]`.

- The function definitions no longer have a `N` handler. Instead the `evaluate`
  handler has an optional `{numericApproximation}` argument.

### Issues Resolved

- **#188** Throw an error when invalid expressions are boxed, for example
  `ce.box(["Add", ["3"]])`.

- Some LaTeX renderer can't render `\/`, so use `/` instead.

- When definitions are added to the LaTeX dictionary, they now take precedence
  over the built-in definitions. This allows users to override the built-in
  definitions.

- Improved parsing of functions, including when a mixture of named and
  positional arguments are used.

- **#175** Matching some patterns when the target had not enough operands would
  result in a runtime error.

## 0.25.1 _2024-06-27_

### Issues Resolved

- **#174** Fixed some simplifications, such as `\frac{a^n}{a^m} = a^{n-m)`

### New Features

- Rules can be defined using a new shorthand syntax, where each rule is a string
  of LaTeX:

  ```js
  expr.simplify(["\\frac{x}{x} -> 1", "x + x -> 2x"]);
  ```

Single letter variables are assumed to be wildcards, so `x` is interpreted as
the wildcard `_x`.

Additionally, the expanded form can also include LaTeX strings. The previous
syntax using expressions can still be used, and the new and old syntax can be
mixed.

For example:

```js
expr.simplify([
  {
    match: "\\frac{x}{x}",
    replace: "1"
  },
  {
    match: ["Add", "x", "x"],
    replace: "2x"
  }
]);
```

The `condition` function can also be expressed as a LaTeX string.

```js
  expr.simplify([ { match: "\\frac{x}{x}", replace: 1, condition: "x != 0" }, ]);
```

The shorthand syntax can be used any where a ruleset is expected, including with
the `ce.rule()` function.

- A new `ce.getRuleSet()` method gives access to the built-in rules.
- **#171** The `Subtract` and `Divide` function can now accept an arbitrary
  number of arguments. For example, `["Subtract", 1, 2, 3]` is equivalent to
  `["Subtract", ["Subtract", 1, 2], 3]`.

## 0.25.0 _2024-06-25_

### Breaking Changes

- The canonical form of expressions has changed. It is now more consistent and
  simpler and should produce more predictable results.

  For example, previously `ce.parse("1-x^2")` would produce
  `["Subtract", 1, ["Square", "x"]]`.

  While this is a readable form, it introduces some complications when
  manipulating the expression: both the `Subtract` and `Square` functions have
  to be handled, in addition to `Add` and `Power`.

  The new canonical form of this expression is
  `["Add", 1, ["Negate", ["Power", "x", 2]]]`. It is a bit more verbose, but it
  is simpler to manipulate.

- The `ce.serialize()` method has been replaced with `expr.toLatex()` and
  `expr.toMathJson()`. The `ce.latexOptions` and `ce.jsonSerializationOptions`
  properties have been removed. Instead, pass the formating options directly to
  the `toLatex()` and `toMathJson()` methods. The `ce.parse()` method now takes
  an optional argument to specify the format of the input string.

- The default JSON serialization of an expression has changed.

  Previously, the default JSON serialization, accessed via the `.json` property,
  had some transformations applied to it (sugaring) to make the JSON more human
  readable.

  For example, `ce.parse("\frac12").json` would return the symbol `"Half"`
  instead of `["Divide", 1, 2]`.

  However, this could lead to some confusion when manipulating the JSON
  directly. Since the JSON is intended to be used by machine more than humans,
  these additional transformations have been removed.

  The `expr.json` property now returns the JSON representing the expression,
  without any transformations.

  To get a version of JSON with some transformations applied use the
  `ce.toMathJson()` function.

  ```js
  expr = ce.box(["Subtract", 1, ["Square", "x"]]);
  console.log(expr.json);
  // -> ["Add", 1, ["Negate", ["Power", "x", 2]]]
  expr.toMathJson()
  // -> ["Subtract", 1, ["Square", "x"]]
  expr.toMathJson({exclude: "Square"})
  // -> ["Subtract", 1, ["Power", "x", 2]]
  ```

  In practice, the impact of both of these changes should be minimal. If you
  were manipulating expressions using `BoxedExpression`, the new canonical form
  should make it easier to manipulate expressions. You can potentially simplify
  your code by removing special cases for functions such as `Square` and
  `Subtract`.

  If you were using the JSON serialization directly, you may also be able to
  simplify you code since the default output from `expr.json` is now more
  consistent and simpler.

- The name of some number formatting options has changed. The number formatting
  options are an optional argument of `ce.parse()` and `ce.toLatex()`. See the
  `NumberFormat` and `NumberSerializationFormat` types.

- The values +infinity, -infinity and NaN are now represented preferably with
  the symbols `PositiveInfinity`, `NegativeInfinity` and `NaN` respectively.
  Previously they were represented with numeric values, i.e.
  `{num: "+Infinity"}`, `{num: "-Infinity"}` and `{num: "NaN"}`. The numeric
  values are still supported, but the symbols are preferred.

- The method `expr.isNothing` has been removed. Instead, use
  `expr.symbol === "Nothing"`.

### New Features

- When serializing to LaTeX, the output can be "prettified". This involves
  modifying the LaTeX output to make it more pleasant to read, for example:
  - `a+\\frac{-b}{c}` -> `a-\\frac{b}{c}`
  - `a\\times b^{-1}` -> `\\frac{a}{b}`
  - `\\frac{a}{b}\\frac{c}{d}` -> `\\frac{a\\cdot c}{b\\cdot d}`
  - `--2` -> `2`

  This is on by default and can be turned off by setting the `prettify` option
  to `false`. For example:

  ```js
  ce.parse("a+\\frac{-b}{c}").toLatex({prettify: true})
  // -> "a-\\frac{b}{c}"
  ce.parse("a+\\frac{-b}{c}").toLatex({prettify: false})
  // -> "a+\\frac{-b}{c}"
  ```

- Numbers can have a different digit group length for the whole and fractional
  part of a number. For example,
  `ce.toLatex(ce.parse("1234.5678"), {digitGroup: [3, 0]})` will return
  `1\,234.5678`.
- Numbers can now be formatted using South-East Asian Numbering System, i.e.
  lakh and crore. For example:

  ```js
  ce.toLatex(ce.parse("12345678"), {digitGroup: "lakh"})
  // -> "1,23,45,678"
  ```

- Expressions with Integrate functions can now be compiled to JavaScript. The
  compiled function can be used to evaluate the integral numerically. For
  example:

  ```js
  const f = ce.parse("\\int_0^1 x^2 dx");
  const compiled = f.compile();
  console.log(compiled()); // -> 0.33232945619482307
  ```

- **#82** Support for angular units. The default is radians, but degrees can be
  used by setting `ce.angularUnit = "deg"`. Other possible values are "grad" and
  "turn". This affects how unitless numbers with a trigonometric function are
  interpreted. For example, `sin(90)` will return 1 when `ce.angularUnit` is
  "deg", 0.8939966636005579 when `ce.angularUnit` is "grad" and 0 when
  `ce.angularUnit` is "turn".
- Added `expr.map(fn)` method to apply a function to each subexpression of an
  expression. This can be useful to apply custom canonical forms and compare two
  expressions.
- An optional canonical form can now be specified with the `ce.function()`.

### Issues Resolved

- **#173** Parsing `1++2` would result in an expression with a `PreIncrement`
  function. It is now correctly parsed as `["Add", 1, 2]`.
- **#161** Power expressions would not be processed when their argument was a
  Divide expression.
- **#165** More aggressive simplification of expressions with exponent greater
  than 3.
- **#169** Calculating a constant integral (and integral that did not depend on
  the variable) would result in a runtime error.
- **#164** Negative mixed fractions (e.g. `-1\frac23`) are now parsed correctly.
- **#162** Numeric evaluation of expressions with large exponents could result
  in machine precision numbers instead of bignum numbers.
- **#155** The expression
  `["Subtract", ["Multiply", 0.5, "x"], ["Divide", "x", 2]]` will now evaluate
  to `0`.
- **#154** In some cases, parsing implicit argument of trig function return more
  natural results, for example `\cos a \sin b` is now parsed as
  `(\cos a)(\sin b)` and not `\cos (a \sin b)`.
- **#147** The associativity of some operators, including `/` was not applied
  correctly, resulting in unexpected results. For example, `1/2/3` would be
  parsed as `["Divide", 1, ["Divide", 2, 3]]` instead of
  `["Divide", ["Divide", 1, 2], 3]`.
- **#146** When parsing an expression like `x(x+1)` where `x` is an undeclared
  symbol, do not infer that `x` is a function. Instead, infer that `x` is a
  variable and that the expression is a product.
- **#145** The expression `["Or", "False", "False"]`, that is when all the
  arguments are `False`, is now evaluates to `False`.
- Fixed canonical form of `e^x^2`, and more generally apply power rule in more
  cases.
- Added missing "Sech" and "Csch" functions.
- The digit grouping serializing would place the separator in the wrong place
  for some numbers.
- The `avoidExponentsInRange` formating option would not always avoid exponents
  in the specified range.

## 0.24.0 _2024-02-23_

### Issues Resolved

- Fix parsing of very deeply nested expressions.
- Correctly apply rules to deeply nested expressions.
- `expr.print()` now correctly prints the expression when using the minified
  version of the library.
- `expr.isEqual()` now correctly compares equalities and inequalities.
- `expr.match()` has been improved and works correctly in more cases. The
  signature of the `match` function has been changed so that the pattern is the
  first argument, i.e. instead of `pattern.match(expr)` use
  `expr.match(pattern)`.
- Fix `expr.print()` when using the minified version of the library.
- **#142** Accept complex expressions as the subcript of `\ln` and `\log` in
  LaTeX.
- **#139** Parse quantifiers `\forall` and `\exists` in LaTeX.

## 0.23.1 _2024-01-27_

### Issues Resolved

- Using a custom canonical order of `"Multiply"` would not distribute the
  `Negate` function.
- **#141** The canonical form `"Order"` was applied to non-commutative
  functions.

## 0.23.0 _2024-01-01_

### New Features

- Added `ExpandAll` function to expand an expression recursively.
- Added `Factor` function to factor an expression.
- Added `Together` function to combine rational expressions into a single
  fraction.

### Issues Resolved

- The expression `\frac5 7` is now parsed correctly as `\frac{5}{7}` instead of
  `\frac{5}{}7`.
- Do not sugar non-canonical expression. Previously,
  `ce.parse('\\frac{1}{2}', {canonical: false})` would return `Half` instead of
  `['Divide', '1', '2']`.
- **#132** Attempting to set a value to 0 with
  `ce.defineSymbol("count", {value: 0})` would fail: the symbol would be
  undefined.
- Correctly evaluate power expressions in some cases, for example
  `(\sqrt2 + \sqrt2)^2`.
- Comparison of expressions containing non-exact numbers could fail. For
  example: `2(13.1+3.1x)` and `26.2+6.2x` would not be considered equal.

### Improvements

- Significant improvements to symbolic computation. Now, boxing,
  canonicalization and evaluation are more consistent and produce more
  predictable results.
- Adedd the `\neg` command, synonym for `\lnot` -> `Not`.
- Relational expressions (inequalities, etc...) are now properly factored.
- Integers are now factored when simplifying, i.e. `2x = 4x` -> `x = 2x`.

## 0.22.0 _2023-11-13_

### Breaking Changes

- **Rule Syntax**

  The syntax to describe rules has changed. The syntax for a rule was previously
  a tuple `[lhs, rhs, {condition} ]`. The new syntax is an object with the
  properties `match`, `replace` and `condition`. For example:
  - previous syntax: `[["Add", "_x", "_x"], ["Multiply", 2, "_x"]]`
  - new syntax: `{match: ["Add", "_x", "_x"], replace: ["Multiply", 2, "_x"]}`

  The `condition` property is optional, and is either a boxed function or a
  JavaScript function. For example, to add a condition that checks that `_x` is
  a number literal:

  ```js
  {
    match: ["Add", "_x", "_x"],
    replace: ["Multiply", 2, "_x"],
    condition: ({_x}) => _x.isNumberLiteral
  }
  ```

- **`CanonicalForm`**

  The `CanonicalOrder` function has been replaced by the more flexible
  `CanonicalForm` function. The `CanonicalForm` function takes an expression and
  a list of transformations to apply. To apply the same transformations as
  `CanonicalOrder`, use:

  ```json
  ['CanonicalForm', expr, 'Order']
  ```

  These canonical forms can also be specified with `box()` and `parse()`
  options:

  ```js
  ce.box(expr, { canonical: "Order" });
  ce.parse("x^2 + 2x + 1", { canonical: "Order" });
  ```

### Work In Progress

- Linear algebra functions: `Rank`, `Shape`,`Reshape`, `Flatten`, `Determinant`,
  `Trace`, `Transpose`, `ConjugateTranspose`, `Inverse`. See the
  [Linear Algebra](/compute-engine/reference/linear-algebra/) reference guide.
  Some of these function may not yet return correct result in all cases.

### New Features

- Added a `expr.print()` method as a synonym for `console.log(expr.toString())`.
- Added an `exact` option (false by default) to the `expr.match()` pattern
  matching method. When `true` some additional patterns are automatically
  recognized, for example, `x` will match `["Multiply", '_a', 'x']` when `exact`
  is `false`, but not when `exact` is `true`.

### Improvements

- The equation solver used by `expr.solve()` has been improved and can now solve
  more equations.
- The pattern matching engine has been improved and can now match more
  expressions, including sequences for commutative functions.

## 0.21.0 _2023-11-02_

### New Features

- **#125** Parse and serialize environemnts, i.e.
  `\begin{matrix} 1 & 2 \\ 3 & 4 \end{matrix}` will be parsed as
  `["Matrix", ["List", ["List", 1, 2], ["List", 3, 4]]]`.

  A new section on
  [Linear Algebra](/compute-engine/reference/linear-algebra/#formatting) has
  some details on the supported formats.

  The linear algebra operations are limited at the moment, but will be expanded
  in the future.

- Added `IsSame` function, which is the function expression corresponding to
  `expr.isSame()`.
- <s>Added `CanonicalOrder` function, which sorts the arguments of commutative
  functions into canonical order. This is useful to compare two non-canonical
  expressions for equality.</s>

```js
ce.box(["CanonicalOrder", ["Add", 1, "x"]]).isSame(
  ce.box(["CanonicalOrder", ["Add", "x", 1]])
);
// -> true
```

### Issue Resolved

- When evaluating a sum (`\sum`) with a bound that is not a number, return the
  sum expression instead of an error.

## 0.20.2 _2023-10-31_

### Issues Resolved

- Fixed numerical evaluation of integrals and limits when parsed from LaTeX.

```js
console.info(ce.parse("\\lim_{x \\to 0} \\frac{\\sin(x)}{x}").value);
// -> 1

console.info(ce.parse("\\int_{0}^{2} x^2 dx").value);
// -> 2.6666666666666665
```

## 0.20.1 _2023-10-31_

### Issues Resolved

- Fixed evaluation of functions with multiple arguments
- Fixed compilation of some function assignments
- Improved serialization of function assignment

## 0.20.0 _2023-10-30_

### Breaking Changes

- **Architectural changes**: the invisible operator is used to represent the
  multiplication of two adjacent symbols, i.e. `2x`. It was previously handled
  during parsing, but it is now handled during canonicalization. This allows
  more complex syntactic structures to be handled correctly, for example
  `f(x) := 2x`: previously, the left-hand-side argument would have been parsed
  as a function application, while in this case it should be interpreted as a
  function definition.

  A new `InvisibleOperator` function has been added to support this.

  The `applyInvisibleOperator` parsing option has been removed. To support
  custom invisible operators, use the `InvisibleOperator` function.

### Issues Resolved

- **#25** Correctly parse chained relational operators, i.e. `a < b <= c`
- **#126** Logic operators only accepted up to two arguments.
- **#127** Correctly compile `Log` with bases other than 10.
- Correctly parse numbers with repeating patterns but no fractional digits, i.e.
  `0.(1234)`
- Correctly parse `|1+|a|+2|`

### New Features and Improvements

- Function assignment can now be done with this syntax: `f(x) := 2x+1`. This
  syntax is equivalent to `f := x -> 2x+1`.
- Implement the `Mod` and `Congruent` function.
- Correctly parse `11 \bmod 5` (`Mod`) and `26\equiv 11 \pmod5` (`Congruent`)
- Better handle empty argument lists, i.e. `f()`
- When a function is used before being declared, infer that the symbol is a
  function, e.g. `f(12)` will infer that `f` is a function (and not a variable
  `f` multiplied by 12)
- When a constant is followed by some parentheses, don't assume this is a
  function application, e.g. `\pi(3+n)` is now parsed as
  `["Multiply", "Pi", ["Add", 3, "n"]]` instead of `["Pi", ["Add", 3, "n"]]`
- Improved parsing of nested lists, sequences and sets.
- Improved error messages when syntax errors are encountered during LaTeX
  parsing.
- When parsing with the canonical option set to false, preserve more closely the
  original LaTeX syntax.
- When parsing text strings, convert some LaTeX commands to Unicode, including
  spacing commands. As a result, `ce.parse("\\text{dead\;beef}_{16}")` correctly
  gets evaluated to 3,735,928,559.

## 0.19.1 _2023-10-26_

### Issues Resolved

- Assigning a function to an indentifier works correctly now, i.e.

```js
ce.parse("\\operatorname{f} := x \\mapsto 2x").evaluate();
```

## 0.19.0 _2023-10-25_

### Breaking Changes

- The `domain` property of the function definition `signature` is deprecated and
  replaced with the `params`, `optParams`, `restParam` and `result` properties
  instead. The `domain` property is still supported for backward compatibility,
  but will be removed in a future version.

### Issues Resolved

- When invoking a declared function in a numeric operation, correctly infer the
  result type.

```json
["Assign", "f", ["Add", "_", 1]]
["Add", ["f", 1], 1]
// -> 3
```

Previously a domain error was returned, now `f` is inferred to have a numeric
return type.

- Fixed a runtime error when inverting a fraction, i.e. `\frac{3}{4}^{-1}`
- The tangent of π/2 now correctly returns `ComplexInfinity`.
- The exact values of some constructible trigonometric operations (e.g.
  `\tan 18\degree = \frac{\sqrt{25-10\sqrt5}}{5}`) returned incorrect results.
  The unit test case was incorrect and did not detect the problem. The unit test
  case has been fixed and the returned values are now correct.

### New Features

- Implemented `Union` and `Intersection` of collections, for example:

```json
["Intersection", ["List", 3, 5, 7], ["List", 2, 5, 9]]
// -> ["Set", 5]

["Union", ["List", 3, 5, 7], ["List", 2, 5, 9]]
// -> ["Set", 3, 5, 7, 2, 9]
```

- Parse ranges, for example `1..5` or `1, 3..10`. Ranges are collections and can
  be used anywhere collections can be used.

- The functions `Sum`, `Product`, `Min`, `Max`, and the statistics functions
  (`Mean`, `Median`, `Variance`, etc...) now handle collection arguments:
  collections:
  - `["Range"]`, `["Interval"]`, `["Linspace"]` expressions
  - `["List"]` or `["Set"]` expressions
  - `["Tuple"]`, `["Pair"]`, `["Pair"]`, `["Triple"]` expressions
  - `["Sequence"]` expressions

- Most mathematical functions are now threadable, that is their arguments can be
  collections, for example:

```json
["Sin", ["List", 0, 1, 5]]
// -> ["List", 0, 0.8414709848078965, -0.9589242746631385]

["Add", ["List", 1, 2], ["List", 3, 4]]
// -> ["List", 4, 6]
```

- Added `GCD` and `LCM` functions

```json
["GCD", 10, 5, 15]
// -> 5

["LCM", 10, 5, 15]
// -> 30
```

- Added `Numerator`, `Denominator`, `NumeratorDenominator` functions. These
  functions can be used on non-canonical expressions.

- Added `Head` and `Tail` functions which can be used on non-canonical
  expressions.

- Added `display-quotient` and `inline-quotient` style for formatting of
  division expressions in LaTeX.

### Improvements

- Improved parsing of `\degree` command

```js
ce.parse("30\\degree)
// -> ["Divide", "Pi", 6]
```

- Improved interoperability with JavaScript: `expr.value` will return a
  JavaScript primitive (`number`, `boolean`, `string`, etc...) when possible.
  This is a more succinct version of `expr.N().valueOf()`.

## 0.18.1 _2023-10-16_

### Issues Resolved

- Parsing of whole numbers while in `rational` mode would return incorrect
  results.
- The `ND` function to evaluate derivatives numerically now return correct
  values.

```js
ce.parse("\\mathrm{ND}(x \\mapsto 3x^2+5x+7, 2)").N();
// -> 17.000000000001
```

### Improvements

- Speed up `NIntegrate` by temporarily switching the numeric mode to `machine`
  while computing the Monte Carlo approximation.

## 0.18.0 _2023-10-16_

### New Features

- Expanded LaTeX dictionary with `\max`, `\min`, `\sup`, `\inf` and `\lim`
  functions
- Added `Supremum` and `Infimum` functions
- Compilation of `Block` expressions, local variables, return statements and
  conditionals `If`.
- Added numerical evaluation of limits with `Limit` functions and `NLimit`
  functions, using a Richardson Extrapolation.

```js
console.info(ce.parse("\\lim_{x\\to0} \\frac{\\sin x}{x}").N().json);
// -> 1

console.info(
  ce.box(["NLimit", ["Divide", ["Sin", "_"], "_"], 0]).evaluate().json
);
// -> 1

console.info(ce.parse("\\lim_{x\\to \\infty} \\cos \\frac{1}{x}").N().json);
// -> 1
```

- Added `Assign` and `Declare` functions to assign values to symbols and declare
  symbols with a domain.

- `Block` evaluations with local variables work now. For example:

```js
ce.box(["Block", ["Assign", "c", 5], ["Multiply", "c", 2]]).evaluate().json;
// -> 10
```

- When decimal numbers are parsed they are interpreted as inexact numbers by
  default, i.e. "1.2" -> `{num: "1.2"}`. To force the number to be interpreted
  as a rational number, set `ce.latexOptions.parseNumbers = "rational"`. In that
  case, "1.2" -> `["Rational", 12, 10]`, an exact number.

  While regular decimals are considered "inexact" numbers (i.e. they are assumed
  to be an approximation), rationals are assumed to be exact. In most cases, the
  safest thing to do is to consider decimal numbers as inexact to avoid
  introducing errors in calculations. If you know that the decimal numbers you
  parse are exact, you can use this option to consider them as exact numbers.

### Improvements

- LaTeX parser: empty superscripts are now ignored, e.g. `4^{}` is interpreted
  as `4`.

## 0.17.0 _2023-10-12_

### Breaking Changes

- The `Nothing` domain has been renamed to `NothingDomain`
- The `Functions`, `Maybe`, `Sequence`, `Dictionary`, `List` and `Tuple` domain
  constructors have been renamed to `FunctionOf`, `OptArg`, `VarArg`,
  `DictionaryOf`, `ListOf` and `TupleOf`, respectively.
- Domains no longer require a `["Domain"]` expression wrapper, so for example
  `ce.box("Pi").domain` returns `"TranscendentalNumbers"` instead of
  `["Domain", "TranscendentalNumbers"]`.
- The `VarArg` domain constructor now indicates the presence of 0 or more
  arguments, instead of 1 or more arguments.
- The `MaybeBooleans` domain has been dropped. Use
  `["Union", "Booleans", "NothingDomain"]` instead.
- The `ce.defaultDomain` has been dropped. The domain of a symbol is now
  determined by the context in which it is used, or by the `ce.assume()` method.
  In some circumstances, the domain of a symbol can be `undefined`.

### New Features

- Symbolic derivatives of expressions can be calculated using the `D` function.
  For example, `ce.box(["D", ce.parse("x^2 + 3x + 1"), "x"]).evaluate().latex`
  returns `"2x + 3"`.

### Improvements

- Some frequently used expressions are now available as predefined constants,
  for example `ce.Pi`, `ce.True` and `ce.Numbers`.
- Improved type checking and inference, especially for functions with
  complicated or non-numeric signatures.

### Bugs Fixed

- Invoking a function repeatedly would invoke the function in the original scope
  rather than using a new scope for each invocation.

## 0.16.0 _2023-09-29_

### Breaking Changes

- The methods `ce.let()` and `ce.set()` have been renamed to `ce.declare()` and
  `ce.assign()` respectively.
- The method `ce.assume()` requires a predicate.
- The signatures of `ce.assume()` and `ce.ask()` have been simplified.
- The signature of `ce.pushScope()` has been simplified.
- The `expr.freeVars` property has been renamed to `expr.unknowns`. It returns
  the identifiers used in the expression that do not have a value associated
  with them. The `expr.freeVariables` property now return the identifiers used
  in the expression that are defined outside of the local scope and are not
  arguments of the function, if a function.

### New Features

- **Domain Inference** when the domain of a symbol is not set explicitly (for
  example with `ce.declare()`), the domain is inferred from the value of the
  symbol or from the context of its usage.

- Added `Assume`, `Identity`, `Which`, `Parse`, `N`, `Evaluate`, `Simplify`,
  `Domain`.

- Assignments in LaTeX: `x \\coloneq 42` produce `["Assign", "x", 42]`

- Added `ErfInv` (inverse error function)

- Added `Factorial2` (double factorial)

#### Functions

- Functions can now be defined:
  - using `ce.assign()` or `ce.declare()`
  - evaluating LaTeX: `(x, y) \mapsto x^2 + y^2`
  - evaluating MathJSON:
    `["Function", ["Add", ["Power", "x", 2], ["Power", "y", 2]]], "x", "y"]`

- Function can be applied using `\operatorname{apply}` or the operators `\rhd`
  and `\lhd`:
  - `\operatorname{apply}(f, x)`
  - `f \rhd x`
  - `x \lhd f`

See
[Adding New Definitions](https://cortexjs.io/compute-engine/guides/augmenting/)
and [Functions](https://cortexjs.io/compute-engine/reference/functions/).

#### Control Structures

- Added `FixedPoint`, `Block`, `If`, `Loop`
- Added `Break`, `Continue` and `Return` statements

See
[Control Structures](https://cortexjs.io/compute-engine/reference/control-structures/)

#### Calculus

- Added numeric approximation of derivatives, using an 8-th order centered
  difference approximation, with the `ND` function.
- Added numeric approximation of integrals, using a Monte Carlo method with
  rebasing for improper integrals, with the `NIntegrate` function
- Added symbolic calculation of derivatives with the `D` function.

#### Collections

Added support for **collections** such as lists, tuples, ranges, etc...

See [Collections](https://cortexjs.io/compute-engine/reference/collections/)

Collections can be used to represent various data structures, such as lists,
vectors, matrixes and more.

They can be iterated, sliced, filtered, mapped, etc...

```json example
["Length", ["List", 19, 23, 5]]
// -> 3

["IsEmpty", ["Range", 1, 10]]
// -> "False"

["Take", ["Linspace", 0, 100, 50], 4]
// -> ["List", 0, 2, 4, 6]

["Map", ["List", 1, 2, 3], ["Function", "x", ["Power", "x", 2]]]
// -> ["List", 1, 4, 9]

["Exclude", ["List", 33, 45, 12, 89, 65], -2, 2]
// -> ["List", 33, 12, 65]


["First", ["List", 33, 45, 12, 89, 65]]
// -> 33
```

### Improvements

- The [documentation](https://cortexjs.io/compute-engine/) has been
  significantly rewritten with help from an AI-powered writing assistant.

### Issues Resolved

- The LaTeX string returned in `["Error"]` expression was incorrectly tagged as
  `Latex` instead of `LatexString`.

## 0.15.0 _2023-09-14_

### Improvements

- The `ce.serialize()` function now takes an optional `canonical` argument. Set
  it to `false` to prevent some transformations that are done to produce more
  readable LaTeX, but that may not match exactly the MathJSON. For example, by
  default `ce.serialize(["Power", "x", -1])` returns `\frac{1}{x}` while
  `ce.serialize(["Power", "x", -1], {canonical: false})` returns `x^{-1}`.
- Improved parsing of delimiters, i.e. `\left(`, `\right]`, etc...
- Added complex functions `Real`, `Imaginary`, `Arg`, `Conjugate`, `AbsArg`. See
  [Complex](https://cortexjs.io/compute-engine/reference/complex/)
- Added parsing and evaluation of `\Re`, `\Im`, `\arg`, `^\star` (Conjugate).
- **#104** Added the `["ComplexRoots", x, n]` function which returns the nthroot
  of `x`.
- Added parsing and evaluation of statistics functions `Mean`, `Median`,
  `StandardDeviation`, `Variance`, `Skewness`, `Kurtosis`, `Quantile`,
  `Quartiles`, `InterquartileRange`, `Mode`, `Count`, `Erf`, `Erfc`. See
  [Statistics](https://cortexjs.io/compute-engine/reference/statistics/)

## 0.14.0 _2023-09-13_

### Breaking Changes

- The entries in the LaTeX syntax dictionary can now have LaTeX triggers
  (`latexTrigger`) or triggers based on identifiers (`symbolTrigger`). The
  former replaces the `trigger` property. The latter is new. An entry with a
  `triggerIdentifier` of `average` will match `\operatorname{average}`,
  `\mathrm{average}` and other variants.
- The `ce.latexOptions` and `ce.jsonSerializationOptions` properties are more
  robust. They can be modified directly or one of their properties can be
  modified.

### Improvements

- Added more functions and symbols supported by `expr.compile()`:
  - `Factorial` postfix operator `5!`
  - `Gamma` function `\Gamma(2)`
  - `LogGamma` function `\operatorname{LogGamma}(2)`
  - `Gcd` function `\operatorname{gcd}(20, 5)`
  - `Lcm` function `\operatorname{lcm}(20, 5)`
  - `Chop` function `\operatorname{chop}(0.00000000001)`
  - `Half` constant `\frac{1}{2}`
  - 'MachineEpsilon' constant
  - `GoldenRatio` constant
  - `CatalanConstant` constant
  - `EulerGamma` constant `\gamma`
  - `Max` function `\operatorname{max}(1, 2, 3)`
  - `Min` function `\operatorname{min}(13, 5, 7)`
  - Relational operators: `Less`, `Greater`, `LessEqual`, `GreaterEqual`,
    'Equal', 'NotEqual'
  - Some logical operators and constants: `And`, `Or`, `Not`, `True`, `False`

- More complex identifiers syntax are recognized, including `\mathbin{}`,
  `\mathord{}`, etc... `\operatorname{}` is the recommended syntax, though: it
  will display the identifier in upright font and with the propert spacing, and
  is properly enclosing. Some commands, such as `\mathrm{}` are not properly
  enclosing: two adjacent `\mathrm{}` command could be merged into one.

- Environments are now parsed and serialized correctly.

- When parsing LaTeX, function application is properly handled in more cases,
  including custom functions, e.g. `f(x)`

- When parsing LaTeX, multiple arguments are properly handled, e.g. `f(x, y)`

- Add LaTeX syntax for logical operators:
  - `And`: `\land`, `\operatorname{and}` (infix or function)
  - `Or`: `\lor`, `\operatorname{or}` (infix or function)
  - `Not`: `\lnot`, `\operatorname{not}` (prefix or function)
  - `Xor`: `\veebar` (infix)
  - `Nand`: `\barwedge` (infix)
  - `Nor`: `^^^^22BD` (infix)
  - `Implies`: `\implies` (infix)
  - `Equivalent`: `\iff` (infix)

- When a postfix operator is defined in the LaTeX syntax dictionary of the form
  `^` plus a single token, a definition with braces is added automatically so
  that both forms will be recognized.

- Extended the LaTeX dictionary with:
  - `floor`
  - `ceil`
  - `round`
  - `sgn`
  - `exp`
  - `abs`
  - `gcd`
  - `lcm`
  - `apply`

- Properly handle inverse and derivate notations, e.g. `\sin^{-1}(x)`,
  `\sin'(x)`, `\cos''(x)`, `\cos^{(4)}(x)` or even `\sin^{-1}''(x)`

## 0.13.0 _2023-09-09_

### New Features

- **Compilation** Some expressions can be compiled to Javascript. This is useful
  to evaluate an expression many times, for example in a loop. The compiled
  expression is faster to evaluate than the original expression. To get the
  compiled expression, use `expr.compile()`. Read more at
  [Compiling](https://cortexjs.io/compute-engine/guides/compiling)

### Issues Resolved and Improvements

- Fixed parsing and serialization of extended LaTeX synonyms for `e` and `i`.
- Fixed serialization of `Half`.
- Fixed serialization of `Which`
- Improved serialization of `["Delimiter"]` expressions.

## 0.12.7 _2023-09-08_

### Improvements

- Made customization of the LaTeX dictionary simpler. The `ce.latexDictionary`
  property can be used to access and modify the dictionary. The
  [documentation](https://cortexjs.io/compute-engine/guides/latex-syntax/#customizing-the-latex-dictionary)
  has been updated.

## 0.12.6 _2023-09-08_

### Breaking Changes

- New API for the `Parser` class.

### Improvements and Bux Fixes

- The `ComputeEngine` now exports the `bignum()` and `complex()` methods that
  can be used to create bignum and complex numbers from strings or numbers. The
  methods `isBigNum()` and `isComplex()` have also been added to check if a
  value is a bignum (`Decimal`) or complex (`Complex`) number, for example as
  returned by `expr.numericValue`.
- **#69** `\leq` was incorrectly parsed as `Equals` instead of `LessEqual`
- **#94** The `\exp` command was not parsed correctly.
- Handle `PlusMinus` in infix and prefix position, i.e. `a\pm b` and `\pm a`.
- Improved parsing, serialization
- Improved simplification
- Improved evaluation of `Sum` and `Product`
- Support complex identifiers (i.e. non-latin scripts, emojis).
- Fixed serialization of mixed numbers.

## 0.12.1 _2022-12-01_

Work around unpckg.com issue with libraries using BigInt.

## 0.12.0 _2022-11-27_

### Breaking Changes

- The `expr.symbols` property return an array of `string`. Previously it
  returned an array of `BoxedExpression`.

### Improvements

- Rewrote the rational computation engine to use JavaScript `bigint` instead of
  `Decimal` instances. Performance improvements of up to 100x.
- `expr.freeVars` provides the free variables in an expression.
- Improved performance of prime factorization of big num by x100.
- Added `["RandomExpression"]`
- Improved accuracy of some operations, for example
  `expr.parse("1e999 + 1").simplify()`

### Issues Resolved

- When `ce.numericMode === "auto"`, square roots of negative numbers would
  return an expression instead of a complex number.
- The formatting of LaTeX numbers when using
  `ce.latexOptions.notation = "engineering"` or `"scientific"` was incorrect.
- The trig functions no longer "simplify" to the less simple exponential
  formulas.
- The canonical order of polynomials now orders non-lexicographic terms of
  degree 1 last, i.e. "ax^2+ bx+ c" instead of "x + ax^2 + bx".
- Fixed evaluation of inverse functions
- Fixed `expr.isLess`, `expr.isGreater`, `expr.isLessEqual`,
  `expr.isGreaterEqual` and `["Min"]`, `["Max"]`

## 0.11.0 _2022-11-18_

### Breaking Changes

- The signature of `ce.defineSymbol()`, `ce.defineFunction()` and
  `ce.pushScope()` have changed

### Improvements

- When a constant should be held or substituted with its value can now be more
  precisely controlled. The `hold` symbol attribute is now `holdUntil` and can
  specify at which stage the substitution should take place.

### Issues Resolved

- Some constants would return a value as bignum or complex even when the
  `numericMode` did not allow it.
- Changing the value or domain of a symbol is now correctly taken into account.
  Changes can be made with `ce.assume()`, `ce.set()` or `expr.value`.
- When a symbol does not have a value associated with it, assumptions about it
  (e.g. "x > 0") are now correctly tracked and reflected.

## 0.10.0 _2022-11-17_

### Breaking Changes

- `expr.isLiteral` has been removed. Use `expr.numericValue !== null` and
  `expr.string !== null` instead.

### Issues Resolved

- Calling `ce.forget()` would not affect expressions that previously referenced
  the symbol.

### Improvements

- More accurate calculations of some trig functions when using bignums.
- Improved performance when changing a value with `ce.set()`. Up to 10x faster
  when evaluating a simple polynomial in a loop.
- `ce.strict` can be set to `false` to bypass some domain and validity checks.

## 0.9.0 _2022-11-15_

### Breaking Changes

- The head of a number expression is always `Number`. Use `expr.domain` to be
  get more specific info about what kind of number this is.
- By default, `ce.box()` and `ce.parse()` return a canonical expression. A flag
  can be used if a non-canonical expression is desired.
- The API surface of `BoxedExpression` has been reduced. The properties
  `machineValue`, `bignumValue`, `asFloat`, `asSmallInteger`, `asRational`
  etc... have been replaced with a single `numericValue` property.
- `parseUnknownSymbol` is now `parseUnknownIdentifier`

### Improvements

- Support angles in degrees with `30\degree`, `30^\circ` and `\ang{30}`.
- More accurate error expressions, for example if there is a missing closing
  delimiter an `["Error", ["ErrorCode", "'expected-closing-delimiter'", "')'"]]`
  is produced.
- `["Expand"]` handles more cases
- The trig functions can now have a regular exponent, i.e.`\cos^2(x)` in
  addition to `-1` for inverse, and a combination of `\prime`, `\doubleprime`
  and `'` for derivatives.
- `ce.assume()` handle more expressions and can be used to define new symbols by
  domain or value.
- Better error message when parsing, e.g. `\sqrt(2)` (instead of `\sqrt{2}`)
- Better simplification for square root expressions:
  - `\sqrt{25x^2}` -> `5x`
- Improved evaluation of `["Power"]` expressions, including for negative
  arguments and non-integer exponents and complex arguments and exponents.
- Added `Arccot`, `Arcoth`, `Arcsch`, `Arcscc`, `Arsech` and `Arccsc`
- `expr.solve()` returns result for polynomials of order up to 2.
- The `pattern.match()` function now work correctly for commutative functions,
  i.e. `ce.pattern(['Add', '_a', 'x']).match(ce.parse('x+y')) -> {"_a": "y"}`
- Added `ce.let()` and `ce.set()` to declare and assign values to identifiers.
- Preserve exact calculations involving rationals or square root of rationals.
  - `\sqrt{\frac{49}{25}}` -> `\frac{7}{5}`
- Addition and multiplication provide more consistent results for `evaluate()`
  and `N()`. Evaluate returns an exact result when possible.
  - EXACT
    - 2 + 5 -> 7
    - 2 + 5/7 -> 19/7
    - 2 + √2 -> 2 + √2
    - 2 + √(5/7) -> 2 + √(5/7)
    - 5/7 + 9/11 -> 118/77
    - 5/7 + √2 -> 5/7 + √2
    - 10/14 + √(18/9) -> 5/7 + √2
    - √2 + √5 -> √2 + √5
    - √2 + √2 -> 2√2
    - sin(2) -> sin(2)
    - sin(π/3) -> √3/2
  - APPROXIMATE
    - 2 + 2.1 -> 4.1
    - 2 + √2.1 -> 3.44914
    - 5/7 + √2.1 -> 2.16342
    - sin(2) + √2.1 -> 2.35844

- More consistent behavior of the `auto` numeric mode: calculations are done
  with `bignum` and `complex` in most cases.
- `JsonSerializationOptions` has a new option to specify the numeric precision
  in the MathJSON serialization.
- Shorthand numbers can now be strings if they do not fit in a float-64:

```json example
// Before
["Rational", { "num": "1234567890123456789"}, { "num": "2345678901234567889"}]

// Now
["Rational", "1234567890123456789", "2345678901234567889"]
```

- `\sum` is now correctly parsed and evaluated. This includes creating a local
  scope with the index and expression value of the sum.

### Bugs Fixed

- The parsing and evaluation of log functions could produce unexpected results
- The `\gamma` command now correctly maps to `["Gamma"]`
- Fixed numeric evaluation of the `["Gamma"]` function when using bignum
- **#57** Substituting `0` (i.e. with `expr.subs({})`) did not work.
- **#60** Correctly parse multi-char symbols with underscore, i.e.
  `\mathrm{V_a}`
- Parsing a number with repeating decimals and an exponent would drop the
  exponent.
- Correct calculation of complex square roots
  - `\sqrt{-49}` -> `7i`
- Calculations were not always performed as bignum in `"auto"` numeric mode if
  the precision was less than 15. Now, if the numeric mode is `"auto"`,
  calculations are done as bignum or complex numbers.
- If an identifier contained multiple strings of digits, it would not be
  rendered to LaTeX correctly, e.g. `V20_20`.
- Correctly return `isReal` for real numbers

## 0.8.0 _2022-10-02_

### Breaking Changes

- Corrected the implementation of `expr.toJSON()`, `expr.valueOf()` and added
  the esoteric `[Symbol.toPrimitive]()` method. These are used by JavaScript
  when interacting with other primitive types. A major change is that
  `expr.toJSON()` now returns an `Expression` as an object literal, and not a
  string serialization of the `Expression`.

- Changed from "decimal" to "bignum". "Decimal" is a confusing name, since it is
  used to represent both integers and floating point numbers. Its key
  characteristic is that it is an arbitrary precision number, aka "bignum". This
  affects `ce.numericMode` which now uses `bignum` instead of `decimal`,
  `expr.decimalValue`->`expr.bignumValue`, `decimalValue()`->`bignumValue()`

### Bugs Fixed

- Numerical evaluation of expressions containing complex numbers when in
  `decimal` or `auto` mode produced incorrect results. Example: `e^{i\\pi}`

## 0.7.0 _2022-09-30_

### Breaking Changes

- The `ce.latexOptions.preserveLatex` default value is now `false`
- The first argument of the `["Error"]` expression (default value) has been
  dropped. The first argument is now an error code, either as a string or an
  `["ErrorCode"]` expression.

### Features

- Much improved LaTeX parser, in particular when parsing invalid LaTeX. The
  parser now avoids throwing, but will return a partial expression with
  `["Error"]` subexpressions indicating where the problems were.
- Implemented new domain computation system (similar to type systems in
  programming languages)
- Added support for multiple signatures per function (ad-hoc polymorphism)
- Added `FixedPoint`, `Loop`, `Product`, `Sum`, `Break`, `Continue`, `Block`,
  `If`, `Let`, `Set`, `Function`, `Apply`, `Return`
- Added `Min`, `Max`, `Clamp`
- Parsing of `\sum`, `\prod`, `\int`.
- Added parsing of log functions, `\lb`, `\ln`, `\ln_{10}`, `\ln_2`, etc...
- Added `expr.subexpressions`, `expr.getSubexpressions()`, `expr.errors`,
  `expr.symbols`, `expr.isValid`.
- Symbols can now be used to represent functions, i.e. `ce.box('Sin').domain`
  correctly returns `["Domain", "Function"]`.
- Correctly handle rational numbers with a numerator or denominator outside the
  range of a 64-bit float.
- Instead of a `Missing` symbol an `["Error", "'missing'"]` expression is used.
- Name binding is now done lazily
- Correctly handle MathJSON numbers with repeating decimals, e.g. `1.(3)`.
- Correctly evaluate inverse functions, e.g. `ce.parse('\\sin^{-1}(.5)).N()`
- Fixed some LaTeX serialization issues

Read more at
[Core Reference](https://cortexjs.io/compute-engine/reference/core/) and
[Arithmetic Reference]
(https://cortexjs.io/compute-engine/reference/arithmetic/)

### Bugs Fixed

- **#43** If the input of `ce.parse()` is an empty string, return an empty
  string for `expr.latex` or `expr.json.latex`: that is, ensure verbatim LaTeX
  round-tripping
- Evaluating some functions, such as `\arccos` would result in a crash
- Correctly handle parsing of multi-token decimal markers, e.g. `{,}`

## 0.6.0 _2022-04-18_

### Improvements

- Parse more cases of tabular environments
- Handle simplify and evaluate of inert functions by default
- Avoid unnecessary wrapping of functions when serializing LaTeX
- Parse arguments of LaTeX commands (e.g. `\vec{}`)
- **#42** Export static `ComputeEngine.getLatexDictionary`
- Parse multi-character constants and variables, e.g. `\mathit{speed}` and
  `\mathrm{radius}`
- Parse/serialize some LaTeX styling commands: `\displaystyle`, `\tiny` and more

## 0.5.0 _2022-04-05_

### Improvements

- Correctly parse tabular content (for example in
  `\begin{pmatrix}...\end{pmatrix}`
- Correctly parse LaTeX groups, i.e. `{...}`
- Ensure constructible trigonometric values are canonical
- Correct and simplify evaluation loop for `simplify()`, `evaluate()` and `N()`.
- **#41** Preserve the parsed LaTeX verbatim for top-level expressions
- **#40** Correctly calculate the synthetic LaTeX metadata for numbers
- Only require Node LTS (16.14.2)
- Improved documentation, including Dark Mode support

## 0.4.4 _2022-03-27_

### Improvements

- Added option to specify custom LaTeX dictionaries in `ComputeEngine`
  constructor
- `expr.valueOf` returns rational numbers as `[number, number]` when applicable
- The non-ESM builds (`compute-engine.min.js`) now targets vintage JavaScript
  for improved compatibility with outdated toolchains (e.g. Webpack 4) and
  environments. The ESM build (`compute-engine.min.esm.js`) targets evergreen
  JavaScript (currently ECMAScript 2020).

## 0.4.3 _2022-03-21_

### Transition Guide from 0.4.2

The API has changed substantially between 0.4.2 and 0.4.3, however adapting code
to the new API is very straightforward.

The two major changes are the introduction of the `BoxedExpression` class and
the removal of top level functions.

### Boxed Expression

The `BoxedExpression` class is a immutable box (wrapper) that encapsulates a
MathJSON `Expression`. It provides some member functions that can be used to
manipulate the expression, for example `expr.simplify()` or `expr.evaluate()`.

The boxed expresson itself is immutable. For example, calling `expr.simplify()`
will return a new, simplified, expression, without modifying `expr`.

To create a "boxed" expression from a "raw" MathJSON expression, use `ce.box()`.
To create a boxed expression from a LaTeX string, use `ce.parse()`.

To access the "raw" MathJSON expression, use the `expr.json` property. To
serialize the expression to LaTeX, use the `expr.latex` property.

The top level functions such as `parse()` and `evaluate()` are now member
functions of the `ComputeEngine` class or the `BoxedExpression` class.

There are additional member functions to examine the content of a boxed
expression. For example, `expr.symbol` will return `null` if the expression is
not a MathJSON symbol, otherwise it will return the name of the symbol as a
string. Similarly, `expr.ops` return the arguments (operands) of a function,
`expr.asFloat` return `null` if the expression does not have a numeric value
that can be represented by a float, a `number` otherwise, etc...

### Canonical Form

Use `expr.canonical` to obtain the canonical form of an expression rather than
the `ce.format()` method.

The canonical form is less aggressive in its attempt to simplify than what was
performed by `ce.format()`.

The canonical form still accounts for distributive and associative functions,
and will collapse some integer constants. However, in some cases it may be
necessary to invoke `expr.simplify()` in order to get the same results as
`ce.format(expr)`.

### Rational and Division

In addition to machine floating points, arbitrary precision numbers and complex
numbers, the Compute Engine now also recognize and process rational numbers.

This is mostly an implementation detail, although you may see
`["Rational", 3, 4]`, for example, in the value of a `expr.json` property.

If you do not want rational numbers represented in the value of the `.json`
property, you can exclude the `Rational` function from the serialization of JSON
(see below) in which case `Divide` will be used instead.

Note also that internally (as a result of boxing), `Divide` is represented as a
product of a power with a negative exponent. This makes some pattern detection
and simplifications easier. However, when the `.json` property is accessed,
product of powers with a negative exponents are converted to a `Divide`, unless
you have included `Divide` as an excluded function for serialization.

Similarly, `Subtract` is converted internally to `Add`, but may be serialized
unless excluded.

### Parsing and Serialization Customization

Rather than using a separate instance of the `LatexSyntax` class to customize
the parsing or serialization, use a `ComputeEngine` instance and its
`ce.parse()` method and the `expr.latex` property.

Custom dictionaries (to parse/serialize custom LaTeX syntax) can be passed as an
argument to the `ComputeEngine` constructor.

For more advanced customizations, use `ce.latexOptions = {...}`. For example, to
change the formatting options of numbers, how the invisible operator is
interpreted, how unknown commands and symbols are interpreted, etc...

Note that there are also now options available for the "serialization" to
MathJSON, i.e. when the `expr.json` property is used. It is possible to control
for example if metadata should be included, if shorthand forms are allowed, or
whether some functions should be avoided (`Divide`, `Sqrt`, `Subtract`, etc...).
These options can be set using `ce.jsonSerializationOptions = {...}`.

### Comparing Expressions

There are more options to compare two expressions.

Previously, `match()` could be used to check if one expression matched another
as a pattern.

If `match()` returned `null`, the first expression could not be matched to the
second. If it returned an object literal, the two expressions matched.

The top-level `match()` function is replaced by the `expr.match()` method.
However, there are two other options that may offer better results:

- `expr.isSame(otherExpr)` return true if `expr` and `otherExpr` are
  structurally identical. Structural identity is closely related to the concept
  of pattern matching, that is `["Add", 1, "x"]` and `["Add", "x", 1]` are not
  the same, since the order of the arguments is different. It is useful for
  example to compare some input to an answer that is expected to have a specific
  form.
- `expr.isEqual(otherExpr)` return true if `expr` and `otherExpr` are
  mathematically identical. For example `ce.parse("1+1").isEqual(ce.parse("2"))`
  will return true. This is useful if the specific structure of the expression
  is not important.

It is also possible to evaluate a boolean expression with a relational operator,
such as `Equal`:

```ts
console.log(ce.box(["Equal", expr, 2]).evaluate().symbol);
// -> "True"

console.log(expr.isEqual(ce.box(2)));
// -> true
```

### Before / After

| Before                                    | After                                    |
| :---------------------------------------- | :--------------------------------------- |
| `expr = ["Add", 1, 2]`                    | `expr = ce.box(["Add", 1, 2])`           |
| `expr = ce.evaluate(expr)`                | `expr = expr.evaluate()`                 |
| `console.log(expr)`                       | `console.log(expr.json)`                 |
| `expr = new LatexSyntax().parse("x^2+1")` | `expr = ce.parse("x^2+1")`               |
| `new LatexSyntax().serialize(expr)`       | `expr.latex`                             |
| `ce.simplify(expr)`                       | `expr.simplify()`                        |
| `await ce.evaluate(expr)`                 | `expr.evaluate()`                        |
| `ce.N(expr)`                              | `expr.N()`                               |
| `ce.domain(expr)`                         | `expr.domain`                            |
| `ce.format(expr...)`                      | `expr.canonical` <br/> `expr.simplify()` |

## 0.3.0 _2021-06-18_

### Improvements

- In LaTeX, parse `\operatorname{foo}` as the MathJSON symbol `"foo"`.
</ChangeLog>
