Skip to content

Archive Expression Notebook and Compute Engine Adoption

enumeratio edited this page Sep 29, 2026 · 1 revision

Expression notebook & compute-engine adoption

Carried over from the earlier enumeratio. This is the actual decision record for adopting compute-engine, MathJSON and a MathLive-based editor — the foundation the current architecture still stands on. Package and component names have since moved (@enumeratio/expressions and the enumeratio-math-input/-expression-line/-expression-set components no longer exist under those names; see Component Naming and the current notebook design) but the editor/AST/engine decisions themselves stuck.

A Desmos-style notebook of math inputs over the catalog: LaTeX from a MathQuill-like editor → an AST → bound and typed against the catalog (collections as domains, elements of a collection as a type, stats/maps/functions as callables) → lowered to the client's typed IR → evaluated by the engine router. Lines form a dependency graph, so their order does not matter. This page records the design decisions, the compute-engine spike, and the staged adoption plan. Landed 2026-09-06/07 on main; see the @enumeratio/expressions package and the enumeratio-math-input / -expression-line / -expression-set components.

Decisions

  • Editor: MathLive, behind a small MathInputAdapter seam (latex in/out, latex-before-caret, caret rect, replace-before-caret, enter / move-out events). A desmos-MathQuill adapter can be slotted in later if editing feel matters; MathQuill emits LaTeX only, so nothing downstream would change.
  • AST = MathJSON, the Wolfram-FullForm-like array form (["Add", ["Power", "x", 2], 1]). We did not write a parser: @cortex-js/compute-engine ships a standalone latex-syntax entry (~400 KB min) with an extensible dictionary, and MathJSON is a spec, not an engine.
  • Own binder/typer: compute-engine's nominal types are opaque (no declared supertype, no unwrap), so "an element of triangular_numbers, implicitly a natural, with its own next/prev" cannot be expressed in its type system. Its collection handlers are synchronous, so a catalog collection cannot be a lazy CE collection over pglite.
  • Pure IR lowering, no raw SQL. The client IR gained four scalar node kinds: a typed literal, op{op, type} (an algebra operator over a named pg type — resolved through base_type_operation.impl_fn, else one shared native-operator table), handle (a collection handle as a value), and cast (element row → carrier value prints (e).value; otherwise ::type). Per-collection generated functions (next, prev, rank, contains, unrank, element_at, random_element, locate) stay uncurated apply nodes.
  • locate(h, v) is generated by base_realize: the value→element inverse, gated by contains and scanned lazily per fiber. Two Postgres facts shaped it: set-returning SQL functions and WITH ORDINALITY materialize the whole cap before a LIMIT can stop them (a plpgsql FOR … RETURN does not), and a single-field fiber composite makes SELECT … INTO mis-assign (scalar-subquery assignment avoids it). A fiber_rank accel (the mirror of fiber_unrank) is the follow-up.
  • Cross-line values are re-embedded, not referenced: a scalar becomes a typed literal, an element becomes unrank(handle, rank), so every line's Expr is closed and any engine can claim it.

Compute-engine: what the spike established (0.125.0)

fact consequence
MIT; Node ≥ 22.3; six releases in four days with weekly breaking changes exact-pin the version; every import lives under packages/expressions/src/ce/ (and packages/client/src/ce-engine.ts)
latex-syntax entry: LatexSyntax, LATEX_DICTIONARY, entries with symbolTrigger + name + serialize (+ standaloneSymbol: true, or serialize is silently ignored) \operatorname{next}(x) → ["next", "x"], \operatorname{triangular\_numbers} → the symbol; one generated entry per catalog id
no unknown-identifier hook: a bare triangu splits into single-letter symbols bare catalog ids are rewritten to \operatorname{} before parsing; autocomplete inserts the wrapped form
preserveLatex: true decorates every node with its source LaTeX; Error nodes carry sourceOffsets in place spans are recovered by path (a Map<NodePath, Span>), errors by a tree walk
overriding a built-in macro trigger (\mathbb{N}) only works with an entry that has no kind field documented in the facade; not understood
operator definitions have evaluateAsync; sync evaluate() leaves such an operator inert, evaluateAsync() resolves it pglite-backed catalog functions can be first-class CE operators
system operators (Element, Add, …) can be re-declared from the global scope — as a full replacement, not an overlay a catalog Element must delegate the cases it does not own (ce.lookupDefinition before overriding)
re-declaring Add's evaluate does not intercept literal folding typed dispatch over custom carriers needs the type handler and/or DefineFunction multi-clause — a dedicated follow-up spike
precision = 50: 25! exact, 1/3 + 1/6 → Rational 1/2 the kernel is a sound exact oracle; its complex/Hurwitz ζ gaps are ours to fill as catalog functions

Staged adoption

  • Stage A (landed) — parser + MathJSON + our binder/typer/lowering; compute-engine's standard library is the vocabulary we mirror (same operator names, same LaTeX triggers), and one OPERATORS table maps its heads to catalog ids and algebra ops. The full kernel is a dev-time oracle (selfcert-expressions.mts).
  • Stage B (landed) — ceEngine, a third router engine (ts, ce, pg) claiming the scalar_math basket for exprs whose every node maps to a CE operator; exact results only, soft-decline otherwise; a type that registers its own impl_fn for an op (cardinal_add) is declined unless that impl is itself mapped. The kernel is a lazy import. selfcert-engine is a three-way differential.
  • Stage C (gated) — host the catalog inside compute-engine: declare curated functions / stats / maps with evaluateAsync → the router, collections as symbols, Element/At/Count overridden with delegation, carriers as nominal types with multi-clause arithmetic. Exit criteria: x \in triangular_numbers, x = 10, next(x) end to end through evaluateAsync; a composite carrier's + dispatching to gaussian_add; simplify leaving catalog-bound subtrees intact. Blocked on the typed-dispatch spike above.

Follow-ups

Sum/Product binders (an IR range-fold node); fiber_rank; retiring the old ring evaluator (buildExprSql) by routing <enumeratio-expression> through the parser; rewrite rules over base_function_attribute via CE's expr.replace; an approximate (N()) mode via representation; Hurwitz ζ(s, a) and complex ζ as catalog functions; KaTeX rendering of values inside the shadow DOM; saving function definitions as an EngineDelta.

Home

Project

Design: every design page, settled, speculative and archived

Clone this wiki locally