tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

README.md (85863B)


Arboricx browser playground

Explore Tree Calculus, run portable programs, compile tricu source, and export stored definitions in the browser. The site lives in ext/demo-site and uses TypeScript compiled to browser ES modules.

The page flows from the hero into one editor workspace, followed by the ecosystem articles. Names and values loads on arrival, including when no selection fragment is present; loading verifies a package but never executes it or downloads a compiler kit. Haskell is the initial runtime.

The editor combines source, an attached expression prompt, and a single output surface in one column: source → expression → Evaluate/Stop → result. Output interpretation, target and execution controls sit above the expression; choosing a stored definition hides the unused expression inputs. The sticky toolbar keeps Program, Runtime, Settings and status, plus Stop while work is active. Results remain directly beneath the action even with the desktop inspector open. Settings dock on desktop and become a dismissible modal sheet on mobile, with Escape, focus containment/return, and inert background controls. The mobile source editor reaches both viewport edges; padding is inside the editor. Interpretation and execution mode remain visible outside settings. The footer credits Copyright 2026 James Eversole.

On mobile, an explicit Evaluate/Show definition action can reveal the beginning of offscreen output once, below the sticky toolbar. Already-visible results stay put; large output is not scrolled to its end. The first IO output/input/step may use this reveal instead, but later IO events and completion never repeat it. There is no automatic focus transfer or keyboard opening. Desktop evaluations, idle compilation and package arrival do not auto-scroll. Stop, source/settings changes, page departure, or deliberate wheel/touch/scroll-key browsing cancel a pending reveal; an open mobile settings sheet suppresses it. Movement is instant, including with reduced-motion enabled.

Syntax colors and bracket matching

Editable source, reference source, and the expression prompt distinguish comments, strings/escapes, numbers, the kernel t, current keywords, module directives, operators and delimiters. Definition declaration sites have one dedicated color, including ordinary local equations; a simple contract name after @ or =@ has an annotation color. Declaration highlighting uses conservative lexical and layout rules; compiler diagnostics resolve names and validate source.

A collapsed caret immediately beside (), [] or {} highlights its matching partner. An unmatched delimiter gets a wavy underline. Strings and comments are excluded, including non-nesting block comments. Range selections do not highlight a pair. There is no automatic bracket insertion, indentation rewriting or completion. Editing and expression evaluation keep their existing behavior.

The local syntax.ts renderer decorates native textareas. Decorative mirrors are hidden from accessibility APIs and never interpret source as HTML. Selection, undo, resizing and scrolling remain native. During IME composition and in forced-colors mode, native text remains visible instead of the colored mirror. Programmatic source/preset changes explicitly refresh the decoration without synthesizing edits or starting compilation. Sources over 32,768 UTF-16 code units fall back to plain text, bounding decoration work without truncating source or changing execution limits. This also covers large read-only library sources.

Runtime selection

The Runtime selector offers Haskell (default) and Zig. Haskell is the reference evaluator; the Haskell and Zig browser backends run as WASM. Selection is explicit and page-local; failures never silently switch backends. Changing runtime discards the old runtime, preserves source text, and automatically recompiles a draft after switching. Published selections reload on the chosen runtime. Switching is disabled while work is active; Stop first.

The JavaScript evaluator is retired as a user-selectable backend: its compilation latency is unsuitable for the live-compilation workflow. Its independent graph, codec and compiler conformance coverage remains. Test-only JS Workers and their checkpoint protocol live under src/conformance/ (generated into dist/conformance/) and are excluded from the production release. Shared JavaScript authentication, literal codecs and tree formatting remain in use alongside the native runtimes.

The release packages the pinned Haskell reactor, loader, WASI shim and authenticated compiler kit under runtime/haskell/. Expression evaluation and source compilation load the compiler kit on demand, retaining the loaded definitions. Draft compilation checks the runtime's original compiler-bundle identity against compiler.json, then resolves explicit native imports or the selected empty, practical or default-contract standalone environment. Missing assets or mismatched pins are errors. Zig packages a freestanding reactor and private adapter under runtime/zig/. It has no WASI imports. Its compiler session fetches the authenticated compiler bundle selected by compiler.json. Both use the same environment/profile pins and names.

Both runtimes use shared bundle authentication, literal validation, immutable stored-definition export and the same page IO protocol. runtime-session.ts defines the coarse adapter boundary: load, compile, run/runExpression with async IO callbacks, diagnostics and disposal. Private pointers, IO tokens and heap checkpoints do not cross backend types. wasm-worker.ts wraps the selected session; haskell-session.ts maps the built Haskell client's protocol, including its nested Worker and asynchronous resume tokens. Host cleanup always terminates the owning Worker tree; there is no default compile/run timer. Zig's private ext/wasm/site-session.mjs owns its handles, serializes operations, and retains IO continuations across awaited host callbacks. Its exclusive async scope frees all transient handles on success or failure; compiled definitions remain rooted. Zig's site demands, bulk Unicode decoding and bounded previews explicitly select unbounded work, not a large substitute fuel value. The native ABI's finite-fuel operations remain available for native callers and negative-control tests.

WASM diagnostics report linear memory. Haskell's fixed RTS limits are 256 MiB heap and 8 MiB Haskell stack; both reactors cap linear memory at 512 MiB. Zig also has an 8 MiB WASM stack. Zig's strict scalar decoder rejects redundant high-zero numeral representations that the permissive JS text decoder tolerates; valid Unicode scalar strings agree. These limits apply to the runtime's heap, stack and linear memory. Exhaustion or a fatal trap discards the session and requires explicit reinitialization.

The Haskell binary requires WebAssembly SIMD, available starting with Chrome 91, Firefox 89 and Safari/iOS 16.4. Integration tests use Chromium 148, Firefox 150 and Linux WebKit. Runtime selection is explicit; clients can select Zig if Haskell initialization fails. Haskell uses a bundled WASI shim and a nested module Worker.

Linux WebKit testing

The pinned Linux WebKit runner exercises the built site:

ARBORICX_SITE_ROOT=$(nix build .#arboricxSite --no-link --print-out-paths) \
TEST_IDE=1 TEST_BACKEND=haskell-wasm \
  nix run .#arboricxWebkitCheck

Repeat with TEST_BACKEND=zig-wasm and with TEST_MODULES=1, TEST_LOCAL_MODULES=1, or TEST_ASSET_CACHE=1 in place of TEST_IDE=1. The IDE, complete demo/module, local-file and cache suites pass on both backends with Playwright 1.59.1 / WebKit build 2272. The runner also checks layout at 320, 390 and 1280 pixels after textarea mirror resize observers settle. SCREENSHOT=/tmp/arboricx.png saves a screenshot at each width.

Each run uses a fresh, temporary disk-backed browser profile, matching the Chromium/Firefox harnesses. Private ephemeral WebKit contexts can refetch large WASM assets despite immutable headers; they are a different cache experiment. The Nix wrapper supplies the matching driver, WebKit and patched libraries. Run from the repository root.

Finding libraries and writing with assistants

Add import… in the source actions opens a modal library browser. Search matches library names, descriptions and public exports; the first view selects the newest published version of each library. Details contain an export filter, available calling conventions and tested examples, a historical-version selector, namespace field and full import preview. Validated local files appear in the same browser.

A cancellable, separate Worker uses the selected native backend to inspect the header and compile the candidate source. Namespace conflicts and compiler failures appear before insertion. Existing imports offer a jump action; unfinished source can use Copy import. Insertion prepends a qualified directive while preserving all original source, supplies one native textarea undo transaction, and follows normal idle compilation. The dialog uses native focus containment, Escape cancellation and focus restoration. Each candidate check has a 60-second cutoff. A revision guard rejects stale replies; insertion rechecks source and backend selection. Inspecting headers preserves the session's compiled scope.

The footer's For AI assistants link and HTML rel="help" metadata point to llms.txt. This emerging discovery convention supplements ordinary HTTP links. The entrypoint links to a playground guide, library directory, language/protocol contracts, raw source references and general worked examples. Each example gives complete source, expression, interpretation, execution mode and expected output. The references are static text and also work for readers using a browser.

reference.py derives every published library interface from the bundle export table, including reexports, and checks its bundle SHA-256. It generates the picker's reference/libraries.json and per-release Markdown from the same records. Calling conventions in reference-input/libraries.json are version-scoped; reference-input/examples.json pins explicit module versions and independent expected results. The browser matrix compiles and runs these examples on both backends. Source pages are labelled repository references with source hashes; the published export table describes the selected release's actual interface.

References join the immutable asset tree. The root llms.txt links to that build's versioned pages and should revalidate alongside HTML. Serve .md as text/markdown or text/plain; raw source copies use .tri.txt for ordinary plain-text delivery. A release check validates interface coverage, exact export names, relative documentation links and the quickstart's tested example.

Run the picker/reference matrix against a built site using TEST_IMPORT_PICKER=1 TEST_BACKEND=haskell-wasm or zig-wasm with the Chromium, Firefox or Linux WebKit harness. It covers exact insertion and undo, native header/scope behavior, export search, historical versions, namespace conflicts, malformed-source copy fallback, cancellation and local-file selection. The WebKit runner also checks modal keyboard focus, Escape and narrow layouts.

Programs and controls

All curated and arbitrary packages take the same verified bundle/heap execution path. Curated catalogue entries add suggested literals, codecs, and optional source.

The selector starts with Basic Examples, in learning order:

Each lesson supplies an initial expression, interpretation and expected result, followed by a small edit exercise. Practical Examples contains the application examples below. Language & Runtime Explorations starts with Language reference, an independent syntax sampler covering definitions, lambdas, partial application, literals, patterns, captures, pins, local bindings, recursion, case, pipes, explicit do and contracts. Its sixteen definitions have runnable presets; module-header alternatives and protocol guides are explained alongside the examples.

These examples identify behavior by authenticated bundle hash + export name. Multiple versions share a package, so selecting an export does not change the whole-bundle hash. Pin both in a receipt; pin inputs, codec/wire schema, and any host-effect policy too when reproducing execution. A remote recipient can run the same closed tree in its evaluator. This site loads authenticated local packages; Conversation IO and repository hosts illustrate effect-protocol adaptation.

Published libraries

Libraries uses short display labels. Identical native manifest identities appear once, preferring the complete filesystem-library publication over its older package alias. Different interfaces retain separate rows, including the small convenience interfaces and combined compiler tooling. Package names, versions and both hashes remain in details; all historical releases, including the retired Start Here example, remain available by bundle hash.

The retained index also includes tricu-base, tricu-file-io, and arboricx-inspection at 0.1.0 and 0.1.1. The latter are rebuilt with the current published stable compiler; original releases remain immutable. These are reusable native library releases, not curated editable demos. The same verified download and stored-definition export controls apply. IO exports construct actions; the browser does not acquire native filesystem permissions by downloading them.

The native offline bundle inspector consumes all three through one native project declaration/lock. Its opt-in acceptance/staging command uses the shipped stable compiler and a clean offline consumer build before adding library pins/assets to this site's existing release/seed pipeline. prepare-index.py retains these entries when refreshing curated demo releases. No library compilation or source dependency resolution happens during site build, seeding, or browser package selection.

Compiler packages, practical libraries and scanning demos

The Published packages group also contains these 0.1.0 and 0.1.1 interfaces:

Parser combinators under Examples is the separate parser-demo package. Try "red,green,blue", "red,,blue", "red," or "red green". parseNames requires complete input; parsePrefix exposes any unconsumed suffix. A comma commits to another lowercase name. Scalar records show the expected label, remaining input, consumption and commitment, or the accepted names/count. The source is editable in place. Its manifest-pinned imports use the same native header/resolver path as other browser modules. Try changing the lowercase range to 65 90 to parse uppercase names, or edit a diagnostic label.

Located lexer scans under Examples is the separate lexer-demo package. scanWord scans a lowercase ASCII prefix; scanWhitespace scans spaces, LF and TAB. Records show matched text, remaining input, end line/column, character offset and whether anything was consumed. Try "hello,world", "abcé" and whitespace containing a newline/tab. Empty prefixes succeed without consuming anything; these scans return the matching prefix and remaining input. The source is editable: change a predicate and evaluate again. Restore original returns to the authenticated published source and behavior.

To reproduce and stage this cohort locally, first build the Haskell host through Nix, then run the opt-in bounded preparation:

nix build .#
python3 ext/demo-site/prepare-showcase.py result/bin/tricu \
  --directory /tmp/arboricx-showcase --version NEW_UNUSED_VERSION --stage-site
# Refresh the package set under a new immutable version:
python3 ext/demo-site/prepare-showcase.py result/bin/tricu \
  --directory /tmp/arboricx-showcase-next --version ANOTHER_UNUSED_VERSION --stage-site

Preparation authenticates retained fixtures and requires all recorded working sources to match them. Nine library interfaces retain exact constructor/child-ID roots from those accepted artifacts, with renamed exports and preserved DAGs. The two compiler packages retain their exact accepted bundle bytes. The list traversal library, formatter and parser/lexer demos compile with the authenticated shipped stable builder and pinned native dependency interfaces. Acceptance includes native builds through the published builder (a local dependency succeeds; a missing dependency fails without emitting a bundle), arithmetic and malformed-byte controls, predicate dispatch/early-stop folds, real formatter cases, and independent complete parser/lexer records. Publication and a clean indexed import must agree on every manifest before staging.

Ordinary packages must fit the browser's 16 MiB/100,000-node input bounds. The indexed compiler loads under the ordinary package profile. Draft compilation separately retains a 2 MiB compiler input limit. SHA-256, streaming limits, node/export validation and loading deadlines remain enforced. Native commands use 2 GiB RTS bounds, per-command deadlines and a ten-minute whole-job deadline.

The output directory retains commands, source/artifact identities and acceptance results. --reuse-directory permits reuse of its local CAS, while rewriting sources and rechecking native receipts on every run. Staging is an exclusive local administrator operation. Existing release pins cannot be replaced, all assets precede index discovery, and conflicts fail before staging. Native-prepared catalogue entries have preparation: "native"; prepare.py preserves them and prepare-index.py preserves their accepted editability metadata. Current examples use environment: "empty": their native headers provide all library bindings. Existing immutable versions are unchanged. --version selects the exact label for the entire prepared package set (default 0.1.1); a changed bundle cannot replace an existing name/version. The patterns interface comes from the support fixture.

Published packages and local drafts

The package selector reads packages.json, a versioned static index of exact name/version → native manifest + whole-bundle pins. A single Program selector controls both result evaluation and stored-definition output, with grouped Examples, Published packages, and a visible Load by bundle hash… option. Versions are listed newest-first and choosing an example or published package immediately loads and verifies its newest indexed release, without requiring the Load button in Settings, independently of publication order. Conventional MAJOR.MINOR.PATCH versions use SemVer precedence (including prereleases); other accepted labels sort below them in numeric-aware label order. Examples use curated source and suggested inputs only when the selected release matches their catalogue pin; otherwise they load as compiled packages without borrowing source or inputs from another release. Shared bundle-hash links remain exact pins. Demo names appear only under Examples, not again under Published packages. Examples with multiple releases show the version picker and Load package; changing versions invalidates prior results, and older releases load their exact bundle without borrowing current editable source. Single-release examples also expose their version in Settings → Published package, alongside verification details, the hash loader, sharing and original bundle download. Changing program, version or hash invalidates the preceding executable and output; evaluation stays disabled until verification succeeds. Published packages expose their exports to expressions and stored-definition output. Available reference source is displayed alongside them. Editable demos open directly in the source editor. Until the source changes, expressions use the authenticated package's exports. Changing source switches to Local draft and queues compilation after 650 ms of idle time. Compile now starts immediately; neither path executes user programs. The editor remains available during compilation. Edits are coalesced rather than interrupting a compilation on every keystroke; stale successes and diagnostics are discarded, and only the latest revision becomes runnable. IME composition defers compilation until committed. Stop or changing packages cancels queued work; compilation has no execution deadline.

Evaluation is disabled whenever source differs from the compiled revision, during compilation, or after a failed compilation. There is no fallback to old definitions or the published executable. Previous output remains visible, marked stale; compilation diagnostics appear beside the source without replacing that output. Restore published source reinstates the original bundle and editable source. The editor/authorship modified color clears only when the current revision is compiled or the published source is restored.

Ordinary run errors retain usable compiled definitions in the live Worker. Evaluation is disabled while the session is busy, including while waiting for IO. Stop must precede replacement of an active attempt. Hard Stop or a fatal WASM failure loses that session's definitions, but preserves source. Compile now or another edit rebuilds them; Stop does not automatically restart work. Changing backend rebuilds a draft from its source. Sessions remain active while scrolling through the page. An elapsed compilation indicator and Stop remain available.

Settings → Download .arboricx returns the original bounded, hash-verified bytes for the selected version/hash. It verifies independently and leaves the visible result and its Copy/Download payload unchanged. Download/sharing and stored-definition output are disabled for drafts; restore published source first. Use Export compiled module to save a draft.

Draft source lives in the open page and survives compilation failure, Stop and backend changes. Reloading starts fresh from the published selection or selection link. Copy important source elsewhere before leaving.

Arguments, results, and IO

Evaluate tricu expression is the default input. Examples include identity (first 42 7) in Language reference and append "hello " "world" in the published base library. Expressions may use literals, applications, lambdas and local bindings. Their environment contains exactly the selected package's exports or current draft scope (local definitions plus explicit imports). Native modules import their prelude explicitly. Put import directives in the source editor. Read-only and source-free packages work identically: the compiler binds names to existing tree handles.

Expressions compile only when Evaluate is pressed, then run in the selected pure/IO mode. A fresh, collision-free temporary definition wraps one expression; its name is never installed in the package or draft. Diagnostics map wrapper coordinates back to expression lines/columns. The wrapped source shares the 16,384-scalar source bound, so layout/wrapper overhead counts toward that bound. Expression compilation and observation do not mutate the public definition table; stored-definition exports and original-bundle downloads always use the authenticated original bundle. Export compiled module is a separate action for the current draft, described below.

Settings → Use structured arguments retains the data-only input interface. Each field is one argument: 42, "hello", [1 2 3], t, or (t t t). No fields means no application; [] is one empty-list argument. Switching back to expressions renders the structured call as tricu source; it does not parse an arbitrary expression into argument fields. Result interpretation and pure/browser IO mode remain visible in the output toolbar. Export metadata does not declare arity/types; no signature inference occurs.

The structured argument editor accepts a data-only subset of tricu syntax: nonnegative decimal numbers, scalar strings with tricu escapes, whitespace-separated lists, and Leaf/Stem/Fork constructor expressions (group constructor children with parentheses). It does not evaluate names, lambdas, general applications, comments, or source programs, and does not download the compiler. Boolean trees can be written as t / (t t); byte lists use ordinary natural list syntax. Suggested bytes/Boolean inputs retain their original wire tags until edited, while edited literals produce the same canonical trees.

Settings → Structured argument encoding exposes the alternative typed JSON array editor for exact wire kinds and bounded ternary function trees, including trees too deep for the literal editor. Raw kinds remain string, natural decimal string, Boolean, bytes, recursively typed list, and ternary tree. Switching editors preserves valid values; invalid literal fields must be corrected before switching to raw. Partial and multiple applications are ordinary left-folded Tree Calculus application. The Basic Examples use expression input and the current result interpretations; historical package releases retain their original content.

Value interpretations include text, natural number, Boolean, bytes and scalar record. t can represent zero, false or an empty string/list; interpretation is explicit, not inferred. Former tree/list graph-JSON presets now select tricu tree source. The internal bounded graph previews remain available to conformance harnesses, not as a public editor output.

Tree interpretations are tricu · t source, △ source, DAG · shared △, and Ternary. Evaluation formats the actual applied result (or completed IO value), not its function definition. A complete, physical-sharing snapshot is bounded to 100,000 observed nodes; incomplete or cyclic snapshots are refused rather than emitted as complete files. Reduction needed to observe a constructor has no fuel/time ceiling and may diverge: Stop terminates the Worker. Expanded output has a 32 MiB byte bound and DAG has 128 MiB. Preflight rejects oversized expansion without constructing the exponentially large string. This complete-tree demand differs deliberately from the retired shallow JSON preview.

Pure mode never interprets an IO-shaped result as authority. Browser IO is an explicit profile for existing tricuIO, version 1, tags 0/1/2/10/11: pure, bind, abortIO, putStr, getLine. putStr resumes with t; getLine returns a canonical Unicode scalar list. Unsupported actions terminate the run. Abort reports its diagnostic handle without demanding arbitrary payloads. This narrower profile is not the full Haskell driver/error protocol. The bundle capability list must currently be empty, matching the existing format/profile; that list does not identify which exports produce IO trees.

Scalar record wire convention

The generic Scalar record result codec renders ordinary tree data as a JSON object. Wire value: a list of three-item lists [name tag value], with tag 0 for scalar text, 1 for a safe natural integer, 2 for a canonical Boolean. It is an explicit DTO convention. Names are unique, nonempty text (128 scalars); at most 32 fields, text values 4096 scalars, aggregate output 16,384 scalars. Numeric values must be <= JavaScript's safe-integer maximum. Malformed fields, duplicate names, unknown tags, and invalid scalar values fail closed. A null-prototype dictionary handles keys such as __proto__ as data. Tree source and DAG interpretations expose the actual representation.

Export stored definitions

Choose Output → Stored definition, select a definition and tree interpretation, then Show definition. It reads the authenticated stored tree without applying arguments, interpreting IO, loading a compiler, or normalizing that tree. Execution mode is disabled for this target. Local drafts cannot be silently substituted: choose Evaluated result or restore published source.

Stored definitions and evaluated results use the same output area, Copy and Download actions. The provenance label distinguishes them and identifies the bundle hash/name for stored definitions. Switching target clears the old output; changing interpretation marks it stale until the next explicit action. A saved payload retains its own format/filename and is never relabeled by changed controls. Original-bundle download and selection-link notifications stay in Settings rather than replacing the result payload. Errors disable copying/downloading a purported result, except that background compiler diagnostics preserve clearly marked prior output.

Formats:

Copy and Download contain the complete generated tree. A bounded preview shows at most 16,384 characters and explicitly identifies partial previews. Expanded formats have a 32 MiB UTF-8 output limit, preflighted over DAG records before unfolding; oversized expansions fail with a suggestion to use DAG. DAG output has a 128 MiB UTF-8 limit. Both traversal and writing are iterative, including deeply nested trees. Stop, replacement and page departure retain the Worker termination boundary; stored-definition output allows 120 active seconds. Clear output is an idle-only display action and does not cancel an active program. Writers flush bounded text-fragment batches so expanded output does not retain one array entry per logical node. Loading retains its independent 60-second deadline. These output limits remain in force independently of program execution.

Compile source

The editor downloads the authenticated reference compiler bundle when idle source compilation, Compile now, or expression evaluation needs it. It invokes source_unit.compileStableModuleDefinitions with an explicit environment. Native headers are interpreted by the same pinned compiler's source_module entrypoints. The packaged compiler uses stable lowering.

Scratch examples: identity, checked identity, pair constructor, two dependency-ordered definitions, a raw tricuIO line-input program, and Module imports. Compile produces a bounded definition selector; evaluate expressions against those definitions, or use the optional structured argument fields, with the same result/IO codecs as ordinary packages. The Worker retains compiled handles. Only the result spine and up to 16 local-definition names (128 scalars each) are decoded. Native public interfaces may select/reexport up to 4,096 names. Program normalization can remain deferred until application/inspection. Parser errors retain structured numeric codes, available line/column, and bounded expected labels/token kinds.

Native manifest imports

Choose Module imports under Scratch examples, or use the import snippet shown for a selected named package. The full module-manifest hash is the source reference, not the whole-bundle download hash:

!import 7f87549d587fa8c3be88af0ea988db3adcc3b35f1ffb48973553491b075614db as N
!export answer

answer = N.add 40 2

Both Haskell and Zig support qualified imports, !import HASH *, explicit local exports, !export *, and !export * from HASH, including header-only reexports. Native modules begin with an empty environment and explicitly import their prelude and contract helpers. Standalone scratch examples use their documented environments. A native module requires a nonempty public interface: !export * selects locals, while !export * from HASH selects a dependency's public interface. Lexical access to dependency bindings requires !import. Duplicate imports/exports and imported/local name collisions are errors.

Referenced published modules are fetched on demand. packages.json maps each manifest to a bundle pin; verification checks both the whole-file hash and native indexed-import manifest identity. Closed exports contain their transitive dependencies. Browser imports accept full manifest hashes available through the local collection or published index. IO execution uses the browser capability profile described above.

The runtime retains local and imported trees, including private definitions, for subsequent expressions. The public selector exposes declared exports. Interface selection retains native tree handles, deferring observation until execution or export. Each compile replaces the previous definitions and import scope; failure clears them. Successful runs and recoverable result/IO errors preserve that scope. Stop and backend replacement terminate it.

Source is limited to 16,384 scalars and 16 local definitions. Header names and qualifiers allow 128 scalars; the import scope and public interface each permit 4,096 bindings. Evaluation cost and browser memory depend on the program.

Local files and compiled module exports

Under Settings → Local modules, choose Open local module to open a .arboricx file locally. A separate Worker validates its bundle structure and manifest identity. The collection shows exported names, bundle SHA-256 and a copyable !import MANIFEST as Local statement. Use that statement in the source editor, declare your exports, and compile. Qualified imports, open imports and full reexports work as they do for published modules.

The collection lives in page memory. Files survive Stop, program changes and backend replacement; reloading or closing the page clears them. File validation has a Cancel file check button and a 60-second deadline.

Resolution checks the local collection first, then the published index. Local matches resolve entirely from their retained bytes; compiler/runtime assets load on demand. Each referenced bundle is verified against its requested manifest inside the runtime boundary. Corrupt local bytes fail compilation. Duplicate manifests reuse the existing entry, even when filenames or bundle hashes differ. Remove discards the file and rebuilds the current draft with its dependencies resolved again. A published copy of the same manifest can then satisfy the import.

Export compiled module downloads the successfully compiled draft's public interface. Download .arboricx saves the original published package. Open an exported file in Local modules to reuse it. The file names declared exports (or standalone unit definitions) and includes their reachable trees. Function representations, including reachable private values, remain inspectable. These closed executable bundles also work with the native CLI.

Export observes all public trees using pure reduction. It may compute or diverge; Stop terminates the Worker while preserving the local collection. Successful export preserves the compiled expression scope. Export requires a complete, acyclic shared snapshot within the size bounds. Canonical indexing preserves sharing across exports and combines structurally identical nodes.

Limits are 16 MiB per file, 32 local modules and 64 MiB total retained file bytes, with the existing bundle semantics/ABI, 100,000-node and 4,096-export bounds. Compiled export's 100,000-node snapshot includes its temporary export-list spine. Browser memory and computation costs also depend on decoding and evaluation.

Included contract environment

contracts.ts supplies 38 bare-name bindings from the same authenticated compiler bundle. Its allowlist includes the annotation lowerer's matchResult, withContract and err, direct checks/Result helpers, contract combinators and the pure contract-effect helpers. Exported function trees already contain their transitive dependencies; bindings reference their existing handles. id, pair, general base helpers, and capitalized type aliases remain available for user definitions. Contract names are ordinary source names and must be distinct from top-level definitions in this compiler profile.

The checked-identity example is id x =@nat? x, applied to natural 42. Both argument and result checks use id x@nat? =@nat? x; more guards cost more work, so larger examples can take longer and need more memory. Failed annotations return error Results; Tree source/DAG exposes the failure tag/payload. id =@nat? (x : x) checks the function tree, not its future output. That tree fails the canonical-natural contract, as do redundant high-zero representations. Contracts inspect tree structure at runtime.

The full binding list is in src/contracts.ts. Compiler and application share one heap; phase collection retains compiled handles. The independent Haskell captures cover ordinary source, annotations, failure propagation, and collection contracts.

All ten curated demos are native modules with explicit exports and manifest-pinned imports. Standalone profiles in environments.json supply authenticated compatibility/test inputs. Plain scratch programs retain the default contract environment described above.

Compiler input is limited to 16,384 Unicode scalars. Diagnostic labels are limited to 512 scalars, and local definitions to 16 names of 128 scalars each. Input size alone does not bound compilation work or memory. Long compilation is permitted. Compilation has no application node, work, frame or elapsed-time ceiling, but still has engine/OS limits. Stop terminates the Worker.

Each backend retains its lazy definition set inside its live runtime Worker. Successful runs and recoverable result-decoding/IO errors keep that set for reuse; IO continuations/results are cancelled after each finished attempt. Fatal errors and hard Worker termination discard it. Failed compilation clears the previous program.

Sessions and cancellation

The catalogue and selected package load on arrival. Hash selection links load the exact pinned package/export. Evaluate starts execution. The first source edit queues compilation; expression evaluation loads its compiler on demand. Explicitly selecting the compiler package simply loads that library. Work continues while using Settings or scrolling through the page. Source remains editable during compilation; active evaluation/IO locks editing until completion or Stop. Page departure, Stop or replacement terminates the owning Worker.

Compilation and program execution have no configurable node, evaluator-frame, work or active-time ceilings. The former optional-limit editors, settings and Worker request fields have been removed, rather than retained as hidden defaults. Garbage collection remains enabled. Stop and page departure terminate running Workers; actual browser/OS memory restrictions still apply. A divergent program can run until cancelled, and a large live result can exhaust browser memory.

Package loading retains a 60-second deadline covering Worker startup, transfer, verification and heap construction. Downloads and bounded definition exports also retain their independent deadlines; these are transport/format safeguards, not program execution budgets. Download, schema, literal, IO, preview and output bounds remain enabled. Native tools remain preferable for large builds.

Failures retain their underlying diagnostic with the operation named. Timeouts identify loading, download or definition export; compilation and runs do not have elapsed-time cutoffs. Manual Stop reports cancellation rather than an operation failure.

Ordinary package resource and safety boundaries

Download and format checks are independent of program execution. Every bundle must match its whole-file SHA-256. A bundle selected for both compiler and ordinary-package roles must satisfy the smaller download cap.

Output bounds apply independently of execution. No application setting promises to bound total browser memory; JS objects, parser buffers and GC add overhead. Published demos remain available without compilation. JS objects, decoded bundle records, hash buffers, queues, and GC have overhead. A 32-node DAG denoting 4,294,967,295 logical nodes loads/returns cheaply and is not rejected for its logical size. Actual programs may still traverse all occurrences, allocate huge results or diverge. Logical size must count repeated edges; physical sharing must never change intensional observations. Full textual expansion and the arboricx.tree-term.v1 digest still describe all logical occurrences. An indexed hash cannot be substituted under that flat domain.

Serve / deploy

nix build .#arboricxSite --out-link result-site
nix build .#arboricxSiteDeps --out-link result-site-deps
# Caller selects the persistent HTTP registry store, NOT a compiler CLI store:
result-site-deps/bin/arboricx-seed-site "${REGISTRY_STORE:?select an absolute persistent store path}"

arboricxSite contains index.html and a complete asset tree under assets/<release-sha256>/: CSS, bundled JS, both WASM runtimes and their support files, catalogue.json, packages.json, environments.json, and compiler.json. assets.json records each file's path, size and SHA-256. The release identity hashes that sorted manifest with the arboricx.site-assets.v1 domain. Changes anywhere in the tree produce a new namespace. HTML references that namespace; relative module imports, nested Workers and runtime fetches stay inside it. Registry bundle URLs retain their independent content hashes. A small nix-support reference retains its matching dependency pack in the Nix closure for rollback. arboricxSiteDeps contains the pinned demo/compiler/environment/library bundles and retained indexed releases, a share/arboricx-site-deps/dependencies.json manifest (whole-byte SHA-256, size, uses), and bin/arboricx-seed-site. Release building checks every dependency's actual bytes without recompiling programs or relabeling compiler fixtures.

All executable downloads use same-origin GET /_arboricx/bundle/hash/<sha256>, including curated examples and the compiler. There is no static bundles/ fallback. Metadata keeps pins, not local download paths. Downloads remain deferred: The selected package loads on arrival; the compiler loads for source compilation or expression evaluation. Serve .js as JavaScript. Relative static paths support subdirectory deployment; the registry route is origin-rooted. Workers/Web Crypto require HTTPS or trusted localhost; file:// is unsupported. No CDN, analytics, external font or runtime dependency is loaded.

Serve the site output with nginx and proxy /_arboricx/ to the existing registry. The registry still serves inline HTML at its /, not a static directory; remove an exact nginx = / proxy when serving the homepage as static files. Public GETs need no public publication access. Serve HTML and the root assets.json with Cache-Control: no-cache. Serve successful assets/<release-sha256>/ responses with Cache-Control: public, max-age=31536000, immutable; use no-store for missing/error responses. These headers are deployment configuration: the static Nix output alone cannot set HTTP headers. Keep immutable caching for successful registry hash responses too. Registry health is /_arboricx/health; a /health convenience route must rewrite to it. An ordinary static-only server can show the ecosystem but cannot run these demos or the compiler without the registry route.

Icons and link previews

The page provides a canonical homepage URL, generic Open Graph metadata, a 1200×630 PNG preview, SVG/PNG favicons and a 180×180 touch icon. Artwork uses the site's Leaf/Stem/Fork shapes and palette. The release places these images in the same immutable asset namespace as its runtime files and rewrites HTML references; the preview's absolute URL targets https://arbori.cx/. Alternate deployments should update that origin and the canonical/preview metadata together.

Edit images/icon.svg or images/preview.svg, then regenerate the PNGs using the pinned renderer and fonts:

artwork=$(nix build .#arboricxArtwork --no-link --print-out-paths)
cp "$artwork"/*.png ext/demo-site/images/

The site build verifies the checked-in PNGs against these generated assets. Footer feedback uses a direct email link.

Production JavaScript bundles

The Nix release typechecks TypeScript, verifies the checked-in development JS, and bundles it with the pinned esbuild. Seven ESM entrypoints preserve the runtime boundaries and the directory bases used by import.meta.url:

Each file includes its static dependencies, with tree shaking and readable output. Shared chunks are deliberately avoided across Worker boundaries. The default homepage loads four JS files; WASM binaries and authenticated compiler data retain separate downloads. Compiler initialization, the unused backend and local-file validation remain lazy. The complete bundled tree receives the release identity used for immutable caching.

Development keeps individual modules in dist/. nix-support/bundle-graphs.json records esbuild inputs/outputs for inspection; the release check verifies the entrypoint set and self-contained static imports. The loopback browser harness serves a separate test API facade from nix-support/probe-api.js for adapter and golden-value tests. That facade is excluded from the versioned production graph; page tests exercise the shipped bundles directly.

Measure a fresh homepage with production-like cache headers:

ARBORICX_SITE_ROOT=$(nix build .#arboricxSite --no-link --print-out-paths) \
TEST_COLD_LOAD=1 TEST_ASSET_CACHE=1 BENCHMARK_REPORT=/tmp/arboricx-cold.json \
  nix shell --inputs-from . nixpkgs#nodejs nixpkgs#chromium -c \
  node ext/demo-site/test/backend-browser.mjs unused

The report counts successful homepage/runtime requests at the server, excludes probe traffic, and records uncompressed response-body bytes and time to Ready. It checks that compiler assets, Zig and local validation stay lazy. Compare repeated fresh-profile runs; localhost timings describe harness overhead rather than WAN latency, and deployed gzip changes transferred bytes. Firefox uses the matching backend-firefox.mjs harness and nixpkgs#firefox.

Immutable asset caching

For nginx, add this map in the http context:

map $status $arboricx_asset_cache {
    default "no-store";
    200 "public, max-age=31536000, immutable";
    206 "public, max-age=31536000, immutable";
    304 "public, max-age=31536000, immutable";
}

In the static site's server, use its existing document root and add:

location ~ "/assets/[0-9a-f]{64}/" {
    try_files $uri =404;
    add_header Cache-Control $arboricx_asset_cache always;
}

This also matches subdirectory deployments. Ensure other location rules allow this handler to serve versioned assets. Keep HTML revalidation in the ordinary static handler. A fresh Worker then uses the browser HTTP cache for its complete JS dependency graph, even after demo changes, Stop or recompilation. Runtime heaps and module instances remain isolated per Worker.

Deploy the HTML and asset directory together. To support pages left open during a deployment, retain older asset directories alongside the current one for the chosen support window. Missing old releases return an error; they must not be rewritten to current assets or the HTML entrypoint. Reloading selects the current release. Keep matching registry bundles available as well.

The source checkout retains unversioned paths for development. Cache validation uses the built site with production-like headers and counts server requests:

ARBORICX_SITE_ROOT=$(nix build .#arboricxSite --no-link --print-out-paths) \
TEST_ASSET_CACHE=1 TEST_BACKEND=haskell-wasm \
  nix shell --inputs-from . nixpkgs#nodejs nixpkgs#chromium -c \
  node ext/demo-site/test/backend-browser.mjs unused

Repeat with TEST_BACKEND=zig-wasm. For Firefox, use nixpkgs#firefox and backend-firefox.mjs. The probe exercises actual demo switching, expression execution, recompilation and Stop, and verifies each code URL reaches the server once. The release check verifies every versioned file and deterministic identity.

Disk seeding and NixOS ordering

The HTTP store layout is objects/<first-three-hash-characters>/<sha256>.arboricx. It is distinct from the CLI's domain-separated module/export CAS. The seeder verifies every source and every existing target before adding missing files, serializes cooperating seeders with a store lock, and installs fsynced complete files with atomic no-clobber links. Matching files retain inode/mtime; corrupt or symlink objects fail rather than being overwritten. No mutable aliases need registration. The pack also seeds immutable packages/NAME/VERSION records after their bundle objects, using the same no-clobber checks. Conflicting existing versions fail before installing missing objects, preserving existing objects and releases. The target filesystem must support hard links; the store and its ancestor permissions are controlled by the operator. New directories are subject to the service umask; new bundle objects are read-only/world-readable.

Pin the tricu release as a NixOS flake input and use both outputs from that same input. A root oneshot arboricx-site-seed runs the seeder against the persistent host store with RequiresMountsFor, RemainAfterExit, and dependency-package restartTriggers. nginx and the OCI registry service require it and start after it, also restarting when dependencies change. nginx must restart rather than merely reload on a changed release so its ordering prerequisite is honored. Serving site assets before successful seeding risks transient missing pins. Rollback restores the paired site/dependency release; newer/older disk objects remain available to cached pages and shared selections. Keep the registry's persistent host-store mount at /app/store; don't put seeds beneath that image path because the mount hides them. Rebuild the native server from the current source to enable its exact-release GET endpoint. The browser uses the static index and pinned bundle routes; native CLI resolution uses those seeded release records.

For development or deployment checks, the opt-in Chromium harness provides a local registry with all demo/compiler/environment dependencies and deliberately denies static bundle paths. Production registry/container deployment and non-Chromium behavior are separate from those tests.

The hash is plain SHA-256 of whole bundle bytes, not a domain-separated CLI module-manifest/export CAS pin. Requests use the same-origin registry route, omit credentials, and reject redirects. A selection link uses the page fragment for bundle/export (and an optional curated entry ID); arrival loads metadata but never automatically executes. Literal inputs are not included in selection links.

Compiler provenance

The browser uses the authenticated compiler-library fixture. compiler.json records its whole-file hash and full publication commit; compiler-reference.v1 records source provenance. Tests authenticate the recorded snapshots and compare the shipped bytes with the repository fixture. This multi-export package differs from the single-entrypoint native builder in bootstrap/.

Preparation defaults to the shipped pin. An upgrade requires an explicit compiler bundle and full origin commit, verified against that commit's fixture and metadata before publication. Keep matching site and dependency outputs together for rollback.

Compiler promotion refresh

Complete this after every full compiler publication, before final retention cleanup. The native publication and this site's compiler pin are separate: ordinary preparation defaults to the historical site pin and does not upgrade it. Use the full commit that actually published the accepted compiler/support fixtures, not the earlier compiler-source commit.

Keep a matching prior site and dependency pack for rollback. In an exclusively owned local checkout, use the existing preparation commands in this order:

nix build .#
nix build .#inspectorOracle -o result-inspector-oracle
python3 test/shipped_compiler.py
python3 test/fixture_sources.py current
WORK=$(mktemp -d) # Outside the repository's inherited workspace mappings.
PUBLICATION_COMMIT=FULL_40_HEX_FIXTURE_PUBLICATION_COMMIT
VERSION=NEW_UNUSED_RELEASE_VERSION

python3 ext/demo-site/test/prepare-compiler.py result/bin/tricu \
  --compiler-bundle test/fixtures/compiler-library.arboricx \
  --reference-commit "$PUBLICATION_COMMIT"
python3 ext/demo-site/prepare.py result/bin/tricu --backend stable
python3 ext/demo-site/test/prepare-conformance.py result/bin/tricu
python3 ext/demo-site/test/prepare-contracts.py result/bin/tricu
python3 test/library_inspector.py result/bin/tricu \
  --oracle result-inspector-oracle/bin/inspector-oracle \
  --directory "$WORK/inspection" --version "$VERSION" --stage-site ext/demo-site
python3 ext/demo-site/prepare-showcase.py result/bin/tricu \
  --directory "$WORK/showcase" --version "$VERSION" --stage-site

Then run prepare-index.py with explicit --version NAME=VERSION arguments for changed non-native demos, as described below. Unchanged demos can retain their existing release. This step regenerates the practical environment against the new compiler. The inspector preparation refreshes foundation, file-IO and inspection libraries; the showcase preparation refreshes the other native packages and parser/lexer demos. Neither substitutes for the other. A changed native catalogue entry must advance through a fresh accepted version of its already indexed package; old releases remain immutable.

Run all checks in Build / regenerate / validate: JS parity, the bounded serial Node suite, independent Haskell/compiler and contract comparisons, and the bounded Chromium test. Update backend-sensitive expectations only against independently checked manifests/results. If a faster compiler no longer exercises a resource control, extend its specimen rather than weakening the control (for example, the greater-than-six-million-allocation GC check). Never regenerate expected results using the browser being tested.

Stage new bundle files so Git-flake builds can see them, then build and check:

git add ext/demo-site
nix build .#arboricxSite -o result-site
nix build .#arboricxSiteDeps -o result-site-deps
nix flake check

Inspect the new compiler pin, provenance, exact release versions, environments, source hashes and goldens together. Commit the refreshed assets and tests, retain validation evidence, and complete scratch retention. The site/dependency outputs must match; do not activate a site without its corresponding registry dependencies. These steps prepare and validate local release assets for deployment.

Shared browser code and JavaScript conformance evaluator

The JavaScript graph implementation below is retained for independent tests and shared literal/format helpers, not as a production program-execution backend.

In the test-only JS drivers, one run does not mutate shared constructor data. Only its own application thunks are memoized. No cross-run cache or runtime constructor hash-consing is implemented; completed package runs and discarded/replaced compiler sessions discard the entire Worker. Successful compiler runs retain definition roots for reuse. Rerunning verifies/loads a fresh package and resets the heap. Within a run, IO resumption never serializes/rebuilds that heap. Metadata loading does not force exports. Graph.apply returns a handle; it does not normalize or serialize it.

JavaScript graph collection and roots

The compiler enables iterative, non-forcing mark/sweep at reducer safe points, initially at 250k live cells with a survival-adjusted threshold. Constructor, application, and forwarding edges are traced, never normalized, duplicated or hash-consed. IDs are monotonic and never reused; invalid collected handles fail rather than silently aliasing new values. Sweeping rebuilds the backing Map, without changing surviving IDs or objects. Native JS GC controls when that old storage actually leaves browser memory.

During force, automatic collection protects current + continuation-frame roots and conservatively every handle existing at force entry. Constructor builders and codec-local intermediates therefore need no fragile hidden rooting rules. This conservative rooting retains some garbage across small observers to keep caller handles valid.

Graph.collect(roots) is an explicit lifetime boundary: list every handle needed afterward. Unreachable unlisted handles become invalid. It consumes fuel and commits the sweep only on success. Compilation lists all saved definition handles, allowing compiler/input/decoder garbage to go. Compiler IO explicitly owns its graph: runIO(..., true) traces current action + saved continuations between steps, and may invalidate unrelated caller handles. Its ordinary API keeps this off by default. No collector runs concurrently during a user pause. Package sessions enable tracing (initial threshold 100k) and own their IO graph, retaining application/continuation roots only. Compiler tracing starts at 250k; compiler IO also lists all saved definitions as additional retained roots.

Stats separate live cells (nodes), cumulative cell allocations (allocations), peak live cells, collections, reclaimed cells, and collector work. Both browser runners use unbounded application budgets and rely on Stop/Worker termination and actual browser limits. Finite graph budgets remain available to reference tests, not as runtime settings. These are graph-cell metrics, not total JavaScript object/heap byte measurements. Large live results can exhaust memory; constructor batches can allocate before another reducer safe point.

The TEST_BASIC_EXAMPLES=1 browser suite checks the beginner arrival, selector categories and identity deduplication, all six initial examples and edit exercises, restoration, every Language reference preset, source editing, and a retained historical share link. Run it for each backend using the Chromium, Firefox or Linux WebKit harness against a built site.

Application preparation

All sixteen current catalogue sources in demos/ are editable native modules. The six Basic Examples teach one concept at a time; Language reference supplies an editable syntax sampler. Conversation imports IO; RGB imports canonical-byte validation. Tree microscope keeps the structural traversal visible but imports successor/Boolean operations. Business examples import base arithmetic and, where needed, Arboricx's Unicode scalar-text validator. Their small record literals describe the demo output schema directly. Parser and lexer import the complete prelude/parser/lexer interfaces.

prepare.py compiles the core sources and lessons through the authenticated shipped native compiler, importing their exact dependency bundles into a temporary indexed store. tutorials.py holds lesson settings and independent expectations. The retired intro.tri is still checked as a historical runtime/codec fixture; it is excluded from the active catalogue. It checks public interfaces, independent application/IO goldens, malformed values and nonforcing demand probes before updating assets. prepare-index.py uses practical-support.tri to reproduce the standalone test profile.

To refresh the parser/lexer demo releases:

python3 ext/demo-site/prepare-showcase.py result/bin/tricu \
  --directory /tmp/arboricx-demo-refresh --demos-only \
  --version NEW_UNUSED_VERSION --stage-site

This mode still authenticates compiler inputs, dependencies, native results and manifest identities. Source-only changes to these catalogue entries also require an explicit new accepted release. Keep historical named versions immutable. The browser module matrix recompiles all ten demos on both backends and compares every result/terminal transcript with native captures, then edits and restores the parser/lexer examples through the real page.

Complete library publication

prepare-libraries.py publishes one complete public interface for every lib/**/*.tri source under tricu-lib-MODULE, including compiler helpers, experiments, and libraries requiring native host capabilities. It derives coverage from the source tree. Fixture-backed modules retain every authenticated public root; other library modules compile against those exact interfaces with an explicitly pinned stable compiler.

nix build .#
python3 ext/demo-site/prepare-libraries.py result/bin/tricu \
  --version 0.1.0 --stage-site

Omit --stage-site for validation without changing site assets. Reusing a version requires identical executable and manifest identities. Changed libraries require a fresh version; existing releases remain immutable and indexed. Preparation checks current fixture sources, transport limits, native publication, and an independent indexed import before staging the complete batch. Temporary stores are removed on exit. The ordinary site dependency pack includes these releases without additional seeder configuration.

Each release records two distinct pins: the SHA-256 of transport bytes and the native module-manifest identity. module-identity.ts reproduces native indexed import identity without unfolding executable trees; module-resolver.ts checks both identities when fetching an explicitly indexed manifest. The module tests compare these calculations with native-published library manifests and check full source/interface coverage and corrupt or mismatched transport rejection. Use the published manifest pins with the native source import syntax above.

Build / regenerate / validate

Tooling comes from the repository's locked nixpkgs:

./ext/demo-site/build.sh             # strict TS → dist/*.js (TypeScript 5.9.3)
./ext/demo-site/build.sh --check     # fresh temporary compile; exact shipped-JS parity
nix shell --inputs-from . nixpkgs#nodejs -c python3 ext/demo-site/test/run-bounded.py \
  --seconds 60 -- node --max-old-space-size=512 --test --test-concurrency=1 ext/demo-site/test/*.test.mjs
# Independent native CLI oracle for browser module semantics:
python3 ext/demo-site/test/native-modules.py result/bin/tricu
# Actual Runtime-selector integration on the packaged site (Chromium + Firefox)
nix build .#arboricxSite --no-link
SITE=$(nix path-info .#arboricxSite)
ARBORICX_SITE_ROOT="$SITE" nix shell --inputs-from . nixpkgs#nodejs nixpkgs#chromium -c \
  node ext/demo-site/test/backend-browser.mjs "$SITE/runtime/haskell" chromium
ARBORICX_SITE_ROOT="$SITE" nix shell --inputs-from . nixpkgs#nodejs nixpkgs#firefox -c \
  node ext/demo-site/test/backend-firefox.mjs "$SITE/runtime/haskell" firefox
# Same real-page suite for Zig, plus full independent golden/embedding checks:
TEST_BACKEND=zig-wasm ARBORICX_SITE_ROOT="$SITE" nix shell --inputs-from . nixpkgs#nodejs nixpkgs#chromium -c \
  node ext/demo-site/test/backend-browser.mjs "$SITE/runtime/haskell" chromium
TEST_BACKEND=zig-wasm ARBORICX_SITE_ROOT="$SITE" nix shell --inputs-from . nixpkgs#nodejs nixpkgs#firefox -c \
  node ext/demo-site/test/backend-firefox.mjs "$SITE/runtime/haskell" firefox
# Manifest-import integration, both production backends and both engines:
for BACKEND in haskell-wasm zig-wasm; do
  TEST_MODULES=1 TEST_BACKEND="$BACKEND" ARBORICX_SITE_ROOT="$SITE" \
    nix shell --inputs-from . nixpkgs#nodejs nixpkgs#chromium -c \
    node ext/demo-site/test/backend-browser.mjs "$SITE/runtime/haskell" chromium
  TEST_MODULES=1 TEST_BACKEND="$BACKEND" ARBORICX_SITE_ROOT="$SITE" \
    nix shell --inputs-from . nixpkgs#nodejs nixpkgs#firefox -c \
    node ext/demo-site/test/backend-firefox.mjs "$SITE/runtime/haskell" firefox
done
# Local file/export round trip, both backends (also use backend-firefox.mjs):
for BACKEND in haskell-wasm zig-wasm; do
  TEST_LOCAL_MODULES=1 TEST_BACKEND="$BACKEND" ARBORICX_SITE_ROOT="$SITE" \
    BENCHMARK_REPORT="/tmp/local-$BACKEND.json" \
    nix shell --inputs-from . nixpkgs#nodejs nixpkgs#chromium -c \
    node ext/demo-site/test/backend-browser.mjs "$SITE/runtime/haskell" chromium
done
# Native import/run + independent compilation must agree with browser exports:
python3 ext/demo-site/test/local-module-native.py result/bin/tricu \
  /tmp/local-haskell-wasm.json /tmp/local-zig-wasm.json
# Heavy compiler comparisons: explicit opt-in, not included in *.test.mjs
nix shell --inputs-from . nixpkgs#nodejs -c node --max-old-space-size=768 ext/demo-site/test/compiler-conformance.mjs
nix shell --inputs-from . nixpkgs#nodejs -c node --max-old-space-size=768 ext/demo-site/test/contracts-conformance.mjs
ARBORICX_SITE_ROOT="$SITE" nix shell --inputs-from . nixpkgs#nodejs nixpkgs#chromium -c python3 ext/demo-site/test/run-bounded.py \
  --seconds 300 -- node --max-old-space-size=512 ext/demo-site/test/browser.mjs chromium
# Optional screenshot prefix as a final argument:
# node ext/demo-site/test/browser.mjs chromium /tmp/arboricx-editor
# Writes -390.png and -1440.png full-page views.
# Optional deterministic cost screen against a caller-selected retained prior site:
nix shell --inputs-from . nixpkgs#nodejs -c node --max-old-space-size=768 \
  ext/demo-site/test/demo-cost.mjs "${REFERENCE_SITE:?select a retained prior site directory}"
nix flake check

Generated development JS is committed and covered by the explicit parity check. Nix builds the bundled release. Graph, package and browser checks have explicit validation commands. Chromium CDP tests require Node with global WebSocket support and use a temporary profile plus --no-sandbox for container compatibility; only run them against this trusted local fixture site. Browser tests exercise a local registry.

The backend-selector suites serve the actual packaged page at a nested URL, with a local authenticated registry and packaged runtime assets. They check published programs, IO, stored export, default/empty/practical compilation, repeated retained runs, output-error recovery, missing-runtime and compiler-pin failure without fallback, Stop/recompile (including non-returning reduction), stepping, stale/wrong-kind/invalid-line resume rejection and backend-switch isolation. Invalid responses do not consume a pending request ID. Both WASM backends pass the integration suites in Chromium 148, Firefox 150 and Linux WebKit. The Haskell embedding's independent 117-application parity suite remains in ext/haskell-wasm; it is distinct from these real-page integration checks. The Zig extension of the page suite also checks all 117 complete published Haskell result/terminal goldens, all eight source/environment profiles, seven codecs and negative controls, physical sharing, nonforcing divergent preview and abort payloads, IO limits and GC across input waits, compile/host-error recovery, and 50 retained runs with stable owned-handle counts and linear-memory plateau. Complete bounded observation supplies tree-file result interpretations and conformance tests. Scalar interpretations use their bounded codecs. Parity checks exercise the common site capabilities through each backend's private protocol.

Matched production-backend benchmark

These measurements predate live compilation and expression input. They measure explicit compilation and structured-argument Worker calls.

Use a built site. The harness alternates backend order across three fresh browser profiles per backend/engine, loads all eight source demos as published packages, then compiles each in a fresh production Worker. Each compilation is followed by 21 retained applications; the first is recorded separately from the 20 warm applications. Complete selected-codec results and IO terminal text are checked against independent Haskell captures. Only the first captured input per demo is timed; the broader parity suite remains separate.

nix build .#arboricxSite --no-link
SITE=$(nix path-info .#arboricxSite)
nix shell --inputs-from . nixpkgs#nodejs nixpkgs#chromium nixpkgs#firefox -c \
  node ext/demo-site/test/benchmark-backends.mjs "$SITE" /tmp/tricu-backend-timing
nix shell --inputs-from . nixpkgs#nodejs -c \
  node ext/demo-site/test/summarize-backends.mjs /tmp/tricu-backend-timing/report.json
# Separate memory pass: idle/live holds permit PSS/private-page sampling.
# Its timings are intentionally not used as performance measurements.
BENCHMARK_ROUNDS=1 BENCHMARK_PSS=1 nix shell --inputs-from . nixpkgs#nodejs nixpkgs#chromium nixpkgs#firefox -c \
  node ext/demo-site/test/benchmark-backends.mjs "$SITE" /tmp/tricu-backend-memory

Reports include platform/browser identity, runtime hashes, phase timings, actual linear-memory size, and sampled browser-process-tree RSS. RSS sums double-count shared pages; they are not private memory. The separate memory pass samples smaps_rollup PSS (shared pages apportioned) and private pages during baseline, retained-session holds and final idle. Measurements require readable Linux /proc; PID-namespace translation is handled explicitly. Sampling is not a hard peak-memory guarantee. PSS can also depend on other processes sharing libraries. No forced GC or runtime policy changes are used. Browser startup/background work and code caching can affect the baseline, so absolute RSS is not attributed to the evaluator alone.

A matched desktop run on Ryzen AI MAX+ 395, Linux, Chromium 148 and Firefox 150 used Zig reactor 11fb47689911066d0691419a5bd8d039ebbaf8f8524566b8e7fbfb06f3b327bb and Haskell reactor ddefcf48270c5be630ce606bc7f96788388ef794f0b3712a827b8c6701af1575. Median first-package readiness was Zig/Haskell 25/47 ms in Chromium and 22/40 ms in Firefox. Shipping-demo compilation including initialization and environment loading was 1122/1864 ms in Chromium and 1193/1293 ms in Firefox; warm compiled applications were 44/2.8 ms and 47/2 ms respectively. These production-adapter results are not the earlier isolated reducer/compiler microbenchmarks: Zig was not universally faster, particularly for repeated compiled applications.

Across the timed workloads Zig package sessions used 8.75–16.63 MiB linear memory and compiled sessions 76.56–76.69 MiB; Haskell used 9.06–9.13 MiB and 12.13–23.88 MiB respectively. Neither grew between the first and twenty-first retained application. A separate one-profile-per-backend/engine memory pass observed Zig's retained compiled sessions approximately 23–32 MiB higher in browser-tree PSS than Haskell in Chromium and 13–47 MiB higher in Firefox, comparing corresponding demo holds. These are bounded desktop observations, not mobile capacity guarantees or memory thresholds. Larger programs and different inputs can cost more.

All HTTP is localhost, uncompressed and no-store; subsequent Workers may benefit from engine code caches. These figures do not measure Internet cold-download latency, mobile battery use or isolated reducer throughput. The benchmark does not alter the site's default backend or impose execution deadlines on users.

test/native-modules.py independently checks native CLI import/export behavior, including header-only reexports, collisions and lexical scope. It also checks complete canonical function-tree digests in test/module-goldens.json; the module browser probe compares both runtimes against those digests, not just application results. Intentional compiler changes require an independently reviewed native recapture (--capture PATH), never expectations generated by the browser.

The bounded runner uses a Linux per-process writable-data limit, an aggregate process-group RSS watchdog and a whole-job deadline. The watchdog is sampled, not a cgroup guarantee. Run Node tests serially. Binary artifact checks compare hashes rather than expanding giant assertion diffs. The site compiler is verified against its retained publication metadata and source snapshots, not assumed equal to the repository's latest compiler fixture.

To refresh named releases and prebaked environments after preparing demos:

python3 ext/demo-site/prepare-index.py result/bin/tricu
# Changed executable bytes require an explicit new version:
python3 ext/demo-site/prepare-index.py result/bin/tricu --version shipping=0.2.0

Old index entries and bundles are retained. release.py includes all indexed releases in the dependency pack, so both current and historical selections can be seeded before site activation. Publication metadata is separate from source and runnable presets. These commands prepare release assets locally.

Long full-source draft comparisons are explicitly opt-in (optional final argument selects a single demo). They use independent Haskell result captures, keep the finite reference-harness memory guard, and can fail at that guard on large examples:

nix shell --inputs-from . nixpkgs#nodejs -c python3 ext/demo-site/test/run-bounded.py \
  --seconds 600 -- node --max-old-space-size=1024 ext/demo-site/test/editable-conformance.mjs language-reference

To genuinely recompile curated packages / Haskell goldens:

nix build .#
python3 test/fixture_sources.py verify
python3 ext/demo-site/prepare.py result/bin/tricu --backend stable
python3 ext/demo-site/test/prepare-conformance.py result/bin/tricu
# Republish exact reference compiler bytes/descriptor and independent compiler goldens:
python3 ext/demo-site/test/prepare-compiler.py result/bin/tricu
# Upgrade explicitly from a committed authenticated publication, then regenerate:
python3 ext/demo-site/test/prepare-compiler.py result/bin/tricu \
  --compiler-bundle test/fixtures/compiler-library.arboricx \
  --reference-commit "${PUBLICATION_COMMIT:?select the full fixture publication commit}"
python3 ext/demo-site/prepare.py result/bin/tricu --backend stable
# Capture default contract-environment applications independently under Haskell:
python3 ext/demo-site/test/prepare-contracts.py result/bin/tricu

Demo preparation uses the authenticated shipped native compiler and fresh indexed stores. Native headers resolve the libraries pinned in the authored sources. The lower source_unit.compileStableModuleDefinitions API still compiles isolated demand probes against published demo exports. Contract captures assemble their selected base bindings independently. Selected applications execute under Haskell; IO uses its driver. Bundle/source hashes, native and unit compiler identities, host identity and captured results are recorded in demos.json and the catalogue. Compiler and library fixtures/fingerprints stay unchanged.

test/prepare-compiler.py captures compiled applications and parser/resolver diagnostics with the Haskell host. The conformance runner checks exact compiled trees, results, and diagnostics; the cheap compiler.test.mjs tests pin/schema/ decoder behavior without executing the compiler.

The independent Haskell corpus covers small normal-tree applications, discarded divergence, partial/multiple applications, demo policies, canonical codecs, and IO. Chromium exercises streamed registry bytes, malformed/hash/oversize controls, shared DAGs, cancellation, modes, literals, repeated runs, source-free export, Unicode IO, input/step pauses, reset/replacement, safe text rendering, selection links, Worker termination, and narrow layouts.

Sources use the repository ISC license. No third-party evaluator/runtime code is vendored.

Single-page editor regression checks

The actual-page probe covers all eight demo application goldens as expressions, source-free library calls, local bindings, temporary-name isolation, diagnostics, IO, unused divergent definitions and hard Stop. Controlled late Worker replies exercise edit coalescing and stale success/error rejection; checks also cover IME composition, previous-output retention, package changes and backend rebuilds. The suite also checks stored/result provenance, all four tree representations, IO result serialization, complete copy/download behind a bounded visible preview, expansion refusal, immutable bundle download, pin failures/retries and viewport edge/focus behavior using the packaged compiler and native evaluators. The former tab-oriented UI expectations were retired; lower-level codec, package, security and runtime conformance controls remain.

SITE=$(nix build .#arboricxSite --no-link --print-out-paths)
TEST_IDE=1 ARBORICX_SITE_ROOT="$SITE" nix shell --inputs-from . nixpkgs#nodejs nixpkgs#chromium -c \
  node ext/demo-site/test/backend-browser.mjs "$SITE/runtime/haskell" chromium
# TEST_BACKEND=zig-wasm selects the other production runtime.
# Use backend-firefox.mjs and nixpkgs#firefox for Firefox.