Skip to content

Speculative Kernels and Front Ends

enumeratio edited this page Oct 1, 2026 · 11 revisions

Kernels and front ends

Speculative design. Open, not settled — thinking and proposals, not shipped behavior. See Roadmap for what's in flight.

Status: speculative (2026-09-30). Who owns an expression's code, where it runs, and who draws the answer. Follows Lazy Engine (the resolver) and Overloads as Data (which decides when an engine can be shared); Compute Host (notatio serve) is one placement of what this calls a kernel.

1. Where we are

Four engines, and none of them resolves:

  • The page engine (frontend/src/engine.ts): one per page, on the main thread. The theme declares every library into it at startup (engine-libraries.ts). It parses LaTeX and Epsil, and evaluates every cell outside a worker module.
  • Session workers: one dedicated Worker per <notatio-dynamic-module evaluator="Worker">, with the same library list. The page parses and sends MathJSON. A SharedWorker factory exists and nothing uses it.
  • The CLI's Session: its own hand-picked subset (combinatorics, statistics, formats).
  • The reference engine (build time, Node): one per package, the resolver's only user (its tests).

2. Terms

  • Engine: a compute-engine ComputeEngine and the declarations in it: symbol table, canonicaliser, evaluator. A library object: no I/O, no lifecycle, no notion of who asks.

  • Kernel: a service holding one or more engines and running them for callers. It owns:

    • the protocol;
    • sessions (a scope, history, Out: a notebook is a session);
    • resolution and loading, under the host's policy;
    • time limits and aborts;
    • messages;
    • the requests it sends to a front end.

    Wolfram's kernel is both; we separate them because a kernel may hold several engines.

  • Host: the site, the app, the CLI install. It owns:

    • the library catalogue: what's available, installed and enabled by default;
    • resolution policy (lazy or eager, pinned versions, registries, who may override);
    • system defaults (precision, notation, forms);
    • the capabilities it grants: network and registry fetch, storage, clipboard, time and randomness, GPU, files;
    • shared caches.

    Everything a host starts inherits its configuration by default.

  • System: what the host runs on: a browser and origin, a Node process, Electron, an OS. Tracked as its own record (Wolfram's $System, $OperatingSystem); the rendering environment (Rendering Environments) is a front end's slice of it.

  • Front end: anything that updates itself: it takes input from a user or the system, or messages from a kernel, and redraws. A page is one, and so is a TUI or an app. A printed page isn't, and neither is a build that renders boxes to HTML. A front end needn't be a notebook interface.

  • Document: a unit inside a front end with its own evaluator (Wolfram's notebook and its Evaluator option; our <notatio-dynamic-module evaluator=…>). One front end per page; a page may still talk to several kernels, one per document that needs its own.

Evaluating needs no front end. A build, a pipe or an API call asks a kernel for values and boxes and never redraws. A front end adds interactivity to the same outputs where its capabilities allow.

3. Sharing an engine

A kernel's engine can serve many callers as long as what's declared into it only adds:

all disjoint from what's there. Order stops mattering and no caller's answer changes. Anything that overrides, specialises or changes ambient context makes the engine diverge from its prototype, and needs an engine of its own.

That gives two levels:

  • Host-wide. The host's base configuration, which every kernel it starts shares. It can change, but changing it is loud and deliberate: compiled artifacts and caches keyed on it go dirty and are recomputed. It decides:
    • which packages are available, preinstalled, or enabled by default, varying by site and its purpose;
    • read-only data packages. How they rest (a CDN, disk, IndexedDB) is the host's and system's business; a caller wanting to mutate one gets its own kernel, with an overlay.
  • Per document. A notebook that installs a package, overrides a case, or changes its context gets a custom kernel of its own, forked from the prototype.

Sessions are scopes, and cheap. A session is a compute-engine scope over the engine's own: a notebook's bindings, assumptions and Out[n] live in it, and dropping it forgets them all. So a transient session (process a transcript, then forget it) costs a scope, not a kernel: a call that names no session gets a scope of its own, discarded after it, and a session can be closed. A child scope sees its parent's bindings and writes its own, so forking a session is copy-on-write for bindings. What a scope can't hold is anything that mutates shared definitions in place (an override, a specialisation, engine-wide settings), which is what a custom kernel is for. Expressions are immutable in compute-engine; its definitions and scopes aren't, and nothing suggests persistent, structurally shared environments are coming, so scopes are the unit.

Lazily loading an additive, catalogued library into the shared engine is neither: it is how the base kernel grows, and it needs no fork.

4. Placement

On the web:

  • Service worker = host. It holds the configuration and the catalogue, and serves the shared cache: library chunks and registry fetches, through the Cache API. It precaches every library chunk the kernel loads on install, so the kernel starts warm and the site works offline; pages are cached as they're visited. It is stopped whenever idle, so configuration persists in IndexedDB. It is an authority, not a runtime. It caches files, not answers: memoising an evaluation can cost more than redoing it.
  • SharedWorker = the base kernel. It runs the host's defaults, is available as soon as the site is installed, and any front end can use it that doesn't need a kernel of its own.
  • Sessions as data, not more kernels. Calls go to the kernel by message. A session is its configuration and a journal in IndexedDB, which any engine replays, so a reload, a second tab or a dead worker needs no kernel of its own. A pool of workers, routed by input hash, waits until one kernel saturates. See Sessions as Data (and the job-store spike before it).
  • Dedicated worker = a custom kernel for a document that diverges, with its own pool if it needs one.
  • Main thread: a fallback kernel, and the place for what can't cross a thread.

Support (MDN's data, 2026-10):

  • SharedWorker: Chrome, Firefox, Safari 16+; Chrome on Android only from 148, and not Samsung Internet. Where there's none, a page has a dedicated base kernel of its own.
  • Worker inside a SharedWorker: Firefox only (Chromium 40695450, WebKit 265263), hence engine workers are started by pages.

On Node:

  • the host is the process (a configuration file, a disk cache);
  • kernels are worker threads or child processes;
  • the build is a kernel with no front end;
  • the CLI is a kernel plus a terminal front end.

Remote: the same protocol over a socket (Compute Host).

5. What crosses the boundary

Front end to kernel. Evaluate an expression in a session (MathJSON, or text the kernel parses), abort, complete, describe.

Kernel to front end. Each result carries:

  • MathJSON, the value, to reuse, copy or bind;
  • boxes (Syntax and Formats, @enumeratio/boxes) in the forms the front end asked for. Formatting belongs to definitions (a package's MakeBoxes rules and forms), so the kernel builds boxes. The front end renders them: KaTeX or MathML on a page, toText/toAscii in a terminal, components for graphics heads. TeX is one rendering, not the contract;
  • messages;
  • the libraries the call declared.

It can also send requests the front end honours or refuses by capability. This is our FrontEndExecute: print a cell, write into a document, ask for input, set a control, show progress, offer a download.

Compiled code instead of an engine in the front end. For interaction that must be fast (a knob dragged at 60 fps), the kernel ships a compiled function (compute-engine's JavaScript and WGSL targets, which Manipulate and the complex-function portraits already use), and the front end runs it. Anything that needs definitions stays in the kernel.

Parsing. The kernel parses, since notation comes with packages. The front end keeps notation data (the manifest's LaTeX dictionaries) for editing. A page then needs no engine at all.

6. Notation

A package owns its heads' notation, in three parts:

  • Output (TraditionalForm): box rules for makeBoxes, registered on the engine when the package declares (registerNotation), so they arrive with the definitions. Display only: F_n is Fibonacci on the way out, but typed in it stays a subscripted symbol.
  • Input (StandardForm): LaTeX dictionary entries, each a parse and serialise pair. A package's input notation is its StandardForm: what StandardForm writes parses back to the same expression, and what the parser accepts is what StandardForm writes. InputForm (Epsil) round-trips the same way; TraditionalForm needn't.
  • Editing: MathLive macros for any LaTeX command the package introduces, or the editor shows it as an error.

Dressed shorthands. Many notations are shorthands that can't parse back on their own (F_n, μ(n), P_n), and read differently in different packages. StandardForm can still use them, dressed: an InterpretationBox whose display is the shorthand and whose expression is the head it stands for (Fibonacci(n)), drawn with a visible marker (a faint tint or dotted underline) and a tooltip naming the symbol. Copying or editing it gives the expression back.

  • In LaTeX, one wrapper command carries both, e.g. \sym{Fibonacci}{F_{n}}: a MathLive macro over \mathtip shows the shorthand with its tip, and one compute-engine dictionary entry reads the head name and ignores the display. An editor can offer the fold as you type (Fibonacci(n) offers the dressed F_n).
  • A plain, typed F_n is still a subscripted F: only the dressed form carries a symbol.
  • TraditionalForm and TeXForm keep the bare shorthand, for print; InputForm stays Fibonacci(n).
  • A package's TraditionalForm rule is the display; marking it a shorthand makes StandardForm wrap it. Since the display is never parsed, conflicting shorthands across packages don't collide: the parser needs one trigger (the wrapper), not one per notation, and a package keeps parse entries only for notation that is unambiguous as typed.

Output can load with a package's definitions; input can't. compute-engine takes a LaTeX dictionary only when an engine is built, so a kernel needs every catalogued package's parse entries before its first call. Each package therefore exposes its notation as its own light entry point (rules, dictionary entries, macros; no definitions), and a host loads all of them when it builds a kernel while declaring definitions lazily.

Ambiguity is ours to check. MathLive flags LaTeX it can't read, not two packages reading the same input differently, and compute-engine warns on duplicate entry names, not on shared triggers. Three checks:

  1. static: across every package's entries, no trigger claimed by two entries of one kind;
  2. StandardForm round trip: each head with an entry, written and read back, gives the same MathJSON;
  3. interference: each package's example inputs parse the same with only its own entries as with everyone's, the check an external package's arrival needs most.

Formats

Every other syntax an expression is read from or written to (AsciiMath, MathML, Wolfram, Python, GLSL, WGSL, JavaScript) is a format, and a format is a package too. Two kinds:

  • A fixed syntax (AsciiMath, MathML, Wolfram): the format package owns the whole mapping, a table from head names to spellings (and back), naming the symbols it covers without importing them. A symbol package doesn't claim AsciiMath syntax; AsciiMath defines it.
  • An open target (the code forms): what a head compiles to can't be listed up front, so symbol packages contribute their heads' rules to it, registered on the engine as notation is (registerFormat(ce, "python", rules)), over compute-engine's own per-target rules. A fixed syntax can take the same hook for the heads its table doesn't cover.

A format also owns its language's globals, and renames variables around them both ways, so no symbol has to know which names are taken there: see Foreign Names. And which packages' notation is in force can vary per scope: see Package Scopes.

Writing a format needs whatever its rules need: AsciiMath and MathML come off the TraditionalForm boxes (no engine), the code forms compile against the engine's definitions (so they're written in the kernel and sent back as text).

7. Building on it without notebooks

Much of the use is sites and interfaces that never show a notebook, where someone takes a dependency to:

  • write documents, interactive or not;
  • build whole interfaces from the universal syntax, the packages and the shared caches.

So each layer stands alone:

  • a host configuration and catalogue;
  • a base kernel to evaluate against;
  • boxes to render with any renderer;
  • elements to draw with.

None of them should assume the others beyond the protocol.

8. compute-engine

As far as we have found, compute-engine offers engine-level controls only: scopes, timeLimit and deadlines, precision, assumptions. It has no kernel, session protocol or worker placement, so this layer is ours. If it grows one, we follow its names and shapes rather than diverge.

9. The build as a kernel

A site build is a front end with no reader: it renders every cell once, so the page has something to show before (or without) its scripts.

  • What it runs. Reference examples trust their recorded values (the tests re-evaluate them). Guide cells go through NOTEBOOK_KERNEL in Node, in page order, so a binding carries forward. A volatile example (random, time) is left to the browser, with a placeholder.
  • What it writes. The typeset HTML (KaTeX, the same as at runtime) goes straight into the page, outside <ClientOnly>. The element takes over on load from a small data island: the value, the display, and the input as written (text and format), always kept for debugging. It renders the precomputed answer first and asks the kernel only when edited. Prerendering is the default; a cell opts out with prerender="false".
  • Everything, ClientOnly or not. A plot is drawn once at its stated domain and samples (the SVG renderers are pure), and a Manipulate at its controls' initial values. That gives a preview before load and a final picture for print or a static viewer.
  • Preload hints. Each page names the chunks it will use (the cell elements, KaTeX), so they're fetched with the page rather than discovered at idle.

Outputs and exports. Internally an output is a bundle of the same answer in several MIME types: application/x-mathjson, text/x-epsil, text/latex, text/html, image/svg+xml and text/plain, as a Jupyter notebook's outputs are. The site build takes its HTML; nothing ipynb-shaped goes into a page. Writing a notebook or transcript as .ipynb is an explicit export, with options for the render target (a github preset among them), built on the same multipart output writer every file format uses.

10. Steps

  1. Session workers resolve (landed with this page). A worker kernel starts with no libraries, and declares each one from the host's catalogue (web/.vitepress/theme/worker-catalogue.ts) when a call first needs it (@enumeratio/evaluation's createKernel). Its answers report what they declared.
    • Libraries the resolver doesn't know yet (formats' graphics, geometric, the frontend's carriers) are declared with the first call that declares anything.
    • A library's requirements come from the package hierarchy (packages/manifest/src/hierarchy.ts), not package.json: structures comes before analytic, and analytic loads only where a call needs it. The catalogue plans frontend itself, which extends structures, not the analytic its plots import.
    • Load order doesn't change an answer: widening a signature joins it with what's there (#522), so Floor and Ceil keep structures' widening whichever declares first. The reference examples of Floor, Min, Max, Clamp, Sign, Inverse, Element, Sort and Reverse agree with the full engine under scrambled library orders.
  2. Boxes in results, and a page that parses and renders without an engine (#504–#531). Plots sample code their kernel compiled (#545).
  3. The base kernel in a SharedWorker, under a host configuration served by a service worker, with the cache.
  4. Custom kernels forked for diverging documents.
  5. The build as a kernel (§9): cells rendered ahead of time, preload hints.
  6. The CLI on the full catalogue.

Open

  • Overrides and specialisations from external packages: how conflicts resolve, and when they force a host to load everything that could apply.
  • Kernel-to-front-end requests: the capability vocabulary, and which requests a front end may refuse silently.
  • Caches and read-only data shared across hosts.
  • Parsing latency: a keystroke-level parse is a round trip to the kernel.

Home

Project

Design: every design page, settled, speculative and archived

Clone this wiki locally