Repository navigation
Speculative Native Rendering
Speculative design. Open, not settled — thinking and proposals, not shipped behavior. See Roadmap for what's in flight.
Status: research, nothing built. Idea (the maintainer, 2026-10-06): realize our standard Boxes natively, on mobile, with no lit elements. Companion to Speculative Rendering Environments (the axes) and Rendering Environments (what landed).
This is general layout, not math layout. Math is one client. Combinatorics produces shapes
that look nothing like formulas (tableaux, paths, trees, diagrams, tilings); collections want
tables and lists; graphics want scenes; prose wants rich, formatted, sometimes interactive text.
Boxes, the generic graphics primitives and the vdom together are a general, semantic markup:
notatio markup should be useful to someone (or some agent) with no interest in mathematics at
all, a durable way to mark up meaning and its presentation, the thing HTML set out to be. Every
choice below is judged against that, not against typesetting formulas.
The short version: boxes are the one thing every environment can draw, so make them the only contract. Each environment gets a small primitive library that draws boxes with its own dependencies; generic components lower to boxes; a registry lets a library hint per environment. The hard part is layout in general (math, tables, grids, structures, rich text, graphics), and it is one layout engine we write once, pure and measured through a callback, not a dependency per target.
-
Primitives (
src/box.ts,BoxNode):RowBox,TextBox, the script boxes (Super/Sub/Subsuper/Over/Under/Underover),FractionBox,SqrtBox,RadicalBox,GridBox,StyleBox,FrameBox,TagBox,InterpretationBox,ErrorBox; prose (TextCell,TextData,ButtonBox);FormBox(TeX held as written);TemplateSlot/TemplateExpression(holes the environment fills). A leaf is a token string, classed by one lexer (tokenClass). Options are a trailing object (FractionLine,Background,FontSize,FontWeight…). -
Serialisers (
src/render/):toLatex,toMathML/parseMathML,toText/toAscii(one line,linear.ts),toHtml(prose only, typesetter passed in),toMarkdown/readMarkdown. All are boxes to a string; none lays anything out. There is no 2-D text serialiser yet (Boxes "Next" item 5). -
Not boxes yet (Boxes "Still to come"):
GraphicsBox,DynamicBox,TemplateBox, the layout formattersRow/Column/Grid/Panel/LabeledasmakeBoxesrules,AdjustmentBox/PaneBox/PanelBox,TooltipBox; a control box (ButtonBoxis the only interactive box and is prose-only). - Layering:
boxesisbase(no DOM, no engine at runtime);/renderispresentation(packages/boxes/package.json).
-
notatio-outtypesets viamakeBoxesthentoLatexinto MathLive/KaTeX (packages/components/src/notatio-out.ts,mathlive.ts). So the web draws boxes only by way of LaTeX, not bytoMathML. - A head that draws is looked up in
VISUAL_SYMBOLSand lowered to aRendering{tag, attributes, children}(packages/frontend/src/symbols.ts,renderingOf); the tags are Lit elements (packages/components/src/notatio-*.ts). This path bypasses boxes entirely: plots, charts, the Control family,Manipulate,Dynamic,Row/Column/Grid/Panel/Labeled, the notebook. - Plots and glyphs are pure SVG-string functions (
frontend/src/plot.ts,glyphs.ts,primitives.ts,complex-plot.ts);ComplexPlotwants WebGPU, with a CPU path (gpu-eval.ts) and@enumeratio/raster(resvg, Node) for PNG. - Controls share one contract (
frontend/src/controls.ts):name, a MathJSONbinding,notatio-control-changeevents. The arithmetic of scrubbing and playback is DOM-free (scrub.ts,playback.ts,reactive.ts). Declarations are readable without a DOM (declarations,pin,sampleValuesinreduce.ts).
structuralOf / vdomOf / toVNode (frontend/src/vdom.ts) turn MathJSON into a
{tag, props, children} tree for any framework's h; @enumeratio/formats/markup reads and
writes it as JSX. Framework-neutral, but its tags are notatio-* elements, so a native
host would mount the same Rendering with native views instead — the vdom is the expression
tree, boxes are what an element draws inside itself (Boxes "The link back to meaning").
-
Environment(frontend/src/environment.ts):engine,interaction,time,surface,form,layout,colour,theme,staticpolicy; presetsWEB,PRINT,TTY,PIPE,COMPACT.reduce(expr, env)(reduce.ts) rewrites in the expression what the record cannot honour (pin/sample controls, rasterize GPU plots, stack rows). - The TUI is a mount over
Rendering, not boxes:cli/src/drive.ts(keyed sliders strip, body pinned per keypress),present.ts,textual.ts+frontend/src/textplot.ts(braille plot). Typeset math at a terminal istoText/toAsciione-liners; images via kitty/iTerm (cli/src/node-graphics.ts).
| need | today |
|---|---|
| math layout (frac, radical, scripts, grid, limits) | boxes exist; layout only inside MathLive/KaTeX/MathML |
| text styling |
StyleBox options, drawn by LaTeX/MathML only |
| layout containers |
Rendering tags, not boxes |
| graphics | SVG strings, not boxes |
| controls, readouts |
Rendering tags + DOM events, not boxes |
| prose | boxes (TextCell…), HTML/Markdown only |
| theming | CSS custom properties inside SVG strings |
All claims below were checked 2026-10-06 against the cited page; "unverified" marks gaps.
- Model: real native views driven from JS; one JS thread, no web view. Core 9.x (9.1 post) is on V8 14.9 with Vite 8 HMR and lists Vue, Angular, React and Solid; Svelte is "future work" there. NativeScript ships Node-API engine previews with Hermes, QuickJS and JSC as options (8.9 previews); the 9.1 post names only V8 — which engine is default and supported is unverified.
- nativescript-vue: Vue 3 is GA (repo); small project (~6.5k stars, ~140 commits on main), community-run. Last-commit date and Vue 3.5/3.6 compatibility unverified. Vue is our stack's side (the site is VitePress/Vue).
-
JS/TS logic: V8 means BigInt, modern JS, structuredClone,
TextDecoder, workers (9.1 post). WebAssembly: not mentioned in the 9.1 post, V8 supports it, NativeScript exposing it is unverified (spike item). Compute-engine and our packages should run as plain ESM. -
Native APIs: direct access to UIKit/Android SDK from TS — the only option here where we
could call CoreText/
StaticLayoutfor measurement, or SwiftMath/AndroidMath, from JS without writing a native module. - Size: ships a V8 snapshot; the iOS metadata alone is ~11 MB (9.1 post). Measured app size unverified.
- Model: React components to native views via Fabric. 0.84 (2026-02-11) made Hermes V1 the default and added WebAssembly in Hermes (Callstack).
-
Drawing:
react-native-svg(15.15.x, Fabric) draws an SVG display list;@shopify/react-native-skia(2.x, in Expo) hasParagraphand glyph APIs and a GPU canvas — the better fit for plots and a math display list. -
JS/TS logic: Hermes. BigInt: a feature since v0.12 per the
issue trail, JSI creation/extraction landed (#775); full
conformance on V1 unverified — compute-engine's
Decimalpath and our native-bigint kernels must be run, not assumed. Workers: no web Worker;react-native-worklets/threads exist (unverified for our use).Intlcoverage is partial (unverified current state). - Team fit: React, which the stack uses only for the (removed) wrappers; nothing in the repo is React-native-shaped.
- Size: small bare app; Expo adds. Not measured.
Capacitor 8 (8.5 on 2026-07-31, Ionic) wraps the existing web in a WKWebView / Android WebView. Zero new renderer: our lit elements, MathLive/KaTeX, WebGPU- where-available run as is; cost is feel, the web-view GPU limits, and Android WebView version drift. This is the baseline any native effort must beat, and the answer to "mobile, now". Tauri 2 mobile is the same model with a Rust shell: stable 2.11.x, mobile "newer and less mature", system webviews differ per platform (survey).
ByteDance's framework, open-sourced 2025, v3.x, monthly releases from mid-2026 (roadmap). Web-flavoured: CSS-styled native elements, ReactLynx official, Vue Lynx community. Its own JS engine (PrimJS, QuickJS-derived — from memory, unverified) makes BigInt/WASM the biggest risk. Credible and web-shaped; young. Watch, don't spike first.
Flutter/Compose Multiplatform: strong custom canvases, but Dart/Kotlin — our TS packages would run in an embedded JS engine; out. Native Swift/Kotlin hosts with a JS engine (JavaScriptCore / QuickJS) running the same boxes: the minimal-dependency end of the spectrum, most work per platform, and the honest reference implementation of the contract.
| option | how | notes |
|---|---|---|
| SwiftMath (iOS/macOS), AndroidMath | native views from a LaTeX string; TeX rules; bundled math fonts (SwiftMath: 12) | maintained as of 2026 per search results; LaTeX in, so we'd round-trip toLatex (loses InterpretationBox, hole structure); no hook for our hover/selection; Android port maturity unverified
|
| MathJax/KaTeX to SVG |
toLatex then SVG string, drawn by react-native-svg/Skia or NS ImageView |
fidelity good; needs a DOM-less run (MathJax has mathjax-full SVG output, works in plain JS; KaTeX has no SVG output); react-native-mathjax-svg is 5 years stale |
| KaTeX in a web view | per formula web view | heavy, per-formula cost, no real integration |
| own layout over boxes | §3 | no LaTeX round trip; touchable pieces |
Boxes are already the right input: a tree with no parsing. What is missing is the function boxes to a positioned display list (glyph runs, rules, boxes, baselines), after which any surface draws it. The engine has to cover general layout, not only formulas:
-
flow and rich text: paragraphs, inline runs with styles, links and inline interactive
pieces, line breaking (prose boxes:
TextCell,TextData,ButtonBox); -
tables and grids:
GridBoxwith spans, alignment, rules and headers, for collections and results, not just matrices; - combinatorial structures: tableaux, lattice paths, trees, permutation diagrams, tilings, which are grids and graphics, not formulas;
- containers: rows, columns, panels, labels, frames, panes with scrolling and priorities on narrow screens;
-
graphics: the display list doubles as the graphics primitive set (
GraphicsBox); - math: the TeX rules below, as one specialization.
The math-specific choices that follow are the hardest typographic part, not the whole engine.
Option A: our layout engine over boxes. Pure TS, layout(box, style, metrics) -> DisplayList,
implementing TeX's Appendix G rules (as KaTeX's buildHTML and iosMath do) for the box heads
above: fraction (num/denom shift, rule thickness), radical (surd glyph size variants, vinculum),
scripts (shifts from superscriptShiftUp etc.), limits, GridBox (row/column gaps), atom spacing
from tokenClass (ord/op/rel classes, as the MathML operator dictionary). The constants come from an
OpenType MATH table of one bundled font (Latin Modern Math or STIX Two Math), extracted at build
into JSON; only metrics.measure(glyphs, font) is environment-supplied (CoreText / Paint on
native, canvas on web, cell width on a TUI). Stretchy delimiters and the surd need the font's glyph
variants (MATH MathVariants) — real work, and the part that most separates TeX-quality from
"stacked labels". Effort: a few weeks for fractions, scripts, radicals, grids, fences; a long tail
(stretchy operators, accents, \\ multi-line, line breaking) after. Fidelity: close to TeX for
the covered set; identical across iOS, Android and the web fallback, which is a feature (goldens).
Option B: an existing library (SwiftMath/AndroidMath). Days to a first result, best typography
for free, but per-platform native code, two separately maintained engines (one per OS), LaTeX as
the interface, and our InterpretationBox, selection and hover cannot reach into it. Fine as an
@enumeratio/boxes-native fallback leaf for a FormBox(tex) island.
Option C: SVG. toLatex through MathJax SVG, or our own display list serialised as SVG
(the same list A produces). Cheapest and portable (react-native-svg, Skia, NS ImageView);
non-selectable, no native text scaling/accessibility unless a label is added from toText.
Recommendation: A as the destination with C as its first drawer (the display list as SVG
runs everywhere, including the web for a golden-tested comparison with MathML Core), B only for
TeX islands we do not own. Accessibility: every drawn box also gets toText(box) as its label.
The same layout with a character-cell metrics is the two-dimensional text serialiser the
terminal lacks (Boxes "Next" 5), so A pays for the TUI too.
boxes (base) box tree, makeBoxes, notation, registry
└ boxes/render (presentation) toLatex, toMathML, toText ... (exists)
boxes-layout (base, pure) layout(box, style, metrics) -> DisplayList; MATH constants JSON
boxes-ui (presentation, pure) the interface boxes: layout formatters, controls, graphics, dynamics
├ boxes-web (lit / DOM) today's notatio-* elements, rebased onto boxes-ui, in time
├ boxes-tui (Node, cells) the cli drive/present/textplot code, rebased
└ boxes-native (NS or RN) one library per host framework: boxes-nativescript, boxes-react-native
Names are placeholders (the rename rules in Component-Naming still apply; a library is
@enumeratio/…, and a new package declares layer and extends per the hierarchy test).
Constraints: the first two import no DOM and no framework; boxes-ui imports boxes and frontend's
pure parts (controls.ts, reduce.ts), no environment; each environment library imports only
boxes-ui, boxes-layout and its own framework. A downstream library never imports an
environment library.
Today a head lowers to a Rendering of notatio-* tags. The proposal adds a second lowering,
interfaceBoxes(expr), to the interface boxes, so a head with no environment-specific form
still draws:
- layout:
Row,Column,Grid,Panel,LabeledasGridBox/FrameBox/PaneBox(the Boxes "Next" 3 item), so a native renderer needs no layout heads of its own; -
GraphicsBox(primitives as inprimitives.ts, plots asGraphicsBoxof polylines/axes): the pure SVG functions become one display-list producer, and SVG is one drawer; -
DynamicBox(expr)and a control boxControlBox(name, kind, spec, options)(kindfromCONTROL_HEADS,specthe range/choices/initial); a scope owns bindings exactly asnotatio-dynamic-moduledoes now.
Not every symbol needs a native-specific component: anything unhinted lowers to boxes, and boxes
always draw. A symbol that wants more (ComplexPlot on a GPU) registers a hint (below).
interface BoxRenderer<Out> { // one per environment
readonly environment: Environment; // the existing capability record
readonly metrics: Metrics; // text measure, font, em, theme resolver
render(box: Box, ctx: RenderContext): Out; // dispatch on box head
readonly heads: Partial<Record<BoxHead | InterfaceHead, HeadRenderer<Out>>>;
readonly capabilities: readonly BoxHead[]; // heads it draws itself (others get §below)
}-
Per-box-kind renderers: a head not in
headsfalls back, in order, to (1) a hint for it (below), (2) its reduction (a box rewrite to simpler boxes:RadicalBoxtoSqrtBoxwith a prefix,GridBoxto aRowBoxof rows), (3)toText(box)in a label. Boxes always draw. -
Capabilities extend, not replace,
Environment:headsplus new axes the record lacks —fonts(has a MATH font),gestures(touch drag, long-press, second finger),focus(keys, none),textSelection,a11y.reducestays an expression rewrite upstream; the box-level reductions are a sibling pass,reduceBoxes(box, env). -
Contract test: a corpus of boxes (the existing
tests/corpus.ts) must render without throwing in every library and match a display-list golden where the library useslayout.
A downstream library should say "on a phone, draw my CollectionTable as a list" without
depending on any environment library. Three layers, in order of cost:
-
Hints as data on boxes (zero dependency): the option bag carries namespaced keys,
{ "native.Hint": "list", "tui.Width": 40 }; unknown keys are ignored (as Wolfram options). Carried throughTagBox/StyleBoxoptions — no new head. Good for sizes, orientation,Priority(what to drop first on a narrow screen). -
A registry keyed by (head, environment), in the library's
notation-style data (package.jsonentry plus a JSON file, like per-package notation, Speculative Overloads as Data):{ head: "CollectionTable", native: { lower: "<boxes expression as markup>" }, tui: { lower: … } }. A hint's body is boxes or vdom markup, so it needs no environment code; the environment library reads the registry at mount. This is the default route. -
A code hook, only where data cannot say it: the library exports an optional entry
./native(or./tui) whose default export is(ctx) => HeadRenderer; the environment libraryimport()s it when present. The library'spackage.jsonlists it underenumeratio.environments; absent means unhinted. Imports flow down the hierarchy: the hook importsboxes-uitypes only, neverboxes-nativescript.
Resolution order for one box: explicit option hint, registry row for this environment, code hook, the environment's own head renderer, reduction, text label.
A control is specified once, as data, in the vocabulary the reference already has
(Slider(k, (0, 5)), PopupMenu(x, {…})):
-
Required fields:
name,kind,initial,domain(range/step or choices),label. These determine the pin and sample readings (Speculative Rendering Environments §5), so every control works on paper; nothing else may be required to be meaningful. -
Intent, not widget: a control declares what it chooses (
continuous,choice,toggle,point2d,text), the environment picks the widget: slider/stepper/picker on a phone, a key-driven strip in the TTY, a menu in the web.ControlTypestays a preference. -
Options are environment-keyed and ignorable: unknown options and
native.*/tui.*keys are advisory, never errors; an option every environment honours has a plain name and gets a row in the control table before it ships. A new control or option PR adds its row for web, TUI, native and static, or says "inherits from X". -
Interaction contract (already
controls.ts): binding in,ControlChangeout, DOM-free. A native library mapsControlChangeto its framework's state; gears (coarse/fine) become gestures (second finger, long press) per theInteractionaxis. -
TUI improves in place: because the TUI draws the same boxes and the same control table,
a better 2-D layout (via
boxes-layoutwith cell metrics) improves every result at once.
Environment stays the capability record and reduce the expression-level rewrite; this note adds
the layer below it. Surface gains native, Interaction is unchanged (touch), form for
native is the box default (no LaTeX string). A native environment is a record like COMPACT with
engine: true, surface: ["native","vector"], layout: "compact"; the hooks key off
environment.name. Open: whether a hint keys on a name (native) or on a predicate over the record
(touch && compact) — a name is simpler and enough for now.
Smallest proof: NativeScript-Vue renders a fraction and a slider-driven Manipulate from boxes on an iOS simulator, with no lit anywhere.
-
Run the engine (the risk first). A bare NS-Vue app imports
@cortex-js/compute-engine, ourboxes, and evaluatesBinomial(5, 2)and oneBigIntand one 50-digitN. Pass: it runs on V8 as shipped; note bundle size, cold start, whether WebAssembly exists. Repeat on RN/Hermes V1 only if NS fails. -
Draw boxes as labels. A
<BoxView>component walksRowBox/SuperscriptBox/TextBoxinto NSStackLayout/Labels (no real layout): proves the dispatch and the vdom mount. Pass:x² + 1. -
Layout engine slice.
layout()forFractionBox, scripts andSqrtBoxwith one bundled math font andmeasurevia CoreText through NS; draw the display list as absolutely positioned labels plus rule views, and as SVG for comparison. Pass: a fraction, a binomial and a radical match MathML Core in Chromium at the same size (eyeball plus a display-list golden). -
A Manipulate.
ControlBoxslider andDynamicBoxreadout frominterfaceBoxes: the slider is NSSlider; changing it re-evaluates in the engine and re-lays out the box. Pass: aManipulate(Binomial(n, k), (n, 0, 10), (k, 0, 5))strip works at 60 fps by eye. -
One hint. A downstream registry row that changes a
CollectionTableto a list onnative. - Decide: keep NS, switch to RN (Skia drawer), or fall back to Capacitor for mobile.
Cost: roughly a week for 1–2, two to three for 3–4; step 1 can end the effort in a day.
- Feel or reach: both; one is better than none. Capacitor stays the reach baseline.
- NativeScript-Vue or React Native: the better fit, NativeScript-Vue. React Native pieces get ported as needed.
- Layout engine or SwiftMath/AndroidMath: the focus is layout, not math ergonomics. Drop in SwiftMath/AndroidMath for typeset math, as KaTeX is used for non-interactive rendering on the web, and spend the layout engine on everything else (§3). Math input is a separate question: rather than reimplementing MathLive, a MathQuill-style editor with MathLive's notation hooks is a possible direction later.
- Where the interface boxes live: open. It depends on how the split falls out; each environment may need its own base package, since kernel needs and capabilities differ by environment.
-
Web components: implement a web component per box primitive, and build the more abstract
heads (
notatio-*) out of those smaller boxes. If that works, more generic implementations fall out of it. - Hint keys: capabilities by preference; the exact environment kind where it matters (e.g. for choosing dependencies). So both: a predicate over the record, with the environment's name as one of its fields.
- BigInt: every expected host has a recent V8 or equivalent, so BigInt is assumed. Where a host lacks something, decline to interpret the affected symbols rather than answer wrong.
NativeScript 9.1 · Node-API engine previews · nativescript-vue · React Native 0.84 (Callstack) · Hermes BigInt #510 · react-native-svg · react-native-skia · Capacitor 8.5 · Lynx roadmap 2026 · Vue Lynx · SwiftMath · iosMath · Tauri 2 mobile survey