tricu

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

README.md (32054B)


Zig runtime and C ABI

The Zig runtime evaluates tree-calculus programs and provides IO through an owned-handle C ABI. Build and validate it with Nix:

nix build .#tricu-zig .#tricu-zig-tests --no-link
nix build .#tricu-js-tests .#tricu-php-tests --no-link

The executable and shared library build in ReleaseSafe, with Zig runtime safety checks enabled. Reducer, collector and libuv scheduler-root tests run in both Debug and ReleaseSafe.

Cross-runtime compiler reproduction compares complete compiler generations executed by Haskell and Zig. The workflow includes independent object audits, program smokes and retention checks. Haskell supplies common artifact transport and packaging.

The WASM adapter reuses this reducer without native IO. The demo site selects Zig or Haskell for native browser execution, with IO implemented by the owning session. The JS reference graph supports pure probes. Browser work uses unbounded reducer/Unicode/ preview entry points. Structured Stop waits for IO settlement; emergency disposal terminates the Worker. Finite-fuel native entry points retain their semantics.

Basic performance smoke (Linux)

nix build .#tricu-zig-performance-smoke --no-link
PERF=$(nix path-info .#tricu-zig-performance-smoke)
cat "$PERF/report.json"

This target checks complete results for string identity, append of two independently constructed inputs, deep constant-selection chains, distribution chains, and repeated calls in one context. It records three fresh-process samples per case, phase timings, fixture hashes, CPU information and memory observations. The C driver is optimized and the linked runtime is ReleaseSafe. Each sample records the runtime optimization mode; mixed-mode samples fail validation.

Construction, WHNF reduction, complete observation and explicit collection are timed separately. Lazy work may occur during observation, and automatic GC time is included in whichever phase triggers it. Explicit collection runs every 16 iterations, at the midpoint, and at the end. Peak process RSS comes from Linux VmHWM; midpoint/end RSS comes from /proc/self/statm after collection.

Each child applies a 512 MiB address-space limit, 8 MiB stack limit and 60-second CPU limit before initializing the runtime; the harness enforces a 65-second wall limit. A wrong result, crash, exhausted limit or timeout fails the smoke. Timings are recorded for comparison across runs.

Nix may reuse the stored measurement report. For fresh measurements with the same built runner:

nix develop -c python3 ext/zig/tests/performance_smoke.py \
  "$PERF/bin/performance-smoke" test/fixtures /tmp/tricu-zig-performance-smoke.json

Small compiler probe

test/cross_host_generation.py runs a two-module native map-builder job through the shared generation runner on both Haskell and Zig, using the authenticated bootstrap/rebuild kit. Haskell authenticates the captured input bundles. Each execution host applies the retained compiler and adapter and drives IO. Publication passes an independent store audit, and the resulting program returns 41 on Haskell. A second invocation checks warm reuse in a fresh runtime context.

nix build .#tricu-zig .# --no-link
ZIG=$(nix path-info .#tricu-zig)
HASKELL=$(nix path-info .#)
nix develop -c python3 test/cross_host_generation.py \
  --library "$ZIG/lib/libarboricx.so" --tricu "$HASKELL/bin/tricu" \
  --directory /tmp/zig-compiler-probe-new

Choose a new output directory. Limits are 1 GiB per child and 120 seconds per generation invocation, with an 8 MiB stack for Zig. Cold and warm runs must publish identical complete receipt and object sets across hosts. The output records inputs, runtime hashes, logs and completion status. Unsupported IO actions produce an error result identifying the action tag.

Add --compatibility for reexports, multiple exports, direct and local recursion, patterns, sequencing, lazy discard, UTF-8 comments, dependency changes, warm reuse, compiler error tags, corrupt checkpoints/executables, guarded interruption and restart, and missing-input controls. The supervisor checks BOMs and headers before execution. Runtime Debug/ReleaseSafe tests cover source/indexed IO permission denial and malformed inputs. The execution child records string diagnostics or a bounded numeric prefix for structured diagnostics.

Evaluator allocation headroom

The default collector runs after max(1048576, last_live_nodes) allocations since the last collection. It recomputes this threshold after each collection, so a transient live-heap peak does not permanently delay later collections. Construction still never collects automatically. These thresholds count nodes, not bytes, and are not RSS limits.

C ABI clients can choose the memory/speed tradeoff per context:

/* More allocation headroom for a memory-rich compiler job. Check the status. */
arb_status_t status = arb_set_gc_policy(ctx, 4194304, 4);

The two positive uint32_t arguments are the minimum allocation interval and live-node multiplier. The threshold is their maximum after multiplying the most recently measured live-node count (with saturating arithmetic). Changing policy does not allocate, collect, or reset the allocation counter. The next reducer safepoint checks the new threshold. Zero arguments return ARB_INVALID_DATA without changing the policy. arb_collect still requests an explicit collection regardless of the threshold. Older ABI-v2 libraries may lack this additive entrypoint; callers requiring it must select a library that exports it, not silently fall back.

The generation runner accepts the same explicit policy and records it in input provenance, status, invocation and the execution report. For a bounded native math compilation, using the runtime paths from the small compiler probe:

nix develop -c python3 test/bootstrap_generations.py /tmp/zig-math-headroom-new \
  --bootstrap-kit bootstrap/rebuild --tricu "$HASKELL/bin/tricu" \
  --execution-host zig --zig-library "$ZIG/lib/libarboricx.so" \
  --root native_math --fresh --memory-mib 4096 --wall-seconds 180 \
  --zig-gc-policy 4194304 4

Use a fresh directory for each sample. Omit --zig-gc-policy to measure the library default; it is also accepted by test/cross_host_generation.py for cold/warm parity and compatibility checks. The existing isolated 1 GiB probe continues to use the default. Larger headroom may need several GiB, even on a single module; increasing it further need not improve execution time. Keep external resource guards and measure both time and peak RSS on the actual job. The collector retains backing capacity, including its mark/work buffers, and the reducer retains continuation-buffer capacity. Collection trims dead node array tails without moving live IDs, but does not promise to return memory to the OS.

For comparisons, alternate original/candidate order on one allowed CPU after builds finish. Compare execution.json execution times and status.json child peak RSS, not just supervisor wall time. Require independent audits and identical complete cas/objects inventories. A native-math measurement is not a full compiler-generation measurement; use the cross-runtime reproduction workflow for compiler fixed-point acceptance.

Indexed tree transport

The indexed-tree v1 facility supplies authenticated read/hash/write/verify operations under IO protocol 2. The domain remains arboricx.indexed-tree.v1; facility revisions and content domains are not IO protocol versions.

The codec uses iterative traversal, memoizes physical identities and interns constructor/child-ID keys. Equal finite trees produce the same canonical bytes regardless of physical sharing. Reads authenticate before decoding and preserve DAG sharing. Native facilities use OS process authority.

Source-owned storage composes expected-digest checks, no-replace publication, exact winner verification and synchronization. Publication is not rolled back on a later failure. Serialized buffers and decoded trees can occupy substantial memory; use external process limits. See the storage contract.

Source-text IO

The utf8 facility encodes/decodes strict Unicode scalar lists and byte chunks. Source-owned whole-file readers preserve raw octets and decoded text, reject malformed UTF-8 and an initial BOM, and do not normalize newlines or Unicode. Compiler policy supplies the source-size budget. Ordinary text encoding permits U+FEFF; source-file reading owns initial-BOM rejection.

ABI v3 ownership

include/arboricx.h defines ABI v3. arb_abi_version() returns 3; the JS, PHP and Python test bindings check this version before using handles.

For example (check every status in application code):

arb_ctx_t *ctx = arboricx_init();
if (!ctx) return 1;
arb_handle_t leaf, application, result;
if (arb_leaf(ctx, &leaf) != ARB_OK ||
    arb_app(ctx, leaf, leaf, &application) != ARB_OK ||
    arb_release(ctx, leaf) != ARB_OK ||
    arb_reduce(ctx, application, 1000, &result) != ARB_OK ||
    arb_release(ctx, application) != ARB_OK ||
    arb_collect(ctx) != ARB_OK) {
    arboricx_free(ctx);
    return 1;
}
/* result owns Stem Leaf, including the child released above. */
int is_stem;
if (arb_is_stem(ctx, result, &is_stem) != ARB_OK || !is_stem) {
    arboricx_free(ctx);
    return 1;
}
if (arb_release(ctx, result) != ARB_OK) {
    arboricx_free(ctx);
    return 1;
}
arboricx_free(ctx);

Reduction and collection contract

The reducer is a heap-framed, in-place, call-by-need WHNF machine. It evaluates terms on demand. Constructor demands dispatch directly; only suspended function, discriminator, argument and update demands use heap frames. Already-normal observations avoid borrowing continuation storage unless collection is due. Distribution and fork-argument triage also fuse immediately demanded constructor/constant applications, without allocating their private intermediate thunks. These are kernel-rule reductions, not source-name intrinsics: they do not demand discarded arguments, and charge the same application-entry fuel as the unfused rules. Low-fuel cases fall back to the unfused path. Tests compare single-invocation fuel boundaries and resumed results against a literal unfused evaluator.

The arena uses non-moving mark/sweep and reuses dead slots. Collection occurs at reducer loop safepoints, with the current term and continuation redexes rooted. The ABI supplies live handles and its kernel root; the CLI supplies the invocation root. The IO scheduler snapshots all tasks' current terms, environments, state, results and continuation frames for each turn. Async callbacks allocate and link nodes. Root scopes unwind on errors.

Construction allocates and links nodes; collection runs during reduction or an explicit arb_collect call. Construction-only clients should call arb_collect periodically. Graphs unreachable from the remaining roots become eligible for reclamation at the next collection. Evaluation and IO operate directly on the shared graph. Internally, keep raw node IDs within their registered root lifetime and reacquire pointers into the growable node array after allocations.

Resource limits

Tests cover all kernel rules, independent finite-reference comparisons, undemanded divergent terms, shared updates, deep evaluation, allocation-failure cleanup, GC slot reuse, scheduler roots, stale/foreign handles, independent ownership, and C/Python/JS/PHP fixture programs. tests/helpers.h supplies checked constructors for fixture tests.

Paired Haskell/Zig runtime measurements

nix build .#reducerComparison --no-link
nix run .#reducerComparison -- /tmp/reducer-comparison.json

Run measurements after builds finish, with other benchmark/build workloads idle. This Linux harness compares the Haskell reducer (GHC -O2, one RTS capability) with Zig ReleaseSafe. Both use the same retained identity/append bundles, canonical byte inputs, and constant-selection/distribution workloads. Every iteration checks its complete result. Each case has five fresh-process samples per host, alternating host order, pinned to one allowed CPU. Results include binary/fixture hashes, compiler version, raw samples, timing ranges and process peak RSS. Package checks require deliberately wrong workers to fail result validation.

The primary timing combines WHNF reduction and full output decoding and checking, including codecs and automatic GC. Build and explicit-GC phases are recorded separately and as part of the combined total. Haskell input normal forms are forced before application timing; opaque suspension boxes prepare unevaluated applications. Harness-only anti-sharing flags and varying input bytes give each round fresh work. Production Haskell runtime modules compile normally. Zig's construction includes owned-handle C ABI calls; Haskell's includes input forcing and suspension preparation.

Haskell may collect during construction; Zig collects automatically at reduction safepoints. Compare both evaluation/observation and combined totals. Initialization contributes to process RSS and has its own timing. Common guards are 512 MiB sampled RSS, 60 CPU seconds, 65 wall seconds and an 8 MiB native stack per child. Zig also has a 512 MiB address-space cap; Haskell has a 512 MiB managed-heap cap and an 8 MiB RTS stack cap.

Isolated mapped-library probe

test/bootstrap_cross_host.py --probe runs the frozen native_math closure on both hosts, with identical kits and a 1 GiB/120-second budget per job. It requires Linux bwrap and util-linux unshare with working user/PID/mount namespaces, and a Nix Python environment:

nix develop -c python3 test/bootstrap_cross_host.py --probe \
  --source-commit COMMITTED_SOURCE --directory /tmp/cross-host-probe-new \
  --tricu "$HASKELL/bin/tricu" --zig-library "$ZIG/lib/libarboricx.so"

Each sandbox mounts the read-only source snapshot and kit, Nix dependencies, an initially empty writable work directory, and ephemeral system directories. The generation supervisor runs inside the sandbox, where its RSS monitor observes the execution child. The probe compares complete independently audited object inventories. Its experiment definition records the source commit/archive, mapped-input hashes, runtimes, Python, tooling, limits and comparison boundaries. Snapshot extraction excludes the tracked build-output symlink ext/zig/result.

The launcher creates a private PID namespace with a matching procfs mount, then starts the filesystem sandbox. Both isolation tool identities are recorded. For a full experiment stopped before a job creates its generation directory, --continue-experiment reverifies frozen sources, runtimes and completed generations, preserves the failed launch and original tooling, and starts the pending job fresh. A partially executed generation requires a new experiment.

IO protocol boundary

The native CLI validates canonical tricuIO version 2 before demanding its action. Historical v1 execution requires a retained runtime. V2 implements the structured core and the compiler/file profile: regular files, path/directory mutations and sync, stages, SHA-256, strict UTF-8 and authenticated indexed transport. IO.capabilities reports exactly those operations and their restrictions; IO.inspect classifies one header without demanding its arguments. Standard streams, directory cursors, networking, processes, clocks and context are not implemented in this Zig profile. Unknown named facilities/revisions return ordinary unsupported Results.

V2 uses OS process authority and rejects --unsafe-io before action demand. Unknown envelope versions fail without interpreter fallback. Terminal failures/cancellation fail the CLI without rendering opaque user payloads. Each native run gets a unique, nonwrapping identity. Synchronous filesystem calls make no prompt-cancellation or deadline promise. Explicit text:VALUE arguments encode Unicode scalars (including Unicode paths); historical unprefixed argument forms retain byte-string semantics. Owned-handle C ABI version3 remains a separate contract, not an IO interpreter. Pure evaluation is independent of IO-envelope execution, and the freestanding WASM ABI has no native IO driver; the site's adapter supplies the shared structured v2 core/task/terminal profile. See the validated program/backend matrix before selecting an execution host or attempting a compiler/default transition.

src/io_v2_protocol.zig is the host-independent foundation for the separate v2 port: bounded canonical core/header decoding, with temporary GC rooting and opaque payloads. It does not execute requests, resolve facilities, or enable v2 in the CLI, C ABI or browser. Its Debug/ReleaseSafe controls run with tricu-zig-tests, including computed headers, excess-element non-demand, divergent payloads, and allocator/GC failure unwinding. Reducer fuel is explicitly per demand, not a request deadline.

The separate v2 resource/task state machines and private pure-core probe are also validated through these test targets. The native CLI uses these state machines through the separate v2 boundary. Resource admission, active leases and asynchronous close claims have separate lifetimes; cancellation does not detach outstanding host callbacks. Task results survive only an unconsumed ticket or a claimed wait, not a completion archive. Lexical scopes share their task identity for join-cycle detection.

For actual source/handler controls on both Haskell and the private Zig core:

nix build .# .#tricu-zig-tests --no-link
ZIG=$(nix path-info .#tricu-zig)
HASKELL=$(nix path-info .#)
PYTHONDONTWRITEBYTECODE=1 python3 test/io_v2_zig_core.py "$HASKELL/bin/tricu" \
  "$ZIG/bin/io-v2-core-probe" --directory /tmp/zig-v2-core-new

Use a fresh output directory. The probe supports the structured core only; all named facilities return ordinary unsupported Results. It rejects v1 rather than retrying it and never renders terminal diagnostic/reason payloads. It is not a compiler execution host, native filesystem port or browser adapter. The source controls include fresh forced 4096-cell payloads in repeated joined/discarded tasks; the 128-MiB Zig process guard covers those workloads, not arbitrary programs or future native IO. Unit tests additionally sweep allocation failure and force GC.

A separate private io-v2-file-probe exercises synchronous regular-file IO through that core, including the compiler storage transports described below. It uses the same facilities/capability catalogue as the native CLI, with extra private raw-tree argument support for controls. Path opens reject known nonregular types, use nonblocking/no-controlling-terminal opens, verify the opened descriptor before truncation, and follow ordinary host symlinks. These checks are backend type support, not confinement: raced device opens can have effects. Standard streams are separate and are not implemented by this probe. Synchronous regular-file calls may delay all tasks and cancellation; they have no hard deadline.

PYTHONDONTWRITEBYTECODE=1 python3 test/io_v2_zig_files.py "$HASKELL/bin/tricu" \
  "$ZIG/bin/io-v2-file-probe" --directory /tmp/zig-v2-files-new

Controls compare exact file bytes and result trees, including borrowed/transferred handles, implicit joins, cancellation without rollback, positioned/update/append operations and malformed/unsupported requests. Native unit controls inject a path substitution after preflight and close failures after descriptor release, and sweep allocation failures through real acquisition and core unwind. Host cleanup error codes have fixed-size storage even when allocator/reducer failure prevents a tree reply. This synchronous host interface cannot be used for asynchronous browser input or detached native work: those need an explicit settlement pump.

The private file probe also implements path/stat, lstat, remove-file, rename and directory/create, remove, sync. Stat/lstat are observations, not pinned claims about future paths. Rename uses the OS namespace operation, without copy fallback or rollback. Directory synchronization opens/checks a directory descriptor with nonblocking and close-on-exec flags, preserves primary failures, and attempts close exactly once. Directory enumeration is not implemented by this subset.

PYTHONDONTWRITEBYTECODE=1 python3 test/io_v2_zig_paths.py "$HASKELL/bin/tricu" \
  "$ZIG/bin/io-v2-file-probe" --directory /tmp/zig-v2-paths-new

The private host implements scoped digest/open, update, finish for SHA-256. It retains fixed-size hash state, not prior chunks; finish consumes the active state. Algorithm rejection preserves at most128 Unicode scalars of diagnostic text. Typed backend tags keep digest handles distinct from file/stream operations.

PYTHONDONTWRITEBYTECODE=1 python3 test/io_v2_zig_digest.py "$HASKELL/bin/tricu" \
  "$ZIG/bin/io-v2-file-probe" --directory /tmp/zig-v2-digest-new

Private stage/open, seal, publish supports scope-owned private writers, same-filesystem cross-directory link/rename publication, no-replace exists, and explicit replacement. Seal synchronizes and closes the file before publication; publication synchronizes destination then distinct source directories. Syscall failure poisons the stage; uncertain publication is never retried or copied across filesystems. Later failure/cancellation/OOM cleans private names, never rolls back a visible destination. exists does not prove identity or durability: caller code must authenticate the winner. Source artifact policy is not in the native host.

PYTHONDONTWRITEBYTECODE=1 python3 test/io_v2_zig_publication.py "$HASKELL/bin/tricu" \
  "$ZIG/bin/io-v2-file-probe" --directory /tmp/zig-v2-publication-new
PYTHONDONTWRITEBYTECODE=1 python3 test/io_v2_zig_stages.py "$HASKELL/bin/tricu" \
  "$ZIG/bin/io-v2-file-probe" --directory /tmp/zig-v2-stages-new

The latter control requires a writable /dev/shm on a different filesystem for actual EXDEV validation; absence is a failed/incomplete control, not a silent skip. Fault-injected state-machine tests cover every publication step; allocator sweeps cover acquisition and reply encoding before/after visibility with real private files. These remain private integration tools, not release/compiler acceptance.

Private utf8/encode and utf8/decode implement bounded stateless strict transport (1024 scalars / 4096 bytes). Nonfinal decode returns at most three carry bytes; invalid available prefixes fail immediately. No replacement, normalization or BOM stripping. Source code retains text/source policy and streaming carry.

PYTHONDONTWRITEBYTECODE=1 python3 test/io_v2_text.py "$HASKELL/bin/tricu" \
  --zig-probe "$ZIG/bin/io-v2-file-probe" --directory /tmp/zig-v2-text-new

The shared independent controls cover binary NUL, Unicode, carry boundaries, source BOM rejection, partial writes and excess-list non-demand. Private probes accept tree-file:PATH arguments containing exact raw 0/1/2 constructor trees (maximum 10 MiB per argument), parsed iteratively with trailing roots rejected. These private transport arguments are not production CLI options.

The indexed codec also offers a graph-proportional canonical Plan, with repeatable emit calls using at most 4096-byte borrowed chunks and no retained encoded payload. The existing whole-value encode interface wraps that emitter; its bytes remain unchanged. Preparation still needs a resident DAG and graph-proportional tables, not a heap/CPU quota. Sink failure stops emission without retry. This foundation alone does not implement the private host's authenticated indexed-tree facilities.

The private file host implements indexed-tree/hash, write, and verify with that plan. Writes preflight size and digest before sending bytes, including to stages; verification compares exact canonical bytes and consumes only one frame, without an EOF probe. Resource kind/lease checks precede tree preparation. Caller code still owns winner authentication, EOF policy and publication. Authenticated reads use the snapshot transport described below.

PYTHONDONTWRITEBYTECODE=1 python3 test/io_v2_indexed_write.py "$HASKELL/bin/tricu" \
  --zig-writer-probe "$ZIG/bin/io-v2-file-probe" --directory /tmp/zig-v2-indexed-new

That explicitly writer-only subset does not run or claim authenticated round-trip controls. Omitting the option retains the complete Haskell suite. Controls include independent wire bytes, deep linear trees, corrupt/excess winners, preflight preservation, exact frame tails and wrong-kind non-demand; native allocation sweeps cover lease cleanup and failure after a completed write without rollback.

Private indexed-tree/read now authenticates an exact frame into a scope-registered anonymous file before decoding. The private name is unlinked before input is consumed; disk usage is proportional to frame length, transfers/cache are <=4096 bytes, metadata is limited to 64 sections / 65536 manifest bytes, and node tables are graph-proportional. No original-input reread, following-byte EOF probe, encoded payload buffer, implicit publication or durable snapshot promise. Normal Results and terminal allocation failures retire the internal descriptor; release errors cannot replace a primary failure. These synchronous calls have no deadline.

PYTHONDONTWRITEBYTECODE=1 python3 test/io_v2_indexed.py "$HASKELL/bin/tricu" \
  --zig-probe "$ZIG/bin/io-v2-file-probe" --directory /tmp/zig-v2-snapshots-new
PYTHONDONTWRITEBYTECODE=1 python3 test/io_v2_indexed_write.py "$HASKELL/bin/tricu" \
  --zig-probe "$ZIG/bin/io-v2-file-probe" --directory /tmp/zig-v2-roundtrips-new

The complete private mode includes authenticated shared-DAG round trips. Read controls include exact cursor boundaries, authentication before malformed-format rejection, failed preflight without consumption, and opaque transport growth to 64 MiB under a sampled RSS guard. This does not establish large-DAG capacity or constant aggregate memory. Format diagnostics currently use invalid indexed frame rather than Haskell's more specific text; category/progress remain explicit. Public native/browser v2 entrypoints and release/compiler acceptance are unchanged.

Actual compiler callers can be exercised without enabling the production driver:

PYTHONDONTWRITEBYTECODE=1 python3 test/io_v2_compiler_store.py "$HASKELL/bin/tricu" \
  --zig-probe "$ZIG/bin/io-v2-file-probe" --compiler-smoke \
  --directory /tmp/zig-v2-compiler-new

This builds a current source-mapped candidate on Haskell, runs its v2 compiler worker on private Zig for cold/warm dependency builds, authenticates the returned receipt, loads/executes the resulting store on Haskell, and compares the complete application bundle. Invalid source controls compare ordinary failure trees across hosts. Source-object, corrupt-winner, concurrent alias and cleanup controls remain included; pure helper checks run on Haskell and are distinguished in the report. This is a compiler-workflow smoke, not generation-two acceptance, browser support, production driver enablement, or support-catalogue publication.

Production native boundary and compiler smoke controls:

PYTHONDONTWRITEBYTECODE=1 python3 test/io_v2_native_cli.py "$HASKELL/bin/tricu" \
  "$ZIG/bin/tricu-zig" --directory /tmp/zig-v2-native-new \
  --compiler-bundle .bootstrap-run/io-v2-cohort/generation2/build-stable.arboricx

The compiler bundle is an explicit v2 sourceBuildGraph worker, not inferred from its filename. The optional compiler smoke selects that export into a separate bundle, executes cold/warm dependency builds through the real CLI, and authenticates/ executes the resulting store on Haskell. It neither builds a new compiler generation nor changes packaged compiler, browser, catalogue or registry defaults.

Native owned-ABI clients select arb_run_io_v2 for IO protocol 2. It uses the CLI interpreter/profile, takes no grants, preserves ordinary Results and leaves the output handle untouched on error. Native IO is rejected in WASM; the browser adapter uses the shared structured scheduler instead.

Owned-handle C ABI 3 removes arb_run_io and the legacy permission struct. IO protocol 2 is independent of that ABI version. Explicit historical execution requires a retained ABI-2 library; current clients never fall back.

The generation executor selects the symbol from captured execution-version metadata, validates it against generation inputs, and uses Unicode-scalar trees for v2 arguments. Historical records lacking version metadata select v1 explicitly. The bounded comparison is reproducible with an explicit authenticated v2 kit:

PYTHONDONTWRITEBYTECODE=1 python3 test/io_v2_generation.py "$HASKELL/bin/tricu" \
  --kit /path/to/v2-kit --zig-library "$ZIG/lib/libarboricx.so" \
  --directory /tmp/zig-v2-generation-boundary-new

This exercises actual mapped-library builds and complete object inventories on both hosts, plus version/identity-selection controls. It is not a full compiler pair. Use the isolated cross-host workflow with explicit --io-version 2 --bootstrap-kit /path/to/v2-kit for full H1/H2/H3/Z3 reproduction. Its pair, inventory, program-smoke, corruption and retention gates are independent of the bounded comparison.