Repository navigation
Archive Expression Notebook and 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/expressionsand theenumeratio-math-input/-expression-line/-expression-setcomponents 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.
-
Editor: MathLive, behind a small
MathInputAdapterseam (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-engineships a standalonelatex-syntaxentry (~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 ownnext/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 throughbase_type_operation.impl_fn, else one shared native-operator table),handle(a collection handle as a value), andcast(element row → carrier value prints(e).value; otherwise::type). Per-collection generated functions (next,prev,rank,contains,unrank,element_at,random_element,locate) stay uncuratedapplynodes. -
locate(h, v)is generated bybase_realize: the value→element inverse, gated bycontainsand scanned lazily per fiber. Two Postgres facts shaped it: set-returning SQL functions andWITH ORDINALITYmaterialize the whole cap before aLIMITcan stop them (a plpgsqlFOR … RETURNdoes not), and a single-field fiber composite makesSELECT … INTOmis-assign (scalar-subquery assignment avoids it). Afiber_rankaccel (the mirror offiber_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'sExpris closed and any engine can claim it.
| 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 |
-
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
OPERATORStable 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 thescalar_mathbasket for exprs whose every node maps to a CE operator; exact results only, soft-decline otherwise; a type that registers its ownimpl_fnfor an op (cardinal_add) is declined unless that impl is itself mapped. The kernel is a lazy import.selfcert-engineis 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/Countoverridden with delegation, carriers as nominal types with multi-clause arithmetic. Exit criteria:x \in triangular_numbers,x = 10,next(x)end to end throughevaluateAsync; a composite carrier's+dispatching togaussian_add;simplifyleaving catalog-bound subtrees intact. Blocked on the typed-dispatch spike above.
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.