tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

README.md (111838B)


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 is source-first. Clicking an underlined definition name opens its compiled value inside the editor; write applications as source definitions rather than in a separate evaluator. Auto recognizes supported IO envelopes before trying value interpretations. IO cards offer an explicit Run IO, inline terminal/input, Stop, and secondary stepping/trace/return-format controls. Recognition never executes actions. Copy/Download in the card retain complete results behind the bounded preview. Source-free packages expose searchable stored definitions, without generating source or applications.

The sticky toolbar keeps Program, Libraries, Settings, Restart, status and Stop while work is active or queued. Settings dock on desktop and become a dismissible modal sheet on mobile, with Escape, focus containment/return and an inert background. The mobile source editor reaches both viewport edges; padding is inside the editor. Cards stay inside the source viewport and scroll their own contents. Definition and module cards share compact labeled headers and quiet actions; module cards and Libraries use two-column export lists with wrapping names. Input is focused only after an explicit IO run requests a line. The footer credits Copyright 2026 James Eversole.

Presentation checks for editor cards and library browsing use fixture metadata and inspection replies, with desktop/mobile viewport and export-label bounds checks:

nix shell nixpkgs#nodejs nixpkgs#chromium -c node ext/demo-site/test/editor-amenities.mjs

Set SCREENSHOTS=/tmp/editor-amenities to save the light/dark screenshots.

Color scheme

The site follows the browser/system light or dark preference, including live changes without reloading until a preference is explicitly chosen. The header’s sun/moon button switches modes and remembers the choice in local storage; if storage is blocked, it still works for the current page. Clearing that stored preference restores system following. The choice is applied before the first paint, independently of the playground runtime. Controls, output, dialogs follow the scheme; the source editor keeps its dark surface in both. Inline cards and their native controls stay dark too; the surrounding controls and supported browser chrome follow the page scheme. Forced-colors mode retains native editor text.

Run the presentation-only browser regression (no runtime assets needed):

nix shell nixpkgs#nodejs nixpkgs#chromium -c node ext/demo-site/test/theme.mjs

It checks light → dark → light changes, text contrast, browser theme metadata, source caret colors, keyboard toggling, persistence, mobile header layout, blocked storage and the forced-colors fallback.

Syntax colors and bracket matching

Editable and reference source 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. Enter inserts a plain newline, preserving explicit indentation. Highlighting updates as the document and selection change.

CodeMirror 6 owns the canonical document, selection and undo history. The local syntax.ts lexer supplies highlighting and declaration sites; CodeMirror renders source as text. Document loads reset history; import insertion is one isolated undoable transaction. Native input and clipboard events use canonical UTF-16 document offsets. Sources over 32,768 UTF-16 code units use plain highlighting. Drafts retain their complete text, and compilation enforces the 16,384-scalar input limit with diagnostics.

Import hashes display their first three and last three characters with literal dots (abc...xyz), in muted teal with a dotted underline. Editing an import line reveals its full hash. Only cursor lines (the head of each selection) expand, including when focus moves outside the editor. Clicking a folded hash places the cursor at its end. Clicking an expanded hash opens an inline, searchable list of the exact module’s definitions, using the same library index and export rendering as library search. Local modules use their registered exports. Copying, saving and compilation use complete canonical source; folding and selection preserve the document and undo history.

The pinned editor sources, fixed-output hashes, licenses and declaration build are in codemirror/. Nix builds and bundles the editor locally. Physical mobile Safari, real IME and screen-reader behavior require device/manual validation.

Runtime

The playground uses the canonical Haskell evaluator compiled to WebAssembly. Stop terminates the owning Worker tree. Compiling again starts a fresh runtime from the page-owned source and local modules. Missing or invalid runtime assets produce an error.

The independent JavaScript graph, codec and compiler conformance tools live under src/conformance/ (generated into dist/conformance/) and are excluded from the production release. Shared JavaScript authentication, literal codecs and tree formatting support the Haskell runtime.

The release packages the pinned Haskell reactor, loader, WASI shim and authenticated compiler kit under runtime/haskell/. Compilation loads the kit on demand and retains compiled definitions. Published inspection and named IO need no compiler. Draft compilation authenticates compiler.json and resolves explicit native imports or the selected standalone environment. Missing assets and mismatched pins are errors.

runtime-session.ts defines the coarse load/compile/inspection/IO boundary. haskell-session.ts maps the Haskell client's protocol, including its nested Worker and asynchronous resume tokens. Private pointers, IO tokens and heap checkpoints remain inside that session. Cleanup terminates the Worker tree; there is no default compile/run timer.

Diagnostics report WASM linear memory. The Haskell RTS limits are 256 MiB heap and 8 MiB Haskell stack; the reactor caps linear memory at 512 MiB. Exhaustion or a fatal trap discards the session and requires explicit reinitialization. WebAssembly SIMD is required (Chrome 91+, Firefox 89+, Safari/iOS 16.4+). Integration tooling covers Chromium, Firefox and Linux WebKit.

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_MODULES=1, TEST_LOCAL_MODULES=1, or TEST_ASSET_CACHE=1 in place of TEST_IDE=1. The runner uses Playwright 1.59.1 / WebKit build 2272 and also checks layout at 320, 390 and 1280 pixels after editor viewport measurements settle. For longer IDE runs, set TEST_WEBKIT_TIMEOUT_MS=360000 (default: 240000). TEST_DIAGNOSTICS=1 reports read-only page status every 30 seconds. 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

Explore libraries… in the toolbar 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 isolated editor 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, the definition name to click, card interpretation/IO instructions 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 select their version (default 0.1.0). A name@version entry supplies release-specific conventions; reference-input/examples.json pins explicit module versions and independent expected results. A {{name@version}} placeholder selects that exact release; {{name}} selects version 0.11.0. The browser matrix compiles and runs these examples on the Haskell runtime. 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 using absolute https://arbori.cx/ URLs 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 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:

Lessons introduce syntax in source comments and invite small edits. Click or tap underlined definition names to inspect their values. To try a function call, add a named result definition; edits compile after a pause. The basic lessons include named results alongside their functions and use open imports from base for arithmetic and list operations. 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. The result tree contains 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. Result trees contain 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 inspect the corresponding source binding 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 result trees. 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 named roots to inspection and stored-definition export. Available reference source is displayed alongside them. Editable demos open in the source editor. Until source changes, inspection uses authenticated package roots. Source-free/read-only packages have a stored-definition search; library imports are already available through the explorer and need no generated experiment bindings. 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.

Inspection and Run IO are 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. Old inline results and IO recognition are invalidated immediately; compilation diagnostics appear beside the source. 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. Other inspection/execution 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. A stopped observation requires another click on its definition name after rebuilding. Published sessions offer Reload package, which retries the same selection without a full page reload; a failed first load must not turn an editable demo into a source-free package. After Stop, compiling a draft rebuilds its runtime from 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 without changing the selected version/hash. It discards the active runtime; Reload package restores inspection afterward. 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.

Source bindings, results, and IO

There is no page expression prompt, guided-argument editor, or pure/IO switch. Write an application as a source binding, then click its underlined name to inspect it. Explicit imports and local definitions form that binding's environment. No temporary bindings are generated by the library picker. The runtime-level run/runExpression helpers and typed literal validation remain for conformance/embedding; they are not an alternate page UI.

Auto first calls probeIO(name) on the retained binding. The Haskell reactor checks only the root pair, the seven-character tricuIO sentinel and version 2. It leaves action payloads and continuations undemanded. An unsupported version or non-envelope continues through ordinary value interpretation. This can require unbounded reduction, so Stop terminates the Worker if it diverges.

A recognized envelope exposes explicit Run IO. runDefinitionIO starts from that same locally compiled/private or published binding with no arguments and no expression compilation. IO events, line input and Stop live in the card. Details holds trace, pause/Next, and returned-value formatting for the next run. Opening, compiling, detecting, changing formats or selecting a program never executes IO. Closing an active IO card cancels its owning Worker. Source revisions invalidate recognition and saved output; a stopped observation stays suspended across recompilation until an explicit retry rather than immediately diverging again.

Value interpretations include text, natural number, Boolean, bytes and a bounded list of text/natural values. 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.

Inspection never interprets an IO-shaped result as execution authority. The current Conversation demo uses the structured IO terminal profile described below. Current adapters execute only IO protocol 2. Historical v1 bundles remain inspectable data; they require retained runtimes for execution. No JavaScript reference interpreter supplies IO compatibility. The bundle capability list must currently be empty, matching the existing format/profile; that list does not identify which exports produce IO trees.

Export stored definitions

Use Settings → Published package → Stored definition export. Select a stored name and format, then Download stored definition. This reads authenticated original DAG records without applying arguments, interpreting IO, loading a compiler, or reducing the tree. It is unavailable for drafts; use inline inspection or Export compiled module for locally compiled definitions.

Inline Copy/Download use the complete observed result, not necessarily original stored syntax. Their payload retains its own format/filename and is not relabeled by changed controls. Errors and source changes clear it rather than offering a partial or stale file. Original-bundle download and selection-link notifications remain in Settings. Source-free packages retain direct inspection and original stored-record export without an expression interface.

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 or Compile now 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. Inspect their definitions in source; applications also belong in source. The Worker retains compiled handles. Only the result spine and up to 4,096 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

Haskell supports 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 4,096 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 4,096 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. Clicking a definition name starts inspection; only Run IO starts host IO. The first source edit queues compilation and 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. IO Stop drains its structured session; page departure, hard Stop of pure work, 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. Pure Stop and page departure terminate running Workers; v2 Stop waits for structured settlement; 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. 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. Five 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 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

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
python3 ext/demo-site/prepare-libraries.py result/bin/tricu \
  --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. The reference graph is pure-only; it is not an IO executor. Native browser adapters retain their own structured IO session roots, including pending work and completed results until settlement/decoding. Reference compiler tracing starts at 250k cells; package reference tracing starts at 100k. These reference settings are not native runtime memory limits.

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 the published IO and text modules, checks read/write Results, and distinguishes an empty line from EOF. Send appends LF; End input returns an empty name with an end-of-input message. Structured Stop retires the prompt and permits rerunning without recompilation. 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 the Haskell runtime 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 and uses declared tricu.workspace aliases before filename derivation for non-fixture modules. Fixture/workspace disagreements and alias collisions fail before publication. 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             # pinned Nix editor bundle + strict TS → dist/*.js
./ext/demo-site/build.sh --check     # exact generated-JS and license 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
# Manifest-import integration, the production runtime and both engines:
for BACKEND in haskell-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 (also use backend-firefox.mjs):
for BACKEND in haskell-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
# 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 runtime-restart isolation. Invalid responses do not consume a pending request ID. The Haskell WASM runtime passes 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. Complete bounded observation supplies tree-file result interpretations and conformance tests. Scalar interpretations use their bounded codecs.

Production-runtime benchmark

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

Use a built site. The harness runs three fresh browser profiles per 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.

Historical paired-runtime observations

The following retained observations used matched alternating-backend runs before retirement of the alternate runtime. Their artifact hashes and values are historical; the current harness measures Haskell only.

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 the Haskell runtime 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 source-first actual-page probes cover ordinary definition inspection, complete Copy/Download behind bounded previews, all tree formats, expansion refusal, immutable bundle download and non-evaluating stored export. Delayed native Worker replies exercise coalescing and stale success/error rejection, IME, source invalidation, Stop, and backend rebuilding. The module probe retains all sixteen complete independent demo goldens, header/import/security controls and parser/lexer edit/restore checks. The IO probe covers envelope-only detection with a divergent action, explicit execution, private roots, read/write/abort, stepping, malformed input/resume controls, cancellation and revision invalidation. Former expression- and structured-argument UI expectations are retired; lower-level literal, 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
# Use backend-firefox.mjs and nixpkgs#firefox for Firefox.

Use TEST_DEFINITION_NAMES=1 for declaration-name inspection, editing/focus, Unicode offsets, invalidation, IME lifecycle, and wrapped inline hit areas. Chromium also sends trusted mouse/touch and Tab/Enter/Space/Escape events, verifies native rename/undo and cross-name drag selection, touch-hold selection and panning cancellation at 390/1440 pixels. TEST_SCREENSHOT_PREFIX saves closed/open card screenshots at those widths. Firefox runs the shared compiled-definition and DOM/geometry checks; it does not run the CDP input checks. Physical mobile Safari keyboard/selection-handle behavior is not covered by these automated checks.

Use TEST_IMPORT_FOLDS=1 for hash recognition, independent toggles, per-line/range expansion, canonical source/revision invariance, and dynamic import undo/redo. Chromium additionally checks trusted mouse/touch caret placement, partial/full system clipboard contents, native pasted-import undo/redo, single-character hash deletion, plain Enter, and read-only editing at 390/1440 pixels.

The focused inline IO suite can be run with TEST_INLINE_IO=1 in that harness. It includes source-free IO, private entries, envelope-only recognition of a program with a divergent action, explicit Run, CPU-bound cancellation, closing an active card, recompilation/retry, and source-revision invalidation. Use TEST_SCHEME=light or dark for an explicit page scheme. Screenshot captures with TEST_SCREENSHOT_PREFIX check IO card bounds at 390 and 1440 pixels.

For longer IDE suites, TEST_CDP_TIMEOUT_MS=300000 and run-bounded.py --seconds 330 select explicit larger diagnostic budgets. TEST_DIAGNOSTICS=1 logs phases and read-only page status every 30 seconds.

Inline definition inspection and library discovery

Underlined top-level declaration names toggle an anchored compiled-definition card. Names are interactive only for the current successfully compiled scope (public roots for published packages). References and local binders stay ordinary editable text. Tab then Enter/Space also opens the card. Edit name closes it and selects the identifier in the canonical editor for renaming; read-only source disables that action. A touch/pen hold selects the identifier in the native editor without inspecting it (also available through Edit name); selection drags and native context menus do not trigger inspection. IME composition disables name activation. Names are inline editor decorations, so wrapped fragments use their native text hit areas. Opening starts with Auto, checking the supported IO envelope, then preferring readable text, a natural number, a decoded list of text/naturals, a Boolean, bytes, then tricu tree source. The status names the chosen interpretation (for example Auto → Text). An empty tree defaults to natural zero. Readable text allows Unicode letters/marks/numbers/punctuation/symbols/spaces, TAB/LF/CR, and joining characters, but excludes other controls, private-use and unassigned scalars. Manual Text still accepts any valid Unicode scalar string. A manual selection remains selected while the open card refreshes after compilation; reopening uses Auto again. The inspector offers List · text / numbers: at most 4,096 elements, each read as nonempty readable text or a natural, preserving up to 4,096 natural bits without JS number conversion. Empty elements become zero. The existing text limit is 4,096 scalars per decoded string. Unsupported list items fail rather than showing a partial list.

The interpretation selector observes the existing binding directly; it never compiles an expression, applies arguments, or runs the IO interpreter. Both runtime adapters provide inspectDefinition, including private bindings in locally compiled module scope. Published bundles contain only public roots. Observation can reduce or diverge, and Stop terminates the owning Worker. Decoder failures preserve the scope; fatal failures discard it. Source revisions invalidate inline output, and successful compilation refreshes an open card unless the user stopped that observation. Inline IO stays in the same card and requires explicit Run IO. Auto tries bounded decoders sequentially on the same retained binding, without applying arguments. Reduction itself has no time/fuel ceiling: a decoder can diverge, in which case Stop discards the Worker rather than guaranteeing a fallback to the next view.

There is no scalar-object result codec. Practical/parser/lexer demo reports remain ordinary program data, shown as tree source by default. Their authenticated bundles and complete independent native tree goldens retain their identities; only presentation presets changed. Native preparation still checks every report against independently constructed expected trees. Parser/lexer edit checks use ordinary tricu projections with Text, not a host schema-specific decoder.

Explore libraries… is available in the toolbar with or without source. It separates library and definition matches, offers clickable exports and existing versioned references, and preserves the draft while browsing. Insert import checks/inserts the complete module import; Copy qualified name remains available. No application is generated. Without editable source, Open library loads the selected library and its searchable stored-definition interface. This does not introduce selective imports or per-definition hash syntax. The Program selector lists examples and, when applicable, the selected library rather than the entire library catalogue.

Validate both adapters and UI paths with TEST_IMPORT_PICKER=1 in the bounded Chromium harness above; TEST_VIEWPORT_WIDTH=390 for a narrow layout. The probe covers compiled/private inspection, interpretation failure/recovery, revision invalidation, cancellable observation, anchored cards, definition search, draft-preserving browsing, source-free exploration, and the existing import safety/undo controls.

Recovering an unresponsive Playground

Reload Playground in the always-present toolbar performs a full page reload, replacing all page state and runtime/compiler/validation Workers. It remains available during loading, compilation, evaluation and errors. A local draft or opened local module triggers a confirmation before navigation; cancelling leaves that work and the current operation untouched. Copy anything needed before confirming: drafts, inspection results, IO state and the in-memory local-module collection are not restored. Arrival follows the normal URL/default selection rules and never automatically evaluates a program. This is a recovery path, not a diagnosis or prevention of long-idle runtime/browser failures.

Add TEST_RELOAD=1 to a Chromium backend harness invocation to exercise warning cancellation, recovery during divergent execution, full navigation, cleared local state, fresh WASM inspection and demo switching after recovery.

Firefox background-idle soak

The opt-in WebDriver BiDi runner uses real Firefox tabs and packaged runtime code, without a CUA stack or mocked evaluator. Each checkpoint has independent published and locally compiled Haskell sessions; later checkpoints are never exercised or rebuilt by earlier ones. A neutral tab stays active. The driver verifies the site tabs are hidden and does not poll them during the requested idle interval. On resumption it checks the original retained binding before replacing any session, then exercises fresh inspection, demo switching, source applications, editing/compilation, restore, and ordinary decoder/compile diagnostics without leaving the editor read-only.

SITE=$(nix build .#arboricxSite --no-link --print-out-paths)
ARBORICX_SITE_ROOT="$SITE" IDLE_REPORT=/tmp/tricu-firefox-idle.json \
  nix shell --inputs-from . nixpkgs#nodejs nixpkgs#firefox -c \
  python3 ext/demo-site/test/run-bounded.py --seconds 2100 --memory-mib 8192 -- \
  node --max-old-space-size=512 ext/demo-site/test/firefox-idle.mjs

Default intervals are 10, 20 and 30 minutes. IDLE_MINUTES=0.02,0.04,0.06 provides a short harness smoke, not evidence about long-idle behavior. The JSON report is saved after setup and every checkpoint; it includes actual hidden intervals, browser version, visibility changes, UI/diagnostic snapshots, unexpected console/network errors, and the immutable tested site path. A checkpoint failure also attempts a screenshot beside the report. The runner does not reload or silently recover a failure. Temporary browser profiles are removed on exit.

Headless background tabs do not reproduce every desktop lifecycle condition. This check does not inject OS suspend, memory pressure, tab unloading, extensions, or production network/deployment changes. A clean run is not proof that an intermittent user-reported failure cannot occur.

IO version boundary

The production adapter requires bounded, canonical tricuIO version 2 framing before observing the computation. Unsupported/noncanonical versions reject without retry. probeIO recognizes version 2 without inspecting actions or arguments. Historical v1 trees may be loaded and inspected, not executed. The reference JS graph supports pure evaluation only.

IO uses the shared structured task/resource interpreter, not the old single-read loop. IO.capabilities reports the backend (haskell-wasm), browser-terminal authority, core operations, implemented named revision-1 facilities (runtime, standard, stream, resource, utf8) and limits. Resource close implements borrowed-standard-handle rejection, not owned file handles. File, network, process and clock facilities are unsupported and leave their arguments opaque. IO.inspect classifies one header without forcing its payload, continuations or unknown facility arguments.

Public embedding callers supply IOHost.structured with identity-bound open/event/close transport callbacks. These callbacks enqueue cloneable commands and must not throw or synchronously reenter a turn. A legacy text-only host rejects v2 before callbacks; it is never silently adapted to lossy byte output or uncancellable reads. Stale replies cannot consume later waits. Unrelated compilation/inspection is rejected while a v2 run owns the runtime. Selected result codecs run only after all tasks, leases and host work settle, with the same full ValueView contract as pure execution—not the private probe's bounded-natural summary. Result-decoder errors leave a settled session reusable.

The main-page terminal keeps exact stdout/stderr bytes and displays valid UTF-8, marked invalid/incomplete byte escapes and exact hexadecimal. Input is explicitly line-oriented, with separate LF-producing Send and EOF controls. Stop sends one structured cancellation request, retires pending input before acknowledging it, and waits for cleanup. Completed cancellation retains compiled definitions. After acknowledged settlement the terminal retires before result decoding; a subsequent divergent pure result observation can therefore be hard-stopped without abandoning IO work (and that hard Stop discards the runtime definitions). Emergency runtime loss also rejects idle reads/steps, explicitly marks the terminal unacknowledged and discards the unusable runtime; it does not invent cancellation replies. Emergency-disposal listeners are removed after every wait/turn, rather than accumulating until the session is destroyed. Stepping pauses all tasks before one scheduler advance (including lifecycle progress), not before a native reduction or just a single selected child. Already admitted terminal operations may complete while logical scheduling is paused. Stop bypasses a paused step and cleanup is never step-gated. Non-stepped runs advance in batches of at most128; completely blocked terminal waits do not spin-poll.

There is no hard pure-reduction deadline or prompt-cancellation guarantee. In v2, a divergent native observation can prevent reaching a cancellation boundary; reload/navigation/fatal-runtime teardown is emergency disposal, not evidence of cleanup. Pure evaluation retains hard Worker Stop. IO trace hooks run only for scheduler stepping and never inspect arbitrary payloads. Current source and application IO use structured. The browser compiler remains the independently pinned pure module compiler, not the native filesystem compiler.

TEST_IO_VERSION=1 checks inert current recognition and retired/unknown-version rejection, missing-structured-host rejection before callbacks, and separately pinned pure compilation. For public v2, compile test/io/browser.tri as below and select TEST_IO_PUBLIC=1 in the same Chromium harness on the Haskell runtime. It exercises compiled direct/handled programs, scoped reader cancellation, binary/line/EOF output, every public codec,

64-bit naturals, expression execution, capability/inspection queries, concurrent operation rejection, codec-error recovery and real-page structured Stop/reuse.

Inert IO wire controls

io-protocol.ts classifies one core/request header without executing it or resolving a facility. Its handles are borrowed; callers retain the action while using them. Canonical numbers, bounded ASCII names and revision bounds follow IO.Wire, shared by native Haskell and its browser embedding. This is not a capability declaration or a v2 execution entrypoint.

nix build .#arboricxSite --no-link
SITE=$(nix path-info .#arboricxSite)
TEST_IO_PROTOCOL=1 TEST_BACKEND=haskell-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

The private Haskell worker inspection operation is checked against independent constructor-only bundles: ten core headers, future facility headers, exact bounds and malformed encodings. Reference-graph tests additionally trap forbidden payload/excess-head/excess-tail observations. Keep TEST_IO_VERSION=1 controls alongside these: current adapters reject retired versions and hosts without structured transport before callbacks.

Browser v2 lifecycle components

io-tasks.ts owns lexical/spawned scope states, supervision, consumable joins, cycle checks, bounded secondary diagnostics and pending host-call ownership. Normal return closes admission and joins children; failure/cancellation requests subtree stop. Neither ordinary Results nor arbitrary payloads are inspected. payloads() lists lifecycle-held evaluator roots, not body/continuation roots. Consumed/discarded outcomes and closed scopes are removed rather than archived.

io-host-work.ts starts work without blocking sibling actions and separates abort delivery from physical completion acknowledgement. Cancelled and stale replies cannot revive bodies. Cleanup is a separate, non-cancellable batch per scope, using that scope's reserved identity so body-call identity exhaustion cannot prevent release. Cleanup failures still reach the outcome accumulator. The embedding must own all host work through this queue, release resources in reverse order, and consume cleanup results before settling a scope. Providers must settle promises only after their actual cleanup; a provider that ignores cancellation can delay scope exit indefinitely. No hard deadlines, JS allocation- failure recovery or aggregate heap bound are implied.

These components do not yet implement tree-action execution or the browser session event bridge. They are not advertised through runtime capabilities by themselves; public execution uses the composed provider described above. Lifecycle and provider controls run with:

nix shell --inputs-from . nixpkgs#nodejs -c node --test \
  ext/demo-site/test/io-tasks.test.mjs \
  ext/demo-site/test/io-host-work.test.mjs

After building the site, use TEST_IO_LIFECYCLE=1 in the Chromium command above for a real DOM read with delayed cleanup acknowledgement and concurrent pure work on each selected WASM runtime. This is a lifecycle/provider integration probe, not execution of a v2 language program. Repeated-task tests establish logical retirement/root removal, not measured JS heap or throughput bounds.

io-resources.ts supplies the registry through lifecycle(), which binds task release/settlement to resource guards. Use that factory rather than constructing an unguarded task machine alongside a registry. Acquisitions are admitted before host effects; a failed publication leaves its admission live until the caller releases the raw backend and aborts. Admitted acquisitions may complete after scope admission closes. Existing descendants retain borrowing access while a normal parent exit joins them.

Transfer validates the entire bounded handle list before task admission, rekeys all old aliases, and preserves acquisition order regardless of argument order. Leases reject wrong-kind, stale, closed and out-of-scope access before provider work. Close claims block new operations; a busy claim is a wait, not permission to retry physical IO. HostWork.cancelBody() can interrupt one provider operation without cancelling its task. Release/lease acknowledgement is still mandatory.

The guarded scope cleanup batch attempts owned backends once, in reverse publication order, and retires wrappers for borrowed standard streams without closing those external backends. Release errors remain failures, never an implicit retry or a claim that an uncertain physical release succeeded. Secondary diagnostics retain at most16 values; omitted errors are counted without calling the diagnostic builder. A diagnostic-builder failure uses a prebuilt fallback and does not skip remaining releases. Add io-resources.test.mjs to the focused Node command above. The Chromium lifecycle probe also checks handoff alias invalidation, a real DOM reader lease, and physical resource removal before join. These are provider/registry controls, not public v2 language execution support.

Private v2 tree interpreter validation

io-core.ts connects the guarded lifecycle to tree-calculus actions and bind continuations. Its advance() performs a progress turn without awaiting a host operation. The embedding must yield between batches, retain roots() across turns, and keep temporary evaluator handles alive for the whole turn. Providers return deferred ordinary-Result encoders: host completion is acknowledged first; encoding happens in a later interpreter turn, and cancelled encoders never run. Providers must enumerate additional retained tree handles through roots(). The original request arguments remain rooted through provider acknowledgement. Evaluator/embedding faults are distinct from protocol faults and ordinary Results; hasFault also records rejection with undefined. Cleanup drains before the embedding rethrows the saved fault. No hard pure-reduction deadline is implied.

The private io-core worker request executes bare computations, not public IO envelopes. Haskell's BrowserTrees.hs keeps lazy trees in a worker-private handle table with non-reused checked identities and between-turn root sweeps. Evaluator handles stay inside that session. The JS reference evaluator supplies independent conformance coverage. Private providers reject all named facilities as ordinary unsupported Results; this is not the browser terminal profile or a capability advertisement. Public sessions compose this core with the terminal provider.

# Include the existing lifecycle, resource and non-demand controls.
nix shell --inputs-from . nixpkgs#nodejs -c node --test ext/demo-site/test/io-*.test.mjs
nix build .#arboricxSite --no-link
SITE=$(nix path-info .#arboricxSite)
TEST_IO_CORE=1 TEST_BACKEND=haskell-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

These constructor-fixture probes cover actual native callback application, lexical scopes, supervision, opaque terminal outcomes, ordinary unsupported requests, malformed computation rejection, repeated reuse and per-turn root retirement. Source-program terminal execution and resumable session events still need their own integration controls before production dispatch changes.

Private resumable stream sessions

io-session.ts couples the interpreter to io-events.ts using cloneable run IDs, operation IDs and bounded byte data only. A turn advances at most128 progress steps and never waits for a terminal operation. Multiple pending operations remain visible; replies, polls and structured stop are separate turns. The Haskell worker exposes private io-session-start/io-session-turn requests, keeping the tree table alive across turns and rejecting unrelated work until settlement. Public RuntimeSession uses the same session with opaque outcomes and selected full codecs.

The shared provider implements named standard input/output/error, byte-stream read/write and borrowed-resource close rejection. Chunks are1–4096 bytes; EOF is an explicit empty read and is sticky for this browser provider. Positive partial write acknowledgement is returned to source, never silently retried. Both output slots share a16384 attempted-byte budget: partial progress does not refund an attempt, and failed/cancelled output poisons that slot before later byte payloads are demanded. External standard backends are not closed during scope retirement. The shared session also implements bounded strict UTF-8 encode/decode. The public profile adds runtime capability and inert-inspection queries.

Wrong-run, stale, wrong-kind and malformed replies preserve pending operations. An operation cancelled before dispatch has no host effect; once dispatched, abort delivery is not completion. The peer must remove listeners/drain work before acknowledging. A valid normal completion racing cancellation counts as physical acknowledgement, but its data is discarded. Event copies cannot mutate internal validation state. Structured stop drains leases and scopes; destroying a worker is still emergency disposal, not evidence of structured cleanup. No hard deadline for pure reduction or an uncooperative provider is promised.

Use TEST_IO_SESSION=1 with the Chromium command above for both backends. The probe executes a native computation tree that starts a read, performs a sibling write, and cancels the reader; a real DOM provider must finish cleanup before the run completes. It also rejects wrong-run stop/replies and checks empty lifecycle and transport records. Node event/session controls independently cover native read-value framing, EOF, output poisoning, partial progress/budget, cancellation races, identity exhaustion and repeated operation retirement. These remain private constructor-based checks, not compiled source-program acceptance.

Private UTF-8 and compiled-source validation

Named utf8 revision1 implements encode (at most1024 Unicode scalars) and decode (at most4096 bytes). It rejects surrogate/overlong/out-of-range encodings and invalid incomplete prefixes. Nonfinal valid incomplete suffixes are copied as at most3 bytes of source-owned carry; final incomplete input is an ordinary error. Generic decoding preserves BOM, NUL, CR/LF and combining characters without replacement or normalization. Source readSource keeps its separate initial-BOM policy. Excess scalar/byte list cells are rejected before their heads or tails are demanded. Result encoding remains deferred to a live interpreter turn.

test/io/browser.tri exercises actual io, io_text and handler libraries through the private native sessions. Compile it with a selected Nix-built host, not a JS compiler or substituted evaluator:

WORK=$(mktemp -d)
TRICU=/path/to/selected-nix-built-tricu
"$TRICU" arboricx compile --file test/io/browser.tri \
  --store "$WORK/store" --output "$WORK/browser.arboricx" --compile-timeout 240
nix build .#arboricxSite --no-link
SITE=$(nix path-info .#arboricxSite)
IO_SOURCE_BUNDLE="$WORK/browser.arboricx" TEST_IO_SOURCE=1 \
  TEST_BACKEND=haskell-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

The test-only server route serves the explicitly selected bundle and its SHA-256; normal catalogue loading/publication is not bypassed by production sessions. Independent controls compare complete output bytes and result42 for direct and handler-backed output, a byte-at-a-time Unicode round trip, scoped reader cancellation, and malformed UTF-8/scalar rejection. Positive one-byte write acknowledgements exercise source writeAll, including multibyte splits. Input round trips include BOM/NUL/combining scalars and CR/LF. These tests use the private byte-event interface, not the text-only user-interface widget.

Byte-preserving output presentation model

io-output.ts is the presentation-only buffer for the terminal adapter; it is not public v2 dispatch and does not acknowledge host operations. It retains at most16384 accepted bytes shared across stdout/stderr. The session separately limits attempted output. Append validation is atomic; inputs and snapshots do not share mutable byte arrays with the buffer.

Snapshots expose exact raw bytes, canonical hexadecimal, and typed text/byte presentation tokens with stream-local offsets. Valid UTF-8 remains unchanged, including BOM, NUL, combining characters and CR/LF. Invalid bytes are separate byte tokens, not replacement characters. A valid incomplete suffix is pending until more bytes arrive on the SAME stream; end-of-presentation marks it incomplete without creating an IO failure. Actual byte tokens must be distinguishable from literal escape-looking text when rendered. Do not interpret terminal control sequences or insert program text as HTML. The public widget uses these tokens without changing IO acknowledgement semantics.

io-output.test.mjs checks exact byte reconstruction, hex, malformed sequences, chunk-split invariance, stream separation, copied ownership and shared bounds.

Private terminal adapter validation

io-terminal.ts consumes shared session byte events. Input is explicitly a line widget, not binary stdin: at most256 Unicode scalars, no embedded CR/LF or unpaired surrogates, followed by exactly one LF. Empty Send produces LF; the separate EOF control produces empty EOF. Encoded bytes are retained across bounded reads (at most1025 bytes per submitted line). Invalid/stale input does not consume a wait. Byte write acceptance is independent of display UTF-8 decoding.

io-terminal-view.ts renders text through text nodes, marks actual escaped bytes distinctly from literal escape-looking text, and provides exact per-stream hex. It does not interpret HTML or terminal control sequences. Each input wait owns fresh DOM controls/listeners; cancellation clears/removes them before its acknowledgement. Stop enqueues structured cancellation; it does not destroy the worker. Finishing retires listeners only after pending input has settled.

For the compiled-source probe above, add TEST_IO_TERMINAL=1 while retaining TEST_IO_SOURCE=1. Both native evaluators run the same source bundle through real DOM controls: text/handler output, line/EOF, scoped cancellation, malformed codec Results, binary output and external structured Stop. Additional DOM controls check old prompts cannot consume new waits, empty lines differ from EOF, invalid lines remain retryable, markup is inert, and repeated Stop is ignored.

These probes also remain available independently of public execution. Use TEST_IO_PUBLIC=1 instead of TEST_IO_SOURCE=1 for the public adapter and real-page acceptance. No test-only bare-computation route is used by the page.