commit b3adb450f4281bfc41d02d868877e5e4115ca3c4
parent 6e3afa5896f5a80f29d74571264b3c9181af478d
Author: James Eversole <james@eversole.co>
Date: Sat, 24 Oct 2026 16:06:49 -0500
Retire Zig runtimes and move native generation workflows to Haskell
Diffstat:
141 files changed, 891 insertions(+), 16073 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
@@ -82,7 +82,7 @@ the historical Haskell frontend has been removed.
- `.hs` - Haskell source
- `.tri` - tricu language source (used in `lib/`, `test/`, `demos/`)
- `.arboricx` - Portable executable bundle
-- `.dag` - Serialized kernel DAG (used by `gen_kernel.zig` at build time)
+- `.dag` - Serialized kernel DAG
### Haskell tests
diff --git a/docs/build-and-run.md b/docs/build-and-run.md
@@ -8,10 +8,8 @@ and [PHP](../ext/php/README.md). A [core-only C prototype](../ext/haskell/README
provides lazy tree reduction and value codecs for applications with their own IO:
`nix build .#tricu-core-archive --no-link`. This x86-64 Linux variant links PIC
Haskell archives into the shared object; `tricu-core` retains the dynamic-library
-baseline. The [Zig runtime and owned-handle C ABI](../ext/zig/README.md)
-have their own Nix validation targets. The
-[cross-runtime reproduction workflow](compiler-rebuilding.md.html#isolated-cross-runtime-fixed-point-reproduction)
-verifies identical compiler generations on Haskell and Zig.
+baseline. The [compiler rebuilding guide](compiler-rebuilding.md.html#execution-interface-selection)
+covers CLI/shared-library execution checks and compiler acceptance.
## Build the host
diff --git a/docs/compiler-rebuilding.md b/docs/compiler-rebuilding.md
@@ -59,29 +59,35 @@ selection uses `sourceMapBuildStable` or `sourceMapBuild`, while application
selection uses the chosen application bundle. Both paths require the selected
backend and entrypoint.
-### Execution-host selection
-
-`--execution-host zig --zig-library /nix/store/.../lib/libarboricx.so` selects
-Zig execution through the owned ABI. `--tricu` selects the Haskell runtime for
-authenticated input transport and output packaging. The selected execution host
-performs compilation and publication; the default is Haskell. `--backend`
-independently selects the lowering backend. Missing libraries or exports fail
-preparation.
-
-Provenance records the execution and transport runtimes separately. Historical
-records lacking `execution_host` describe Haskell execution. Zig records separate
-fingerprints for its shared library and Python launcher. The shared supervisor
-checks source consistency, independently audits the store, enforces publication
-and whole-job deadlines, and records completion status. Zig also applies an
-address-space ceiling, CPU ceiling, and an 8 MiB native stack.
-
-IO v1 routes grant broad file IO to trusted compiler jobs. Native Haskell and Zig
-IO v2 use OS process authority without legacy grants. The Zig executor calls the
-same v2 interpreter as its CLI through the explicit owned-ABI entrypoint. Use OS isolation to restrict filesystem access. Small-closure validation
-uses `test/cross_host_generation.py`, documented in the
-[Zig runtime guide](../ext/zig/README.md#small-compiler-probe). Ordinary pair
-acceptance and retention toolchain roles use Haskell generations; the cross-host
-verifier compares Haskell and Zig generations.
+### Execution-interface selection
+
+The default `--execution-host haskell` uses the native CLI. Select
+`--execution-host haskell-ffi --haskell-library /nix/store/.../lib/libtricu-haskell.so`
+for the full [Haskell embedding ABI](../ext/haskell/README.md). Both execute the
+same canonical reducer and IO-v2 driver. `--tricu` supplies authenticated input
+transport, independent store auditing and output packaging for either interface.
+`--backend` independently selects the lowering backend. Missing or incompatible
+libraries and exports fail preparation, without interpreter fallback.
+
+Provenance binds the execution library, executor and Python binding separately
+from the transport executable. The executor rechecks the exact six arguments,
+IO version and captured bundle identities before effects. Supervision enforces
+publication, CPU, RSS/address-space and whole-job bounds. The shared library uses
+one GHC capability; `--capabilities` controls CLI execution and transport.
+
+The shared-library executor accepts IO v2 with OS process authority. Historical
+v1 jobs require a retained CLI runtime. Use OS isolation for path confinement.
+Bounded cold/warm, syntax, invalidation and corruption controls use:
+
+```sh
+nix build .#tricu-generation-ffi-test --no-link
+```
+
+These checks compare complete receipts and object inventories against the CLI;
+they compare interfaces, not independent evaluator implementations. Ordinary
+pair acceptance and retention toolchain roles use CLI Haskell generations.
+Historical records retain their original runtime identities and use their frozen
+validation tools; current execution selection supports Haskell interfaces.
### Explicit IO framing and transitions
@@ -128,7 +134,7 @@ retention probes select the recorded produced version explicitly.
An opt-in preparation/control builds a v2 map-builder and adapter using the retained
compiler, writes a versioned kit, stages the full v2 map and builds only
-`native_math`. It compares the complete manifest with a v1 execution control and
+`native_math`. It compares the complete manifest with the authenticated packaged kit and
rejects conflicting versions, missing transition adapters and unsupported hosts:
```sh
@@ -137,97 +143,31 @@ python3 test/io_v2_generation.py "$TRICU" --directory /tmp/new-v2-generation-pro
This probe covers preparation and one module. It also checks mixed native, unit
and reexport maps: exact overlays and independently authenticated raw/overlay
-digests agree with the packaged compiler, including on Zig when selected.
+digests agree with the packaged compiler, including through the shared library
+when `--haskell-library` is selected.
Full pair acceptance requires source
freezing, complete reproduction, smokes and rolling retention. Accepted native v2 pairs support
-[recovery archives](#recovery-archives); seed-only v2 archives remain rejected. The
-cross-host generation experiment accepts explicitly selected v2 seeds and maps.
-Zig native executes the v2 compiler/file profile, but bounded workflow smokes
-are not full cross-host fixed-point acceptance. Never relabel a
-v2 kit as v1 to bypass that boundary. Application `provenance` capture/rebuild uses a separate
-[versioned descriptor and explicit trust gate](modules.md.html#retained-build-inputs-and-reproduction),
-not these generation records.
-
-### Isolated cross-runtime fixed-point reproduction
-
-The Linux workflow builds two Haskell generations, then uses the second compiler
-to build a Haskell control and a Zig-executed third generation:
-
-```text
-seed --Haskell--> H1 --Haskell--> H2 --Haskell--> H3
- `----Zig----> Z3
-```
+[recovery archives](#recovery-archives); seed-only v2 archives remain rejected. The native CLI and shared-library probes compare interfaces of the same Haskell
+implementation; independently expected semantic and IO controls remain separate.
+Application `provenance` capture/rebuild uses its own
+[versioned descriptor and explicit trust gate](modules.md.html#retained-build-inputs-and-reproduction).
-```sh
-nix develop -c python3 test/bootstrap_cross_host.py \
- --source-commit COMMITTED_SOURCE --directory "$SCRATCH/cross-runtime-new" \
- --tricu "$TRICU" --zig-library "$ZIG/lib/libarboricx.so"
-```
-
-The default remains the historical v1 experiment. Select a v2 cohort explicitly:
-
-```sh
-# An authenticated caller-selected v2 kit is mandatory; never relabel a v1 kit.
-nix develop -c python3 test/bootstrap_cross_host.py \
- --source-commit COMMITTED_SOURCE --directory "$SCRATCH/cross-runtime-v2-new" \
- --io-version 2 --bootstrap-kit "$V2_KIT" \
- --tricu "$TRICU" --zig-library "$ZIG/lib/libarboricx.so"
-```
+### Shared-library program observation
-Use the same selectors with `--probe` first. The seed manifest and both verified
-payloads are copied into `seed-kit`; later jobs never reopen the external kit.
-Version2 selects the committed `bootstrap/compiler-source-map.io-v2.v1` map.
-Derived kits carry the generation's **produced** framing and authenticated
-produced adapter, independently of its execution framing. Continuation requires
-the same selected version, frozen seed, sources and runtimes. These selectors
-enable an experiment; they do not constitute acceptance or change defaults.
-
-Select exact Nix runtime paths and a new output directory. Python itself must
-resolve inside `/nix/store` (as in `nix develop`); a profile symlink outside the
-mounted tree cannot be executed inside the sandbox. The source commit
-supplies the frozen map, sources, kit and inner generation runner. Each job uses
-a fresh store in a filesystem sandbox containing its source snapshot, selected
-compiler/adapter kit, Nix dependencies and writable work directory. Haskell
-performs common transport and packing; the selected execution host performs
-compilation and publication. Jobs run sequentially with fixed limits of 32 GiB
-and one hour per job; Haskell uses eight capabilities. See the
-[isolated probe](../ext/zig/README.md#isolated-mapped-library-probe) for namespace
-requirements and bounded validation before a full run.
-
-`experiment.json` records source/archive hashes, mapped inputs, runtime/Python
-identities, tooling and comparison boundaries. Logs, invocations and runtime
-provenance identify each execution separately. The independent verifier checks
-H1/H2 and H2/H3 acceptance, verifies that Z3 used H2's exact compiler, and compares
-the actual H2/H3/Z3 object inventories, module checkpoints/receipts and all three
-packaged compiler artifacts. `accepted-cross-host.json` records the verified
-reproduction result. `--verify-only` rechecks existing artifacts and provenance.
-
-Post-generation validation runs the H2 program smoke and checks its complete
-results on Zig:
+`test/shared_library_smoke.py` compiles a small import/recursion/invoice program
+with the packaged stable compiler, independently audits its workspace, and
+compares twelve complete results with both the CLI and Haskell C ABI:
```sh
-nix develop -c python3 test/bootstrap_smoke.py "$TRICU" \
- --generation "$EXPERIMENT/H2/generation" \
- --acceptance "$EXPERIMENT/accepted-pair.json" \
- --source-root "$EXPERIMENT/source" --directory "$EXPERIMENT/haskell-smoke"
-nix develop -c python3 test/bootstrap_cross_host_smoke.py "$EXPERIMENT" \
- --library "$ZIG/lib/libarboricx.so" --directory "$EXPERIMENT/zig-program-smoke"
-nix develop -c python3 test/bootstrap_cross_host_controls.py "$EXPERIMENT" \
- --directory "$SCRATCH/cross-host-controls-new"
+nix build .#tricu-generation-ffi-test --no-link
```
-The program smoke covers native imports, recursive lists, and invoice totals and
-decimal text against independent complete trees. Haskell executes the program's
-compilation using the bit-identical accepted compiler; both hosts execute the
-resulting program. Negative controls mutate independent copies and require the
-verifier to reject false host/runtime/freshness/compiler claims, corrupt objects,
-changed packaged artifacts and additional valid objects. Restored copies must
-pass again.
-
-Finish rolling retention with H2 as the current Haskell toolchain and an
-independently useful rollback. Protect H3/Z3, frozen inputs, cross-host reports,
-and a registered Nix root for the Zig runtime as additional evidence. Common
-preparation, transport and packaging use Haskell throughout this workflow.
+The same opt-in gate runs cold/warm generation-runner jobs, source invalidation,
+corruption/interruption/restart and exact object/receipt comparisons, plus the
+scoped `native_math` map-builder controls. These are bounded application/library
+jobs, not compiler-generation pairs. Full compiler checkpoint acceptance and
+rolling retention use the ordinary CLI procedures below. Existing historical
+cross-runtime evidence retains its original frozen tools and runtime identities.
### Explicit bootstrap kit
diff --git a/docs/host-abi.md b/docs/host-abi.md
@@ -249,8 +249,7 @@ Native execution uses OS process authority and explicit resource ownership. The
wire shapes, ordinary Results, terminal outcomes, cancellation and cleanup.
Native Haskell supports filesystem, TCP, process/context and clock facilities.
-Zig supports the compiler/file profile; browser hosts support core/task and
-terminal operations. Query capabilities for the selected backend
+The Haskell browser host supports core/task and terminal operations. Query capabilities for the selected backend
before choosing a facility.
## Source bytes and publication
diff --git a/ext/demo-site/README.md b/ext/demo-site/README.md
@@ -84,55 +84,34 @@ Safari, real IME and screen-reader behavior require device/manual validation.
## Runtime selection
-The **Runtime** selector offers **Haskell** (default) and **Zig**. Haskell is the reference evaluator; the Haskell and Zig browser
-backends run as WASM. Selection is explicit and page-local; failures never silently switch
-backends. Changing runtime discards the old runtime, preserves source
-text, and automatically recompiles a draft after switching. Published selections reload on
-the chosen runtime. Switching is disabled while work is active; Stop first.
-
-The JavaScript evaluator is retired as a user-selectable backend: its compilation
-latency is unsuitable for the live-compilation workflow. Its independent graph,
-codec and compiler conformance coverage remains. Test-only JS Workers and their
-checkpoint protocol live under `src/conformance/` (generated into
-`dist/conformance/`) and are excluded from the production release. Shared JavaScript authentication, literal codecs and tree formatting remain in
-use alongside the native runtimes.
+The playground uses the canonical Haskell evaluator compiled to WebAssembly.
+The Runtime control identifies **Haskell**; invalid backend selections reject
+without evaluator fallback. Restarting a runtime preserves page-owned source and
+local modules and recompiles drafts. Stop terminates the owning Worker tree.
+
+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/`. Source compilation loads the compiler kit on demand, retaining the compiled
-definitions. Published definition inspection and named IO do not need a compiler. Draft compilation checks the runtime's original compiler-bundle
-identity against `compiler.json`, then resolves explicit native imports or the
-selected empty, practical or default-contract standalone environment. Missing assets or mismatched pins are errors.
-Zig packages a freestanding reactor and private adapter under `runtime/zig/`.
-It has no WASI imports. Its compiler session fetches the authenticated
-compiler bundle selected by `compiler.json`. Both use the same environment/profile pins and names.
-
-Both runtimes use shared bundle authentication, literal validation, immutable
-stored-definition export and the same page IO protocol. `runtime-session.ts`
-defines the coarse adapter boundary: load, compile, inspectDefinition, probeIO and runDefinitionIO with async IO callbacks,
-diagnostics and disposal. Private pointers, IO tokens and heap checkpoints do not
-cross backend types. `wasm-worker.ts` wraps the selected session;
-`haskell-session.ts` maps the built Haskell client's protocol, including its
-nested Worker and asynchronous resume tokens. Host cleanup always terminates the
-owning Worker tree; there is no default compile/run timer. Zig's private
-`ext/wasm/site-session.mjs` owns its handles, serializes operations, and retains
-IO continuations across awaited host callbacks. Its exclusive async scope frees
-all transient handles on success or failure; compiled definitions remain rooted.
-Zig's site demands, bulk Unicode decoding and bounded previews explicitly select
-unbounded work, not a large substitute fuel value. The native ABI's finite-fuel
-operations remain available for native callers and negative-control tests.
-
-WASM diagnostics report linear memory. Haskell's fixed RTS limits are 256 MiB heap and 8 MiB
-Haskell stack; both reactors cap linear memory at 512 MiB. Zig also has an 8 MiB
-WASM stack. Zig's strict scalar decoder rejects redundant high-zero numeral
-representations that the permissive JS text decoder tolerates; valid Unicode
-scalar strings agree. These limits apply to the runtime's heap, stack and linear memory. Exhaustion
-or a fatal trap discards the session and requires explicit reinitialization.
-
-The Haskell binary requires WebAssembly SIMD, available starting with Chrome 91,
-Firefox 89 and Safari/iOS 16.4. Integration tests use Chromium 148, Firefox 150
-and Linux WebKit. Runtime selection is explicit; clients can select Zig if
-Haskell initialization fails. Haskell uses a bundled WASI shim and a nested
-module Worker.
+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
@@ -144,11 +123,11 @@ TEST_IDE=1 TEST_BACKEND=haskell-wasm \
nix run .#arboricxWebkitCheck
```
-Repeat with `TEST_BACKEND=zig-wasm` and with `TEST_MODULES=1`,
+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 the longer
-Zig IDE run, set `TEST_WEBKIT_TIMEOUT_MS=360000` (default: 240000).
+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.
@@ -199,7 +178,7 @@ plain-text delivery. A release check validates interface coverage, exact export
names, relative documentation links and the quickstart's tested example.
Run the picker/reference matrix against a built site using
-`TEST_IMPORT_PICKER=1 TEST_BACKEND=haskell-wasm` or `zig-wasm` with the Chromium,
+`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
@@ -460,9 +439,9 @@ 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 and
-Zig adapter check only the root pair, the seven-character `tricuIO` sentinel and
-version 1. They do not traverse action payloads or continuations. An unsupported
+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.
@@ -573,7 +552,7 @@ not the whole-bundle download hash:
answer = N.add 40 2
```
-Both Haskell and Zig support qualified imports, `!import HASH *`, explicit local
+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
@@ -804,15 +783,13 @@ Footer feedback uses a direct email link.
### Production JavaScript bundles
The Nix release typechecks TypeScript, verifies the checked-in development JS,
-and bundles it with the pinned esbuild. Seven ESM entrypoints preserve the runtime
+and bundles it with the pinned esbuild. Five ESM entrypoints preserve the runtime
boundaries and the directory bases used by `import.meta.url`:
- `dist/app.js`: page UI.
- `dist/wasm-worker.js`: native-runtime session driver.
- `runtime/haskell/client.mjs` and `runtime/haskell/worker.mjs`: Haskell client and
its nested runtime Worker.
-- `runtime/zig/site-session.mjs` and `dist/zig-helpers.js`: on-demand Zig adapter
- and shared helpers.
- `dist/local-module-worker.js`: on-demand local-file validation.
Each file includes its static dependencies, with tree shaking and readable output.
@@ -840,7 +817,7 @@ TEST_COLD_LOAD=1 TEST_ASSET_CACHE=1 BENCHMARK_REPORT=/tmp/arboricx-cold.json \
The report counts successful homepage/runtime requests at the server, excludes
probe traffic, and records uncompressed response-body bytes and time to Ready.
-It checks that compiler assets, Zig and local validation stay lazy. Compare repeated
+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`.
@@ -889,7 +866,7 @@ TEST_ASSET_CACHE=1 TEST_BACKEND=haskell-wasm \
node ext/demo-site/test/backend-browser.mjs unused
```
-Repeat with `TEST_BACKEND=zig-wasm`. For Firefox, use `nixpkgs#firefox` and
+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.
@@ -1123,7 +1100,7 @@ python3 ext/demo-site/prepare-showcase.py result/bin/tricu \
This mode still authenticates compiler inputs, dependencies, native results and
manifest identities. Source-only changes to these catalogue entries also require
an explicit new accepted release. Keep historical named versions immutable.
-The browser module matrix recompiles all ten demos on both backends and compares
+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.
@@ -1178,13 +1155,8 @@ ARBORICX_SITE_ROOT="$SITE" nix shell --inputs-from . nixpkgs#nodejs nixpkgs#chro
node ext/demo-site/test/backend-browser.mjs "$SITE/runtime/haskell" chromium
ARBORICX_SITE_ROOT="$SITE" nix shell --inputs-from . nixpkgs#nodejs nixpkgs#firefox -c \
node ext/demo-site/test/backend-firefox.mjs "$SITE/runtime/haskell" firefox
-# Same real-page suite for Zig, plus full independent golden/embedding checks:
-TEST_BACKEND=zig-wasm ARBORICX_SITE_ROOT="$SITE" nix shell --inputs-from . nixpkgs#nodejs nixpkgs#chromium -c \
- node ext/demo-site/test/backend-browser.mjs "$SITE/runtime/haskell" chromium
-TEST_BACKEND=zig-wasm ARBORICX_SITE_ROOT="$SITE" nix shell --inputs-from . nixpkgs#nodejs nixpkgs#firefox -c \
- node ext/demo-site/test/backend-firefox.mjs "$SITE/runtime/haskell" firefox
-# Manifest-import integration, both production backends and both engines:
-for BACKEND in haskell-wasm zig-wasm; do
+# 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
@@ -1192,8 +1164,8 @@ for BACKEND in haskell-wasm zig-wasm; do
nix shell --inputs-from . nixpkgs#nodejs nixpkgs#firefox -c \
node ext/demo-site/test/backend-firefox.mjs "$SITE/runtime/haskell" firefox
done
-# Local file/export round trip, both backends (also use backend-firefox.mjs):
-for BACKEND in haskell-wasm zig-wasm; do
+# 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 \
@@ -1201,7 +1173,7 @@ for BACKEND in haskell-wasm zig-wasm; do
done
# Native import/run + independent compilation must agree with browser exports:
python3 ext/demo-site/test/local-module-native.py result/bin/tricu \
- /tmp/local-haskell-wasm.json /tmp/local-zig-wasm.json
+ /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
@@ -1228,27 +1200,20 @@ with a local authenticated registry and packaged runtime assets. They check
published programs, IO, stored export, default/empty/practical compilation,
repeated retained runs, output-error recovery, missing-runtime and compiler-pin
failure without fallback, Stop/recompile (including non-returning reduction),
-stepping, stale/wrong-kind/invalid-line resume rejection and backend-switch
-isolation. Invalid responses do not consume a pending request ID. Both WASM
-backends pass the integration suites in Chromium 148, Firefox 150 and Linux WebKit. The
+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.
-The Zig extension of the page suite also checks all 117 complete published
-Haskell result/terminal goldens, all eight source/environment profiles, seven
-codecs and negative controls, physical sharing, nonforcing divergent preview and
-abort payloads, IO limits and GC across input waits, compile/host-error recovery,
-and 50 retained runs with stable owned-handle counts and linear-memory plateau.
Complete bounded observation supplies tree-file result interpretations and
-conformance tests. Scalar interpretations use their bounded codecs. Parity checks
-exercise the common site capabilities through each backend's private protocol.
+conformance tests. Scalar interpretations use their bounded codecs.
-### Matched production-backend benchmark
+### 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 alternates backend order
-across three fresh browser profiles per backend/engine, loads all eight source
+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
@@ -1279,6 +1244,12 @@ 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`.
@@ -1306,7 +1277,7 @@ not alter the site's default backend or impose execution deadlines on users.
`test/native-modules.py` independently checks native CLI import/export behavior,
including header-only reexports, collisions and lexical scope. It also checks
complete canonical function-tree digests in `test/module-goldens.json`; the module
-browser probe compares both runtimes against those digests, not just application
+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.
@@ -1399,7 +1370,6 @@ package, security and runtime conformance controls remain.
SITE=$(nix build .#arboricxSite --no-link --print-out-paths)
TEST_IDE=1 ARBORICX_SITE_ROOT="$SITE" nix shell --inputs-from . nixpkgs#nodejs nixpkgs#chromium -c \
node ext/demo-site/test/backend-browser.mjs "$SITE/runtime/haskell" chromium
-# TEST_BACKEND=zig-wasm selects the other production runtime.
# Use backend-firefox.mjs and nixpkgs#firefox for Firefox.
```
@@ -1425,14 +1395,9 @@ 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.
-The full Zig adapter corpus (the default harness probe with
-`TEST_BACKEND=zig-wasm`, without an opt-in UI-suite flag) also recompiles all
-current and explicitly retained historical demo sources. For that longer run,
-set `TEST_CDP_TIMEOUT_MS=300000` and use `run-bounded.py --seconds 330` around
-Node. Use that longer budget for `TEST_IDE=1 TEST_BACKEND=zig-wasm` as well:
-its repeated full-module compilation and expansion checks can exceed the default
-180-second deadline. `TEST_DIAGNOSTICS=1` logs test phases and read-only page status
-snapshots every 30 seconds; it is off by default, including idle-soak checks.
+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
@@ -1489,8 +1454,7 @@ 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; add `TEST_BACKEND=zig-wasm` for Zig and
-`TEST_VIEWPORT_WIDTH=390` for a narrow layout. The probe covers compiled/private
+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.
@@ -1546,15 +1510,14 @@ intermittent user-reported failure cannot occur.
## IO version boundary
-Both production native adapters require bounded, canonical `tricuIO` version 2
+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.
V2 uses the shared structured task/resource interpreter, not the old single-read
-loop. `IO.capabilities` reports the actual selected backend (`haskell-wasm` or
-`zig-wasm`), browser-terminal authority, core operations, implemented named
+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
@@ -1601,7 +1564,7 @@ application IO use v2. The browser compiler remains the independently pinned
rejection, missing-structured-host rejection before callbacks, and separately
pinned pure compilation. For public v2,
compile `test/io-v2/browser.tri` as below and select `TEST_IO_V2_PUBLIC=1` in the
-same Chromium harness on both backends. It exercises compiled direct/handled
+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.
@@ -1611,8 +1574,7 @@ operation rejection, codec-error recovery and real-page structured Stop/reuse.
`io-v2-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
-`IOV2.Wire` (shared by native Haskell and its browser embedding) and Zig's wire
-reader. This is not a capability declaration or a v2 execution entrypoint.
+`IOV2.Wire`, shared by native Haskell and its browser embedding. This is not a capability declaration or a v2 execution entrypoint.
```sh
nix build .#arboricxSite --no-link
@@ -1620,14 +1582,11 @@ SITE=$(nix path-info .#arboricxSite)
TEST_IO_V2_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
-# Repeat with TEST_BACKEND=zig-wasm.
```
-The private Haskell worker inspection operation and the Zig WASM adapter are
+The private Haskell worker inspection operation is
checked against independent constructor-only bundles: ten core headers, future
-facility headers, exact bounds and malformed encodings. The test server exposes
-the same Zig adapter source separately because release bundling inlines it.
-Reference-graph tests additionally trap forbidden payload/excess-head/excess-tail
+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.
@@ -1711,10 +1670,9 @@ embedding rethrows the saved fault. No hard pure-reduction deadline is implied.
The private `io-v2-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. Zig uses
-its native owned handles and `sweepScope()` inside an exclusive async scope.
-Neither sends evaluator handles over RuntimeSession or substitutes the JS
-reference evaluator. Private providers reject all named facilities as ordinary
+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.
@@ -1726,7 +1684,6 @@ SITE=$(nix path-info .#arboricxSite)
TEST_IO_V2_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
-# Repeat with TEST_BACKEND=zig-wasm.
```
These constructor-fixture probes cover actual native callback application, lexical
@@ -1743,7 +1700,7 @@ 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-v2-session-start`/`io-v2-session-turn`
requests, keeping the tree table alive across turns and rejecting unrelated work
-until settlement. Zig uses the same session inside its exclusive owned scope.
+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
@@ -1801,7 +1758,6 @@ IO_V2_SOURCE_BUNDLE="$WORK/browser.arboricx" TEST_IO_V2_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
-# Repeat with TEST_BACKEND=zig-wasm and the identical bundle.
```
The test-only server route serves the explicitly selected bundle and its SHA-256;
diff --git a/ext/demo-site/bundle-assets.py b/ext/demo-site/bundle-assets.py
@@ -7,7 +7,6 @@ import shutil
import subprocess
ENTRIES = ['dist/app.js', 'dist/wasm-worker.js', 'dist/local-module-worker.js',
- 'dist/zig-helpers.js', 'runtime/zig/site-session.mjs',
'runtime/haskell/client.mjs', 'runtime/haskell/worker.mjs']
diff --git a/ext/demo-site/dist/runtime-backends.js b/ext/demo-site/dist/runtime-backends.js
@@ -1,5 +1,4 @@
export const backends = {
- 'zig-wasm': { label: 'Zig', worker: './wasm-worker.js?backend=zig-wasm' },
'haskell-wasm': { label: 'Haskell', worker: './wasm-worker.js?backend=haskell-wasm' },
};
export function backend(value) {
diff --git a/ext/demo-site/dist/runtime-session.js b/ext/demo-site/dist/runtime-session.js
@@ -1,9 +1,5 @@
import { HaskellSession } from './haskell-session.js';
export async function createRuntimeSession(name, compiler) {
- if (name === 'zig-wasm') {
- const module = await import(new URL('../runtime/zig/site-session.mjs', import.meta.url).href);
- return module.create({ base: new URL('../', import.meta.url), compiler });
- }
if (name !== 'haskell-wasm')
throw new Error('Unsupported runtime backend');
return HaskellSession.create(compiler);
diff --git a/ext/demo-site/dist/zig-helpers.js b/ext/demo-site/dist/zig-helpers.js
@@ -1,15 +0,0 @@
-// Lazy shared-helper entrypoint for the Zig adapter; bundled as one release file.
-export * as bundle from './bundle.js';
-export * as codec from './codecs.js';
-export * as comp from './compiler.js';
-export * as contracts from './contracts.js';
-export * as packages from './packages.js';
-export * as expressions from './expression.js';
-export * as treeResults from './tree-result.js';
-export * as modules from './module-stage.js';
-export * as moduleBundle from './module-bundle.js';
-export * as ioV2Protocol from './io-v2-protocol.js';
-export * as ioV2Private from './io-v2-private.js';
-export * as ioV2Session from './io-v2-session.js';
-export * as ioV2Profile from './io-v2-profile.js';
-export * as ioV2Driver from './io-v2-driver.js';
diff --git a/ext/demo-site/index.html b/ext/demo-site/index.html
@@ -42,7 +42,7 @@
<section id="editor" class="workspace" aria-label="tricu editor">
<div class="editor-toolbar" data-inspector-background>
<div class="toolbar-selection"><label for="program-picker">Program</label><select id="program-picker" disabled><option value="">Loading Names and values…</option></select></div>
- <div class="toolbar-runtime"><label for="runtime-backend">Runtime</label><select id="runtime-backend"><option value="haskell-wasm" selected>Haskell</option><option value="zig-wasm">Zig</option></select></div>
+ <div class="toolbar-runtime"><label for="runtime-backend">Runtime</label><select id="runtime-backend"><option value="haskell-wasm" selected>Haskell</option></select></div>
<div class="toolbar-actions"><button id="add-import" disabled>Explore libraries…</button><button id="stop" disabled hidden>Stop</button><button id="reload-playground" title="Reload the page to restart all runtimes. Local drafts, output and opened modules are cleared.">Reload Playground</button><button id="settings-toggle" aria-controls="settings-panel" aria-expanded="false">Settings</button></div>
<span id="status" role="status">Loading Names and values</span>
</div>
diff --git a/ext/demo-site/reference-input/getting-started.md b/ext/demo-site/reference-input/getting-started.md
@@ -3,7 +3,7 @@
tricu is the source language. Arboricx bundles carry its compiled, named tree
values between runtimes. The playground at [arbori.cx](https://arbori.cx/) lets
you edit modules, inspect compiled definitions and explicitly run IO using Haskell
-or Zig WebAssembly.
+WebAssembly.
**Basic Examples** starts with Names and values, then introduces arithmetic,
functions, lists, transformations and choices. Each lesson includes definitions,
diff --git a/ext/demo-site/release.nix b/ext/demo-site/release.nix
@@ -1,4 +1,4 @@
-{ pkgs, haskellRuntime, zigRuntime }:
+{ pkgs, haskellRuntime }:
let
siteSource = pkgs.lib.cleanSourceWith {
src = ./.;
@@ -43,10 +43,6 @@ let
chmod -R u+w $out/dist
# JS evaluator drivers/checkpoints are conformance tools, not a site backend.
rm -r $out/dist/conformance
- mkdir -p $out/runtime/zig
- cp ${zigRuntime}/lib/arboricx.wasm $out/runtime/zig/
- cp ${../wasm/runtime.mjs} $out/runtime/zig/runtime.mjs
- cp ${../wasm/site-session.mjs} $out/runtime/zig/site-session.mjs
mkdir -p $out/runtime/haskell
cp ${haskellRuntime}/{client.mjs,worker.mjs,bridge.mjs,pins.mjs,reactor.wasm} $out/runtime/haskell/
cp -r ${haskellRuntime}/{shim,data,site} $out/runtime/haskell/
diff --git a/ext/demo-site/src/runtime-backends.ts b/ext/demo-site/src/runtime-backends.ts
@@ -1,8 +1,7 @@
import type { GraphStats } from './graph.js';
-export type Backend = 'haskell-wasm' | 'zig-wasm';
+export type Backend = 'haskell-wasm';
export type RuntimeStats = GraphStats | { backend: string; linearMemoryBytes: number };
export const backends = {
- 'zig-wasm': { label: 'Zig', worker: './wasm-worker.js?backend=zig-wasm' },
'haskell-wasm': { label: 'Haskell', worker: './wasm-worker.js?backend=haskell-wasm' },
} as const;
export function backend(value: string): Backend {
diff --git a/ext/demo-site/src/runtime-session.ts b/ext/demo-site/src/runtime-session.ts
@@ -24,10 +24,6 @@ export interface RuntimeSession {
dispose(): void;
}
export async function createRuntimeSession(name: string | null, compiler: boolean): Promise<RuntimeSession> {
- if (name === 'zig-wasm') {
- const module = await import(new URL('../runtime/zig/site-session.mjs', import.meta.url).href) as { create(options: { base: URL; compiler: boolean }): Promise<RuntimeSession> };
- return module.create({ base: new URL('../', import.meta.url), compiler });
- }
if (name !== 'haskell-wasm') throw new Error('Unsupported runtime backend');
return HaskellSession.create(compiler);
}
diff --git a/ext/demo-site/src/zig-helpers.ts b/ext/demo-site/src/zig-helpers.ts
@@ -1,16 +0,0 @@
-// Lazy shared-helper entrypoint for the Zig adapter; bundled as one release file.
-export * as bundle from './bundle.js';
-export * as codec from './codecs.js';
-export * as comp from './compiler.js';
-export * as contracts from './contracts.js';
-export * as packages from './packages.js';
-export * as expressions from './expression.js';
-export * as treeResults from './tree-result.js';
-export * as modules from './module-stage.js';
-export * as moduleBundle from './module-bundle.js';
-export * as ioV2Protocol from './io-v2-protocol.js';
-export * as ioV2Private from './io-v2-private.js';
-export * as ioV2Session from './io-v2-session.js';
-
-export * as ioV2Profile from './io-v2-profile.js';
-export * as ioV2Driver from './io-v2-driver.js';
diff --git a/ext/demo-site/test/asset-release.py b/ext/demo-site/test/asset-release.py
@@ -14,8 +14,7 @@ with tempfile.TemporaryDirectory() as temporary:
entries = ['dist/app.js', 'dist/wasm-worker.js', 'dist/local-module-worker.js',
'dist/dependency.js', 'runtime/haskell/worker.mjs',
'runtime/haskell/shim/index.js', 'runtime/haskell/reactor.wasm',
- 'runtime/haskell/data/compiler.dag', 'runtime/zig/site-session.mjs',
- 'runtime/zig/arboricx.wasm', 'catalogue.json', 'packages.json',
+ 'runtime/haskell/data/compiler.dag', 'catalogue.json', 'packages.json',
'compiler.json', 'environments.json', 'style.css', 'theme.js', 'images/icon.svg',
'reference/libraries/example.md', 'reference/libraries.md', 'reference/getting-started.md', 'reference/libraries.json', 'images/favicon.png', 'images/apple-touch-icon.png', 'images/preview.png']
def build(number, changed=None, reverse=False):
diff --git a/ext/demo-site/test/backend-benchmark.mjs b/ext/demo-site/test/backend-benchmark.mjs
@@ -2,7 +2,7 @@
export async function run(control,backend,memoryOnly=false){
const base=new URL('../',document.querySelector('script[type=module][src]').src);
const [fixtures,catalogue,{Graph},{view}]=await Promise.all([
- fetch('/zig-fixtures.json').then(r=>r.json()),fetch(new URL('catalogue.json',base)).then(r=>r.json()),
+ fetch('/program-fixtures.json').then(r=>r.json()),fetch(new URL('catalogue.json',base)).then(r=>r.json()),
import(new URL('dist/probe-api.js',base)).then(m=>m.graph),import(new URL('dist/probe-api.js',base)).then(m=>m.codecs),
]);
const assert=(ok,message)=>{if(!ok)throw Error(message);};
diff --git a/ext/demo-site/test/backend-probe.mjs b/ext/demo-site/test/backend-probe.mjs
@@ -34,9 +34,8 @@ export async function run(control,backend='haskell-wasm'){
document.querySelector('[data-compiler-example=identity]').click();input('source-editor','answer = 42\nloop = (x : x x) (x : x x)');await status('Compiled');
document.querySelector('[aria-label="Inspect compiled definition loop"]').click();await wait(()=>el('status').textContent==='Inspecting loop','CPU-bound observation');await pause(100);el('stop').click();
assert(!el('compile').disabled,'Stop discards runtime and permits recompile');
- const source=globalThis.__sourceEditor.getSource();change('runtime-backend',backend==='haskell-wasm'?'zig-wasm':'haskell-wasm');await status('Compiled');assert(globalThis.__sourceEditor.getSource()===source,'Backend switching preserves source');
- assert(await inspect('answer','number')==='42','Backend switch rebuilds independent definitions');
+ const source=globalThis.__sourceEditor.getSource();change('runtime-backend',backend);await status('Compiled');assert(globalThis.__sourceEditor.getSource()===source,'Runtime restart preserves source');
+ assert(await inspect('answer','number')==='42','Runtime restart rebuilds independent definitions');
await(await import('/backend-resume-probe.mjs')).probeResume(backend);
- const parity=backend==='zig-wasm'?await(await import('/zig-session-probe.mjs')).run(control):null;
- return {...report,parity,checks:[...report.checks,'real Haskell default; package/practical compilation; native statistics; decoder recovery; artifact/pin failures without fallback; CPU-bound Stop; backend isolation; stale/malformed resume rejection']};
+ return {...report,checks:[...report.checks,'real Haskell default; package/practical compilation; native statistics; decoder recovery; artifact/pin failures without fallback; CPU-bound Stop; backend isolation; stale/malformed resume rejection']};
}
diff --git a/ext/demo-site/test/backend-server.mjs b/ext/demo-site/test/backend-server.mjs
@@ -3,7 +3,7 @@ import {createServer} from 'node:http';
import {readFile} from 'node:fs/promises';
import {resolve,extname,sep} from 'node:path';
import {randomBytes,createHash} from 'node:crypto';
-import {fixtures} from './zig-negative-fixtures.mjs';
+import {fixtures} from './runtime-fixtures.mjs';
export async function startSiteServer(directory,autoRun=false) {
const root=resolve(process.env.ARBORICX_SITE_ROOT||'ext/demo-site'),runtime=resolve(directory),registry=resolve('ext/demo-site/bundles');
const assetManifest=await readFile(resolve(root,'assets.json'),'utf8').then(JSON.parse).catch(()=>null);
@@ -37,9 +37,8 @@ export async function startSiteServer(directory,autoRun=false) {
if(path==='/definition-names-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./definition-names-probe.mjs',import.meta.url)));return;}
if(path==='/inline-io-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./inline-io-probe.mjs',import.meta.url)));return;}
if(path==='/asset-cache-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./asset-cache-probe.mjs',import.meta.url)));return;}
- if(path==='/zig-negative.json'){res.setHeader('Content-Type','application/json');res.end(JSON.stringify(fixtures()));return;}
- if(path==='/zig-fixtures.json'){res.setHeader('Content-Type','application/json');const data=JSON.parse(await readFile(new URL('../demos.json',import.meta.url)));for(const id of ['parser','lexer'])data.demos[id]=JSON.parse(await readFile(new URL('../'+id+'-demo.json',import.meta.url)));res.end(JSON.stringify(data));return;}
- if(path==='/io-v2-runtime.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('../../wasm/runtime.mjs',import.meta.url)));return;}
+ if(path==='/runtime-fixtures.json'){res.setHeader('Content-Type','application/json');res.end(JSON.stringify(fixtures()));return;}
+ if(path==='/program-fixtures.json'){res.setHeader('Content-Type','application/json');const data=JSON.parse(await readFile(new URL('../demos.json',import.meta.url)));for(const id of ['parser','lexer'])data.demos[id]=JSON.parse(await readFile(new URL('../'+id+'-demo.json',import.meta.url)));res.end(JSON.stringify(data));return;}
if(path==='/io-v2-lifecycle-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./io-v2-lifecycle-probe.mjs',import.meta.url)));return;}
if(path==='/io-v2-source.json'&&process.env.IO_V2_SOURCE_BUNDLE){const bytes=await readFile(process.env.IO_V2_SOURCE_BUNDLE);res.setHeader('Content-Type','application/json');res.end(JSON.stringify({bytes:[...bytes],hash:createHash('sha256').update(bytes).digest('hex')}));return;}
if(path==='/io-v2-public-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./io-v2-public-probe.mjs',import.meta.url)));return;}
@@ -48,7 +47,6 @@ export async function startSiteServer(directory,autoRun=false) {
if(path==='/io-v2-core-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./io-v2-core-probe.mjs',import.meta.url)));return;}
if(path==='/io-v2-protocol-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./io-v2-protocol-probe.mjs',import.meta.url)));return;}
if(path==='/io-version-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./io-version-probe.mjs',import.meta.url)));return;}
- if(path==='/zig-session-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./zig-session-probe.mjs',import.meta.url)));return;}
if(path==='/backend-resume-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./backend-resume-probe.mjs',import.meta.url)));return;}
if(path==='/backend-benchmark.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./backend-benchmark.mjs',import.meta.url)));return;}
if(path==='/syntax-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./syntax-probe.mjs',import.meta.url)));return;}
@@ -60,7 +58,7 @@ export async function startSiteServer(directory,autoRun=false) {
if(path==='/ide-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./ide-probe.mjs',import.meta.url)));return;}
if(path==='/live-ux-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./live-ux-probe.mjs',import.meta.url)));return;}
if(path==='/backend-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./backend-probe.mjs',import.meta.url)));return;}
- if(failRuntime&&(path.endsWith('/reactor.wasm')||path.endsWith('/arboricx.wasm')))throw Error('Injected artifact failure');
+ if(failRuntime&&path.endsWith('/reactor.wasm'))throw Error('Injected artifact failure');
if(compilerMismatch&&path.endsWith('/compiler.json')) {
const descriptor=JSON.parse(await readFile(resolve(assetRoot,'compiler.json')));
res.setHeader('Content-Type','application/json');res.end(JSON.stringify({...descriptor,hash:'0'.repeat(64)}));return;
diff --git a/ext/demo-site/test/benchmark-backends.mjs b/ext/demo-site/test/benchmark-backends.mjs
@@ -9,7 +9,7 @@ if(!site||!output)throw Error('Usage: benchmark-backends.mjs PACKAGED_SITE OUTPU
await mkdir(output,{recursive:true});
const reports=[],rounds=Number(process.env.BENCHMARK_ROUNDS??3);
if(!Number.isInteger(rounds)||rounds<1||rounds>10)throw Error('BENCHMARK_ROUNDS must be 1–10');
-for(const browser of ['chromium','firefox'])for(let round=0;round<rounds;round++)for(const backend of round%2?['haskell-wasm','zig-wasm']:['zig-wasm','haskell-wasm']){
+for(const browser of ['chromium','firefox'])for(let round=0;round<rounds;round++)for(const backend of ['haskell-wasm']){
const file=resolve(output,`${browser}-${backend}-${round}.json`);
console.log('Benchmark',browser,backend,round);
const child=spawn(process.execPath,[`ext/demo-site/test/backend-${browser==='chromium'?'browser':'firefox'}.mjs`,resolve(site,'runtime/haskell'),browser],{
@@ -19,7 +19,7 @@ for(const browser of ['chromium','firefox'])for(let round=0;round<rounds;round++
reports.push({browser,round,...JSON.parse(await readFile(file,'utf8'))});
}
const artifacts={};
-for(const [backend,name] of [['zig','arboricx'],['haskell','reactor']]){
+for(const [backend,name] of [['haskell','reactor']]){
const bytes=await readFile(resolve(site,`runtime/${backend}/${name}.wasm`));artifacts[backend]={bytes:bytes.length,sha256:createHash('sha256').update(bytes).digest('hex')};
}
const fixtures={};
diff --git a/ext/demo-site/test/cold-load-probe.mjs b/ext/demo-site/test/cold-load-probe.mjs
@@ -9,6 +9,6 @@ export async function run(control){
const bundled=manifest.files.filter(r=>/\.(?:m?js)$/.test(r.path)).length===7;
const js=transfers.filter(r=>/\.(?:m?js)(?:\?|$)/.test(r.path));
if(bundled&&js.length!==4)throw Error('Bundled homepage must load four JS entrypoints');
- if(transfers.some(r=>/compiler\.(?:json|dag)|environment\.tsv|runtime\/zig|local-module-worker|reference\//.test(r.path)))throw Error('Homepage fetched a lazy asset');
- return {userAgent:navigator.userAgent,checks:['cold homepage ready; compiler, Zig, local validation and library references stay lazy'],requests:transfers.length,jsRequests:js.length,bytes:transfers.reduce((n,r)=>n+r.bytes,0),jsBytes:js.reduce((n,r)=>n+r.bytes,0),readyMs:globalThis.__readyMs,transfers};
+ if(transfers.some(r=>/compiler\.(?:json|dag)|environment\.tsv|local-module-worker|reference\//.test(r.path)))throw Error('Homepage fetched a lazy asset');
+ return {userAgent:navigator.userAgent,checks:['cold homepage ready; compiler, local validation and library references stay lazy'],requests:transfers.length,jsRequests:js.length,bytes:transfers.reduce((n,r)=>n+r.bytes,0),jsBytes:js.reduce((n,r)=>n+r.bytes,0),readyMs:globalThis.__readyMs,transfers};
}
diff --git a/ext/demo-site/test/ide-probe.mjs b/ext/demo-site/test/ide-probe.mjs
@@ -4,7 +4,7 @@ const assert=(condition,message)=>{if(!condition)throw Error(message);};
export async function run(control,backend='haskell-wasm'){
const phase=name=>fetch(control+'/phase',{method:'POST',body:JSON.stringify({name:'ide: '+name})});
await status('Ready');await syntaxProbe();await status('Ready');
- assert([...el('runtime-backend').options].map(o=>o.value).join(',')==='haskell-wasm,zig-wasm','Only production runtimes');
+ assert([...el('runtime-backend').options].map(o=>o.value).join(',')==='haskell-wasm','Only production runtimes');
for(const path of ['worker.js','compiler-worker.js','conformance/worker.js','conformance/compiler-worker.js'])assert((await fetch(new URL('./'+path,document.querySelector('script[type=module][src]').src))).status===404,'JS conformance Workers not shipped');
assert(!el('expression-editor')&&card().hidden,'Arrival is source-first and does not inspect automatically');
el('settings-toggle').click();assert(!el('settings-panel').hidden,'Settings opens');el('settings-close').click();assert(document.activeElement===el('settings-toggle'),'Settings returns focus');
diff --git a/ext/demo-site/test/io-v2-core-probe.mjs b/ext/demo-site/test/io-v2-core-probe.mjs
@@ -2,36 +2,11 @@
// envelopes and terminal providers remain gated; no JS evaluator substitution.
export async function run(_control,backend){
const base=new URL('../',document.querySelector('script[type=module][src]').src);
- const fixtures=await(await fetch('/zig-negative.json')).json(),checks=[];
+ const fixtures=await(await fetch('/runtime-fixtures.json')).json(),checks=[];
let execute,dispose;
- if(backend==='haskell-wasm'){
const {CompilerWorker}=await import(new URL('runtime/haskell/client.mjs',base));
const client=new CompilerWorker();dispose=()=>client.dispose();await client.request({type:'init',compiler:false});
execute=async fixture=>{await client.request({type:'load',bytes:new Uint8Array(fixture.bytes).buffer,hash:fixture.hash});return JSON.parse((await client.request({type:'io-v2-core',name:'root',arguments:[]})).text);};
- }else{
- const {WasmRuntime}=await import('/io-v2-runtime.mjs'),{ioV2Private}=await import(new URL('dist/zig-helpers.js',base));
- const g=await WasmRuntime.create(new Uint8Array(await(await fetch(new URL('runtime/zig/arboricx.wasm',base))).arrayBuffer()));
- dispose=()=>g.dispose();let run=0n;
- await g.scopeAsync(async()=>{
- const kept=g.stem(g.leaf),retired=g.stem(g.leaf),owned=g.stats().owned;
- let rejected=false;try{g.sweepScope([kept,-1n]);}catch(error){rejected=error.message==='Stale interpreter root';}
- if(!rejected||g.stats().owned!==owned)throw Error('Invalid sweep partially retired handles');
- g.sweepScope([kept,g.leaf]);
- if(g.observe(kept).node.kind!=='stem')throw Error('Live sweep root lost');
- rejected=false;try{g.observe(retired);}catch{rejected=true;}
- if(!rejected)throw Error('Retired native alias stayed usable');
- });
- checks.push('invalid root sweep is atomic; retired native handles reject');
- execute=async fixture=>{
- const owned=g.stats().owned;
- const out=await g.scopeAsync(async()=>{
- const action=g.load(new Uint8Array(fixture.bytes),'root');
- const tree=ioV2Private.constructors({leaf:g.leaf,observe:g.observe.bind(g),stem:g.stem.bind(g),fork:g.fork.bind(g),app:g.app.bind(g)});
- return ioV2Private.runPrivate(tree,action,++run,roots=>g.sweepScope(roots));
- });
- if(g.stats().owned!==owned)throw Error('Native owned handle history retained');return out;
- };
- }
const cases=[['pure','completed'],['bind','completed'],['scope','completed'],['child-0','failed'],['child-1','completed'],['unsupported','completed'],['discard-invalid','completed'],['terminal-2','failed'],['terminal-9','cancelled'],['malformed','failed']];
try{
for(const [name,kind]of cases){
@@ -41,7 +16,7 @@ export async function run(_control,backend){
checks.push(name);
}
for(let i=0;i<5;i++)await execute(fixtures['v2-run-scope']);
- checks.push('repeated run retirement and reuse; native evaluation with per-turn root sweeps');
+ checks.push('repeated run retirement and reuse');
}finally{dispose();}
return {backend,checks};
}
diff --git a/ext/demo-site/test/io-v2-lifecycle-probe.mjs b/ext/demo-site/test/io-v2-lifecycle-probe.mjs
@@ -16,7 +16,7 @@ export async function run(_control,backend){
const release=async id=>{work.release(id,()=>resources.releaseScope(id,r=>r.backend.remove(),e=>e,'cleanup-failed'));await work.wait();const reply=work.take();assert(reply?.outcome.ok,'release failed');return tasks.settle(id,reply.outcome.value);};
const finish=async id=>{tasks.bodyDone(id,{kind:'completed',value:null});assert(tasks.beginRelease(id),'release blocked');return release(id);};
try{
- const fixture=(await(await fetch('/zig-negative.json')).json())['pure-number'];
+ const fixture=(await(await fetch('/runtime-fixtures.json')).json())['pure-number'];
await session.load(new Uint8Array(fixture.bytes),fixture.hash);
work.start(read.key,async abort=>{
signal=abort;const lease=resources.beginLease(read.key,handles[0],'reader','read');
diff --git a/ext/demo-site/test/io-v2-protocol-probe.mjs b/ext/demo-site/test/io-v2-protocol-probe.mjs
@@ -1,20 +1,12 @@
// Real WASM engines, inert header inspection only, no source compiler substitute.
export async function run(_control,backend){
const base=new URL('../',document.querySelector('script[type=module][src]').src);
- const fixtures=await(await fetch('/zig-negative.json')).json(),checks=[];
+ const fixtures=await(await fetch('/runtime-fixtures.json')).json(),checks=[];
let inspect,dispose;
- if(backend==='haskell-wasm'){
const {CompilerWorker}=await import(new URL('runtime/haskell/client.mjs',base));
const client=new CompilerWorker();dispose=()=>client.dispose();
await client.request({type:'init',compiler:false});
inspect=async fixture=>{await client.request({type:'load',bytes:new Uint8Array(fixture.bytes).buffer,hash:fixture.hash});return JSON.parse((await client.request({type:'io-v2-header',name:'root',arguments:[]})).text);};
- }else{
- const {WasmRuntime}=await import('/io-v2-runtime.mjs');
- const {ioV2Protocol}=await import(new URL('dist/zig-helpers.js',base));
- const graph=await WasmRuntime.create(new Uint8Array(await(await fetch(new URL('runtime/zig/arboricx.wasm',base))).arrayBuffer()));
- dispose=()=>graph.dispose();
- inspect=async fixture=>graph.scope(()=>ioV2Protocol.header(graph,graph.load(new Uint8Array(fixture.bytes),'root')));
- }
const equal=(got,want)=>{if(JSON.stringify(got)!==JSON.stringify(want))throw Error('Header mismatch: '+JSON.stringify(got));};
try{
for(let code=0;code<10;code++)equal(await inspect(fixtures['v2-header-core-'+code]),{kind:'core',code});
diff --git a/ext/demo-site/test/io-v2-session-probe.mjs b/ext/demo-site/test/io-v2-session-probe.mjs
@@ -1,7 +1,7 @@
// Native tree sibling cancellation through cloneable events, with a real DOM
// provider. No raw handles leave the worker/session boundary.
export async function run(_control,backend){
- const base=new URL('../',document.querySelector('script[type=module][src]').src),fixture=(await(await fetch('/zig-negative.json')).json())['v2-session-streams'];
+ const base=new URL('../',document.querySelector('script[type=module][src]').src),fixture=(await(await fetch('/runtime-fixtures.json')).json())['v2-session-streams'];
const assert=(ok,message)=>{if(!ok)throw Error(message);};
async function exercise(start,turn,stop){
let report=await start();const events=[];
@@ -38,20 +38,10 @@ export async function run(_control,backend){
}finally{input.removeEventListener('input',listener);input.remove();}
}
for(const stop of [false,true]){
- if(backend==='haskell-wasm'){
const {CompilerWorker}=await import(new URL('runtime/haskell/client.mjs',base)),client=new CompilerWorker();
try{await client.request({type:'init',compiler:false});await client.request({type:'load',bytes:new Uint8Array(fixture.bytes).buffer,hash:fixture.hash});
await exercise(async()=>JSON.parse((await client.request({type:'io-v2-session-start',name:'root',arguments:[]})).text),async command=>JSON.parse((await client.request({type:'io-v2-session-turn',...command})).text),stop);
}finally{client.dispose();}
- }else{
- const {WasmRuntime}=await import('/io-v2-runtime.mjs'),{ioV2Private,ioV2Session}=await import(new URL('dist/zig-helpers.js',base));
- const g=await WasmRuntime.create(new Uint8Array(await(await fetch(new URL('runtime/zig/arboricx.wasm',base))).arrayBuffer()));
- try{await g.scopeAsync(async()=>{
- const action=g.load(new Uint8Array(fixture.bytes),'root'),tree=ioV2Private.constructors({leaf:g.leaf,observe:g.observe.bind(g),stem:g.stem.bind(g),fork:g.fork.bind(g),app:g.app.bind(g)});
- const session=new ioV2Session.Session(tree,action,'1',roots=>g.sweepScope(roots));
- await exercise(()=>session.turn({run:'1',kind:'poll'}),command=>session.turn(command),stop);
- });}finally{g.dispose();}
- }
}
return {backend,checks:['native tree read permits sibling write','wrong-run stop and reply reject','tree cancellation waits for DOM cleanup acknowledgement','complete42; no retained task/resource/event records','external structured stop awaits both provider acknowledgements']};
}
diff --git a/ext/demo-site/test/io-v2-source-probe.mjs b/ext/demo-site/test/io-v2-source-probe.mjs
@@ -93,7 +93,6 @@ export async function run(_control,backend,terminalMode=false){
}finally{parent.remove();}
}
const names=terminalMode?['direct','handled','roundTrip','cancellation','invalidUTF8','invalidScalar','binary','externalStop']:['direct','handled','roundTrip','cancellation','invalidUTF8','invalidScalar'];
- if(backend==='haskell-wasm'){
const {CompilerWorker}=await import(new URL('runtime/haskell/client.mjs',base)),client=new CompilerWorker();
try{
await client.request({type:'init',compiler:false});await client.request({type:'load',bytes:new Uint8Array(fixture.bytes).buffer,hash:fixture.hash});
@@ -101,16 +100,5 @@ export async function run(_control,backend,terminalMode=false){
async()=>JSON.parse((await client.request({type:'io-v2-session-start',name:name==='externalStop'?'roundTrip':name,arguments:[]})).text),
async command=>JSON.parse((await client.request({type:'io-v2-session-turn',...command})).text));
}finally{client.dispose();}
- }else{
- const {WasmRuntime}=await import('/io-v2-runtime.mjs'),{ioV2Private,ioV2Session}=await import(new URL('dist/zig-helpers.js',base));
- const g=await WasmRuntime.create(new Uint8Array(await(await fetch(new URL('runtime/zig/arboricx.wasm',base))).arrayBuffer()));let run=0n;
- try{
- for(const name of names)await g.scopeAsync(async()=>{
- const action=g.load(new Uint8Array(fixture.bytes),name==='externalStop'?'roundTrip':name),tree=ioV2Private.constructors({leaf:g.leaf,observe:g.observe.bind(g),stem:g.stem.bind(g),fork:g.fork.bind(g),app:g.app.bind(g)});
- const session=new ioV2Session.Session(tree,action,String(++run),roots=>g.sweepScope(roots));
- await drive(name,()=>session.turn({run:session.run,kind:'poll'}),command=>session.turn(command));
- });
- }finally{g.dispose();}
- }
return {backend,checks};
}
diff --git a/ext/demo-site/test/io-version-probe.mjs b/ext/demo-site/test/io-version-probe.mjs
@@ -3,7 +3,7 @@ export async function run(_control,backend){
const assert=(value,message)=>{if(!value)throw Error(message);};
const base=new URL('../',document.querySelector('script[type=module][src]').src);
const {createRuntimeSession}=(await import(new URL('dist/probe-api.js',base))).runtimeSession;
- const fixtures=await(await fetch('/zig-negative.json')).json();
+ const fixtures=await(await fetch('/runtime-fixtures.json')).json();
const checks=[];const s=await createRuntimeSession(backend,false);
const host={before:async()=>{},after:()=>{},read:async()=>'',write:()=>{},running:()=>{}};
const load=async name=>{const f=fixtures[name];await s.load(new Uint8Array(f.bytes),f.hash);};
diff --git a/ext/demo-site/test/live-ux-probe.mjs b/ext/demo-site/test/live-ux-probe.mjs
@@ -22,7 +22,7 @@ export async function run(control,backend='haskell-wasm'){
hold=false;held.shift()();await status('Compiled');assert(el('compile-diagnostic').hidden,'Stale diagnostics discarded');
input('source-editor','answer = missingLatestName');await status('Compilation failed');assert(!el('compile-diagnostic').hidden&&card().querySelector('select').disabled&&result()==='','Latest diagnostic invalidates old inspection');
input('source-editor','identity value = value\nanswer =\n identity\n 46');await status('Compiled');assert(await inspect('answer','number')==='46','Indented continuation');
- change('runtime-backend',backend==='haskell-wasm'?'zig-wasm':'haskell-wasm');await status('Compiled');assert(await inspect('answer','number')==='46','Backend change rebuilds source');
+ change('runtime-backend',backend);await status('Compiled');assert(await inspect('answer','number')==='46','Runtime restart rebuilds source');
change('runtime-backend',backend);await status('Compiled');
input('source-editor','answer = 47');el('stop').click();await pause(900);assert(el('status').textContent==='Stopped','Stop suppresses queued compilation');
input('source-editor','answer = 48');await status('Compiled');assert(await inspect('answer','number')==='48','Source edits recover after Stop');
@@ -35,5 +35,5 @@ export async function run(control,backend='haskell-wasm'){
input('source-editor','id x = x\nanswer = id 51\nbomb = (x : x x) (x : x x)');await status('Compiled');assert(await inspect('answer','number')==='51','Unused divergent binding is not forced');
document.querySelector('[aria-label="Inspect compiled definition bomb"]').click();await wait(()=>el('status').textContent==='Inspecting bomb','divergent observation');await pause(150);el('stop').click();await status('Stopped');
}finally{globalThis.Worker=NativeWorker;}
- return {userAgent:navigator.userAgent,backend,checks:['real native delayed completion; revision invalidation; stale-error rejection; compile coalescing; editable compilation; Stop suppresses queued work; IME; backend rebuilding; no implicit IO; unused divergence; CPU-bound Stop']};
+ return {userAgent:navigator.userAgent,backend,checks:['real native delayed completion; revision invalidation; stale-error rejection; compile coalescing; editable compilation; Stop suppresses queued work; IME; runtime rebuilding; no implicit IO; unused divergence; CPU-bound Stop']};
}
diff --git a/ext/demo-site/test/local-module-probe.mjs b/ext/demo-site/test/local-module-probe.mjs
@@ -81,13 +81,13 @@ export async function run(control,backend){
el('export-compiled-module').click();await status('Exporting module');await delay(150);el('stop').click();await status('Stopped');
assert(el('export-compiled-module').disabled,'Stop invalidates exportable draft');
input('source-editor',consumer);el('compile').click();await status('Compiled');
- change('runtime-backend',backend==='haskell-wasm'?'zig-wasm':'haskell-wasm');
- el('compile').click();await status('Compiled');equal(await evaluateSource('answer'),'42','Local bytes survive Stop and backend replacement');
+ change('runtime-backend',backend);
+ el('compile').click();await status('Compiled');equal(await evaluateSource('answer'),'42','Local bytes survive Stop and runtime replacement');
const frame=document.createElement('iframe');frame.src=new URL('index.html?noProbe=1',location.href).href;document.body.append(frame);
await wait(()=>frame.contentDocument?.getElementById('status')?.textContent==='Ready');
equal(frame.contentDocument.getElementById('local-module-list').children.length,0,'A new page has no persisted local modules');frame.remove();
[...el('local-module-list').querySelectorAll('button')].find(b=>b.textContent==='Remove').click();
await status('Compilation failed');equal(el('local-module-list').children.length,0,'Removal clears module');
assert(el('export-compiled-module').disabled,'Removed dependency cannot use a stale compiled scope');
- return {userAgent:navigator.userAgent,source,manifest:exported.manifest,bytes:[...exported.bytes],checks:['local file validation; offline local-first imports; public portable export/reexport; shared DAGs; no private divergence; actual export/file import; duplicates/corruption; Stop/backend retention; page-only lifetime; removal invalidation']};
+ return {userAgent:navigator.userAgent,source,manifest:exported.manifest,bytes:[...exported.bytes],checks:['local file validation; offline local-first imports; public portable export/reexport; shared DAGs; no private divergence; actual export/file import; duplicates/corruption; Stop/runtime retention; page-only lifetime; removal invalidation']};
}
diff --git a/ext/demo-site/test/module-probe.mjs b/ext/demo-site/test/module-probe.mjs
@@ -25,7 +25,7 @@ export async function run(control,backend='haskell-wasm'){
const value=async expression=>(await s.runExpression(expression,'number','pure',host)).value;
try {
const catalogue=await(await fetch(new URL('catalogue.json',base))).json();
- const fixtures=await(await fetch('/zig-fixtures.json')).json();
+ const fixtures=await(await fetch('/program-fixtures.json')).json();
for(const demo of catalogue){
await phase('compile/check demo '+demo.id);
assert(demo.environment==='empty','Every demo is editable without hidden helper injection');
diff --git a/ext/demo-site/test/release.py b/ext/demo-site/test/release.py
@@ -115,8 +115,8 @@ def main():
for retired in ['worker.js', 'compiler-worker.js']:
assert not (args.site / 'dist' / retired).exists()
entries = {'dist/app.js', 'dist/wasm-worker.js', 'dist/local-module-worker.js',
- 'dist/zig-helpers.js', 'runtime/haskell/client.mjs',
- 'runtime/haskell/worker.mjs', 'runtime/zig/site-session.mjs'}
+ 'runtime/haskell/client.mjs',
+ 'runtime/haskell/worker.mjs'}
# The early theme script is copied verbatim, not an esbuild entrypoint.
assert {p.relative_to(args.site).as_posix() for p in args.site.rglob('*')
if p.suffix in ['.js', '.mjs']} == entries | {'theme.js'}
@@ -132,12 +132,12 @@ def main():
assert not any('runtime/' in p for p in graphs['dist/app.js']['inputs'])
assert not (args.site / 'dist/probe-api.js').exists()
# Preserve the no-static-bundle-fallback control in each bundled download owner.
- for script in ['dist/wasm-worker.js', 'dist/zig-helpers.js',
- 'runtime/haskell/worker.mjs', 'runtime/zig/site-session.mjs']:
+ for script in ['dist/wasm-worker.js', 'runtime/haskell/worker.mjs']:
source = (args.site / script).read_text()
assert '/_arboricx/bundle/hash/' in source and 'bundles/' not in source
- for backend, binary in [('haskell', 'reactor.wasm'), ('zig', 'arboricx.wasm')]:
+ for backend, binary in [('haskell', 'reactor.wasm')]:
assert (args.site / 'runtime' / backend / binary).read_bytes()[:8] == b'\x00asm\x01\x00\x00\x00'
+ assert {p.name for p in (args.site / 'runtime').iterdir()} == {'haskell'}
runtime = args.site / 'runtime/haskell'
pins = json.loads((support / 'haskell-pins.mjs').read_text().split('export default ', 1)[1].split(';', 1)[0])
for name in ['compiler.dag', 'environment.tsv', 'compiler-exports.tsv']:
diff --git a/ext/demo-site/test/runtime-backends.test.mjs b/ext/demo-site/test/runtime-backends.test.mjs
@@ -5,11 +5,11 @@ import {backend,backends,runtimeStats} from '../dist/runtime-backends.js';
import {createRuntimeSession} from '../dist/runtime-session.js';
test('runtime selection is explicit, fixed and fails without fallback',async()=>{
- assert.deepEqual(Object.keys(backends),['zig-wasm','haskell-wasm']);
+ assert.deepEqual(Object.keys(backends),['haskell-wasm']);
assert.equal(backend('haskell-wasm'),'haskell-wasm');
- assert.deepEqual(Object.values(backends).map(x=>x.label),['Zig','Haskell']);
- for(const name of ['js','toString','constructor','unknown'])assert.throws(()=>backend(name),/Unknown/);
- for(const name of [null,'js','unknown'])await assert.rejects(createRuntimeSession(name,false),/Unsupported/);
+ assert.deepEqual(Object.values(backends).map(x=>x.label),['Haskell']);
+ for(const name of ['js','zig-wasm','toString','constructor','unknown'])assert.throws(()=>backend(name),/Unknown/);
+ for(const name of [null,'js','zig-wasm','unknown'])await assert.rejects(createRuntimeSession(name,false),/Unsupported/);
});
test('runtime diagnostics do not invent graph counters for WASM',()=>{
const value=runtimeStats({backend:'haskell-wasm',linearMemoryBytes:12*1048576});
@@ -17,15 +17,14 @@ test('runtime diagnostics do not invent graph counters for WASM',()=>{
assert.doesNotMatch(value,/work|heap nodes|allocated/);
});
-test('the real page explicitly defaults to Haskell and retains alternative runtimes',async()=>{
+test('the real page offers only the Haskell runtime',async()=>{
const html=await readFile(new URL('../index.html',import.meta.url),'utf8');
const selector=html.match(/<select id="runtime-backend">([\s\S]*?)<\/select>/)[1];
assert.match(selector,/^<option value="haskell-wasm" selected>Haskell<\/option>/);
- assert.match(selector,/>Zig<\/option>/);
assert.doesNotMatch(selector,/JavaScript|value="js"/);
assert.doesNotMatch(selector.replace(/<[^>]*>/g,''),/WASM|reference/i);
assert.equal((selector.match(/\bselected\b/g)||[]).length,1);
- for(const name of ['zig-wasm'])assert.match(selector,new RegExp('value="'+name+'"'));
+ assert.equal((selector.match(/<option /g)||[]).length,1);
});
test('production editor has no checkpoint restore or JS Worker branch',async()=>{
diff --git a/ext/demo-site/test/zig-negative-fixtures.mjs b/ext/demo-site/test/runtime-fixtures.mjs
diff --git a/ext/demo-site/test/summarize-backends.mjs b/ext/demo-site/test/summarize-backends.mjs
@@ -5,7 +5,7 @@ const mib=n=>Math.round(n/1048576*100)/100,ms=n=>Math.round(n*100)/100;
const summary={memoryOnly:input.memoryOnly??false,browsers:{}};
for(const browser of new Set(input.reports.map(r=>r.browser))){
const result=summary.browsers[browser]={};
- for(const backend of ['zig-wasm','haskell-wasm']){
+ for(const backend of ['haskell-wasm']){
const reports=input.reports.filter(r=>r.browser===browser&&r.backend===backend);
if(!reports.length)continue;
const samples=reports.flatMap(r=>r.browserMemory.samples);
diff --git a/ext/demo-site/test/zig-session-probe.mjs b/ext/demo-site/test/zig-session-probe.mjs
@@ -1,112 +0,0 @@
-import {terminalHost} from './terminal-host.mjs';
-// Independent complete Haskell captures, exercised through Zig's coarse adapter.
-const assert=(ok,message)=>{if(!ok)throw Error(message);};
-const equal=(a,b,label)=>assert(JSON.stringify(a)===JSON.stringify(b),label+': '+JSON.stringify(a).slice(0,160));
-async function rejects(fn,pattern){try{await fn();}catch(e){assert(pattern.test(e.message),'Wrong rejection: '+e.message);return;}throw Error('Expected '+pattern);}
-export async function run(control) {
- const phase=async name=>{if(control)await fetch(control+'/phase',{method:'POST',body:JSON.stringify({name})});};
- const base=new URL('../',document.querySelector('script[type=module][src]').src);
- const {Terminal}=(await import(new URL('dist/probe-api.js',base))).ioV2Terminal;
- const {create}=await import(new URL('runtime/zig/site-session.mjs',base));
- const fixtures=await(await fetch('/zig-fixtures.json')).json();
- const catalogue=await(await fetch(new URL('catalogue.json',base))).json();
- const releases=(await(await fetch(new URL('packages.json',base))).json()).releases;
- const report={packages:0,applications:0,compiledDemos:0,checks:[],memory:[]};
- const host=input=>({before:async()=>{},after:()=>{},read:async()=>input,write:()=>{},running:()=>{}});
- const bytes=async hash=>new Uint8Array(await(await fetch('/_arboricx/bundle/hash/'+hash)).arrayBuffer());
- const s=await create({base,compiler:false});
- try {
- for(const [name,demo] of Object.entries(fixtures.demos)) {
- await phase("full golden "+name);
- await s.load(await bytes(demo.hash),demo.hash);report.packages++;
- for(const c of demo.haskellCases){
- let terminal='',before;
- const hooks=demo.mode==='io'?terminalHost(Terminal,c.input):host(c.input);
- hooks.before=async e=>{assert(!before,'Missing after');before=e;};
- hooks.after=e=>{assert(e.step===before?.step&&e.name===before.name,'Trace ordering/name');before=undefined;};
- if(demo.mode==='io'){const event=hooks.v2.event;hooks.v2.event=e=>{s.collect();event(e);};}
- else {hooks.read=async()=>{s.collect();return c.input;};hooks.write=text=>{terminal+=text;};}
- equal(await s.inspect(c.export,c.arguments.map(value=>({type:'tree',value})),demo.mode,hooks),c.result,'Full golden '+name);
- equal(demo.mode==='io'?hooks.terminalText:terminal,c.terminal,'Terminal '+name);report.applications++;
- }
- }
- const negatives=await(await fetch('/zig-negative.json')).json();
- {
- const f=negatives['v1-output'];await s.load(new Uint8Array(f.bytes),f.hash);
- let calls=0;const forbidden=()=>{calls++;throw Error('retired callback');};
- await rejects(()=>s.run('root',[],'tree','io',{...host(),before:forbidden,write:forbidden,read:forbidden}),/Unsupported tricuIO version/);
- equal(calls,0,'Retired v1 output never invokes host');
- }
- for(const [name,pattern] of [['version',/version/],['version-effect',/version/],['unsupported',/Unsupported/],['abort',/Unsupported tricuIO version/],['output',/Unsupported tricuIO version/],['actions',/Unsupported tricuIO version/]]){
- const f=negatives[name];await s.load(new Uint8Array(f.bytes),f.hash);
- await rejects(()=>s.run('root',[],'tree','io',host()),pattern);
- }
- {
- const f=negatives['version-effect'];await s.load(new Uint8Array(f.bytes),f.hash);
- let callbacks=0;const forbidden=()=>{callbacks++;throw Error('version gate reached host callback');};
- await rejects(()=>s.run('root',[],'tree','io',{before:forbidden,after:forbidden,read:forbidden,write:forbidden,running:forbidden}),/version/);
- equal(callbacks,0,'Unsupported version invoked host');
- report.checks.push('IO v2 rejected before host callbacks; no v1 retry');
- }
- const shared=negatives.sharing;await s.load(new Uint8Array(shared.bytes),shared.hash);
- const preview=(await s.run('root',[],'tree','pure',host())).value;
- assert(!preview.truncated&&preview.nodes.length===41,'Physical sharing expanded');
- assert(preview.nodes.filter(n=>n.kind==='fork').every(n=>n.children[0]===n.children[1]),'Lost child aliases');
- const intro=fixtures.demos.intro;await s.load(await bytes(intro.hash),intro.hash);
- const invoke=(arg,type)=>s.run('identity',[arg],type,'pure',host());
- for(const vector of fixtures.codecVectors){const type=vector.kind,value=type==='number'?String(vector.value):vector.value;equal((await invoke({type,value},type)).value,value,'Scalar codec');}
- for(const value of [true,false])equal((await invoke({type:'bool',value},'bool')).value,value,'Boolean');
- equal((await invoke({type:'bytes',value:[0,1,127,128,255]},'bytes')).value,[0,1,127,128,255],'Bytes');
- await rejects(()=>invoke({type:'number',value:'256'},'bool'),/Boolean/);
- await rejects(()=>invoke({type:'list',value:[{type:'number',value:'256'}]},'bytes'),/byte/);
- await rejects(()=>invoke({type:'list',value:[{type:'number',value:'55296'}]},'string'),/status 5/);
- await rejects(()=>invoke({type:'string',value:'\ud800'},'string'),/surrogate/);
- const scalars={type:'list',value:[{type:'string',value:'λ🙂'},{type:'number',value:'1267650600228229401496703205376'}]};
- equal((await invoke(scalars,'values')).value,scalars.value,'Decoded list preserves text and exact big naturals');
- await rejects(()=>invoke({type:'list',value:[{type:'bool',value:true}]},'values'),/number/);
- equal((await invoke({type:'bytes',value:Array(4096).fill(42)},'values')).value.length,4096,'Decoded lists are not capped at 32 preview items');
- equal((await invoke({type:'list',value:[{type:'number',value:'42'}]},'list')).value.length,1,'List');
- const original=await bytes(intro.hash);
- await rejects(()=>s.load(original,'0'.repeat(64)),/SHA-256 mismatch/);
- const mutable=original.slice(),loading=s.load(mutable,intro.hash);mutable.fill(0);await loading;
- equal((await invoke({type:'number',value:'42'},'number')).value,'42','Load must authenticate an immutable snapshot');
- const baseline=s.diagnostics().owned;
- for(let i=0;i<50;i++){
- equal((await invoke({type:'string',value:'λ雪🙂'},'string')).value,'λ雪🙂','Retained run');
- s.collect();equal(s.diagnostics().owned,baseline,'Temporary owned-handle leak');
- report.memory.push(s.diagnostics().linearMemoryBytes);
- }
- assert(Math.max(...report.memory.slice(25))<=Math.max(...report.memory.slice(0,25))+65536,'Memory failed to plateau');
- report.checks.push(`${report.applications} complete published goldens; v2 IO GC and retired framing rejection; scalar/list/tree codecs and negative controls; physical sharing; ownership and memory plateau`);
- const greet=fixtures.demos.greet;await s.load(await bytes(greet.hash),greet.hash);
- let entered;const paused=new Promise(resolve=>{entered=resolve;});
- const pausedHost=terminalHost(Terminal,'');pausedHost.v2.stepping=true;pausedHost.before=()=>{entered();return new Promise(()=>{});};
- const running=s.run(greet.haskellCases[0].export,[],'string','io',pausedHost);
- running.catch(()=>{});await paused;s.dispose();await rejects(()=>running,/Disposed/);assert(s.disposed,'Disposed session reused');
- }finally{s.dispose();}
- const c=await create({base,compiler:true});
- try {
- for(const [name,demo] of Object.entries(fixtures.demos)){
- const entry=catalogue.find(x=>x.hash===demo.hash);
- assert(entry||name==='intro'&&releases.some(r=>r.name==='intro'&&r.version==='0.2.0'&&r.hash===demo.hash),'Current catalogue pin or explicitly retained historical intro publication');
- await phase("source compile "+name);
- await c.compile(demo.source,entry?.environment??'empty');
- const first=demo.haskellCases[0];
- equal(await c.inspect(first.export,first.arguments.map(value=>({type:'tree',value})),demo.mode,demo.mode==='io'?terminalHost(Terminal,first.input):host(first.input)),first.result,'Compiled '+name);report.compiledDemos++;
- }
- const intro=fixtures.demos.intro;await c.compile(intro.source,'empty');
- await rejects(()=>c.compile('name = unavailableName','empty'),/Compiler diagnostic/);
- // Retired stale-definition retention: a failed replacement cannot expose the
- // previous program or its imported scope. Runtime/IO errors still retain it.
- await rejects(()=>c.run('answer',[],'number','pure',host()),/Missing definition/);
- const greet=fixtures.demos.greet;await c.compile(greet.source,'empty');
- const failingHost=terminalHost(Terminal,'x');failingHost.v2.stepping=true;failingHost.before=async()=>{throw Error('host failed');};
- await rejects(()=>c.run(greet.haskellCases[0].export,[],'string','io',failingHost),/host failed/);
- const first=greet.haskellCases[0];equal(await c.inspect(first.export,[],greet.mode,terminalHost(Terminal,first.input)),first.result,'Host-error cleanup preserves definitions');
- await c.compile('q x = x x\nlazy = t (q q) t\naborted = t \"tricuIO\" (t 2 (t 2 (q q)))','empty');
- const partial=await c.preview('lazy',1);assert(partial.nodes.length===1&&partial.truncated,'Preview forced excluded divergent child');
- await rejects(()=>c.run('aborted',[],'tree','io',terminalHost(Terminal,null)),/IO v2 failed/);
- report.checks.push('current published browser source demos and retained historical fixtures; compile-error invalidation/host-error retention; nonforcing divergent preview/abort');
- }finally{c.dispose();}
- return report;
-}
diff --git a/ext/haskell/README.md b/ext/haskell/README.md
@@ -3,7 +3,7 @@
The Haskell shared library exposes tricu trees and the native IO-v2 driver through
[`include/tricu.h`](include/tricu.h), embedding ABI 1. It uses the same
`Runtime.Tree`, value codecs, bundle verifier and IO driver as the Haskell
-executable. The Zig host remains independently available. Native consumers use the
+executable. Native consumers use the
[Python](#python-example), [JavaScript](../js/README.md), and
[PHP](../php/README.md) bindings.
@@ -40,7 +40,30 @@ The artifact installs `lib/libtricu-haskell.so`, `include/tricu.h`, and
`share/tricu/tricu.py`. On Linux its Haskell shared-library dependencies are
installed beside it; native dependencies retain their Nix runtime paths. Keep the
complete Nix closure available. Copying just the main `.so` is insufficient.
-Other native targets and browser/WASM embedding require their own acceptance.
+Other native targets require their own acceptance. Browser embedding has its
+[Haskell WASM contract](../haskell-wasm/README.md).
+
+## Compiler-workflow embedding checks
+
+```sh
+nix build .#tricu-generation-ffi-test --no-link
+CHECK=$(nix path-info .#tricu-generation-ffi-test)
+cat "$CHECK/compatibility/report.json" "$CHECK/programs/report.json" "$CHECK/scoped/report.json"
+```
+
+The generation runner accepts `--execution-host haskell-ffi --haskell-library PATH`
+with explicit IO-v2 kit selection. A fresh execution process authenticates the
+captured compiler/adapter, six arguments, runtime, executor and binding identities
+before compiler effects. Its one-capability RTS is independent of CLI transport
+capabilities. External CPU/address-space/RSS/wall guards bound the job; compiler
+failures remain distinct from opaque terminal task outcomes.
+
+The opt-in gate compares cold/warm publication, source changes, corruption,
+interruption/restart and complete receipts/objects against the CLI. It also checks
+twelve complete import/recursion/invoice results against independent oracles,
+plus scoped `native_math` generations and request-tampering rejection. These are
+application/library workflows; compiler-pair acceptance and retention use the
+[compiler rebuilding](../../docs/compiler-rebuilding.md) procedure.
## Core-only prototype
diff --git a/ext/js/README.md b/ext/js/README.md
@@ -58,7 +58,7 @@ left opaque. `--` ends option parsing. Integer arguments are available through t
programmatic `number` method. Explicit formats describe the caller's intended
interpretation because tree encodings overlap.
-The native bindings use Haskell value/IO semantics. Zig fuel, manual collection,
+The native bindings use Haskell value/IO semantics. Fuel, manual collection,
kernel-root helpers, implicit default-export selection, heuristic type decoding,
and the old `inspect` CLI are retired from this binding. Tests use independent
expected values and retain malformed-bundle, missing-export and ownership controls.
diff --git a/ext/php/README.md b/ext/php/README.md
@@ -65,5 +65,5 @@ supervision and is not an untrusted-computation sandbox.
The CLI requires `run BUNDLE EXPORT`, accepts text arguments, and supports `--io`
and `--format string|number|bool|bytes` (default string; bytes render as hex).
`--` ends option parsing. Terminal task failure/cancellation exits nonzero with
-its reason left opaque. Native Zig fuel/collection helpers, implicit export
+its reason left opaque. Fuel/collection helpers, implicit export
selection, heuristic decoding, and the old `inspect` CLI are retired here.
diff --git a/ext/wasm/README.md b/ext/wasm/README.md
@@ -1,279 +0,0 @@
-# Experimental Zig/WASM evaluator
-
-An isolated experiment, **not the demo site's deployed runtime**. It compiles the
-existing Zig owned-handle ABI and reducer to freestanding wasm32 ReleaseSafe,
-without WASI, libc, libuv, or JavaScript imports. Native IO returns `ARB_IO_ERROR`.
-The Node/browser-compatible adapter implements bundle loading, application,
-reduction, constructor observation, byte codecs and Unicode scalar text codecs.
-Native ABI behavior is unchanged.
-
-## Reproduce
-
-From the repository root:
-
-```sh
-nix build .#tricu-wasm .#tricu-wasm-tests .#tricu-wasm-comparison --no-link
-REPORT=$(nix path-info .#tricu-wasm-comparison)
-cat "$REPORT/report.json"
-cat "$REPORT/preview-report.json"
-
-# Opt-in headless Chromium Worker validation and measurements
-nix build .#tricu-wasm-browser --no-link
-BROWSER=$(nix path-info .#tricu-wasm-browser)
-cat "$BROWSER/report.json"
-```
-
-The WASM artifact is `$(nix path-info .#tricu-wasm)/lib/arboricx.wasm`.
-Nix caches measurement outputs. These are correctness smokes with recorded timings,
-not performance thresholds. Run without concurrent heavy workloads for useful
-comparisons; the harnesses are not CPU-pinned.
-
-### Node checks and measurements
-
-Tests cover 300 independent Haskell conformance vectors, Haskell text codec
-vectors, stale and foreign-context handles within an instance, GC roots,
-malformed bundles, exhausted fuel and resume, lazy discard, byte roundtrips,
-unsupported IO and detached memory views after growth. They also check malformed
-UTF-8, invalid scalar trees, unpaired surrogates, exact scalar limits, allocation
-ceiling failure and recovery, nested scope escape/failure cleanup, and 200
-retained-context GC cycles with stable owned-root and node counts. Bulk previews
-also run all 300 conformance cases, shared/deep graph bounds, undemanded divergent
-frontiers, failure output atomicity, repeated ownership checks and aggressive GC.
-Native Debug/ReleaseSafe tests inject every allocation failure into the snapshot
-builder.
-
-The comparison alternates JS/WASM order for three fresh-process samples per case.
-It rebuilds the site's TypeScript and uses the site's pinned compiler bundle, not
-a newly generated compiler. It verifies complete string results and four existing
-compiler golden cases (all definition trees plus applied results). Each child
-has a 120-second timeout and Node's 512 MiB old-space limit. Reports include raw
-load, construction, evaluation/full-observation and total timings, peak process
-RSS, linear memory size, fixture and WASM hashes, Node version and CPU.
-
-The `identity`/`append` cases use the same scalar-tree JS codecs on both backends.
-Their `-bulk` variants use the WASM byte ABI for ASCII inputs and complete output
-decoding; JS still uses the site codecs. Their `-text` variants use multibyte
-Unicode text and the WASM scalar codecs. Generic per-node operations still expose
-the cost of host crossings and retained root-table scans. The compiler cases use
-the bulk scalar input codec on WASM. Node/V8 timings are not browser results.
-
-`preview-report.json` separately compares JS per-node observation, WASM per-node
-observation, and WASM bulk snapshots of the same 128-node unary tree. Each fresh
-process performs 100 inspections, with either zero or 2000 additional retained
-host roots. Three samples alternate order. All iterations check the complete
-shape; those checks are included in timing (string comparison for per-node output,
-record comparisons for snapshots). This measures observation/transport plus its
-check, not just reducer time. The fixture deliberately has no sharing, so the
-batch path does not gain an unfair logical-expansion advantage. Root-table
-pressure is synthetic, not a claim about typical browser sessions.
-
-### Browser Worker checks and measurements
-
-`browser.mjs` serves a loopback-only test origin and drives a fresh Chromium
-profile through CDP. `worker.mjs` is an experimental test protocol, **not** the
-site's IO/Worker protocol. Both JS and WASM paths check the supplied bundle digest
-and use the site's strict bundle/DAG profile before instantiation/loading. The
-pins and bytes come from the local harness, not an untrusted remote descriptor.
-
-Each backend runs in a module Worker. Three alternating-order samples use fresh
-Workers, with four compiler cases per retained compiler session. `compileMs`
-measures source encoding, compilation, decoding lazy definition handles and
-replacement of previous exports. `observeRoundTripMs` separately measures full
-independent definition/result checks, including message round trips. These phases
-are different from the Node combined evaluation/observation timing. Reports record
-Chromium's user agent, artifact hashes and graph/linear-memory diagnostics, **not
-browser RSS or total memory**. Compilation benchmarks omit Worker initialization
-and bundle loading, so they are not end-to-end page latency measurements.
-
-Controls check:
-
-- Hash mismatch and a malformed bundle with a matching hash are rejected.
-- Unicode text survives Worker transport, including NUL and initial U+FEFF;
- rejected surrogates do not prevent a subsequent valid operation.
-- Repeated compilation/replacement/collection stabilizes owned roots and live
- graph size after warm-up; retained compiled exports remain callable.
-- Bulk snapshots of compiled definitions reconstruct the independent golden
- trees across Worker messaging. `previewMs` records their capture time separately
- from full logical inspection and message round trips.
-- Existing compiler diagnostic goldens match, a failed compilation preserves
- previous exports, and a subsequent compilation succeeds.
-- Divergent reduction leaves the main-thread heartbeat responsive. Hard Worker
- termination stops the job; a fresh Worker initializes and runs successfully.
-
-Each request has a main-thread 30-second deadline; the test process has a
-240-second guard. Chromium is headless with `--no-sandbox` for Nix build-sandbox
-compatibility, using only local fixtures. This is not a security test of a public
-browser deployment. Firefox, WebKit, mobile devices, background-tab throttling,
-large compiler jobs and the production IO protocol are not covered.
-
-## Backend session boundary
-
-The experimental Worker selects a backend explicitly on initialization:
-
-```js
-worker.postMessage({
- type: 'init', backend: 'zig-wasm', bundle: bundleBytes, hash: bundleSha256,
- name: exportName, artifacts: { wasm: wasmBytes }
-});
-```
-
-`backends.mjs` owns the trusted factory registry (`js`, `zig-wasm`) and common
-bundle authentication/profile checks. Backend code is selected from that registry,
-never from a caller-supplied script URL. `backends/js.mjs` and
-`backends/zig-wasm.mjs` own runtime loading, text codecs, roots, collection and
-cleanup. `worker.mjs` only dispatches session operations and returns cloneable
-values. Unknown backends, missing artifacts and initialization failures are errors;
-there is no automatic fallback. The ready reply identifies the selected backend.
-Selection is immutable for that Worker; a second initialization is rejected.
-
-The current session operations are `runText`, `compile`, `inspect`, `preview`,
-`collect`, `stats`, `dispose`, and a harness-only `runTree` probe. Operations may
-return values or Promises. Compile retains definition handles privately and only
-returns names; inspect/preview return copied values. Failed compilation preserves
-the previous definitions within this experimental session. Dispose is idempotent;
-other calls after disposal fail. Hard Stop still terminates the entire Worker,
-without relying on graceful disposal running during a blocked evaluator.
-
-No handles, WASM pointers, runtime heap checkpoints or allocator conventions cross
-this boundary. `stats` contains backend-specific diagnostics, not a portable
-resource budget. The shared `tree-session.mjs` helper is an implementation detail
-of the current adapters, not an interface another backend must implement. A new
-backend may supply its own session implementation and artifact loader without
-using the Zig C ABI or the JavaScript `Graph` API. Async-adapter, disposal,
-authentication-before-initialization and no-fallback controls run in
-`backend-check.mjs`; Chromium exercises both registered implementations.
-
-This boundary currently covers the experiment, not the deployed app's full IO,
-package environment or retained-draft protocol. The site has a Haskell/Zig selector and coarse session adapter (see
-`ext/demo-site/src/runtime-session.ts`). `site-session.mjs` is Zig's private
-production adapter; the older JS/Zig experiment remains an isolated harness.
-No backend shares another runtime's memory, handles or checkpoints.
-
-## Ownership and retained sessions
-
-```js
-import {WasmRuntime} from './runtime.mjs';
-const runtime = await WasmRuntime.create(wasmBytes);
-try {
- const identity = runtime.load(bundleBytes); // explicitly owned session root
- const result = runtime.scope(() => {
- const input = runtime.ofText('λ雪🙂');
- return runtime.toText(runtime.app(identity, input));
- }); // all temporary handles released, including on failure
- console.log(result);
- runtime.collect(); // identity remains rooted
- runtime.release(identity);
-} finally {
- runtime.dispose();
-}
-```
-
-Handles are BigInts local to their WASM instance; never transfer them between
-instances. Constructors borrow inputs and return owned handles. `observe` and
-`pair` return owned child handles. `retain` produces an independent owned alias.
-`apply` returns an owned handle even with zero arguments. `fromTernary` and
-`toTernary` release internal temporaries as they traverse.
-
-`scope(callback)` tracks newly returned handles, including observed children.
-It releases them on normal return or failure. `keep(handle)` marks a newly owned
-handle for transfer to the parent scope (or session) **only on successful return**:
-
-```js
-const saved = runtime.scope(({keep}) => keep(runtime.ofText('retained')));
-// ... later ...
-runtime.release(saved);
-```
-
-Scopes are synchronous, not async lifetime managers. Never schedule asynchronous
-work inside a scope. Borrowed outer handles are not released by a scope. Handles
-returned without `keep` are stale after scope exit. Outside scopes, callers must
-explicitly release handles or destroy the context. Direct raw ABI calls bypass
-adapter ownership accounting and are for trusted tests only.
-
-Collection reclaims unreachable graph nodes but does not promise to shrink the
-handle table, arena backing allocations, or WASM linear memory. Generic graph
-codecs can create large root-table high-water marks even when eventually scoped;
-use bulk codecs for text/bytes. `stats()` reports handle slots, owned roots, arena
-node-array length, and `liveAfterCollect` (the most recent collector live count,
-not a current count between collections). These are not byte limits.
-
-## Bounded bulk tree previews
-
-`runtime.preview(handle, limit = 64, fuel = 1_000_000n)` returns a copied snapshot:
-
-```js
-{ root: 0, nodes: [{ id: 0, kind: 'fork', children: [1, 1] }, /* ... */],
- truncated: false }
-```
-
-The node limit must be 1–256. One WASM call traverses constructors, preserving
-physical sharing without logically expanding repeated subtrees. Records are
-visited depth-first, left before right. Children outside the captured frontier
-have labels but no records; `truncated` is true if any unvisited label remains.
-Pending duplicates alone do not cause truncation. The frontier is not forced
-merely to decide whether the snapshot is complete.
-
-Labels are **snapshot-local numbers**, not owned handles, arena node IDs, content
-identities, or cross-snapshot keys. Snapshots own no runtime roots and remain
-usable after collection or context destruction. This is a bounded view, not a
-paginated cursor or full tree-export format; passing labels back to the evaluator
-is invalid. Compiler logical-result checks still use the separate bounded
-`toTernary` path. No deployed tree-view UI has been switched to these snapshots.
-
-Fuel is a u64 ceiling **per constructor demand**, not a total preview budget.
-A node cap limits transport and visited graph size, not the cost of evaluating
-each node. A divergent node within the captured region can exhaust fuel; a
-frontier node outside the region is left undemanded. As with reduction, failure
-may leave partial evaluation progress in the shared graph, but publishes no
-partial output buffer or handles. Use a Worker deadline for whole-job limits.
-
-## Unicode and resource boundaries
-
-`ofText`/`toText` encode **lists of Unicode scalars**, not UTF-8 byte lists or UTF-16
-code units. They reuse Zig's strict source-text scalar codec. NUL, CRLF, initial
-U+FEFF and noncharacters are preserved; unpaired surrogates, malformed UTF-8,
-noncanonical numeral trees, surrogate values and values above U+10FFFF are
-rejected. In particular, this strict decoder does not inherit the generic site's
-`readNatural` tolerance for redundant high zero bits. No normalization is applied.
-The default ceiling is 4096 scalars; callers can select a different nonnegative
-u32 ceiling (the Worker compiler uses 16384). A ceiling is not a deadline or fuel
-budget. `ofBytes`/`toBytes` remain byte-oriented and are not Unicode text codecs.
-
-Views are recreated after calls that may grow memory, and decoded buffers are
-copied before being freed. WASM linear memory is capped at 512 MiB with an 8 MiB
-stack. Neither the WASM cap nor Node's old-space limit bounds total process RSS.
-
-The raw adapter accepts trusted host buffers. The experimental Worker and the
-site session separately authenticate bundles and validate their profiles. The
-site session uses shared package/codecs/compiler helpers, keeps original stored
-export in shared JS, and implements bounded browser IO with opaque trace labels.
-It selects `WasmRuntime.create(bytes, {unbounded:true})`: ordinary demands, bulk
-text observation and previews have no fuel ceiling. Explicit finite demands
-remain supported by the raw harness. All execution remains cancellable by hard
-Worker termination; input/output/preview bounds and real allocation failures
-remain enforced. See the [site guide](../demo-site/README.md#runtime-selection)
-for packaged release use and both-browser integration checks.
-
-`scopeAsync` is an exclusive outer operation for the serialized site driver,
-not concurrent access to one WASM instance. Nested synchronous scopes transfer
-kept temporary handles into that outer scope; it releases everything on success
-or host/driver failure. Persistent definitions are owned outside run scopes.
-Resource/trap failures poison the session and require replacement, not reuse.
-The site reports actual linear memory only; raw diagnostic counts are used in
-ownership/collection tests, not presented as JS work or total-process memory.
-
-The production adapter also supports `runExpression(source, output, mode, host)`.
-It lazily authenticates/loads the pinned compiler when needed and binds the
-existing definition handles into a temporary compilation environment. The
-single expression result is scoped through observation/IO; its temporary name
-is not added to definitions. No stored tree is flattened to reconstruct source.
-The same exclusive async scope releases transient handles on success or error.
-
-`WasmRuntime.snapshot` uses the WASM-only `arb_host_snapshot_unbounded` transport
-entrypoint for result files: 1–100,000 observed nodes, with unbounded reduction
-work per observation. The existing preview entrypoints retain their 256-node
-ceiling. Snapshot labels preserve sharing, use bounded dynamic storage, and never
-become session handles. `site-session.mjs` formats complete snapshots with the
-shared serializer; incomplete snapshots and oversized expanded files fail rather
-than producing partial downloads. Stored definition export still reads immutable
-bundle records without evaluation.
diff --git a/ext/wasm/backend-check.mjs b/ext/wasm/backend-check.mjs
@@ -1,37 +0,0 @@
-import assert from 'node:assert/strict';
-import {readFile} from 'node:fs/promises';
-import {createHash} from 'node:crypto';
-import {openSession,backendFactories} from './backends.mjs';
-const bundle=await readFile('test/fixtures/id.arboricx');
-const hash=createHash('sha256').update(bundle).digest('hex');
-const wasm=await readFile(process.argv[2]);
-assert.deepEqual(Object.keys(backendFactories),['js','zig-wasm']);
-for(const backend of ['toString','haskell-wasm','unknown'])
- await assert.rejects(openSession({backend,bundle,hash}),/Unknown backend/);
-for(const backend of Object.keys(backendFactories)) {
- const session=await openSession({backend,bundle,hash,artifacts:{wasm}});
- assert.equal(await session.runText('λ雪🙂'),'λ雪🙂');
- assert.equal(await session.runTree('21100',['2100']),'2100');
- await session.dispose();await session.dispose();
- await assert.rejects(async()=>session.runText('stale'),/disposed/);
-}
-await assert.rejects(openSession({backend:'zig-wasm',bundle,hash}),/Missing zig-wasm artifact/);
-// A future backend may have asynchronous operations and completely different
-// internal ownership. No Graph/WASM exports/BigInt handles are required here.
-let calls=0;
-const factories={alternate:async config=>{
- calls++;assert.equal(config.name,'id');
- return {runText:async text=>text,stats:async()=>({backendMetric:7}),dispose:async()=>{}};
-}};
-await assert.rejects(openSession({backend:'alternate',bundle,hash:'0'.repeat(64)},factories),/hash mismatch/);
-assert.equal(calls,0);
-const alternate=await openSession({backend:'alternate',bundle,hash},factories);
-assert.equal(await alternate.runText('async'),'async');assert.deepEqual(await alternate.stats(),{backendMetric:7});
-await alternate.dispose();assert.equal(calls,1);
-let fallback=false;
-await assert.rejects(openSession({backend:'broken',bundle,hash},{
- broken:async()=>{throw new Error('Selected backend failed');},
- js:async()=>{fallback=true;},
-}),/Selected backend failed/);
-assert.equal(fallback,false);
-console.log('Backend sessions: explicit selection, authentication, isolation/disposal, async adapters and no fallback passed');
diff --git a/ext/wasm/backends.mjs b/ext/wasm/backends.mjs
@@ -1,29 +0,0 @@
-import {decodeBundle} from '../demo-site/dist/bundle.js';
-/**
- * Experimental session boundary. Each method may return a value or Promise.
- * Public values are cloneable data, never runtime handles or heap checkpoints.
- * runText(string) -> string
- * compile(source) -> string[] (retains definitions; failure preserves old ones)
- * inspect(name, ternaryArguments) -> ternary result
- * preview(name, limit) -> bounded snapshot
- * runTree(ternaryProgram, ternaryArguments) -> ternary result (harness probe)
- * collect(), stats() -> backend-specific diagnostics, dispose() -> idempotent
- * New implementations need not use Graph, Koffi, the Zig ABI, or treeSession.
- */
-export const backendFactories=Object.freeze({
- js: async config=>(await import('./backends/js.mjs')).create(config),
- 'zig-wasm': async config=>(await import('./backends/zig-wasm.mjs')).create(config),
-});
-const hash=async bytes=>Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256',bytes)),x=>x.toString(16).padStart(2,'0')).join('');
-// factories is a trusted host/test injection point, never a Worker message field.
-export async function openSession({backend,bundle:input,hash:pin,name,artifacts},factories=backendFactories) {
- if(typeof backend!=='string'||!Object.hasOwn(factories,backend))throw new Error('Unknown backend');
- const bytes=new Uint8Array(input);
- if(await hash(bytes)!==pin)throw new Error('Bundle hash mismatch');
- const bundle=decodeBundle(bytes);
- name ??= bundle.exports[0].name;
- if(!bundle.exports.some(e=>e.name===name))throw new Error('Missing export');
- // Authentication and profile checks are shared. Artifact interpretation and
- // ownership belong to the selected implementation. Never fall back on failure.
- return factories[backend]({bytes,bundle,name,artifacts});
-}
diff --git a/ext/wasm/backends/js.mjs b/ext/wasm/backends/js.mjs
@@ -1,14 +0,0 @@
-import {Graph} from '../../demo-site/dist/graph.js';
-import {loadBundle} from '../../demo-site/dist/bundle.js';
-import {text,readText} from '../../demo-site/dist/codecs.js';
-import {treeSession} from '../tree-session.mjs';
-export function create({bundle,name}) {
- // Finite harness caps, not site settings. This adapter is not yet deployed.
- const graph=new Graph({nodes:6_000_000,fuel:Infinity,stack:100_000},{threshold:250_000});
- const root=loadBundle(graph,bundle).get(name);
- return treeSession(graph,root,{
- scope:fn=>fn({keep:x=>x}),release(){},
- ofText:(value,limit)=>text(graph,value,limit),toText:value=>readText(graph,value),
- collect:roots=>graph.collect(roots),stats:()=>graph.stats,dispose(){},
- });
-}
diff --git a/ext/wasm/backends/zig-wasm.mjs b/ext/wasm/backends/zig-wasm.mjs
@@ -1,15 +0,0 @@
-import {WasmRuntime} from '../runtime.mjs';
-import {treeSession} from '../tree-session.mjs';
-export async function create({bytes,name,artifacts}) {
- if(!artifacts?.wasm)throw new Error('Missing zig-wasm artifact');
- const runtime=await WasmRuntime.create(artifacts.wasm);
- try {
- const root=runtime.load(bytes,name);
- return treeSession(runtime,root,{
- scope:fn=>runtime.scope(fn),release:h=>runtime.release(h),
- ofText:(value,limit)=>runtime.ofText(value,limit),toText:h=>runtime.toText(h),
- collect:()=>runtime.collect(),stats:()=>({...runtime.stats(),memoryBytes:runtime.e.memory.buffer.byteLength}),
- dispose:()=>runtime.dispose(),
- });
- } catch(error) {runtime.dispose();throw error;}
-}
diff --git a/ext/wasm/browser-page.mjs b/ext/wasm/browser-page.mjs
@@ -1,129 +0,0 @@
-// Driven by browser.mjs over CDP; every operation has a main-thread timeout.
-const assert = (ok,message) => { if (!ok) throw new Error(message); };
-const bytes = async path => { const r=await fetch(path); if(!r.ok)throw new Error(path); return r.arrayBuffer(); };
-const digest = async b => Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256',b)),x=>x.toString(16).padStart(2,'0')).join('');
-function client() {
- const worker = new Worker(new URL('./worker.mjs',import.meta.url),{type:'module'});
- let id=0; const waiting=new Map(), unsolicited=[];
- const terminate = () => { worker.terminate(); for(const w of waiting.values()){clearTimeout(w.timer);w.reject(new Error('Terminated'));} waiting.clear(); };
- worker.onmessage=({data})=>{
- const w=waiting.get(data.id);
- if(!w){unsolicited.push(data);return;}
- waiting.delete(data.id);clearTimeout(w.timer);
- if(data.type==='error')w.reject(new Error(data.message));else w.resolve(data);
- };
- worker.onerror=e=>{for(const w of waiting.values()){clearTimeout(w.timer);w.reject(new Error(e.message));}waiting.clear();};
- return {terminate,unsolicited,send:m=>new Promise((resolve,reject)=>{
- const request=++id;
- const timer=setTimeout(()=>{waiting.delete(request);terminate();reject(new Error('Worker deadline'));},30_000);
- waiting.set(request,{resolve,reject,timer});worker.postMessage({...m,id:request});
- })};
-}
-// Only used to validate small, complete snapshots against independent goldens.
-function snapshotTree(snapshot) {
- assert(!snapshot.truncated,'Unexpected truncated fixture preview');
- const nodes=new Map(snapshot.nodes.map(n=>[n.id,n]));
- const aliases=new Map((snapshot.aliases??[]).map(a=>[a.from,a.to]));
- const pending=[snapshot.root], chars=[];
- while(pending.length) {
- assert(chars.length<10000,'Preview fixture expansion limit');
- let id=pending.pop();
- for(let i=0;aliases.has(id);i++){assert(i<256,'Alias cycle');id=aliases.get(id);}
- const node=nodes.get(id);assert(node,'Missing preview node');
- chars.push(String(node.children.length));pending.push(...[...node.children].reverse());
- }
- return chars.join('');
-}
-export async function run() {
- const wasm=await bytes('/arboricx.wasm'), identity=await bytes('/test/fixtures/id.arboricx');
- const goldens=await (await fetch('/ext/demo-site/test/compiler-conformance.json')).json();
- const compiler=await bytes(`/ext/demo-site/bundles/${goldens.compilerBundleSha256}.arboricx`);
- const init=(host,bundle,hash,name)=>({type:'init',backend:host==='wasm'?'zig-wasm':'js',bundle,hash,name,artifacts:{wasm}});
- const idHash=await digest(identity), samples=[], controls=[];
- let ticks=0;const heartbeat=setInterval(()=>ticks++,10);
- try {
- for(const host of ['js','wasm']) {
- const c=client();
- try {
- // A rejected hash never constructs the evaluator. Retry initialization.
- await c.send(init(host,identity,'0'.repeat(64))).then(()=>{throw new Error('Accepted wrong hash');},e=>assert(/hash mismatch/.test(e.message),'Wrong hash error: '+e.message));
- const malformed=new Uint8Array(identity.byteLength+1);malformed.set(new Uint8Array(identity));
- let invalidRejected=false;
- try{await c.send(init(host,malformed.buffer,await digest(malformed)));}catch{invalidRejected=true;}
- assert(invalidRejected,'Accepted malformed bundle with matching hash');
- const ready=await c.send(init(host,identity,idHash));
- assert(ready.backend===(host==='wasm'?'zig-wasm':'js'),'Wrong selected backend');
- await c.send(init(host==='wasm'?'js':'wasm',identity,idHash)).then(
- ()=>{throw new Error('Switched live session backend');},
- e=>assert(/Already initialized/.test(e.message),'Wrong session-switch error'));
- let surrogateRejected=false;
- try{await c.send({type:'text',value:'\ud800'});}catch{surrogateRejected=true;}
- assert(surrogateRejected,'Accepted unpaired surrogate');
- const unicode='\ufeff\0λ雪🙂\uffff\u{10ffff}';
- for(let i=0;i<10;i++)assert((await c.send({type:'text',value:unicode})).value===unicode,'Unicode mismatch');
- } finally {c.terminate();}
- controls.push(`${host}: hash/profile rejection, invalid surrogate recovery and Unicode`);
- }
- // Alternate fresh Worker order. Capture compilation (lazy definition handles)
- // separately from independent full definition/result validation.
- for(let round=0;round<3;round++)for(const host of round%2?['wasm','js']:['js','wasm']) {
- const c=client();
- try {
- await c.send(init(host,compiler,goldens.compilerBundleSha256,goldens.export));
- let repeatStats;
- for(let index=0;index<4;index++) {
- const golden=goldens.cases[index];
- const compiled=await c.send({type:'compile',source:golden.source});
- assert(JSON.stringify([...compiled.names].sort())===JSON.stringify(Object.keys(golden.definitions).sort()),'Definition names mismatch');
- const observeStart=performance.now();
- for(const [name,value] of Object.entries(golden.definitions))
- assert((await c.send({type:'inspect',name,arguments:[]})).value===value,`Definition mismatch ${name}`);
- assert((await c.send({type:'inspect',name:golden.export,arguments:golden.arguments})).value===golden.result,'Applied result mismatch');
- const observedMs=performance.now()-observeStart;
- let previewMs=0;
- for(const [name,value] of Object.entries(golden.definitions)) {
- const preview=await c.send({type:'preview',name,limit:256});
- assert(snapshotTree(preview.value)===value,`Preview mismatch ${name}`);
- previewMs+=preview.ms;
- }
- const {stats}=await c.send({type:'collect'});
- samples.push({host,round,case:index,compileMs:compiled.ms,observeRoundTripMs:observedMs,previewMs,stats});
- }
- // Replace retained exports, then verify compiler-only/definition roots
- // stabilize live counts after warm-up and explicit collection.
- for(let i=0;i<8;i++) {
- await c.send({type:'compile',source:goldens.cases[0].source});
- assert((await c.send({type:'inspect',name:'id',arguments:goldens.cases[0].arguments})).value===goldens.cases[0].result,'Retained compiler changed');
- const {stats}=await c.send({type:'collect'});
- if(host==='wasm' && repeatStats)assert(stats.owned===repeatStats.owned && stats.liveAfterCollect===repeatStats.liveAfterCollect,'Retained-session leak: '+JSON.stringify({stats,repeatStats}));
- repeatStats=stats;
- }
- for(const bad of goldens.cases.filter(x=>x.diagnostic)) {
- let diagnostic;
- try {await c.send({type:'compile',source:bad.source});} catch(error) {diagnostic=error.message;}
- assert(diagnostic?.includes(bad.diagnostic),'Compiler diagnostic mismatch: '+diagnostic);
- assert((await c.send({type:'inspect',name:'id',arguments:goldens.cases[0].arguments})).value===goldens.cases[0].result,'Failed compilation destroyed retained exports');
- }
- await c.send({type:'compile',source:goldens.cases[0].source});
- assert((await c.send({type:'inspect',name:'id',arguments:goldens.cases[0].arguments})).value===goldens.cases[0].result,'Compiler failed to recover');
- } finally {c.terminate();}
- }
- for(const host of ['js','wasm']) {
- const c=client();
- try {
- await c.send(init(host,identity,idHash));
- const before=ticks;
- assert((await c.send({type:'diverge'})).type==='running','No divergence acknowledgment');
- await new Promise(r=>setTimeout(r,100));
- assert(ticks>before,'Main thread blocked by evaluator');
- c.terminate();
- await new Promise(r=>setTimeout(r,100));
- assert(c.unsolicited.length===0,'Divergent evaluator returned before Stop');
- } finally {c.terminate();}
- // A separate fresh Worker must still initialize and execute after Stop.
- const fresh=client();try{await fresh.send(init(host,identity,idHash));assert((await fresh.send({type:'text',value:'restart🙂'})).value==='restart🙂','Restart failed');}finally{fresh.terminate();}
- controls.push(`${host}: hard termination, responsive main thread and fresh restart`);
- }
- return {schema:1,userAgent:navigator.userAgent,wasmSha256:await digest(wasm),compilerSha256:goldens.compilerBundleSha256,samples,controls};
- } finally {clearInterval(heartbeat);}
-}
diff --git a/ext/wasm/browser.mjs b/ext/wasm/browser.mjs
@@ -1,82 +0,0 @@
-import assert from 'node:assert/strict';
-import {spawn} from 'node:child_process';
-import {createServer} from 'node:http';
-import {readFile,mkdtemp,rm,writeFile} from 'node:fs/promises';
-import {tmpdir} from 'node:os';
-import {resolve,extname,sep} from 'node:path';
-const root=resolve('.');
-const server=createServer(async(req,res)=>{
- try {
- const path=new URL(req.url,'http://localhost').pathname;
- if(path==='/'){res.setHeader('Content-Type','text/html');res.end('<!doctype html><title>WASM experiment</title>');return;}
- const file=path==='/arboricx.wasm'?process.argv[2]:resolve(root,'.'+path);
- if(path!=='/arboricx.wasm'&&!file.startsWith(root+sep))throw new Error('outside root');
- res.setHeader('Content-Type',({'.mjs':'text/javascript','.js':'text/javascript','.wasm':'application/wasm','.json':'application/json'})[extname(file)]??'application/octet-stream');
- res.end(await readFile(file));
- } catch {res.writeHead(404);res.end();}
-});
-await new Promise(r=>server.listen(0,'127.0.0.1',r));
-const url=`http://127.0.0.1:${server.address().port}/`;
-const profile = await mkdtemp(resolve(tmpdir(), 'tricu-demo-chromium-'));
-let browser, socket;
-let next = 0; const pending = new Map();
-try {
- browser = spawn(process.argv[3] || 'chromium', ['--headless', '--no-sandbox', '--disable-dev-shm-usage',
- '--js-flags=--max-old-space-size=768', '--remote-debugging-port=0', `--user-data-dir=${profile}`, 'about:blank'], { stdio: ['ignore', 'ignore', 'pipe'] });
- const debuggerURL = await new Promise((resolve, reject) => {
- let stderr = '';
- const timer = setTimeout(() => reject(new Error('Chromium startup timed out: ' + stderr)), 15_000);
- browser.on('error', error => { clearTimeout(timer); reject(error); });
- browser.on('exit', code => { clearTimeout(timer); reject(new Error('Chromium exited: ' + code + '\n' + stderr)); });
- browser.stderr.on('data', chunk => {
- stderr += chunk;
- const match = stderr.match(/DevTools listening on (ws:\/\/\S+)/);
- if (match) { clearTimeout(timer); resolve(match[1]); }
- });
- });
- const endpoint = new URL(debuggerURL);
- const tab = await (await fetch(`http://${endpoint.host}/json/new?${encodeURIComponent(url)}`, { method: 'PUT' })).json();
- socket = new WebSocket(tab.webSocketDebuggerUrl);
- await new Promise((resolve, reject) => { socket.onopen = resolve; socket.onerror = reject; });
- const exceptions = [];
- socket.onmessage = ({ data }) => {
- const event = JSON.parse(data);
- if (event.id) {
- const waiter = pending.get(event.id); pending.delete(event.id);
- if (!waiter) return;
- clearTimeout(waiter.timer);
- if (event.error) waiter.reject(new Error(JSON.stringify(event.error)));
- else waiter.resolve(event.result);
- } else if (event.method === 'Runtime.exceptionThrown') exceptions.push(event.params);
- };
- const send = (method, params = {}) => new Promise((resolve, reject) => {
- const id = ++next;
- const timer = setTimeout(() => { pending.delete(id); reject(new Error('CDP timed out: ' + method)); }, 180_000);
- pending.set(id, { resolve, reject, timer });
- socket.send(JSON.stringify({ id, method, params }));
- });
- await send('Runtime.enable'); await send('Page.enable');
- const evaluate = async expression => {
- const result = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true });
- if (result.exceptionDetails) throw new Error(JSON.stringify(result.exceptionDetails));
- return result.result.value;
- };
- const report=await evaluate("import('/ext/wasm/browser-page.mjs').then(m=>m.run())");
- assert.equal(report.samples.length,24);
- assert.deepEqual(exceptions,[]);
- await writeFile(process.argv[4],JSON.stringify(report,null,2)+'\n');
- for(const index of [0,1,2,3]) {
- const median=host=>report.samples.filter(s=>s.host===host&&s.case===index).map(s=>s.compileMs).sort((a,b)=>a-b)[1];
- console.log(`browser compiler-${index}: JS ${median('js').toFixed(1)} ms; WASM ${median('wasm').toFixed(1)} ms`);
- }
- console.log(report.controls.join('\n'));
-} finally {
- for (const waiter of pending.values()) { clearTimeout(waiter.timer); waiter.reject(new Error('Browser test closed')); }
- pending.clear(); socket?.close();
- if (browser && browser.exitCode === null) {
- const exited = new Promise(resolve => browser.once('exit', resolve));
- browser.kill('SIGKILL'); await exited;
- }
- await new Promise(resolve => server.close(resolve));
- await rm(profile, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
-}
diff --git a/ext/wasm/check.mjs b/ext/wasm/check.mjs
@@ -1,173 +0,0 @@
-import assert from 'node:assert/strict';
-import {readFile} from 'node:fs/promises';
-import {WasmRuntime} from './runtime.mjs';
-const wasm = await readFile(process.argv[2]);
-function snapshotTree(snapshot) {
- assert.equal(snapshot.truncated,false);
- const nodes=new Map(snapshot.nodes.map(n=>[n.id,n])),pending=[snapshot.root],chars=[];
- while(pending.length) {
- assert.ok(chars.length<10000);
- const node=nodes.get(pending.pop());assert.ok(node);
- chars.push(String(node.children.length));pending.push(...[...node.children].reverse());
- }
- return chars.join('');
-}
-const vectors = JSON.parse(await readFile('ext/demo-site/test/conformance.json'));
-for (const c of vectors.cases) {
- const g = await WasmRuntime.create(wasm);
- try {
- const applied=g.apply(g.fromTernary(c.program),c.arguments.map(x=>g.fromTernary(x)));
- assert.equal(snapshotTree(g.preview(applied,256)),c.result);
- assert.equal(g.toTernary(applied),c.result);
- }
- finally { g.dispose(); }
-}
-const g = await WasmRuntime.create(wasm);
-const other = new WasmRuntime({exports: g.e});
-try {
- assert.throws(() => g.force(other.leaf), /status 1/);
- const dead = g.stem(g.leaf); g.release(dead);
- assert.throws(() => g.force(dead), /status 1/);
- assert.throws(() => g.load(new Uint8Array([1,2,3])), /status 5/);
- assert.throws(() => g.handle('arb_run_io_v2', g.leaf), /status 6/);
- const bytes = Uint8Array.from([0,127,128,255]);
- assert.deepEqual(g.toBytes(g.ofBytes(bytes)),bytes);
- const id = g.load(await readFile('test/fixtures/id.arboricx'));
- const arg = g.fromTernary('2100'), app = g.app(id,arg);
- assert.throws(() => g.force(app,0n), /status 3/);
- assert.equal(g.toTernary(app),'2100');
- g.release(id); g.release(arg); g.call('arb_collect');
- assert.equal(g.toTernary(app),'2100');
- const q = g.fromTernary('212110021100'), loop = g.app(q,q);
- assert.equal(g.toTernary(g.app(g.fromTernary('200'),loop)), '0');
- assert.throws(() => g.force(loop,100n), /status 3/);
- const before = g.e.memory.buffer;
- g.e.memory.grow(1);
- assert.notEqual(g.e.memory.buffer, before);
- assert.equal(g.toTernary(app),'2100');
-} finally { g.dispose(); other.dispose(); }
-assert.throws(() => g.force(0n), /Disposed/);
-console.log('WASM: 300 independent conformance vectors, handles, GC, malformed bundle, fuel, laziness, memory growth passed');
-
-const r = await WasmRuntime.create(wasm);
-try {
- const initial = r.stats().owned;
- const vectors = JSON.parse(await readFile('ext/demo-site/demos.json')).codecVectors;
- r.scope(() => {
- for (const v of vectors.filter(v=>v.kind === 'string')) {
- assert.equal(r.toTernary(r.ofText(v.value)),v.tree);
- assert.equal(r.toText(r.fromTernary(v.tree)),v.value);
- }
- });
- assert.equal(r.e.arb_host_alloc(512*1024*1024),0);
- assert.equal(r.toText(r.leaf),'');
- assert.throws(() => r.scope(async () => {}), /synchronous/);
- const unicode = '\ufeff\0a\r\néλ雪🙂\uffff\u{10ffff}';
- r.scope(() => {
- const tree = r.ofText(unicode);
- assert.equal(r.toText(tree), unicode);
- assert.throws(() => r.toText(tree,2), /status 5/);
- assert.throws(() => r.ofText('🙂',0), /limit/);
- assert.throws(() => r.ofText('\ud800'), /surrogate/);
- assert.throws(() => r.ofText('\udc00'), /surrogate/);
- assert.equal(r.toText(r.ofText('',0),0),'');
- const single = n => r.fork(r.handle('arb_of_number',n),r.leaf);
- for (const n of [0xd800n,0xdfffn,0x110000n]) assert.throws(() => r.toText(single(n)), /status 5/);
- const noncanonical = r.fork(r.leaf,r.leaf);
- assert.throws(() => r.toText(r.fork(noncanonical,r.leaf)), /status 5/);
- for (const bytes of [[0x80],[0xc0,0xaf],[0xed,0xa0,0x80],[0xf4,0x90,0x80,0x80]])
- assert.throws(() => r.buffer(Uint8Array.from(bytes),p=>r.handle('arb_host_of_text',p,bytes.length,10)), /status 5/);
- });
- assert.equal(r.stats().owned,initial);
- assert.throws(() => r.scope(() => { r.fromTernary('2100'); throw new Error('unwind'); }), /unwind/);
- assert.throws(() => r.scope(({keep}) => { keep(r.ofText('temporary')); throw new Error('unwind'); }), /unwind/);
- assert.throws(() => r.fromTernary('2100x'), /Invalid/);
- assert.equal(r.stats().owned,initial);
- const kept = r.scope(({keep}) => keep(r.scope(({keep}) => keep(r.ofText(unicode)))));
- r.collect(); assert.equal(r.toText(kept),unicode); r.release(kept);
- const id = r.load(await readFile('test/fixtures/id.arboricx'));
- r.call('arb_set_gc_policy',32,1);
- r.collect(); const baseline = r.stats();
- for (let i=0;i<200;i++) {
- r.scope(() => {
- const input = r.ofText(unicode), applied = r.app(id,input);
- r.release(input); // application owns its graph, not the input handle
- assert.equal(r.toText(applied),unicode);
- assert.throws(() => r.toTernary(applied,2), /expansion/);
- });
- r.collect();
- assert.equal(r.stats().owned,baseline.owned);
- assert.equal(r.stats().nodes,baseline.nodes);
- assert.ok(r.stats().slots < 100);
- }
- r.release(id);
-} finally { r.dispose(); }
-console.log('WASM: strict Unicode, nested scope escape/failure cleanup, and 200 retained-context GC cycles passed');
-
-const previewRuntime = await WasmRuntime.create(wasm);
-try {
- const p = previewRuntime;
- p.call('arb_set_gc_policy',1,1);
- p.scope(() => {
- let root = p.retain(p.leaf);
- for(let i=0;i<31;i++) {const next=p.fork(root,root);p.release(root);root=next;}
- const before=p.stats().owned;
- for(let i=0;i<100;i++) {
- const snapshot=p.preview(root,32);
- assert.equal(snapshot.nodes.length,32);assert.equal(snapshot.truncated,false);
- assert.equal(snapshot.root,0);
- for(let j=0;j<31;j++)assert.deepEqual(snapshot.nodes[j],{id:j,kind:'fork',children:[j+1,j+1]});
- assert.deepEqual(snapshot.nodes[31],{id:31,kind:'leaf',children:[]});
- assert.equal(p.stats().owned,before);
- }
- const deep=p.fromTernary('1'.repeat(1000)+'0');
- const bounded=p.preview(deep,256);
- assert.equal(bounded.nodes.length,256);assert.equal(bounded.truncated,true);
- p.release(deep);
- const short=p.preview(root,1);
- assert.equal(short.nodes.length,1);assert.equal(short.truncated,true);
- assert.deepEqual(short.nodes[0].children,[1,1]);
- assert.throws(()=>p.preview(root,0),/budget/);
- assert.throws(()=>p.preview(root,257),/budget/);
- const dead=p.retain(root);p.release(dead);
- assert.throws(()=>p.preview(dead),/status 1/);
- const q=p.fromTernary('212110021100'),loop=p.app(q,q);
- const lazy=p.stem(loop);
- assert.equal(p.preview(lazy,1,0n).truncated,true); // frontier isn't forced
- assert.throws(()=>p.preview(lazy,2,10n),/status 3/);
- assert.equal(p.stats().owned,before+3);
- // A failed demand leaves output fields untouched, and no snapshot buffer.
- p.view().setBigUint64(p.scratch,0x123456789abcdef0n,true);
- assert.equal(p.e.arb_host_preview(p.ctx,loop,2,10n,p.scratch,p.scratch+8),3);
- assert.equal(p.view().getBigUint64(p.scratch,true),0x123456789abcdef0n);
- assert.deepEqual(p.preview(p.leaf,1),{root:0,nodes:[{id:0,kind:'leaf',children:[]}],truncated:false});
- // Snapshot bytes own their labels; collection/reuse cannot mutate a result.
- const saved=structuredClone(short);p.release(root);p.collect();
- assert.deepEqual(short,saved);
- });
- assert.equal(p.stats().owned,1);
-} finally {previewRuntime.dispose();}
-console.log('WASM: bounded bulk preview sharing, exact limits, lazy frontier, fuel failure, GC and ownership passed');
-
-const unbounded=await WasmRuntime.create(wasm,{unbounded:true});
-try {
- const r=unbounded;
- await r.scopeAsync(async()=>{
- const h=r.retain(r.leaf);
- await assert.rejects(r.scopeAsync(async()=>{}),/exclusive/);
- r.scope(({keep})=>keep(r.retain(h)));
- await Promise.resolve();r.collect();assert.equal(r.toTernary(h),'0');
- });
- assert.equal(r.stats().owned,1);
- await assert.rejects(r.scopeAsync(async()=>{r.retain(r.leaf);throw Error('host failure');}),/host failure/);
- assert.equal(r.stats().owned,1);
- r.scope(()=>{
- const delayed=r.app(r.leaf,r.leaf);
- assert.throws(()=>r.force(delayed,0n),/status 3/);
- assert.equal(r.toTernary(delayed),'10');
- assert.equal(r.preview(delayed).nodes.length,2);
- });
- assert.equal(r.stats().owned,1);
- assert.throws(()=>r.check(2),/status 2/);assert.equal(r.poisoned,true);
-}finally{unbounded.dispose();}
-console.log('WASM: explicit unbounded demands, finite negative control, exclusive async cleanup and fatal allocation classification passed');
diff --git a/ext/wasm/compare.mjs b/ext/wasm/compare.mjs
@@ -1,70 +0,0 @@
-// Fresh-process paired smoke, not a browser performance claim.
-import assert from 'node:assert/strict';
-import {readFile, writeFile} from 'node:fs/promises';
-import {spawnSync} from 'node:child_process';
-import {createHash} from 'node:crypto';
-import {cpus} from 'node:os';
-import {WasmRuntime} from './runtime.mjs';
-import {Graph} from '../demo-site/dist/graph.js';
-import {decodeBundle,loadBundle} from '../demo-site/dist/bundle.js';
-import {text,readText} from '../demo-site/dist/codecs.js';
-import {compiledDefinitions} from '../demo-site/dist/compiler.js';
-const [wasmPath, output, host, job] = process.argv.slice(2);
-const fixtures = JSON.parse(await readFile('ext/demo-site/test/compiler-conformance.json'));
-const cases = ['identity','append','identity-bulk','append-bulk','identity-text','append-text', 'compiler-0','compiler-1','compiler-2','compiler-3'];
-if (host) {
- const start = performance.now();
- const g = host === 'wasm' ? await WasmRuntime.create(await readFile(wasmPath))
- : new Graph({nodes:6_000_000,fuel:Infinity,stack:100_000},{threshold:250_000});
- const compiler = job.startsWith('compiler-');
- const bulk = host === 'wasm' && job.endsWith('-bulk');
- const scalar = host === 'wasm' && job.endsWith('-text');
- const bytes = await readFile(compiler ? `ext/demo-site/bundles/${fixtures.compilerBundleSha256}.arboricx`
- : `test/fixtures/${job.startsWith('identity') ? 'id' : 'append'}.arboricx`);
- const name = compiler ? fixtures.export : undefined;
- const root = host === 'wasm' ? g.load(bytes,name) : (() => {
- const b = decodeBundle(bytes), exports = loadBundle(g,b);
- return exports.get(name ?? b.exports[0].name);
- })();
- const loaded = performance.now();
- let result, expected;
- if (compiler) {
- const c = fixtures.cases[Number(job.split('-')[1])];
- result = g.apply(root,[g.leaf,host==='wasm'?g.ofText(c.source,16384):text(g,c.source)]); expected = c;
- } else {
- const a = (job.endsWith('-text')?'λ雪🙂':'abc012').repeat(100), b = (job.endsWith('-text')?'é\0🦊':'XYZ789').repeat(100);
- result = g.apply(root, (job.startsWith('identity') ? [a] : [a,b]).map(x => scalar ? g.ofText(x) : bulk ? g.ofBytes(new TextEncoder().encode(x)) : text(g,x)));
- expected = job.startsWith('identity') ? a : a+b;
- }
- const constructed = performance.now();
- if (compiler) {
- const defs = compiledDefinitions(g,result);
- assert.deepEqual([...defs.keys()].sort(),Object.keys(expected.definitions).sort());
- for (const [name, tree] of Object.entries(expected.definitions)) assert.equal(g.toTernary(defs.get(name)),tree);
- assert.equal(g.toTernary(g.apply(defs.get(expected.export),expected.arguments.map(x=>g.fromTernary(x)))),expected.result);
- } else assert.equal(scalar ? g.toText(result) : bulk ? new TextDecoder('utf-8',{fatal:true}).decode(g.toBytes(result)) : readText(g,result),expected);
- const end = performance.now();
- console.log(JSON.stringify({host,job,loadMs:loaded-start,constructMs:constructed-loaded,
- evaluateObserveMs:end-constructed,totalMs:end-start,peakRssKiB:process.resourceUsage().maxRSS,
- wasmMemoryBytes:host==='wasm'?g.e.memory.buffer.byteLength:null,
- fixtureSha256:createHash('sha256').update(bytes).digest('hex')}));
- g.dispose?.();
-} else {
- const samples=[];
- for (const job of cases) for(let round=0;round<3;round++) {
- for (const host of round%2 ? ['wasm','js'] : ['js','wasm']) {
- const child=spawnSync(process.execPath,['--max-old-space-size=512',import.meta.filename,wasmPath,'-',host,job],
- {encoding:'utf8',timeout:120_000,maxBuffer:1024*1024});
- if(child.error || child.status!==0) throw new Error(`${job}/${host}: ${child.error ?? child.stderr}`);
- samples.push({...JSON.parse(child.stdout),round});
- }
- }
- const report={schema:1,node:process.version,cpu:cpus()[0]?.model,platform:process.platform,
- wasmSha256:createHash('sha256').update(await readFile(wasmPath)).digest('hex'),
- optimization:'ReleaseSafe',samples};
- await writeFile(output,JSON.stringify(report,null,2)+'\n');
- for (const job of cases) {
- const median=h=>samples.filter(s=>s.job===job&&s.host===h).map(s=>s.evaluateObserveMs).sort((a,b)=>a-b)[1];
- console.log(`${job}: JS ${median('js').toFixed(1)} ms; WASM ${median('wasm').toFixed(1)} ms; ${(median('js')/median('wasm')).toFixed(2)}x`);
- }
-}
diff --git a/ext/wasm/preview-compare.mjs b/ext/wasm/preview-compare.mjs
@@ -1,48 +0,0 @@
-// Compare the same 128-node unary tree: no sharing/expansion advantage hidden
-// in the baseline. Every iteration checks the complete shape in either format.
-import assert from 'node:assert/strict';
-import {readFile,writeFile} from 'node:fs/promises';
-import {spawnSync} from 'node:child_process';
-import {createHash} from 'node:crypto';
-import {cpus} from 'node:os';
-import {WasmRuntime} from './runtime.mjs';
-import {Graph} from '../demo-site/dist/graph.js';
-const [wasmPath,output,mode,pressureArg]=process.argv.slice(2);
-const tree='1'.repeat(127)+'0',iterations=100;
-if(mode) {
- const pressure=Number(pressureArg);
- const g=mode==='js'?new Graph({fuel:Infinity}):await WasmRuntime.create(await readFile(wasmPath));
- const root=g.fromTernary(tree);
- // Extra owned roots simulate a retained host session. JS has no root-table
- // scan in this path, but retain equivalent host references for comparison.
- const retained=Array.from({length:pressure},()=>mode==='js'?g.leaf:g.retain(g.leaf));
- const before=mode==='js'?null:g.stats().owned;
- const start=performance.now();
- for(let i=0;i<iterations;i++) {
- if(mode==='wasm-batch') {
- const p=g.preview(root,128);
- assert.equal(p.truncated,false);assert.equal(p.nodes.length,128);
- for(let j=0;j<128;j++)assert.deepEqual(p.nodes[j],{id:j,kind:j===127?'leaf':'stem',children:j===127?[]:[j+1]});
- } else assert.equal(g.toTernary(root),tree);
- }
- const ms=performance.now()-start;
- if(mode!=='js')assert.equal(g.stats().owned,before);
- assert.equal(retained.length,pressure);
- console.log(JSON.stringify({mode,pressure,iterations,ms,perSnapshotMs:ms/iterations,peakRssKiB:process.resourceUsage().maxRSS}));
- g.dispose?.();
-} else {
- const samples=[];
- for(const pressure of [0,2000])for(let round=0;round<3;round++) {
- for(const mode of round%2?['wasm-batch','wasm-node','js']:['js','wasm-node','wasm-batch']) {
- const child=spawnSync(process.execPath,[import.meta.filename,wasmPath,'-',mode,String(pressure)],{encoding:'utf8',timeout:60_000});
- if(child.error||child.status!==0)throw new Error(`${mode}: ${child.error??child.stderr}`);
- samples.push({...JSON.parse(child.stdout),round});
- }
- }
- await writeFile(output,JSON.stringify({schema:1,node:process.version,cpu:cpus()[0]?.model,
- wasmSha256:createHash('sha256').update(await readFile(wasmPath)).digest('hex'),tree, samples},null,2)+'\n');
- for(const pressure of [0,2000]) {
- const median=mode=>samples.filter(s=>s.mode===mode&&s.pressure===pressure).map(s=>s.perSnapshotMs).sort((a,b)=>a-b)[1];
- console.log(`128-node inspection (${pressure} extra roots): JS ${median('js').toFixed(3)} ms; WASM/node ${median('wasm-node').toFixed(3)} ms; WASM/batch ${median('wasm-batch').toFixed(3)} ms`);
- }
-}
diff --git a/ext/wasm/runtime.mjs b/ext/wasm/runtime.mjs
@@ -1,223 +0,0 @@
-// Experimental browser/Node adapter. Scope temporary handles; dispose after use.
-// Handles returned to callers remain owned until release() or dispose().
-export class WasmRuntime {
- static async create(bytes, {unbounded = false} = {}) {
- const module = await WebAssembly.compile(bytes);
- if (WebAssembly.Module.imports(module).length) throw new Error('Unexpected WASM imports');
- const runtime = new WasmRuntime(await WebAssembly.instantiate(module));
- runtime.unbounded = unbounded;
- return runtime;
- }
- constructor(instance) {
- this.e = instance.exports;
- if (this.e.arb_abi_version() !== 3) throw new Error('ABI v3 required');
- this.ctx = this.e.arboricx_init();
- if (!this.ctx) throw new Error('WASM initialization failed');
- this.scratch = this.e.arb_host_alloc(16);
- if (!this.scratch) { this.e.arboricx_free(this.ctx); throw new Error('Scratch allocation failed'); }
- this.scopes = [];
- this.owners = new Map();
- try { this.leaf = this.handle('arb_leaf'); }
- catch (error) { this.dispose(); throw error; }
- }
- check(status) {
- if ([1,2,7].includes(status)) this.poisoned = true;
- if (status !== 0) throw new Error(`Arboricx status ${status}`);
- }
- call(name, ...args) {
- if (!this.ctx) throw new Error('Disposed WASM runtime');
- try { this.check(this.e[name](this.ctx, ...args)); }
- catch (error) { if (error instanceof WebAssembly.RuntimeError) this.poisoned = true; throw error; }
- }
- // Recreate views after every call: memory.grow detaches previous buffers.
- view() { return new DataView(this.e.memory.buffer); }
- handle(name, ...args) {
- this.call(name, ...args, this.scratch);
- return this.own(this.view().getBigUint64(this.scratch, true));
- }
- own(handle) {
- const scope = this.scopes.at(-1);
- this.owners.set(handle, scope);
- scope?.add(handle);
- return handle;
- }
- // Synchronous scopes only. keep() transfers a newly owned handle to the parent;
- // borrowed inputs are never implicitly released. Failure also unwinds roots.
- scope(fn) {
- if (fn.constructor.name === 'AsyncFunction') throw new Error('Scopes must be synchronous');
- const handles = new Set(), escaped = new Set(), parent = this.scopes.at(-1);
- this.scopes.push(handles);
- const keep = h => {
- if (!handles.has(h)) throw new Error('keep requires a handle owned by this scope');
- escaped.add(h); return h;
- };
- try {
- const result = fn({keep});
- if (result && typeof result.then === 'function') throw new Error('Scopes must be synchronous');
- for (const h of escaped) {
- if (!handles.has(h)) throw new Error('Escaped handle was released');
- }
- for (const h of escaped) { handles.delete(h); parent?.add(h); this.owners.set(h,parent); }
- return result;
- } finally {
- this.scopes.pop();
- for (const h of handles) this.release(h);
- }
- }
- // Exclusive outer operation only. Session callers serialize operations; nested
- // synchronous scopes work normally. No other async operation may enter.
- async scopeAsync(fn) {
- if (this.scopes.length) throw new Error('Async scope requires exclusive ownership');
- const handles = new Set(); this.scopes.push(handles);
- try { return await fn(); }
- finally { this.scopes.pop(); for (const h of handles) this.release(h); }
- }
- // Interpreter turn boundary inside an exclusive scopeAsync. Validate every
- // live root first, then retire only handles owned by this scope. Borrowed
- // caller roots remain caller-owned; no stale root can partially sweep state.
- sweepScope(roots) {
- const scope=this.scopes.at(-1);if(!scope)throw Error('Root sweep requires an owned scope');
- const live=new Set(roots);
- for(const h of live)if(!this.owners.has(h))throw Error('Stale interpreter root');
- for(const h of scope)if(!live.has(h))this.release(h);
- this.collect();
- }
- retain(x) { return this.handle('arb_retain', x); }
- collect() { this.call('arb_collect'); }
- stats() {
- this.call('arb_host_stats', this.scratch, this.scratch + 4, this.scratch + 8, this.scratch + 12);
- const v = this.view();
- return {slots:v.getUint32(this.scratch,true), owned:v.getUint32(this.scratch+4,true), nodes:v.getUint32(this.scratch+8,true), liveAfterCollect:v.getUint32(this.scratch+12,true)};
- }
- buffer(bytes, fn) {
- if (!this.ctx) throw new Error('Disposed WASM runtime');
- const p = this.e.arb_host_alloc(Math.max(1, bytes.length));
- if (!p) { this.poisoned = true; throw new Error('WASM buffer allocation failed'); }
- try { new Uint8Array(this.e.memory.buffer, p, bytes.length).set(bytes); return fn(p); }
- finally { this.e.arb_host_free(p, Math.max(1, bytes.length)); }
- }
- load(bytes, name) {
- if (name !== undefined && (typeof name !== 'string' || name.includes('\0'))) throw new Error('Invalid export name');
- return this.buffer(bytes, p => name === undefined
- ? this.handle('arb_load_bundle_default', p, bytes.length)
- : this.buffer(new TextEncoder().encode(name + '\0'), n => this.handle('arb_load_bundle', p, bytes.length, n)));
- }
- ofBytes(bytes) { return this.buffer(bytes, p => this.handle('arb_of_bytes', p, bytes.length)); }
- textLimit(limit) {
- if (!Number.isSafeInteger(limit) || limit < 0 || limit > 0xffffffff) throw new Error('Invalid scalar limit');
- }
- ofText(value, limit = 4096) {
- this.textLimit(limit);
- if (typeof value !== 'string' || value.length > limit * 2) throw new Error('Text limit/type');
- let count = 0;
- for (const c of value) {
- const scalar = c.codePointAt(0);
- if (scalar >= 0xd800 && scalar <= 0xdfff) throw new Error('Unpaired Unicode surrogate');
- if (++count > limit) throw new Error('Text limit');
- }
- const bytes = new TextEncoder().encode(value);
- return this.buffer(bytes, p => this.handle('arb_host_of_text', p, bytes.length, limit));
- }
- toText(x, limit = 4096) {
- this.textLimit(limit);
- // Preserve an initial U+FEFF; this is text, not a source-file BOM policy.
- return new TextDecoder('utf-8', {fatal:true, ignoreBOM:true}).decode(this.output('arb_host_to_text', x, limit));
- }
- // Labels are snapshot-local numbers, not owned handles or live node IDs.
- preview(x, limit = 64, fuel = undefined) {
- if (!Number.isInteger(limit) || limit < 1 || limit > 256) throw new Error('Invalid preview budget');
- const unlimited = this.unbounded && fuel === undefined;
- fuel ??= 1_000_000n;
- if (typeof fuel !== 'bigint' || fuel < 0n || fuel > 0xffffffffffffffffn) throw new Error('Invalid preview fuel');
- const bytes = unlimited ? this.output('arb_host_preview_unbounded', x, limit) : this.output('arb_host_preview', x, limit, fuel);
- return this.decodeSnapshot(bytes);
- }
- snapshot(x,limit=100000) {
- if(!Number.isInteger(limit)||limit<1||limit>100000)throw Error('Invalid snapshot budget');
- return this.decodeSnapshot(this.output('arb_host_snapshot_unbounded',x,limit));
- }
- decodeSnapshot(bytes) {
- const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
- if (view.getUint32(0,true) !== 1) throw new Error('Unsupported preview format');
- const nodes = [], count = view.getUint32(8,true);
- for (let i=0;i<count;i++) {
- const at = 16+i*16, arity=view.getUint32(at+4,true);
- nodes.push({id:view.getUint32(at,true),kind:['leaf','stem','fork'][arity],
- children:Array.from({length:arity},(_,j)=>view.getUint32(at+8+4*j,true))});
- }
- return {root:view.getUint32(4,true),nodes,truncated:!!view.getUint32(12,true)};
- }
- toBytes(x) { return this.output('arb_to_bytes', x); }
- output(name, ...args) {
- this.call(name, ...args, this.scratch, this.scratch + 8);
- const p = this.view().getUint32(this.scratch, true);
- const n = this.view().getUint32(this.scratch + 8, true);
- try { return new Uint8Array(this.e.memory.buffer, p, n).slice(); }
- finally { this.e.arboricx_free_buf(this.ctx, p, n); }
- }
- stem(x) { return this.handle('arb_stem', x); }
- fork(a, b) { return this.handle('arb_fork', a, b); }
- app(f, x) { return this.handle('arb_app', f, x); }
- apply(f, args) {
- return this.scope(({keep}) => {
- let result = this.retain(f);
- for (const x of args) { const next = this.app(result, x); this.release(result); result = next; }
- return keep(result);
- });
- }
- force(x, fuel = undefined) {
- return this.unbounded && fuel === undefined ? this.handle('arb_host_reduce_unbounded', x) : this.handle('arb_reduce', x, fuel ?? 1_000_000_000n);
- }
- release(x) { this.call('arb_release', x); this.owners.get(x)?.delete(x); this.owners.delete(x); }
- observe(x) {
- const id = this.force(x);
- try {
- for (const kind of ['leaf', 'stem', 'fork']) {
- this.call('arb_is_' + kind, id, this.scratch);
- if (!this.view().getInt32(this.scratch, true)) continue;
- if (kind === 'leaf') return {node: {kind}};
- if (kind === 'stem') return {node: {kind, child: this.handle('arb_get_stem_child', id)}};
- this.call('arb_get_fork_children', id, this.scratch, this.scratch + 8);
- return {node: {kind, left: this.own(this.view().getBigUint64(this.scratch, true)), right: this.own(this.view().getBigUint64(this.scratch + 8, true))}};
- }
- throw new Error('Expected constructor');
- } finally { this.release(id); }
- }
- pair(x) { const {node} = this.observe(x); if (node.kind !== 'fork') throw new Error('Expected pair'); return [node.left, node.right]; }
- fromTernary(s) {
- return this.scope(({keep}) => {
- const stack = [];
- for (let i = s.length - 1; i >= 0; i--) {
- if (s[i] === '0') stack.push(this.retain(this.leaf));
- else if (s[i] === '1' && stack.length >= 1) {
- const child = stack.pop(); stack.push(this.stem(child)); this.release(child);
- } else if (s[i] === '2' && stack.length >= 2) {
- const a = stack.pop(), b = stack.pop(); stack.push(this.fork(a,b)); this.release(a); this.release(b);
- } else throw new Error('Invalid ternary tree');
- }
- if (stack.length !== 1) throw new Error('Invalid ternary tree');
- return keep(stack[0]);
- });
- }
- toTernary(x, limit = 1_000_000) {
- return this.scope(() => {
- const pending = [this.retain(x)], result = [];
- while (pending.length) {
- if (result.length >= limit) throw new Error('Tree expansion limit');
- const current = pending.pop(), {node} = this.observe(current);
- this.release(current);
- if (node.kind === 'leaf') result.push('0');
- else if (node.kind === 'stem') { result.push('1'); pending.push(node.child); }
- else { result.push('2'); pending.push(node.right, node.left); }
- }
- return result.join('');
- });
- }
- dispose() {
- if (!this.ctx) return;
- if (this.scopes.length) throw new Error('Cannot dispose inside a scope');
- this.owners.clear();
- this.e.arb_host_free(this.scratch, 16);
- this.e.arboricx_free(this.ctx); this.ctx = 0;
- }
-}
diff --git a/ext/wasm/site-session.mjs b/ext/wasm/site-session.mjs
@@ -1,191 +0,0 @@
-// Zig's private site adapter. Only cloneable values cross the session boundary;
-// native owned handles stay here. Shared helpers validate/authenticate inputs.
-import {WasmRuntime} from './runtime.mjs';
-export async function create({base,compiler}) {
- const {bundle,codec,comp,contracts,packages,expressions,treeResults,modules,moduleBundle,ioV2Private,ioV2Profile,ioV2Session,ioV2Driver}=await import(new URL('dist/zig-helpers.js',base).href);
- const bytes=await bundle.fetchBytes(new URL('runtime/zig/arboricx.wasm',base));
- const g=await WasmRuntime.create(bytes,{unbounded:true});
- // The shared DAG reader's tick is JS accounting, not a native work ceiling.
- g.tick=()=>{};
- let closed=false,busy=false,definitions=new Map(),library=new Map(),expressionScope=null,compilerHash;
- let nextRun=0n;const lifetime=new AbortController();
- const alive=()=>{if(closed)throw Error('Disposed Zig session');};
- const cleanup=()=>{try{g.dispose();}catch{/* A poisoned instance is discarded, never reused. */}};
- async function exclusive(fn) {
- alive();if(busy)throw Error('Session already busy');busy=true;
- try{return await fn();}
- catch(error){if(g.poisoned||error instanceof WebAssembly.RuntimeError)closed=true;throw error;}
- finally{busy=false;if(closed)cleanup();}
- }
- function importBundle(value) {
- return g.scope(({keep})=>new Map([...bundle.loadBundle(g,value)].map(([name,h])=>[name,keep(g.retain(h))])));
- }
- const release=values=>{for(const h of values.values())g.release(h);};
- function clearDefinitions(){
- release(definitions);definitions=new Map();
- if(expressionScope)release(expressionScope);expressionScope=null;
- }
- const fetchJSON=async(name,limit)=>JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(await bundle.fetchBytes(new URL(name,base),limit)));
- function literal(value) {
- if(value.type==='string')return g.ofText(value.value);
- if(value.type==='list')return codec.list(g,value.value.map(literal));
- return codec.makeLiteral(g,value);
- }
- function view(h,type) {
- if(treeResults.isTreeResult(type))return treeResults.treeResult({...g.snapshot(h),aliases:[]},type);
- if(type==='string')return {type,value:g.toText(h)};
- if(type==='tree')return {type,value:{...g.preview(h),aliases:[]}};
- if(type==='list')return {type,value:codec.readList(g,h,32).map(x=>({...g.preview(x,8),aliases:[]}))};
- return codec.view(g,h,type);
- }
- async function io(handle,host,reader=x=>x) {
- const tree=ioV2Private.constructors({leaf:g.leaf,observe:g.observe.bind(g),stem:g.stem.bind(g),fork:g.fork.bind(g),app:g.app.bind(g)});
- const selected=ioV2Profile.envelope(tree,handle);
- if(!host.v2)throw Error('IO version 2 requires a structured terminal host');
- host.selected?.(2);
- if(nextRun===0xffffffffffffffffn)throw Error('Run identity exhausted');
- const run=String(++nextRun), interpreter=new ioV2Session.Session(tree,selected.action,run,roots=>g.sweepScope(roots),
- {summarize:false,steps:host.v2.stepping===true?1:128,backend:'zig-wasm'});
- return ioV2Driver.drive({run,signal:lifetime.signal,discard:()=>session.dispose(),turn:command=>{alive();return interpreter.turn(command);},view:async()=>{alive();return reader(interpreter.core.result.value);}},host);
- }
- const execute=(name,args,mode,host,reader)=>exclusive(async()=>{
- const root=definitions.get(name);if(root===undefined)throw Error('Missing definition');
- args=codec.literals(args);
- if(!['pure','io'].includes(mode))throw Error('Unsupported execution mode');
- try{return await g.scopeAsync(async()=>{
- const applied=g.apply(root,args.map(literal));
- return mode==='io'?await io(applied,host,reader):reader(applied);
- });}finally{if(!g.poisoned)g.collect();}
- });
- async function ensureCompiler(){
- if(compilerHash)return;
- const hash=comp.compilerDescriptor(await fetchJSON('compiler.json',4096)).hash;
- const next=importBundle(await bundle.fetchBundle(new URL(`/_arboricx/bundle/hash/${hash}`,base),hash,bundle.COMPILER_BUNDLE_LIMIT));
- if(!next.has(comp.COMPILER_EXPORT)){release(next);throw Error('Missing stable compiler entrypoint');}
- library=next;compilerHash=hash;
- }
- const session={
- get disposed(){return closed;},
- load(bytes,hash){return exclusive(async()=>{
- bundle.bundleHash(hash);
- if(!(bytes instanceof Uint8Array)||bytes.byteLength>bundle.BUNDLE_LIMIT)throw Error('Bundle byte limit/type');
- bytes=bytes.slice(); // Authenticate the same immutable snapshot we decode.
- if(await bundle.sha256(bytes)!==hash)throw Error('Bundle SHA-256 mismatch');
- const next=importBundle(bundle.decodeBundle(bytes));clearDefinitions();definitions=next;g.collect();
- });},
- inspectModule(source){return exclusive(async()=>{
- if(!compilerHash)throw Error('Compiler not initialized');
- return g.scope(()=>modules.stageModule(g,library,comp.sourceText(source)));
- });},
- exportModule(){return exclusive(async()=>{
- if(!definitions.size)throw Error('No compiled module');
- return g.scope(()=>moduleBundle.moduleSnapshot([...definitions.keys()],{...g.snapshot(codec.list(g,[...definitions.values()])),aliases:[]}));
- });},
- compile(source,profileName,localModules=[]){return exclusive(async()=>{
- clearDefinitions();
- if(!compilerHash)throw Error('Compiler not initialized');
- try {
- source=comp.sourceText(source);
- const staged=g.scope(()=>modules.stageModule(g,library,source));
- if(staged.native){
- const dependencies=await modules.moduleDependencies(base,staged,localModules);alive();
- const next=g.scope(({keep})=>{
- const loaded=new Map([...dependencies].map(([ref,value])=>[ref,importBundle(value)]));
- const imported=new Map();
- const qualify=library.get('source_module.sourceModuleQualified');
- if(qualify===undefined)throw Error('Missing native module compiler helper');
- for(const [ref,alias] of staged.imports){
- const values=loaded.get(ref);if(!values)throw Error('Missing module dependency');
- for(const [name,h] of values){
- const qualified=g.scope(()=>g.toText(g.apply(qualify,[g.ofText(alias),g.ofText(name)]),4096));
- if(imported.has(qualified))throw Error('duplicate imported name');
- if(imported.size>=4096)throw Error('Module binding limit exceeded');
- imported.set(qualified,h);
- }
- }
- const environment=codec.list(g,[...imported].map(([name,h])=>g.fork(g.ofText(name),h)));
- const result=g.apply(library.get(comp.COMPILER_EXPORT),[environment,g.ofText(staged.body,16384)]);
- const locals=comp.compiledDefinitions(g,result,true);
- const publicValues=modules.selectModuleExports(staged,locals,loaded);
- const own=values=>new Map([...values].map(([name,h])=>[name,keep(g.retain(h))]));
- return {public:own(publicValues),scope:own(new Map([...imported,...locals]))};
- });
- definitions=next.public;expressionScope=next.scope;
- }else{
- let support,selected;
- if(profileName!==undefined){
- selected=packages.environmentProfile(await fetchJSON('environments.json',32768),profileName);
- if(selected.compiler!==compilerHash)throw Error('Demo environment/compiler pin mismatch');
- if(selected.profile)support=await bundle.fetchBundle(new URL(`/_arboricx/bundle/hash/${selected.profile.hash}`,base),selected.profile.hash);
- }
- alive();
- definitions=g.scope(({keep})=>{
- let environment;
- if(profileName===undefined)environment=contracts.contractEnvironment(g,library);
- else {
- const values=support?importBundle(support):new Map();
- environment=codec.list(g,(selected.profile?.names??[]).map(name=>{
- const h=values.get(name);if(h===undefined)throw Error(`Missing demo environment binding ${name}`);
- return g.fork(g.ofText(name),h);
- }));
- }
- const result=g.apply(library.get(comp.COMPILER_EXPORT),[environment,g.ofText(source,16384)]);
- const values=comp.compiledDefinitions(g,result);
- for(const h of values.values())keep(h);
- return values;
- });
- }
- return [...definitions.keys()];
- }finally{if(!g.poisoned&&!closed)g.collect();}
- });},
- runDefinitionIO(name,output,host){return exclusive(async()=>{
- const root=(expressionScope??definitions).get(name);if(root===undefined)throw Error('Missing definition');
- output=codec.outputType(output);
- try{return await g.scopeAsync(async()=>io(root,host,h=>view(h,output)));}finally{if(!g.poisoned)g.collect();}
- });},
- probeIO(name){return exclusive(async()=>{
- const root=(expressionScope??definitions).get(name);if(root===undefined)throw Error('Missing definition');
- try{return g.scope(()=>{
- try{ioV2Profile.envelope({observe:g.observe.bind(g)},root);return true;
- }catch(error){if(g.poisoned||error instanceof WebAssembly.RuntimeError)throw error;return false;}
- });}finally{if(!g.poisoned)g.collect();}
- });},
- inspectDefinition(name,output){return exclusive(async()=>{
- const root=(expressionScope??definitions).get(name);if(root===undefined)throw Error('Missing definition');
- output=codec.outputType(output);
- try{return g.scope(()=>view(root,output));}finally{if(!g.poisoned)g.collect();}
- });},
- run(name,args,output,mode,host){
- output=codec.outputType(output);
- return execute(name,args,mode,host,h=>view(h,output));
- },
- runExpression(source,output,mode,host){return exclusive(async()=>{
- const unit=expressions.expressionUnit(source,(expressionScope??definitions).keys());
- output=codec.outputType(output);
- if(!['pure','io'].includes(mode))throw Error('Unsupported execution mode');
- await ensureCompiler();
- try{return await g.scopeAsync(async()=>{
- const environment=codec.list(g,[...(expressionScope??definitions)].map(([name,h])=>g.fork(g.ofText(name),h)));
- const values=comp.compiledDefinitions(g,g.apply(library.get(comp.COMPILER_EXPORT),[environment,g.ofText(unit.source,16384)]));
- if(values.size!==1||!values.has(unit.name))throw Error('Enter one expression, not a source unit');
- const result=values.get(unit.name);
- return mode==='io'?await io(result,host,h=>view(h,output)):view(result,output);
- });}catch(error){throw expressions.expressionDiagnostic(error);}finally{if(!g.poisoned)g.collect();}
- });},
- // Explicit, bounded full observation for independent embedding conformance;
- // never used to preserve definitions or to implement the site's Run button.
- inspect(name,args,mode,host){return execute(name,args,mode,host,h=>g.toTernary(h,1000000));},
- preview(name,limit=64){return exclusive(()=>g.scope(()=>{
- const root=definitions.get(name);if(root===undefined)throw Error('Missing definition');
- return {...g.preview(root,limit),aliases:[]};
- }));},
- collect(){alive();try{g.collect();}catch(error){if(g.poisoned)session.dispose();throw error;}},
- diagnostics(){alive();return {...g.stats(),linearMemoryBytes:g.e.memory.buffer.byteLength};},
- stats(){alive();return {backend:'zig-wasm',linearMemoryBytes:g.e.memory.buffer.byteLength};},
- dispose(){if(closed)return;closed=true;lifetime.abort(Error('Disposed Zig session'));if(!busy)cleanup();},
- };
- try {
- if(compiler)await ensureCompiler();
- return session;
- }catch(error){session.dispose();throw error;}
-}
diff --git a/ext/wasm/tree-session.mjs b/ext/wasm/tree-session.mjs
@@ -1,34 +0,0 @@
-// Private implementation shared by today's two tree adapters, not a requirement
-// on other backends. Runtime handles and checkpoints never leave this closure.
-import {compiledDefinitions} from '../demo-site/dist/compiler.js';
-export function treeSession(g, root, adapter) {
- let definitions=new Map(), disposed=false;
- const active=()=>{if(disposed)throw new Error('Session disposed');};
- const definition=name=>{const h=definitions.get(name);if(h===undefined)throw new Error('Missing definition');return h;};
- const scoped=fn=>{active();return adapter.scope(fn);};
- return {
- runText(value) {return scoped(()=>adapter.toText(g.app(root,adapter.ofText(value))));},
- compile(source) {
- const next=scoped(({keep})=>{
- const input=adapter.ofText(source,16384);
- const defs=compiledDefinitions(g,g.apply(root,[g.leaf,input]));
- for(const h of defs.values())keep(h);
- return defs;
- });
- for(const h of definitions.values())adapter.release(h);
- definitions=next;
- return [...definitions.keys()];
- },
- inspect(name,args) {return scoped(()=>g.toTernary(g.apply(definition(name),args.map(x=>g.fromTernary(x)))));},
- preview(name,limit) {active();return g.preview(definition(name),limit);},
- // Bounded tree inputs are a harness operation, not source compilation.
- // Worker termination is the deadline for divergent evaluation.
- runTree(program,args) {return scoped(()=>{
- const applied=g.apply(g.fromTernary(program),args.map(x=>g.fromTernary(x)));
- return g.toTernary(g.force(applied,0x7fffffffffffffffn));
- });},
- collect() {active();adapter.collect([root,...definitions.values()]);},
- stats() {active();return adapter.stats();},
- dispose() {if(disposed)return;disposed=true;definitions.clear();try{adapter.dispose();}finally{g=undefined;adapter=undefined;}},
- };
-}
diff --git a/ext/wasm/worker.mjs b/ext/wasm/worker.mjs
@@ -1,38 +0,0 @@
-// Experimental session Worker, not the deployed site's IO protocol. Backend
-// selection is explicit and immutable for this Worker; switching needs a new one.
-import {openSession} from './backends.mjs';
-let session,busy=false;
-self.onmessage=async({data:m})=>{
- if(busy){postMessage({type:'error',id:m.id,message:'Worker busy'});return;}
- busy=true;
- try {
- if(m.type==='init') {
- if(session)throw new Error('Already initialized');
- session=await openSession(m);
- postMessage({type:'ready',id:m.id,backend:m.backend,stats:await session.stats()});
- } else {
- if(!session)throw new Error('Worker not initialized');
- const start=performance.now();
- if(m.type==='text') {
- const value=await session.runText(m.value);
- postMessage({type:'text',id:m.id,value,ms:performance.now()-start});
- } else if(m.type==='compile') {
- const names=await session.compile(m.source);
- postMessage({type:'compiled',id:m.id,names,ms:performance.now()-start});
- } else if(m.type==='inspect') {
- postMessage({type:'result',id:m.id,value:await session.inspect(m.name,m.arguments)});
- } else if(m.type==='preview') {
- const value=await session.preview(m.name,m.limit);
- postMessage({type:'preview',id:m.id,value,ms:performance.now()-start});
- } else if(m.type==='collect') {
- await session.collect();
- postMessage({type:'collected',id:m.id,stats:await session.stats()});
- } else if(m.type==='diverge') {
- postMessage({type:'running',id:m.id});
- await session.runTree('212110021100',['212110021100']);
- throw new Error('Divergence unexpectedly returned');
- } else throw new Error('Unknown message');
- }
- } catch(error){postMessage({type:'error',id:m.id,message:String(error)});}
- finally{busy=false;}
-};
diff --git a/ext/zig/.gitignore b/ext/zig/.gitignore
@@ -1,13 +0,0 @@
-# Zig build artifacts
-.zig-cache/
-zig-out/
-
-# Generated binaries (keep .c sources, ignore compiled artifacts)
-/c_abi_test
-/c_abi_append_test
-c_abi_append_shared
-tests/c_abi_append_test
-
-# Temp files
-*.o
-*.tmp
diff --git a/ext/zig/README.md b/ext/zig/README.md
@@ -1,583 +0,0 @@
-# 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:
-
-```sh
-nix build .#tricu-zig .#tricu-zig-tests --no-link
-# Native language-binding checks use the Haskell embedding library.
-nix build .#tricu-js-tests .#tricu-php-tests .#tricu-python-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](../../docs/compiler-rebuilding.md#isolated-cross-runtime-fixed-point-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](../wasm/README.md) reuses this reducer without native IO.
-The [demo site](../demo-site/README.md#runtime-selection) 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)
-
-```sh
-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:
-
-```sh
-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.
-
-```sh
-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:
-
-```c
-/* 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:
-
-```sh
-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](../../docs/storage.md#verified-native-tree-transport).
-
-## 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 Zig ABI v3. `arb_abi_version()` returns 3; Zig's
-Python test harness checks this version before using handles. The native
-[JS](../js/README.md), [PHP](../php/README.md), and
-[Python](../haskell/README.md) consumer bindings use the Haskell embedding ABI.
-
-* A handle is an opaque 64-bit integer identifying an owned root. Each successful
- handle output owns one root. Constructors borrow their inputs.
-* `arb_retain` returns a **different**, independently owned handle to the same
- node. `arb_release` invalidates that handle. Context destruction releases all
- remaining handles and allocations.
-* Inspection returns independently owned child handles. Child handles remain
- valid after releasing the parent. Handle equality compares handle identities;
- tree equality requires structural comparison.
-* Stale, zero, and foreign-context handles return `ARB_INVALID_HANDLE`.
- Slot reuse has a fresh process-wide generation; generation exhaustion returns
- `ARB_HANDLE_EXHAUSTED`. Handles fit in a positive signed 64-bit integer;
- JavaScript bindings expose them as `bigint`.
-* Operations return `ARB_OK` or an explicit status. Failure leaves output
- parameters unchanged. Multi-handle output publication is all-or-nothing.
-* Reduction updates the shared graph in place. Aliases observe evaluation
- progress, including partial progress after fuel or allocation failure.
- The returned result handle and the input handle are independently owned.
-* Contexts require single-threaded, non-reentrant use. Pointers must be valid,
- and output parameters must be distinct. Callers are responsible for native
- memory safety; handle validation detects stale and foreign references.
-* Decoded byte/string buffers have separate ownership: free them with
- `arboricx_free_buf` before destroying their context. `arboricx_init` returns
- null on allocation failure; other tree operations use status/out parameters.
-
-For example (check every status in application code):
-
-```c
-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
-
-* Arena capacity and collector work buffers retain their high-water allocations.
- Retaining handles retains their reachable graphs. Root-table rebuilding scans
- handle slots at ABI observation/evaluation entry. Apply an external memory
- limit to bound process usage.
-* Fuel counts application entries for one reducer invocation. Codec observations
- use separate budgets. Apply an external deadline to bound a complete IO program.
-* Codecs, structural comparisons, parsers and renderers may use the native stack.
- Complete observations require finite results; observing infinite results can
- diverge.
-
-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
-
-```sh
-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:
-
-```sh
-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](../../test/io-v2/README.md#validated-programbackend-boundary)
-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:
-
-```sh
-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.
-
-```sh
-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.
-
-```sh
-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.
-
-```sh
-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.
-
-```sh
-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.
-
-```sh
-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.
-
-```sh
-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.
-
-```sh
-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:
-
-```sh
-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:
-
-```sh
-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:
-
-```sh
-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](../../docs/compiler-rebuilding.md#isolated-cross-runtime-fixed-point-reproduction)
-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.
diff --git a/ext/zig/build.zig b/ext/zig/build.zig
@@ -1,217 +0,0 @@
-const std = @import("std");
-
-pub fn build(b: *std.Build) void {
- const target = b.standardTargetOptions(.{});
- const optimize = b.standardOptimizeOption(.{});
-
- const reducer_tests = b.addTest(.{ .root_module = b.createModule(.{
- .root_source_file = b.path("src/reduce_test.zig"),
- .target = target,
- .optimize = optimize,
- }) });
- const test_step = b.step("test", "Run reducer, collection and demand tests");
- test_step.dependOn(&b.addRunArtifact(reducer_tests).step);
-
- const preview_tests = b.addTest(.{ .root_module = b.createModule(.{
- .root_source_file = b.path("src/preview.zig"),
- .target = target,
- .optimize = optimize,
- }) });
- test_step.dependOn(&b.addRunArtifact(preview_tests).step);
-
- const v2_protocol_tests = b.addTest(.{ .root_module = b.createModule(.{
- .root_source_file = b.path("src/io_v2_protocol.zig"),
- .target = target,
- .optimize = optimize,
- }) });
- test_step.dependOn(&b.addRunArtifact(v2_protocol_tests).step);
-
- const v2_resource_tests = b.addTest(.{ .root_module = b.createModule(.{
- .root_source_file = b.path("src/io_v2_resources.zig"),
- .target = target,
- .optimize = optimize,
- }) });
- test_step.dependOn(&b.addRunArtifact(v2_resource_tests).step);
-
- const v2_task_tests = b.addTest(.{ .root_module = b.createModule(.{
- .root_source_file = b.path("src/io_v2_tasks.zig"),
- .target = target,
- .optimize = optimize,
- }) });
- test_step.dependOn(&b.addRunArtifact(v2_task_tests).step);
-
- const v2_core_tests = b.addTest(.{ .root_module = b.createModule(.{
- .root_source_file = b.path("src/io_v2_core.zig"),
- .target = target,
- .optimize = optimize,
- }) });
- test_step.dependOn(&b.addRunArtifact(v2_core_tests).step);
-
- const v2_files_mod = b.createModule(.{
- .root_source_file = b.path("src/io_v2_files.zig"),
- .target = target,
- .optimize = optimize,
- });
- v2_files_mod.link_libc = true;
- v2_files_mod.linkSystemLibrary("uv", .{});
- test_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = v2_files_mod })).step);
-
- const v2_file_host_mod = b.createModule(.{
- .root_source_file = b.path("src/io_v2_file_host.zig"),
- .target = target,
- .optimize = optimize,
- });
- v2_file_host_mod.link_libc = true;
- v2_file_host_mod.linkSystemLibrary("uv", .{});
- test_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = v2_file_host_mod })).step);
-
- const v2_native_mod = b.createModule(.{
- .root_source_file = b.path("src/io_v2_native.zig"),
- .target = target,
- .optimize = optimize,
- });
- v2_native_mod.link_libc = true;
- v2_native_mod.linkSystemLibrary("uv", .{});
- test_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = v2_native_mod })).step);
-
- const v2_paths_mod = b.createModule(.{
- .root_source_file = b.path("src/io_v2_paths.zig"),
- .target = target,
- .optimize = optimize,
- });
- v2_paths_mod.link_libc = true;
- v2_paths_mod.linkSystemLibrary("uv", .{});
- test_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = v2_paths_mod })).step);
-
- const v2_digest_mod = b.createModule(.{
- .root_source_file = b.path("src/io_v2_digest.zig"),
- .target = target,
- .optimize = optimize,
- });
- test_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = v2_digest_mod })).step);
-
- const v2_stage_mod = b.createModule(.{
- .root_source_file = b.path("src/io_v2_stage_state.zig"),
- .target = target,
- .optimize = optimize,
- });
- test_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = v2_stage_mod })).step);
-
- const v2_native_stage_mod = b.createModule(.{
- .root_source_file = b.path("src/io_v2_stage.zig"),
- .target = target,
- .optimize = optimize,
- });
- v2_native_stage_mod.link_libc = true;
- v2_native_stage_mod.linkSystemLibrary("uv", .{});
- test_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = v2_native_stage_mod })).step);
-
- const v2_utf8_mod = b.createModule(.{
- .root_source_file = b.path("src/io_v2_utf8.zig"),
- .target = target,
- .optimize = optimize,
- });
- test_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = v2_utf8_mod })).step);
-
- const v2_probe_tests = b.createModule(.{
- .root_source_file = b.path("src/io_v2_core_probe.zig"),
- .target = target,
- .optimize = optimize,
- });
- test_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = v2_probe_tests })).step);
-
- const core_probe = b.addExecutable(.{ .name = "io-v2-core-probe", .root_module = b.createModule(.{
- .root_source_file = b.path("src/io_v2_core_probe.zig"),
- .target = target,
- .optimize = optimize,
- }) });
- b.installArtifact(core_probe);
-
- const file_probe_mod = b.createModule(.{
- .root_source_file = b.path("src/io_v2_file_probe.zig"),
- .target = target,
- .optimize = optimize,
- });
- file_probe_mod.link_libc = true;
- file_probe_mod.linkSystemLibrary("uv", .{});
- b.installArtifact(b.addExecutable(.{ .name = "io-v2-file-probe", .root_module = file_probe_mod }));
-
- // -- kernel generator tool (runs on build host) --
- const gen_kernel_mod = b.createModule(.{
- .root_source_file = b.path("tools/gen_kernel.zig"),
- .target = b.graph.host,
- .optimize = .ReleaseSafe,
- });
- const gen_kernel = b.addExecutable(.{
- .name = "gen_kernel",
- .root_module = gen_kernel_mod,
- });
-
- const run_gen_kernel = b.addRunArtifact(gen_kernel);
- run_gen_kernel.addFileArg(b.path("kernel_run_arboricx_typed.dag"));
- const kernel_embed = run_gen_kernel.addOutputFileArg("kernel_embed.zig");
-
- // -- kernel module shared by exe and lib --
- const kernel_mod = b.createModule(.{
- .root_source_file = kernel_embed,
- });
-
- // Independent browser evaluator: no WASI, libc, libuv, or host imports.
- const wasm_mod = b.createModule(.{
- .root_source_file = b.path("src/c_abi.zig"),
- .target = b.resolveTargetQuery(.{ .cpu_arch = .wasm32, .os_tag = .freestanding }),
- .optimize = .ReleaseSafe,
- });
- wasm_mod.addImport("kernel_embed", kernel_mod);
- const wasm = b.addExecutable(.{ .name = "arboricx", .root_module = wasm_mod });
- wasm.entry = .disabled;
- wasm.rdynamic = true;
- wasm.stack_size = 8 * 1024 * 1024;
- wasm.max_memory = 512 * 1024 * 1024;
- const install_wasm = b.addInstallArtifact(wasm, .{});
- b.step("wasm", "Build the standalone browser evaluator prototype").dependOn(&install_wasm.step);
-
- // -- main CLI executable --
- const exe_mod = b.createModule(.{
- .root_source_file = b.path("src/main.zig"),
- .target = target,
- .optimize = optimize,
- });
- exe_mod.addImport("kernel_embed", kernel_mod);
- exe_mod.link_libc = true;
- exe_mod.linkSystemLibrary("uv", .{});
- const exe = b.addExecutable(.{
- .name = "tricu-zig",
- .root_module = exe_mod,
- });
- b.installArtifact(exe);
-
- const run_cmd = b.addRunArtifact(exe);
- run_cmd.step.dependOn(b.getInstallStep());
- const run_step = b.step("run", "Run tricu-zig");
- run_step.dependOn(&run_cmd.step);
-
- // -- C ABI static library --
- const lib_mod = b.createModule(.{
- .root_source_file = b.path("src/c_abi.zig"),
- .target = target,
- .optimize = optimize,
- });
- lib_mod.pic = true;
- lib_mod.addImport("kernel_embed", kernel_mod);
- lib_mod.link_libc = true;
- lib_mod.linkSystemLibrary("uv", .{});
- const static_lib = b.addLibrary(.{
- .name = "arboricx",
- .root_module = lib_mod,
- });
- b.installArtifact(static_lib);
-
- // -- C ABI shared library (for dynamic language FFI) --
- const shared_lib = b.addLibrary(.{
- .name = "arboricx",
- .root_module = lib_mod,
- .linkage = .dynamic,
- });
- b.installArtifact(shared_lib);
-}
diff --git a/ext/zig/build.zig.zon b/ext/zig/build.zig.zon
@@ -1,13 +0,0 @@
-.{
- .name = .tricu_zig,
- .version = "0.0.1",
- .fingerprint = 0xa9aedd8049d1cce9,
- .minimum_zig_version = "0.16.0",
- .paths = .{
- "build.zig",
- "build.zig.zon",
- "src",
- "tools",
- "kernels",
- },
-}
diff --git a/ext/zig/include/arboricx.h b/ext/zig/include/arboricx.h
@@ -1,100 +0,0 @@
-#ifndef ARBORICX_H
-#define ARBORICX_H
-
-#include <stddef.h>
-#include <stdint.h>
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-typedef struct arb_ctx arb_ctx_t;
-typedef uint64_t arb_handle_t;
-typedef enum {
- ARB_OK = 0, ARB_INVALID_HANDLE = 1, ARB_OUT_OF_MEMORY = 2,
- ARB_FUEL_EXHAUSTED = 3, ARB_TYPE_MISMATCH = 4, ARB_INVALID_DATA = 5,
- ARB_IO_ERROR = 6, ARB_HANDLE_EXHAUSTED = 7
-} arb_status_t;
-
-/* ABI v3: each successful handle output is independently owned. Inputs are
- * borrowed. arb_retain creates another owned handle; arb_release invalidates
- * only that handle. Handles are opaque, context-bound and never serialize.
- * Reduction is lazy and in place: aliases observe shared evaluation progress,
- * including partial progress on failure. Failure leaves output parameters
- * unchanged. Distinct outputs must not alias. All pointers must be valid;
- * buffers/arrays may be empty but must have a non-null pointer. Contexts are
- * single-threaded and non-reentrant. Context destruction releases all handles.
- * Inspection does not force; returned children are owned handles too.
- * Collection reclaims unreachable nodes, not necessarily resident OS pages.
- */
-uint32_t arb_abi_version(void);
-arb_status_t arb_retain(arb_ctx_t* ctx, arb_handle_t handle, arb_handle_t* out);
-arb_status_t arb_release(arb_ctx_t* ctx, arb_handle_t handle);
-arb_status_t arb_collect(arb_ctx_t* ctx);
-/* Set allocation headroom: max(minimum, last_live_nodes * multiplier).
- * Both arguments must be positive; invalid input leaves the policy unchanged.
- * No collection/allocation occurs here. The next reducer safepoint checks the
- * new threshold against allocations since the last collection. Default: 1048576,
- * 1. This is NOT a memory limit; construction does not automatically collect. */
-arb_status_t arb_set_gc_policy(arb_ctx_t* ctx, uint32_t minimum, uint32_t multiplier);
-
-/* Context lifecycle */
-arb_ctx_t* arboricx_init(void);
-void arboricx_free(arb_ctx_t* ctx);
-void arboricx_free_buf(arb_ctx_t* ctx, uint8_t* ptr, size_t len);
-
-/* Tree construction */
-arb_status_t arb_leaf(arb_ctx_t* ctx, arb_handle_t* out);
-arb_status_t arb_stem(arb_ctx_t* ctx, arb_handle_t child, arb_handle_t* out);
-arb_status_t arb_fork(arb_ctx_t* ctx, arb_handle_t left, arb_handle_t right, arb_handle_t* out);
-arb_status_t arb_app(arb_ctx_t* ctx, arb_handle_t func, arb_handle_t arg, arb_handle_t* out);
-
-/* Reduction */
-arb_status_t arb_reduce(arb_ctx_t* ctx, arb_handle_t root, uint64_t fuel, arb_handle_t* out);
-
-/* Codec constructors */
-arb_status_t arb_of_number(arb_ctx_t* ctx, uint64_t n, arb_handle_t* out);
-arb_status_t arb_of_string(arb_ctx_t* ctx, const char* s, arb_handle_t* out);
-arb_status_t arb_of_bytes(arb_ctx_t* ctx, const uint8_t* bytes, size_t len, arb_handle_t* out);
-arb_status_t arb_of_list(arb_ctx_t* ctx, const arb_handle_t* items, size_t len, arb_handle_t* out);
-
-/* Codec destructors (return ARB_OK on success) */
-arb_status_t arb_to_number(arb_ctx_t* ctx, arb_handle_t root, uint64_t* out);
-arb_status_t arb_to_string(arb_ctx_t* ctx, arb_handle_t root, uint8_t** out_ptr, size_t* out_len);
-arb_status_t arb_to_bytes(arb_ctx_t* ctx, arb_handle_t root, uint8_t** out_ptr, size_t* out_len);
-arb_status_t arb_to_bool(arb_ctx_t* ctx, arb_handle_t root, int* out);
-
-/* Result unwrapping (return ARB_OK on success) */
-arb_status_t arb_unwrap_result(arb_ctx_t* ctx, arb_handle_t root, int* out_ok, arb_handle_t* out_value, arb_handle_t* out_rest);
-arb_status_t arb_unwrap_host_value(arb_ctx_t* ctx, arb_handle_t root, uint64_t* out_tag, arb_handle_t* out_payload);
-
-/* Tree inspection (Layer 1 — for custom IO drivers and non-POSIX hosts) */
-arb_status_t arb_is_leaf(arb_ctx_t* ctx, arb_handle_t root, int* out);
-arb_status_t arb_is_stem(arb_ctx_t* ctx, arb_handle_t root, int* out);
-arb_status_t arb_is_fork(arb_ctx_t* ctx, arb_handle_t root, int* out);
-arb_status_t arb_is_app(arb_ctx_t* ctx, arb_handle_t root, int* out);
-arb_status_t arb_get_stem_child(arb_ctx_t* ctx, arb_handle_t root, arb_handle_t* out);
-arb_status_t arb_get_fork_children(arb_ctx_t* ctx, arb_handle_t root,
- arb_handle_t* out_left, arb_handle_t* out_right);
-arb_status_t arb_get_app_func_arg(arb_ctx_t* ctx, arb_handle_t root,
- arb_handle_t* out_func, arb_handle_t* out_arg);
-
-/* Native IO v2 compiler/file profile. OS process authority, no v1 grants.
- * Rejects non-v2 envelopes; never probes or retries another protocol. WASM: ARB_IO_ERROR.
- * Successful ordinary Results remain data; terminal failure/cancellation returns
- * ARB_IO_ERROR without forcing its payload. On error *out is unchanged.
- * ABI version 3 removes the legacy arb_run_io entrypoint and grant struct. */
-arb_status_t arb_run_io_v2(arb_ctx_t* ctx, arb_handle_t program, arb_handle_t* out);
-
-/* Kernel entrypoints */
-arb_status_t arb_kernel_root(arb_ctx_t* ctx, arb_handle_t* out);
-
-/* Native bundle loading (fast path — bypasses the Tricu kernel) */
-arb_status_t arb_load_bundle(arb_ctx_t* ctx, const uint8_t* bytes, size_t len, const char* name, arb_handle_t* out);
-arb_status_t arb_load_bundle_default(arb_ctx_t* ctx, const uint8_t* bytes, size_t len, arb_handle_t* out);
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif /* ARBORICX_H */
diff --git a/ext/zig/kernel_run_arboricx_typed.dag b/ext/zig/kernel_run_arboricx_typed.dag
@@ -1,2578 +0,0 @@
-2576
-leaf
-fork 0 0
-stem 1
-fork 2 0
-fork 0 3
-stem 4
-stem 0
-fork 5 6
-fork 0 7
-stem 8
-fork 0 6
-stem 10
-fork 11 3
-fork 5 12
-stem 13
-fork 14 10
-stem 15
-stem 16
-fork 0 17
-stem 18
-fork 19 6
-fork 0 20
-stem 21
-stem 6
-fork 23 0
-stem 24
-fork 25 24
-stem 26
-stem 27
-fork 0 28
-stem 29
-fork 30 6
-stem 31
-fork 6 1
-fork 0 1
-fork 33 34
-stem 35
-fork 2 36
-fork 0 37
-stem 38
-fork 0 34
-fork 16 40
-fork 0 41
-stem 42
-fork 2 6
-fork 0 44
-stem 45
-stem 46
-fork 43 47
-fork 39 48
-stem 49
-stem 34
-fork 0 51
-stem 52
-stem 53
-fork 0 54
-stem 55
-stem 56
-fork 0 57
-stem 58
-stem 22
-fork 22 60
-stem 61
-fork 0 60
-stem 63
-stem 9
-fork 0 65
-stem 66
-stem 67
-fork 0 68
-stem 69
-fork 0 24
-fork 1 71
-fork 0 71
-fork 72 73
-fork 0 74
-stem 75
-stem 76
-fork 0 77
-stem 78
-stem 79
-fork 70 80
-fork 64 81
-fork 62 82
-fork 59 83
-fork 50 84
-fork 0 85
-stem 86
-stem 20
-fork 0 31
-fork 88 89
-fork 87 90
-fork 0 91
-fork 32 92
-stem 93
-fork 94 1
-fork 22 95
-fork 9 96
-stem 97
-fork 11 6
-fork 22 99
-stem 100
-fork 22 6
-stem 102
-stem 2
-fork 0 104
-stem 105
-fork 106 0
-fork 0 107
-fork 103 108
-fork 101 109
-fork 0 110
-stem 111
-stem 112
-fork 0 113
-stem 114
-fork 0 23
-stem 116
-stem 117
-fork 0 118
-stem 119
-fork 120 0
-fork 0 121
-stem 122
-fork 123 6
-fork 115 124
-fork 5 125
-stem 126
-stem 127
-fork 0 128
-stem 129
-fork 6 0
-fork 0 131
-fork 94 132
-fork 22 133
-fork 9 134
-stem 135
-stem 131
-fork 0 137
-stem 138
-stem 33
-fork 0 140
-stem 141
-fork 6 10
-fork 0 10
-fork 143 144
-fork 112 145
-stem 146
-fork 144 144
-fork 147 148
-fork 0 149
-fork 88 150
-fork 0 151
-stem 152
-fork 153 95
-stem 154
-fork 94 10
-fork 155 156
-fork 76 157
-fork 9 158
-fork 0 159
-fork 88 160
-fork 142 161
-fork 0 162
-stem 163
-fork 164 90
-fork 0 165
-fork 32 166
-fork 76 167
-fork 9 168
-fork 0 169
-fork 88 170
-fork 139 171
-fork 0 172
-stem 173
-fork 174 90
-fork 0 175
-fork 32 176
-fork 22 177
-stem 178
-fork 6 131
-fork 6 180
-fork 0 181
-stem 182
-fork 11 183
-fork 22 184
-stem 185
-fork 123 137
-fork 115 187
-fork 186 188
-fork 179 189
-fork 115 190
-fork 5 191
-stem 192
-stem 193
-fork 0 194
-stem 195
-fork 0 132
-fork 94 197
-fork 22 198
-fork 9 199
-stem 200
-fork 22 167
-stem 202
-stem 132
-fork 123 204
-fork 115 205
-fork 186 206
-fork 203 207
-fork 115 208
-fork 5 209
-stem 210
-stem 211
-fork 0 212
-stem 213
-fork 0 180
-fork 94 215
-fork 22 216
-fork 9 217
-stem 218
-fork 22 157
-stem 220
-stem 180
-fork 123 222
-fork 115 223
-fork 186 224
-fork 221 225
-fork 115 226
-fork 5 227
-stem 228
-stem 229
-fork 0 230
-stem 231
-fork 0 197
-fork 94 233
-fork 22 234
-fork 9 235
-stem 236
-fork 142 6
-fork 0 238
-stem 239
-fork 240 90
-fork 0 241
-fork 32 242
-fork 22 243
-stem 244
-stem 197
-fork 123 246
-fork 115 247
-fork 186 248
-fork 245 249
-fork 115 250
-fork 5 251
-stem 252
-stem 253
-fork 0 254
-stem 255
-fork 6 132
-fork 0 257
-fork 94 258
-fork 22 259
-fork 9 260
-stem 261
-stem 257
-fork 123 263
-fork 115 264
-fork 186 265
-fork 179 266
-fork 115 267
-fork 186 268
-fork 0 269
-fork 262 270
-fork 256 271
-fork 237 272
-fork 232 273
-fork 219 274
-fork 214 275
-fork 201 276
-fork 196 277
-fork 136 278
-fork 130 279
-fork 98 280
-fork 22 281
-fork 9 282
-fork 9 283
-stem 284
-fork 0 2
-stem 286
-stem 287
-fork 0 288
-stem 289
-stem 5
-fork 0 291
-stem 292
-stem 115
-fork 293 294
-fork 5 295
-stem 296
-stem 11
-fork 0 298
-stem 299
-fork 300 0
-fork 0 301
-fork 297 302
-fork 22 303
-stem 304
-fork 9 20
-fork 0 306
-fork 305 307
-fork 290 308
-fork 287 309
-fork 0 310
-fork 88 311
-fork 0 312
-stem 313
-stem 314
-fork 0 315
-stem 316
-stem 317
-fork 0 318
-stem 319
-stem 123
-fork 0 321
-stem 322
-fork 24 0
-stem 324
-fork 0 325
-stem 326
-stem 327
-fork 0 328
-stem 329
-fork 88 21
-fork 70 331
-fork 64 332
-fork 5 333
-stem 334
-fork 64 331
-fork 0 336
-fork 335 337
-fork 330 338
-fork 22 339
-stem 340
-fork 341 21
-fork 0 342
-stem 343
-fork 344 90
-fork 0 345
-fork 32 346
-stem 347
-fork 348 71
-fork 0 349
-fork 88 350
-fork 0 351
-fork 88 352
-fork 323 353
-fork 0 354
-fork 16 355
-fork 0 356
-stem 357
-fork 0 233
-fork 0 359
-fork 6 360
-fork 0 258
-fork 6 362
-fork 0 363
-fork 6 359
-fork 0 365
-fork 6 197
-fork 6 367
-fork 6 368
-fork 6 369
-fork 0 367
-fork 0 371
-fork 6 372
-fork 6 365
-fork 6 257
-fork 0 375
-fork 0 376
-fork 0 377
-fork 378 0
-fork 374 379
-fork 373 380
-fork 364 381
-fork 370 382
-fork 366 383
-fork 364 384
-fork 361 385
-fork 0 386
-fork 16 387
-fork 0 388
-stem 389
-stem 107
-fork 391 1
-fork 0 392
-stem 393
-fork 117 6
-fork 394 395
-fork 9 396
-stem 397
-fork 131 0
-fork 0 399
-fork 400 0
-fork 401 121
-fork 22 402
-fork 9 403
-fork 9 404
-stem 405
-stem 290
-fork 0 407
-stem 408
-fork 16 307
-fork 0 410
-stem 411
-fork 0 296
-fork 88 413
-fork 64 414
-fork 412 415
-fork 409 416
-fork 290 417
-fork 0 418
-stem 419
-fork 88 302
-fork 300 421
-fork 420 422
-fork 9 423
-fork 9 424
-stem 425
-fork 11 204
-fork 22 427
-fork 9 428
-stem 429
-fork 0 294
-stem 431
-stem 306
-stem 398
-fork 0 434
-stem 435
-stem 426
-fork 0 437
-stem 438
-fork 0 93
-fork 88 440
-fork 64 441
-fork 22 442
-fork 0 443
-fork 16 444
-fork 0 445
-stem 446
-stem 64
-fork 0 448
-stem 449
-stem 430
-fork 0 451
-stem 452
-stem 432
-fork 0 454
-stem 455
-fork 88 307
-fork 456 457
-fork 453 458
-fork 450 459
-fork 64 460
-fork 447 461
-fork 439 462
-fork 22 463
-stem 464
-fork 0 405
-fork 465 466
-fork 436 467
-fork 22 468
-stem 469
-fork 470 307
-fork 0 471
-stem 472
-fork 473 90
-fork 0 474
-fork 32 475
-fork 0 476
-fork 433 477
-fork 432 478
-fork 430 479
-fork 64 480
-fork 22 481
-stem 482
-fork 483 444
-fork 426 484
-fork 0 485
-fork 406 486
-fork 398 487
-fork 390 488
-stem 489
-fork 490 24
-fork 314 491
-stem 492
-fork 9 306
-fork 9 494
-stem 495
-stem 496
-fork 0 497
-stem 498
-stem 293
-fork 0 500
-stem 501
-stem 502
-fork 0 503
-stem 504
-stem 394
-fork 0 506
-stem 507
-stem 508
-fork 0 509
-stem 510
-stem 511
-fork 0 512
-stem 513
-stem 514
-fork 0 515
-stem 516
-fork 9 0
-fork 9 518
-fork 0 519
-fork 88 520
-fork 327 521
-fork 0 522
-stem 523
-fork 524 90
-fork 0 525
-fork 32 526
-fork 0 527
-stem 528
-stem 529
-fork 22 530
-stem 531
-fork 22 33
-fork 0 533
-fork 532 534
-fork 287 535
-fork 0 536
-stem 537
-fork 538 90
-fork 0 539
-fork 32 540
-fork 123 541
-fork 0 542
-fork 88 543
-fork 115 544
-fork 22 545
-stem 546
-fork 11 137
-fork 22 548
-fork 0 549
-fork 547 550
-fork 22 551
-fork 9 552
-stem 553
-fork 67 442
-fork 0 555
-fork 554 556
-fork 517 557
-fork 505 558
-fork 502 559
-fork 293 560
-fork 5 561
-stem 562
-stem 563
-fork 0 564
-stem 565
-fork 67 555
-fork 67 567
-fork 5 568
-stem 569
-stem 570
-fork 0 571
-stem 572
-stem 573
-fork 0 574
-stem 575
-fork 88 1
-fork 64 577
-fork 0 578
-fork 16 579
-fork 0 580
-stem 581
-stem 582
-fork 0 583
-stem 584
-stem 585
-fork 0 586
-stem 587
-stem 70
-fork 0 589
-stem 590
-stem 450
-fork 0 592
-stem 593
-fork 16 21
-fork 0 595
-stem 596
-stem 597
-fork 0 598
-stem 599
-stem 399
-fork 0 601
-stem 602
-stem 23
-fork 2 604
-fork 0 605
-stem 606
-fork 11 298
-fork 607 608
-stem 609
-fork 610 1
-fork 603 611
-fork 0 612
-stem 613
-fork 614 90
-fork 0 615
-fork 32 616
-fork 22 617
-fork 0 618
-fork 16 619
-fork 0 620
-stem 621
-fork 622 336
-fork 450 623
-fork 600 624
-fork 594 625
-fork 591 626
-fork 588 627
-fork 22 628
-stem 629
-fork 67 545
-fork 67 631
-fork 5 632
-fork 9 633
-fork 9 634
-fork 9 635
-fork 0 636
-fork 630 637
-fork 576 638
-fork 566 639
-fork 499 640
-fork 0 641
-stem 642
-fork 643 90
-fork 0 644
-fork 32 645
-stem 646
-fork 647 197
-stem 648
-fork 649 1
-stem 650
-fork 651 24
-stem 652
-fork 653 1
-fork 314 654
-stem 655
-fork 647 233
-stem 657
-fork 658 1
-stem 659
-fork 660 24
-stem 661
-fork 662 1
-fork 314 663
-fork 0 664
-fork 16 665
-fork 0 666
-stem 667
-fork 647 359
-stem 669
-fork 670 1
-stem 671
-fork 672 24
-stem 673
-fork 674 1
-fork 314 675
-fork 0 676
-fork 16 677
-fork 0 678
-stem 679
-stem 680
-fork 0 681
-stem 682
-stem 683
-fork 0 684
-stem 685
-stem 323
-fork 0 687
-stem 688
-stem 689
-fork 0 690
-stem 691
-stem 692
-fork 0 693
-stem 694
-fork 9 519
-fork 9 696
-stem 697
-stem 696
-stem 519
-fork 700 1
-fork 0 701
-fork 699 702
-fork 0 703
-fork 698 704
-fork 695 705
-fork 594 706
-fork 686 707
-fork 450 708
-fork 683 709
-fork 64 710
-fork 668 711
-fork 22 712
-stem 713
-fork 0 655
-fork 714 715
-fork 0 716
-fork 656 717
-fork 0 718
-fork 0 719
-fork 493 720
-fork 314 721
-stem 722
-fork 16 1
-fork 0 724
-stem 725
-fork 5 442
-stem 727
-stem 728
-fork 0 729
-stem 730
-stem 731
-fork 0 732
-stem 733
-fork 5 545
-fork 9 735
-fork 9 736
-stem 737
-stem 738
-fork 0 739
-stem 740
-fork 70 624
-fork 585 742
-fork 22 743
-stem 744
-stem 664
-fork 16 715
-fork 0 747
-stem 748
-stem 749
-fork 0 750
-stem 751
-stem 752
-fork 0 753
-stem 754
-stem 686
-fork 0 756
-stem 757
-stem 594
-fork 0 759
-stem 760
-stem 758
-fork 0 762
-stem 763
-stem 761
-fork 0 765
-stem 766
-stem 668
-fork 0 768
-stem 769
-stem 770
-fork 0 771
-stem 772
-stem 773
-fork 0 774
-stem 775
-stem 776
-fork 0 777
-stem 778
-stem 779
-fork 0 780
-stem 781
-stem 767
-fork 0 783
-stem 784
-stem 695
-fork 0 786
-stem 787
-stem 788
-fork 0 789
-stem 790
-stem 791
-fork 0 792
-stem 793
-fork 9 697
-fork 9 795
-fork 9 796
-stem 797
-stem 796
-stem 795
-fork 0 705
-fork 800 801
-fork 0 802
-fork 799 803
-fork 0 804
-fork 798 805
-fork 794 806
-fork 785 807
-fork 782 808
-fork 767 809
-fork 764 810
-fork 761 811
-fork 758 812
-fork 594 813
-fork 755 814
-fork 450 815
-fork 752 816
-fork 64 817
-fork 749 818
-fork 22 819
-stem 820
-fork 821 715
-fork 0 822
-fork 746 823
-fork 314 824
-fork 9 825
-fork 9 826
-fork 9 827
-fork 0 828
-fork 745 829
-fork 741 830
-fork 734 831
-fork 0 832
-stem 833
-fork 834 90
-fork 0 835
-fork 32 836
-fork 0 837
-fork 88 838
-fork 726 839
-fork 726 840
-fork 0 841
-stem 842
-fork 94 359
-fork 0 844
-fork 16 845
-fork 0 846
-stem 847
-stem 848
-fork 0 849
-stem 850
-fork 9 110
-stem 852
-stem 853
-fork 0 854
-stem 855
-stem 856
-fork 0 857
-stem 858
-stem 103
-fork 0 860
-stem 861
-stem 108
-stem 863
-fork 0 864
-stem 865
-stem 866
-fork 0 867
-stem 868
-fork 391 34
-stem 870
-fork 871 40
-fork 0 872
-stem 873
-fork 874 6
-stem 875
-stem 71
-fork 877 95
-fork 876 878
-fork 0 879
-stem 880
-stem 881
-fork 0 882
-stem 883
-stem 884
-fork 0 885
-stem 886
-fork 0 617
-fork 16 888
-fork 0 889
-stem 890
-fork 25 1
-fork 67 892
-fork 891 893
-fork 887 894
-fork 869 895
-fork 862 896
-fork 293 897
-fork 5 898
-stem 899
-stem 874
-fork 0 901
-stem 902
-fork 903 0
-fork 5 904
-stem 905
-fork 76 95
-fork 9 907
-stem 908
-fork 0 95
-fork 909 910
-fork 906 911
-fork 9 912
-fork 0 913
-fork 16 914
-fork 0 915
-stem 916
-stem 917
-fork 0 918
-stem 919
-fork 920 624
-fork 900 921
-fork 22 922
-stem 923
-fork 924 307
-fork 859 925
-fork 502 926
-fork 851 927
-fork 0 928
-stem 929
-fork 930 90
-fork 0 931
-fork 32 932
-fork 726 933
-fork 0 934
-fork 88 935
-fork 0 936
-fork 348 937
-stem 938
-fork 939 1
-fork 0 940
-stem 941
-fork 1 6
-fork 0 943
-fork 1 944
-fork 0 945
-fork 1 946
-fork 942 947
-fork 843 948
-stem 949
-stem 950
-fork 0 951
-stem 952
-stem 433
-fork 0 954
-stem 955
-stem 409
-fork 0 957
-stem 958
-stem 300
-fork 0 960
-stem 961
-stem 962
-fork 0 963
-stem 964
-fork 22 623
-stem 966
-fork 9 852
-fork 9 968
-fork 0 969
-fork 967 970
-fork 734 971
-fork 965 972
-fork 959 973
-fork 956 974
-fork 0 975
-stem 976
-fork 977 90
-fork 0 978
-fork 32 979
-fork 0 980
-fork 88 981
-fork 726 982
-fork 0 983
-stem 984
-fork 0 72
-fork 1 986
-fork 0 987
-fork 1 988
-fork 0 989
-fork 1 990
-fork 942 991
-fork 985 992
-fork 0 993
-fork 88 994
-fork 953 995
-fork 317 996
-fork 5 997
-stem 998
-fork 323 0
-fork 22 1000
-fork 9 1001
-stem 1002
-stem 153
-fork 0 1004
-stem 1005
-fork 22 943
-stem 1007
-fork 5 336
-stem 1009
-fork 131 34
-fork 394 1011
-stem 1012
-stem 1013
-fork 0 1014
-stem 1015
-fork 79 93
-fork 67 1017
-fork 22 1018
-fork 0 1019
-fork 62 1020
-fork 409 1021
-fork 22 1022
-stem 1023
-fork 1024 307
-fork 1016 1025
-fork 22 1026
-stem 1027
-fork 1028 21
-fork 0 1029
-stem 1030
-fork 1031 90
-fork 0 1032
-fork 32 1033
-fork 9 1034
-stem 1035
-fork 1036 944
-fork 1006 1037
-fork 67 1038
-fork 0 1039
-fork 1010 1040
-fork 290 1041
-fork 22 1042
-stem 1043
-fork 1044 21
-fork 0 1045
-stem 1046
-fork 1047 90
-fork 0 1048
-fork 32 1049
-fork 0 1050
-fork 1008 1051
-fork 1006 1052
-fork 5 1053
-fork 0 1054
-fork 88 1055
-fork 287 1056
-fork 0 1057
-stem 1058
-fork 1059 90
-fork 0 1060
-fork 32 1061
-fork 22 1062
-stem 1063
-stem 215
-fork 11 1065
-fork 112 1066
-stem 1067
-stem 1068
-fork 0 1069
-stem 1070
-fork 1071 121
-fork 1064 1072
-fork 317 1073
-fork 0 1074
-fork 1003 1075
-fork 0 1076
-fork 999 1077
-fork 300 1078
-fork 22 1079
-fork 9 1080
-stem 1081
-stem 1019
-fork 22 1034
-fork 0 1084
-fork 1083 1085
-fork 290 1086
-fork 0 1087
-fork 433 1088
-fork 1013 1089
-stem 1090
-fork 0 400
-fork 1091 1092
-fork 0 1093
-stem 1094
-fork 1095 943
-fork 76 1096
-stem 1097
-fork 1091 40
-fork 0 1099
-stem 1100
-fork 1101 945
-fork 1098 1102
-fork 22 1103
-stem 1104
-fork 115 121
-fork 5 1106
-stem 1107
-fork 11 263
-fork 0 1109
-fork 1108 1110
-fork 1105 1111
-fork 317 1112
-fork 0 1113
-fork 1082 1114
-fork 723 1115
-fork 314 1116
-stem 1117
-fork 64 1037
-fork 5 1119
-stem 1120
-stem 1121
-fork 0 1122
-stem 1123
-fork 112 0
-fork 9 1125
-fork 0 1126
-fork 16 1127
-fork 0 1128
-stem 1129
-fork 1130 336
-fork 1124 1131
-fork 290 1132
-fork 22 1133
-stem 1134
-fork 1135 21
-fork 0 1136
-stem 1137
-fork 1138 90
-fork 0 1139
-fork 32 1140
-stem 1141
-fork 0 1092
-fork 0 1143
-fork 1142 1144
-fork 22 1145
-fork 9 1146
-stem 1147
-fork 11 246
-fork 863 1149
-stem 1150
-stem 1151
-fork 0 1152
-stem 1153
-fork 0 947
-fork 1 1155
-fork 0 1156
-fork 1 1157
-fork 0 1158
-fork 1 1159
-fork 0 1160
-fork 1 1161
-fork 942 1162
-fork 22 1163
-stem 1164
-stem 440
-stem 888
-stem 1167
-fork 11 1168
-fork 287 1169
-fork 0 1170
-stem 1171
-fork 1172 90
-fork 0 1173
-fork 32 1174
-fork 1166 1175
-fork 0 1176
-fork 1165 1177
-fork 22 1178
-stem 1179
-stem 549
-fork 88 122
-fork 115 1182
-fork 1181 1183
-fork 5 1184
-fork 0 1185
-fork 1180 1186
-fork 22 1187
-stem 1188
-fork 22 983
-stem 1190
-fork 5 555
-stem 1192
-stem 1193
-fork 0 1194
-stem 1195
-stem 903
-fork 0 1197
-stem 1198
-stem 1199
-fork 0 1200
-stem 1201
-stem 1202
-fork 0 1203
-stem 1204
-fork 0 518
-fork 16 1206
-fork 0 1207
-stem 1208
-stem 1209
-fork 0 1210
-stem 1211
-fork 1212 624
-fork 1205 1213
-fork 1196 1214
-fork 409 1215
-fork 22 1216
-stem 1217
-fork 1218 307
-fork 0 1219
-stem 1220
-fork 1221 90
-fork 0 1222
-fork 32 1223
-fork 0 1224
-fork 88 1225
-fork 726 1226
-fork 9 1227
-fork 0 1228
-fork 1191 1229
-fork 0 1230
-stem 1231
-fork 942 1160
-fork 1232 1233
-stem 1234
-fork 1235 1163
-fork 64 1236
-fork 1189 1237
-fork 0 1238
-fork 88 1239
-fork 22 1240
-stem 1241
-fork 1242 21
-fork 1154 1243
-fork 5 1244
-stem 1245
-fork 11 1149
-fork 11 1247
-fork 0 1248
-fork 1246 1249
-fork 0 1250
-fork 1148 1251
-fork 0 1252
-fork 88 1253
-fork 22 1254
-stem 1255
-fork 1256 21
-fork 320 1257
-fork 293 1258
-fork 5 1259
-stem 1260
-fork 0 1000
-fork 433 1262
-fork 962 1263
-fork 22 1264
-stem 1265
-stem 1266
-fork 0 1267
-stem 1268
-fork 132 0
-fork 0 1270
-fork 0 1271
-fork 0 1272
-fork 0 1273
-fork 1142 1274
-fork 22 1275
-fork 9 1276
-stem 1277
-fork 1278 1251
-fork 0 1279
-fork 88 1280
-fork 22 1281
-stem 1282
-fork 1283 21
-fork 320 1284
-fork 70 1285
-fork 1269 1286
-fork 965 1287
-fork 1261 1288
-fork 300 1289
-fork 290 1290
-fork 22 1291
-stem 1292
-fork 1293 21
-fork 1118 1294
-fork 314 1295
-stem 1296
-fork 323 577
-fork 0 1298
-fork 433 1299
-fork 962 1300
-fork 22 1301
-fork 9 1302
-stem 1303
-fork 0 1034
-stem 1305
-fork 1306 943
-stem 1307
-fork 0 215
-fork 0 1309
-fork 0 1310
-fork 6 1311
-fork 0 182
-fork 6 1313
-fork 0 1314
-fork 6 1310
-fork 0 1316
-fork 6 215
-fork 6 1318
-fork 6 1319
-fork 6 1320
-fork 0 1318
-fork 0 1322
-fork 6 1323
-fork 6 1316
-fork 6 181
-fork 0 1326
-fork 0 1327
-fork 0 1328
-fork 6 375
-fork 0 1330
-fork 6 182
-fork 0 1332
-fork 6 1333
-fork 0 1320
-fork 6 1309
-fork 0 1336
-fork 0 1337
-fork 0 1319
-fork 0 1339
-fork 6 1337
-fork 6 1339
-fork 6 1336
-fork 0 1343
-fork 6 1314
-fork 0 1333
-fork 6 1332
-fork 0 1347
-fork 1316 0
-fork 1348 1349
-fork 1331 1350
-fork 1346 1351
-fork 1345 1352
-fork 1341 1353
-fork 1344 1354
-fork 1324 1355
-fork 1335 1356
-fork 1312 1357
-fork 1342 1358
-fork 1331 1359
-fork 1341 1360
-fork 1340 1361
-fork 1338 1362
-fork 1335 1363
-fork 1334 1364
-fork 1317 1365
-fork 1331 1366
-fork 1329 1367
-fork 1325 1368
-fork 1324 1369
-fork 1315 1370
-fork 1321 1371
-fork 1317 1372
-fork 1315 1373
-fork 1312 1374
-fork 0 1375
-fork 1308 1376
-fork 76 1377
-stem 1378
-stem 944
-fork 1380 72
-fork 1306 1381
-stem 1382
-fork 6 376
-fork 6 1322
-fork 0 1385
-fork 1346 0
-fork 1325 1387
-fork 1341 1388
-fork 1386 1389
-fork 1317 1390
-fork 1321 1391
-fork 1384 1392
-fork 1341 1393
-fork 1340 1394
-fork 1317 1395
-fork 1312 1396
-fork 1346 1397
-fork 1334 1398
-fork 1325 1399
-fork 1341 1400
-fork 1329 1401
-fork 1341 1402
-fork 1384 1403
-fork 1345 1404
-fork 1334 1405
-fork 1340 1406
-fork 1334 1407
-fork 1325 1408
-fork 1340 1409
-fork 1312 1410
-fork 1325 1411
-fork 1384 1412
-fork 1341 1413
-fork 1341 1414
-fork 1315 1415
-fork 1346 1416
-fork 0 1417
-fork 1383 1418
-fork 76 1419
-stem 1420
-stem 986
-fork 1422 72
-fork 1380 1423
-fork 1306 1424
-stem 1425
-fork 1345 1351
-fork 1334 1427
-fork 1340 1428
-fork 1334 1429
-fork 1325 1430
-fork 1340 1431
-fork 1312 1432
-fork 1325 1433
-fork 1384 1434
-fork 1341 1435
-fork 1341 1436
-fork 1315 1437
-fork 1346 1438
-fork 0 1439
-fork 1426 1440
-fork 76 1441
-stem 1442
-fork 1422 1423
-fork 1380 1444
-fork 1306 1445
-stem 1446
-fork 1338 0
-fork 1341 1448
-fork 1329 1449
-fork 1341 1450
-fork 1338 1451
-fork 1335 1452
-fork 1324 1453
-fork 0 1454
-fork 1447 1455
-fork 76 1456
-stem 1457
-fork 1422 1444
-fork 1380 1459
-fork 1306 1460
-stem 1461
-fork 1341 1351
-fork 1338 1463
-fork 1321 1464
-fork 1335 1465
-fork 1331 1466
-fork 1338 1467
-fork 1341 1468
-fork 1329 1469
-fork 1341 1470
-fork 1338 1471
-fork 1335 1472
-fork 1324 1473
-fork 1331 1474
-fork 1329 1475
-fork 1325 1476
-fork 1324 1477
-fork 1315 1478
-fork 1321 1479
-fork 1317 1480
-fork 1315 1481
-fork 1312 1482
-fork 0 1483
-fork 1462 1484
-fork 76 1485
-stem 1486
-fork 1422 1459
-fork 1380 1488
-fork 1306 1489
-stem 1490
-fork 0 1313
-fork 0 1492
-fork 6 1328
-fork 1338 1351
-fork 1312 1495
-fork 1321 1496
-fork 1340 1497
-fork 1494 1498
-fork 1312 1499
-fork 1493 1500
-fork 1331 1501
-fork 1338 1502
-fork 1341 1503
-fork 1329 1504
-fork 1341 1505
-fork 1338 1506
-fork 1335 1507
-fork 1324 1508
-fork 1331 1509
-fork 1329 1510
-fork 1325 1511
-fork 1324 1512
-fork 1315 1513
-fork 1321 1514
-fork 1317 1515
-fork 1315 1516
-fork 1312 1517
-fork 0 1518
-fork 1491 1519
-fork 76 1520
-stem 1521
-fork 1422 1488
-fork 1380 1523
-fork 1306 1524
-stem 1525
-fork 1526 1440
-fork 76 1527
-stem 1528
-fork 1422 1523
-fork 1380 1530
-fork 1306 1531
-stem 1532
-fork 1315 0
-fork 1341 1534
-fork 1338 1535
-fork 1315 1536
-fork 1321 1537
-fork 1384 1538
-fork 1340 1539
-fork 1312 1540
-fork 1342 1541
-fork 1315 1542
-fork 1321 1543
-fork 1335 1544
-fork 0 1545
-fork 1533 1546
-fork 76 1547
-stem 1548
-fork 1422 1530
-fork 1380 1550
-fork 1306 1551
-stem 1552
-fork 1341 1463
-fork 1315 1554
-fork 1346 1555
-fork 1331 1556
-fork 1324 1557
-fork 1317 1558
-fork 1312 1559
-fork 1331 1560
-fork 1329 1561
-fork 1325 1562
-fork 1324 1563
-fork 1315 1564
-fork 1321 1565
-fork 1317 1566
-fork 1315 1567
-fork 1312 1568
-fork 0 1569
-fork 1553 1570
-fork 76 1571
-stem 1572
-fork 1422 1550
-fork 1422 1574
-fork 1380 1575
-fork 1306 1576
-stem 1577
-fork 1578 34
-fork 76 1579
-stem 1580
-fork 0 35
-stem 1582
-fork 0 1011
-stem 1584
-fork 1422 1575
-fork 1380 1586
-fork 1585 1587
-fork 1583 1588
-fork 76 1589
-stem 1590
-fork 1422 1586
-fork 1585 1592
-fork 1583 1593
-fork 1591 1594
-fork 1581 1595
-fork 1573 1596
-fork 1549 1597
-fork 1529 1598
-fork 1522 1599
-fork 1487 1600
-fork 1458 1601
-fork 1443 1602
-fork 1421 1603
-fork 1379 1604
-fork 22 1605
-stem 1606
-stem 1318
-fork 11 1608
-fork 0 1609
-fork 1108 1610
-fork 1607 1611
-fork 0 1612
-fork 88 1613
-fork 317 1614
-fork 67 1615
-fork 0 1616
-fork 1304 1617
-fork 290 1618
-fork 22 1619
-stem 1620
-fork 1621 21
-fork 22 1622
-stem 1623
-fork 0 368
-fork 6 1625
-fork 0 369
-fork 6 233
-fork 6 1628
-fork 0 1629
-fork 6 363
-fork 6 258
-fork 0 1632
-fork 0 1633
-fork 1634 0
-fork 1631 1635
-fork 1630 1636
-fork 1627 1637
-fork 1626 1638
-fork 366 1639
-fork 364 1640
-fork 361 1641
-fork 0 1642
-fork 16 1643
-fork 0 1644
-stem 1645
-fork 1646 488
-stem 1647
-fork 1648 24
-fork 314 1649
-stem 1650
-fork 16 122
-fork 0 1652
-stem 1653
-fork 0 646
-fork 88 1655
-fork 726 1656
-fork 5 1657
-stem 1658
-fork 1659 71
-fork 726 1660
-fork 0 1661
-stem 1662
-fork 1663 940
-fork 317 1664
-fork 1654 1665
-fork 0 1666
-fork 746 1667
-fork 314 1668
-stem 1669
-fork 0 1669
-fork 16 1671
-fork 0 1672
-stem 1673
-stem 1674
-fork 0 1675
-stem 1676
-stem 1677
-fork 0 1678
-stem 1679
-stem 1680
-fork 0 1681
-stem 1682
-stem 1683
-fork 0 1684
-stem 1685
-stem 1686
-fork 0 1687
-stem 1688
-stem 1689
-fork 0 1690
-stem 1691
-stem 785
-fork 0 1693
-stem 1694
-stem 782
-fork 0 1696
-stem 1697
-stem 1698
-fork 0 1699
-stem 1700
-stem 1695
-fork 0 1702
-stem 1703
-fork 9 1669
-fork 9 1705
-fork 9 1706
-fork 0 1707
-fork 745 1708
-fork 741 1709
-fork 734 1710
-fork 0 1711
-stem 1712
-fork 1713 90
-fork 0 1714
-fork 32 1715
-fork 0 1716
-fork 88 1717
-fork 726 1718
-fork 726 1719
-fork 0 1720
-stem 1721
-fork 1722 940
-fork 317 1723
-fork 0 1724
-fork 16 1725
-fork 0 1726
-stem 1727
-stem 1728
-fork 0 1729
-stem 1730
-stem 1731
-fork 0 1732
-stem 1733
-stem 1734
-fork 0 1735
-stem 1736
-stem 1737
-fork 0 1738
-stem 1739
-stem 1740
-fork 0 1741
-stem 1742
-stem 1743
-fork 0 1744
-stem 1745
-stem 1746
-fork 0 1747
-stem 1748
-stem 591
-fork 0 1750
-stem 1751
-stem 1752
-fork 0 1753
-stem 1754
-stem 1755
-fork 0 1756
-stem 1757
-stem 1758
-fork 0 1759
-stem 1760
-stem 1761
-fork 0 1762
-stem 1763
-fork 647 132
-stem 1765
-fork 1766 1
-stem 1767
-fork 1768 24
-stem 1769
-fork 1770 1
-fork 314 1771
-fork 0 1772
-fork 16 1773
-fork 0 1774
-stem 1775
-stem 1776
-fork 0 1777
-stem 1778
-stem 1779
-fork 0 1780
-stem 1781
-stem 1782
-fork 0 1783
-stem 1784
-stem 1785
-fork 0 1786
-stem 1787
-stem 1788
-fork 0 1789
-stem 1790
-stem 1791
-fork 0 1792
-stem 1793
-stem 1794
-fork 0 1795
-stem 1796
-stem 1797
-fork 0 1798
-stem 1799
-stem 1704
-fork 0 1801
-stem 1802
-stem 1701
-fork 0 1804
-stem 1805
-stem 1806
-fork 0 1807
-stem 1808
-stem 1803
-fork 0 1810
-stem 1811
-stem 1001
-fork 1813 1671
-fork 0 1814
-fork 746 1815
-fork 314 1816
-fork 9 1817
-fork 9 1818
-fork 9 1819
-fork 0 1820
-fork 745 1821
-fork 741 1822
-fork 734 1823
-fork 0 1824
-stem 1825
-fork 1826 90
-fork 0 1827
-fork 32 1828
-fork 0 1829
-fork 88 1830
-fork 726 1831
-fork 726 1832
-fork 0 1833
-stem 1834
-fork 1835 940
-fork 317 1836
-fork 0 1837
-fork 16 1838
-fork 0 1839
-stem 1840
-stem 1841
-fork 0 1842
-stem 1843
-stem 1844
-fork 0 1845
-stem 1846
-stem 1847
-fork 0 1848
-stem 1849
-stem 1850
-fork 0 1851
-stem 1852
-stem 1853
-fork 0 1854
-stem 1855
-stem 1856
-fork 0 1857
-stem 1858
-stem 1859
-fork 0 1860
-stem 1861
-stem 1862
-fork 0 1863
-stem 1864
-stem 1865
-fork 0 1866
-stem 1867
-stem 1764
-fork 0 1869
-stem 1870
-stem 1871
-fork 0 1872
-stem 1873
-stem 1809
-fork 0 1875
-stem 1876
-stem 1812
-fork 0 1878
-stem 1879
-fork 692 703
-fork 450 1881
-fork 1677 1882
-fork 64 1883
-fork 1674 1884
-fork 22 1885
-stem 1886
-fork 1887 665
-fork 0 1888
-fork 1670 1889
-fork 314 1890
-fork 9 1891
-fork 9 1892
-fork 9 1893
-fork 0 1894
-fork 745 1895
-fork 741 1896
-fork 734 1897
-fork 0 1898
-stem 1899
-fork 1900 90
-fork 0 1901
-fork 32 1902
-fork 0 1903
-fork 88 1904
-fork 726 1905
-fork 726 1906
-fork 0 1907
-stem 1908
-fork 1909 940
-fork 317 1910
-fork 0 1911
-fork 16 1912
-fork 0 1913
-stem 1914
-stem 1915
-fork 0 1916
-stem 1917
-stem 1918
-fork 0 1919
-stem 1920
-stem 1921
-fork 0 1922
-stem 1923
-stem 1924
-fork 0 1925
-stem 1926
-stem 1927
-fork 0 1928
-stem 1929
-stem 1930
-fork 0 1931
-stem 1932
-stem 1933
-fork 0 1934
-stem 1935
-stem 1936
-fork 0 1937
-stem 1938
-stem 1939
-fork 0 1940
-stem 1941
-stem 1942
-fork 0 1943
-stem 1944
-stem 1874
-fork 0 1946
-stem 1947
-stem 794
-fork 0 1949
-stem 1950
-stem 1951
-fork 0 1952
-stem 1953
-stem 1954
-fork 0 1955
-stem 1956
-stem 1957
-fork 0 1958
-stem 1959
-stem 1960
-fork 0 1961
-stem 1962
-fork 9 797
-fork 9 1964
-fork 9 1965
-fork 9 1966
-fork 9 1967
-stem 1968
-stem 1967
-stem 1966
-stem 1965
-stem 1964
-fork 0 806
-fork 1973 1974
-fork 0 1975
-fork 1972 1976
-fork 0 1977
-fork 1971 1978
-fork 0 1979
-fork 1970 1980
-fork 0 1981
-fork 1969 1982
-fork 1963 1983
-fork 1880 1984
-fork 1948 1985
-fork 1945 1986
-fork 1880 1987
-fork 1877 1988
-fork 1812 1989
-fork 1874 1990
-fork 1868 1991
-fork 1812 1992
-fork 1809 1993
-fork 1803 1994
-fork 1800 1995
-fork 1704 1996
-fork 1764 1997
-fork 1749 1998
-fork 1704 1999
-fork 1701 2000
-fork 1695 2001
-fork 1692 2002
-fork 785 2003
-fork 1689 2004
-fork 767 2005
-fork 1686 2006
-fork 761 2007
-fork 1683 2008
-fork 594 2009
-fork 1680 2010
-fork 450 2011
-fork 1677 2012
-fork 64 2013
-fork 1674 2014
-fork 22 2015
-stem 2016
-fork 2017 1671
-fork 0 2018
-fork 1670 2019
-fork 0 2020
-fork 0 2021
-fork 656 2022
-fork 0 2023
-fork 0 2024
-fork 656 2025
-fork 0 2026
-fork 0 2027
-fork 1651 2028
-fork 314 2029
-stem 2030
-fork 22 940
-stem 2032
-fork 0 441
-fork 16 2034
-fork 0 2035
-stem 2036
-fork 112 395
-fork 9 2038
-fork 9 2039
-stem 2040
-stem 2041
-fork 0 2042
-stem 2043
-fork 962 623
-fork 22 2045
-stem 2046
-fork 0 1665
-fork 1003 2048
-fork 22 2049
-stem 2050
-fork 2051 665
-fork 0 2052
-fork 656 2053
-fork 314 2054
-fork 9 2055
-fork 9 2056
-fork 0 2057
-fork 2047 2058
-fork 2044 2059
-fork 502 2060
-fork 293 2061
-fork 2037 2062
-fork 0 2063
-stem 2064
-fork 2065 90
-fork 0 2066
-fork 32 2067
-fork 0 2068
-fork 2033 2069
-fork 726 2070
-fork 317 2071
-fork 0 2072
-fork 16 2073
-fork 0 2074
-stem 2075
-fork 16 71
-fork 0 2077
-stem 2078
-fork 689 701
-fork 293 2080
-fork 2079 2081
-fork 962 2082
-fork 64 2083
-fork 67 2084
-fork 2076 2085
-fork 64 2086
-fork 668 2087
-fork 22 2088
-fork 9 2089
-stem 2090
-fork 9 2057
-fork 0 2092
-fork 745 2093
-fork 741 2094
-fork 734 2095
-fork 0 2096
-stem 2097
-fork 2098 90
-fork 0 2099
-fork 32 2100
-fork 0 2101
-fork 88 2102
-fork 726 2103
-fork 726 2104
-fork 0 2105
-stem 2106
-fork 2107 940
-fork 317 2108
-fork 0 2109
-fork 2091 2110
-fork 22 2111
-stem 2112
-fork 2113 665
-fork 0 2114
-fork 2031 2115
-fork 314 2116
-fork 0 2117
-fork 1624 2118
-fork 300 2119
-fork 287 2120
-fork 0 2121
-fork 88 2122
-fork 0 2123
-fork 1297 2124
-fork 314 2125
-stem 2126
-stem 2127
-fork 0 2128
-stem 2129
-fork 197 0
-fork 0 2131
-fork 2132 0
-stem 121
-fork 2134 1
-fork 112 2135
-stem 2136
-fork 0 2132
-fork 2137 2138
-fork 9 2139
-stem 2140
-fork 2141 1584
-fork 2133 2142
-fork 863 2143
-stem 2144
-fork 0 2135
-fork 2145 2146
-stem 2147
-fork 0 2138
-fork 0 2149
-fork 2148 2150
-stem 2151
-fork 1335 0
-fork 1324 2153
-fork 1312 2154
-fork 1342 2155
-fork 0 2156
-fork 1142 2157
-fork 2152 2158
-fork 112 2159
-fork 9 2160
-stem 2161
-fork 2132 2135
-fork 2163 2149
-fork 0 2164
-stem 2165
-stem 2166
-fork 0 2167
-stem 2168
-fork 2169 1141
-fork 2162 2170
-fork 5 2171
-stem 2172
-fork 2173 1584
-fork 0 2174
-stem 2175
-fork 2176 1592
-fork 0 2177
-fork 88 2178
-fork 317 2179
-fork 22 2180
-stem 2181
-fork 300 1182
-fork 0 2183
-fork 16 2184
-fork 0 2185
-stem 2186
-fork 1583 95
-fork 0 2188
-fork 16 2189
-fork 0 2190
-stem 2191
-fork 367 0
-fork 0 2193
-fork 0 2194
-fork 16 2195
-fork 0 2196
-stem 2197
-stem 2198
-fork 0 2199
-stem 2200
-fork 1091 34
-fork 0 2202
-fork 16 2203
-fork 0 2204
-stem 2205
-fork 0 33
-fork 0 2207
-fork 391 2208
-stem 2209
-fork 0 2208
-fork 2210 2211
-fork 0 2212
-stem 2213
-stem 2214
-fork 0 2215
-stem 2216
-stem 2217
-fork 0 2218
-stem 2219
-fork 0 133
-fork 2221 34
-fork 0 2222
-stem 2223
-fork 11 0
-fork 1 2225
-fork 2224 2226
-fork 76 2227
-stem 2228
-stem 1176
-fork 2230 258
-fork 2229 2231
-fork 0 2232
-fork 16 2233
-fork 0 2234
-stem 2235
-fork 123 0
-stem 2237
-fork 2238 1
-fork 11 2239
-fork 0 2240
-fork 16 2241
-fork 0 2242
-stem 2243
-stem 2244
-fork 0 2245
-stem 2246
-stem 7
-stem 1224
-fork 2249 233
-stem 2250
-fork 2251 1
-fork 0 2252
-stem 2253
-stem 980
-fork 2255 132
-stem 2256
-fork 2257 1
-fork 2254 2258
-fork 942 2259
-fork 0 2260
-fork 2248 2261
-fork 22 2262
-stem 2263
-fork 2264 21
-fork 320 2265
-fork 2247 2266
-fork 432 2267
-fork 293 2268
-fork 5 2269
-stem 2270
-fork 293 2266
-fork 5 2272
-stem 2273
-fork 726 1000
-fork 300 2275
-fork 0 2276
-fork 16 2277
-fork 0 2278
-stem 2279
-stem 2280
-fork 0 2281
-stem 2282
-fork 2255 258
-stem 2284
-fork 2285 1
-fork 2254 2286
-fork 942 2287
-fork 0 2288
-fork 2248 2289
-fork 22 2290
-stem 2291
-fork 2292 21
-fork 320 2293
-fork 70 2294
-fork 2283 2295
-fork 965 2296
-fork 2274 2297
-fork 2271 2298
-fork 293 2299
-fork 2236 2300
-fork 2220 2301
-fork 293 2302
-fork 2206 2303
-fork 432 2304
-fork 2201 2305
-fork 293 2306
-fork 2192 2307
-fork 5 2308
-fork 9 2309
-stem 2310
-fork 0 852
-fork 16 2312
-fork 0 2313
-stem 2314
-stem 2315
-fork 0 2316
-stem 2317
-fork 622 60
-fork 450 2319
-fork 2318 2320
-fork 1196 2321
-fork 409 2322
-fork 64 2323
-fork 597 2324
-fork 0 2325
-stem 2326
-fork 2327 90
-fork 0 2328
-fork 32 2329
-fork 726 2330
-fork 0 2331
-fork 2311 2332
-fork 0 2333
-stem 2334
-fork 2335 90
-fork 0 2336
-fork 32 2337
-fork 0 2338
-stem 2339
-fork 2340 940
-fork 9 2341
-stem 2342
-fork 2343 986
-fork 317 2344
-fork 67 2345
-fork 2187 2346
-fork 22 2347
-stem 2348
-fork 22 2183
-stem 2350
-stem 676
-fork 153 2202
-stem 2353
-fork 153 2232
-stem 2355
-fork 0 198
-fork 2357 34
-fork 0 2358
-stem 2359
-fork 2360 2226
-fork 76 2361
-stem 2362
-fork 2230 371
-fork 2363 2364
-fork 2356 2365
-fork 2354 2366
-fork 1583 2367
-fork 153 2368
-fork 9 2369
-fork 0 2370
-fork 88 2371
-fork 287 2372
-fork 0 2373
-stem 2374
-fork 2375 90
-fork 0 2376
-fork 32 2377
-fork 22 2378
-stem 2379
-stem 233
-fork 11 2381
-fork 112 2382
-stem 2383
-stem 2384
-fork 0 2385
-stem 2386
-stem 139
-fork 0 2388
-stem 2389
-stem 2390
-fork 0 2391
-stem 2392
-stem 726
-fork 0 2394
-stem 2395
-stem 2396
-fork 0 2397
-stem 2398
-fork 456 2319
-fork 2399 2400
-fork 450 2401
-fork 293 2402
-fork 5 2403
-stem 2404
-fork 597 336
-fork 456 2406
-fork 2399 2407
-fork 450 2408
-fork 293 2409
-fork 5 2410
-stem 2411
-fork 16 440
-fork 0 2413
-stem 2414
-stem 2415
-fork 0 2416
-stem 2417
-fork 391 144
-stem 2419
-fork 0 144
-fork 2420 2421
-fork 0 2422
-stem 2423
-stem 2424
-fork 0 2425
-stem 2426
-stem 2427
-fork 0 2428
-stem 2429
-stem 2430
-fork 0 2431
-stem 2432
-stem 2433
-fork 0 2434
-stem 2435
-fork 600 2320
-fork 2436 2437
-fork 1196 2438
-fork 965 2439
-fork 959 2440
-fork 64 2441
-fork 412 2442
-fork 1205 2443
-fork 505 2444
-fork 502 2445
-fork 2418 2446
-fork 0 2447
-stem 2448
-fork 2449 90
-fork 0 2450
-fork 32 2451
-fork 0 2452
-fork 88 2453
-fork 726 2454
-fork 22 2455
-stem 2456
-fork 2457 21
-fork 450 2458
-fork 0 2459
-fork 2412 2460
-fork 2393 2461
-fork 22 2462
-stem 2463
-fork 2464 307
-fork 0 2465
-stem 2466
-fork 2467 90
-fork 0 2468
-fork 32 2469
-fork 0 2470
-stem 2471
-fork 2 2260
-stem 2473
-fork 2474 1
-fork 112 2475
-stem 2476
-fork 2 2288
-stem 2478
-fork 2479 1
-fork 2474 2480
-fork 2477 2481
-stem 2482
-fork 2483 2232
-fork 874 2484
-stem 2485
-fork 2486 2202
-fork 2472 2487
-fork 22 2488
-stem 2489
-fork 2490 21
-fork 0 2491
-fork 88 2492
-fork 0 2493
-fork 433 2494
-fork 450 2495
-fork 0 2496
-fork 2405 2497
-fork 2393 2498
-fork 64 2499
-fork 597 2500
-fork 0 2501
-stem 2502
-fork 2503 90
-fork 0 2504
-fork 32 2505
-stem 2506
-fork 2507 24
-stem 2508
-fork 2509 1
-fork 22 2510
-stem 2511
-stem 367
-fork 11 2513
-fork 0 2514
-fork 1108 2515
-fork 2512 2516
-fork 2387 2517
-fork 2380 2518
-fork 317 2519
-fork 0 2520
-fork 1003 2521
-fork 22 2522
-stem 2523
-fork 2524 1724
-fork 0 2525
-fork 2352 2526
-fork 314 2527
-stem 2528
-fork 0 427
-fork 1108 2530
-fork 5 2531
-stem 2532
-fork 2533 1584
-fork 0 2534
-fork 2529 2535
-fork 314 2536
-fork 0 2537
-fork 2351 2538
-fork 300 2539
-fork 22 2540
-stem 2541
-stem 2542
-fork 0 2543
-stem 2544
-fork 2545 1285
-fork 300 2546
-fork 290 2547
-fork 22 2548
-stem 2549
-fork 2550 21
-fork 1118 2551
-fork 314 2552
-fork 0 2553
-fork 2349 2554
-fork 0 2555
-stem 2556
-fork 2557 945
-fork 0 2558
-fork 88 2559
-fork 300 2560
-fork 22 2561
-fork 0 2562
-fork 2182 2563
-fork 962 2564
-fork 290 2565
-fork 962 2566
-fork 2130 2567
-fork 317 2568
-fork 67 2569
-fork 358 2570
-fork 320 2571
-fork 0 2572
-fork 285 2573
-stem 2574
-fork 2575 1
diff --git a/ext/zig/result b/ext/zig/result
@@ -1 +0,0 @@
-/nix/store/2sg31y0vamz5bz19aakxagi702glwh24-tricu-zig-0.1.0
-\ No newline at end of file
diff --git a/ext/zig/src/arena.zig b/ext/zig/src/arena.zig
@@ -1,151 +0,0 @@
-const std = @import("std");
-const tree = @import("tree.zig");
-
-pub const ReductionFrame = struct {
- kind: enum { function, discriminator, argument, update },
- redex: u32,
-};
-
-pub const Arena = struct {
- allocator: std.mem.Allocator,
- nodes: std.ArrayList(tree.Node),
- // Opt-in collection: every externally retained ID must be in roots.
- gc_enabled: bool = false,
- roots: std.ArrayList(u32) = .empty,
- free: std.ArrayList(u32) = .empty,
- allocations: usize = 0,
- // Allocation headroom, not a memory limit. Tests can select a small policy
- // to force collections at suspension/allocation boundaries.
- gc_min_interval: usize = 1 << 20,
- gc_live_multiplier: usize = 1,
- gc_interval: usize = 1 << 20,
- collections: usize = 0,
- last_live: usize = 0,
- marks: std.ArrayList(bool) = .empty,
- grey: std.ArrayList(u32) = .empty,
- reduction_frames: std.ArrayList(ReductionFrame) = .empty,
- reduction_roots: std.ArrayList(u32) = .empty,
-
- pub fn init(allocator: std.mem.Allocator) Arena {
- return .{
- .allocator = allocator,
- .nodes = .empty,
- };
- }
-
- pub fn deinit(self: *Arena) void {
- self.nodes.deinit(self.allocator);
- self.roots.deinit(self.allocator);
- self.free.deinit(self.allocator);
- self.marks.deinit(self.allocator);
- self.grey.deinit(self.allocator);
- self.reduction_frames.deinit(self.allocator);
- self.reduction_roots.deinit(self.allocator);
- }
-
- inline fn allocSlot(self: *Arena) !u32 {
- if (self.free.items.len != 0) {
- const idx = self.free.items[self.free.items.len - 1];
- self.free.items.len -= 1;
- self.allocations += 1;
- return idx;
- }
- if (self.nodes.items.len >= std.math.maxInt(u32)) return error.OutOfMemory;
- const idx: u32 = @intCast(self.nodes.items.len);
- _ = try self.nodes.addOne(self.allocator);
- self.allocations += 1;
- return idx;
- }
-
- pub inline fn alloc(self: *Arena, node: tree.Node) !u32 {
- const idx = try self.allocSlot();
- self.nodes.items[idx] = node;
- return idx;
- }
-
- // Reduction allocates applications in its hottest path. Scalar operands
- // avoid constructing and then copying a temporary padded tagged union.
- pub inline fn allocApp(self: *Arena, func: u32, arg: u32) !u32 {
- const idx = try self.allocSlot();
- self.nodes.items[idx] = .{ .app = .{ .func = func, .arg = arg } };
- return idx;
- }
-
- pub fn get(self: *Arena, idx: u32) *tree.Node {
- return &self.nodes.items[idx];
- }
-
- pub fn len(self: *const Arena) u32 {
- return @intCast(self.nodes.items.len);
- }
-
- pub fn collectionDue(self: *const Arena) bool {
- return self.gc_enabled and self.allocations >= self.gc_interval;
- }
-
- pub fn setGcPolicy(self: *Arena, minimum: usize, multiplier: usize) error{InvalidData}!void {
- if (minimum == 0 or multiplier == 0) return error.InvalidData;
- self.gc_min_interval = minimum;
- self.gc_live_multiplier = multiplier;
- self.gc_interval = @max(minimum, self.last_live *| multiplier);
- }
-
- // Inline the per-edge duplicate check; shared edges normally need no enqueue.
- inline fn mark(self: *Arena, idx: u32) !void {
- if (self.marks.items[idx]) return;
- // Mark on enqueue so shared children occupy the work stack only once.
- try self.grey.append(self.allocator, idx);
- self.marks.items[idx] = true;
- }
-
- /// Non-moving mark/sweep. Only reduction safepoints collect automatically;
- /// construction never collects, so newly allocated children can be linked
- /// without temporary root registrations. Failure before sweep changes no IDs.
- pub fn collect(self: *Arena, temporary_roots: []const u32) !void {
- try self.marks.resize(self.allocator, self.nodes.items.len);
- @memset(self.marks.items, false);
- self.grey.clearRetainingCapacity();
- for (self.roots.items) |idx| try self.mark(idx);
- for (temporary_roots) |idx| try self.mark(idx);
- var live: usize = 0;
- var end: usize = 0;
- while (self.grey.pop()) |idx| {
- live += 1;
- end = @max(end, @as(usize, idx) + 1);
- switch (self.nodes.items[idx]) {
- .leaf => {},
- .stem => |s| try self.mark(s.child),
- .fork => |f| {
- try self.mark(f.left);
- try self.mark(f.right);
- },
- .app => |a| {
- try self.mark(a.func);
- try self.mark(a.arg);
- },
- }
- }
- try self.free.ensureTotalCapacity(self.allocator, end - live);
- // Free-list entries outside its logical length are never read. Rebuild
- // without poisoning the previous list only to overwrite it immediately.
- self.free.items.len = 0;
- // Dead trailing slots need neither sweeping nor free-list entries.
- // IDs of live nodes never change; backing capacity remains available.
- // Slice truncation keeps bounds checks without touching dead backing
- // storage. alloc/allocApp initialize a recycled slot before publishing it.
- self.nodes.items = self.nodes.items[0..end];
- var i = end;
- while (i > 0) {
- i -= 1;
- if (!self.marks.items[i]) {
- self.free.appendAssumeCapacity(@intCast(i));
- }
- }
- self.last_live = live;
- self.allocations = 0;
- self.collections += 1;
- // Amortize scanning a large live graph; this is not a hard RSS bound.
- // Recompute rather than retaining a transient live-heap peak forever.
- self.gc_interval = @max(self.gc_min_interval, live *| self.gc_live_multiplier);
- }
-};
diff --git a/ext/zig/src/bundle.zig b/ext/zig/src/bundle.zig
@@ -1,12 +0,0 @@
-//! All entrypoints share the validated indexed reader. Authentication of a
-//! particular object's bytes belongs to its transport, not to bundle parsing.
-const Arena = @import("arena.zig").Arena;
-const indexed = @import("indexed.zig");
-
-pub fn loadBundleExport(arena: *Arena, bytes: []const u8, name: []const u8) !u32 {
- return indexed.load(arena, bytes, name, false);
-}
-
-pub fn loadBundleDefaultRoot(arena: *Arena, bytes: []const u8) !u32 {
- return indexed.load(arena, bytes, null, false);
-}
diff --git a/ext/zig/src/c_abi.zig b/ext/zig/src/c_abi.zig
@@ -1,334 +0,0 @@
-const std = @import("std");
-const wasm = @import("builtin").target.cpu.arch == .wasm32;
-const tree = @import("tree.zig");
-const Arena = @import("arena.zig").Arena;
-const reduce = @import("reduce.zig");
-const codecs = @import("codecs.zig");
-const kernel = @import("kernel.zig");
-const bundle = @import("bundle.zig");
-const handles = @import("handles.zig");
-const Handle = handles.Handle;
-
-// Keep these values synchronized with include/arboricx.h.
-const Status = enum(c_int) {
- ok = 0,
- invalid_handle = 1,
- out_of_memory = 2,
- fuel_exhausted = 3,
- type_mismatch = 4,
- invalid_data = 5,
- io_error = 6,
- handle_exhausted = 7,
-};
-fn status(err: anyerror) Status {
- return switch (err) {
- error.InvalidHandle => .invalid_handle,
- error.OutOfMemory => .out_of_memory,
- error.FuelExhausted => .fuel_exhausted,
- error.HandleExhausted => .handle_exhausted,
- else => .invalid_data,
- };
-}
-
-pub const ArbCtx = struct {
- gpa: std.mem.Allocator,
- arena: Arena,
- kernel_root: u32,
- handles: handles.Table = .{},
-
- fn prepare(self: *ArbCtx) !void {
- self.arena.roots.clearRetainingCapacity();
- try self.arena.roots.append(self.gpa, self.kernel_root);
- try self.handles.appendRoots(&self.arena);
- }
- fn publish(self: *ArbCtx, node: u32, out: *Handle) Status {
- const handle = self.handles.create(self.gpa, node) catch |err| return status(err);
- out.* = handle;
- return .ok;
- }
- // Multi-output operations publish atomically: never leak half a pair.
- fn publishPair(self: *ArbCtx, a: u32, b: u32, out_a: *Handle, out_b: *Handle) Status {
- const ha = self.handles.create(self.gpa, a) catch |err| return status(err);
- const hb = self.handles.create(self.gpa, b) catch |err| {
- self.handles.release(ha) catch unreachable;
- return status(err);
- };
- out_a.* = ha;
- out_b.* = hb;
- return .ok;
- }
-};
-
-export fn arb_abi_version() u32 {
- return 3;
-}
-export fn arboricx_init() ?*ArbCtx {
- const gpa = if (wasm) std.heap.wasm_allocator else std.heap.smp_allocator;
- const ptr = gpa.create(ArbCtx) catch return null;
- ptr.* = .{ .gpa = gpa, .arena = Arena.init(gpa), .kernel_root = undefined };
- ptr.kernel_root = kernel.loadKernel(&ptr.arena) catch {
- ptr.arena.deinit();
- gpa.destroy(ptr);
- return null;
- };
- ptr.arena.gc_enabled = true;
- return ptr;
-}
-export fn arboricx_free(ctx: *ArbCtx) void {
- ctx.handles.deinit(ctx.gpa);
- ctx.arena.deinit();
- ctx.gpa.destroy(ctx);
-}
-export fn arboricx_free_buf(ctx: *ArbCtx, ptr: [*]u8, len: usize) void {
- ctx.gpa.free(ptr[0..len]);
-}
-export fn arb_release(ctx: *ArbCtx, handle: Handle) Status {
- ctx.handles.release(handle) catch |err| return status(err);
- return .ok;
-}
-export fn arb_retain(ctx: *ArbCtx, handle: Handle, out: *Handle) Status {
- const node = ctx.handles.get(handle) catch |err| return status(err);
- return ctx.publish(node, out);
-}
-export fn arb_set_gc_policy(ctx: *ArbCtx, minimum: u32, multiplier: u32) Status {
- ctx.arena.setGcPolicy(minimum, multiplier) catch |err| return status(err);
- return .ok;
-}
-export fn arb_collect(ctx: *ArbCtx) Status {
- ctx.prepare() catch |err| return status(err);
- ctx.arena.collect(&.{}) catch |err| return status(err);
- return .ok;
-}
-export fn arb_leaf(ctx: *ArbCtx, out: *Handle) Status {
- return ctx.publish(ctx.arena.alloc(.leaf) catch |err| return status(err), out);
-}
-export fn arb_stem(ctx: *ArbCtx, child: Handle, out: *Handle) Status {
- const child_node = ctx.handles.get(child) catch |err| return status(err);
- const node = ctx.arena.alloc(.{ .stem = .{ .child = child_node } }) catch |err| return status(err);
- return ctx.publish(node, out);
-}
-export fn arb_fork(ctx: *ArbCtx, left: Handle, right: Handle, out: *Handle) Status {
- const left_node = ctx.handles.get(left) catch |err| return status(err);
- const right_node = ctx.handles.get(right) catch |err| return status(err);
- const node = ctx.arena.alloc(.{ .fork = .{ .left = left_node, .right = right_node } }) catch |err| return status(err);
- return ctx.publish(node, out);
-}
-export fn arb_app(ctx: *ArbCtx, func: Handle, arg: Handle, out: *Handle) Status {
- const func_node = ctx.handles.get(func) catch |err| return status(err);
- const arg_node = ctx.handles.get(arg) catch |err| return status(err);
- const node = ctx.arena.alloc(.{ .app = .{ .func = func_node, .arg = arg_node } }) catch |err| return status(err);
- return ctx.publish(node, out);
-}
-export fn arb_is_leaf(ctx: *ArbCtx, root: Handle, out: *c_int) Status {
- const node = ctx.handles.get(root) catch |err| return status(err);
- out.* = if (ctx.arena.get(node).* == .leaf) 1 else 0;
- return .ok;
-}
-export fn arb_is_stem(ctx: *ArbCtx, root: Handle, out: *c_int) Status {
- const node = ctx.handles.get(root) catch |err| return status(err);
- out.* = if (ctx.arena.get(node).* == .stem) 1 else 0;
- return .ok;
-}
-export fn arb_is_fork(ctx: *ArbCtx, root: Handle, out: *c_int) Status {
- const node = ctx.handles.get(root) catch |err| return status(err);
- out.* = if (ctx.arena.get(node).* == .fork) 1 else 0;
- return .ok;
-}
-export fn arb_is_app(ctx: *ArbCtx, root: Handle, out: *c_int) Status {
- const node = ctx.handles.get(root) catch |err| return status(err);
- out.* = if (ctx.arena.get(node).* == .app) 1 else 0;
- return .ok;
-}
-export fn arb_get_stem_child(ctx: *ArbCtx, root: Handle, out_child: *Handle) Status {
- const idx = ctx.handles.get(root) catch |err| return status(err);
- const node = ctx.arena.get(idx).*;
- if (node != .stem) return .type_mismatch;
- return ctx.publish(node.stem.child, out_child);
-}
-export fn arb_get_fork_children(ctx: *ArbCtx, root: Handle, out_left: *Handle, out_right: *Handle) Status {
- const idx = ctx.handles.get(root) catch |err| return status(err);
- const node = ctx.arena.get(idx).*;
- if (node != .fork) return .type_mismatch;
- return ctx.publishPair(node.fork.left, node.fork.right, out_left, out_right);
-}
-export fn arb_get_app_func_arg(ctx: *ArbCtx, root: Handle, out_func: *Handle, out_arg: *Handle) Status {
- const idx = ctx.handles.get(root) catch |err| return status(err);
- const node = ctx.arena.get(idx).*;
- if (node != .app) return .type_mismatch;
- return ctx.publishPair(node.app.func, node.app.arg, out_func, out_arg);
-}
-export fn arb_reduce(ctx: *ArbCtx, root: Handle, fuel: u64, out: *Handle) Status {
- const node = ctx.handles.get(root) catch |err| return status(err);
- ctx.prepare() catch |err| return status(err);
- const result = reduce.reduce(node, &ctx.arena, fuel) catch |err| return status(err);
- return ctx.publish(result, out);
-}
-export fn arb_of_number(ctx: *ArbCtx, n: u64, out: *Handle) Status {
- return ctx.publish(codecs.ofNumber(&ctx.arena, n) catch |err| return status(err), out);
-}
-export fn arb_of_string(ctx: *ArbCtx, s: [*:0]const u8, out: *Handle) Status {
- return ctx.publish(codecs.ofString(&ctx.arena, std.mem.sliceTo(s, 0)) catch |err| return status(err), out);
-}
-export fn arb_of_bytes(ctx: *ArbCtx, bytes: [*]const u8, len: usize, out: *Handle) Status {
- return ctx.publish(codecs.ofBytes(&ctx.arena, bytes[0..len]) catch |err| return status(err), out);
-}
-export fn arb_of_list(ctx: *ArbCtx, items: [*]const Handle, len: usize, out: *Handle) Status {
- const nodes = ctx.gpa.alloc(u32, len) catch |err| return status(err);
- defer ctx.gpa.free(nodes);
- for (items[0..len], nodes) |h, *node| node.* = ctx.handles.get(h) catch |err| return status(err);
- return ctx.publish(codecs.ofList(&ctx.arena, nodes) catch |err| return status(err), out);
-}
-export fn arb_to_number(ctx: *ArbCtx, root: Handle, out: *u64) Status {
- const node = ctx.handles.get(root) catch |err| return status(err);
- ctx.prepare() catch |err| return status(err);
- const value = (codecs.toNumber(&ctx.arena, node) catch |err| return status(err)) orelse return .type_mismatch;
- out.* = value;
- return .ok;
-}
-export fn arb_to_bool(ctx: *ArbCtx, root: Handle, out: *c_int) Status {
- const node = ctx.handles.get(root) catch |err| return status(err);
- ctx.prepare() catch |err| return status(err);
- const value = (codecs.toBool(&ctx.arena, node) catch |err| return status(err)) orelse return .type_mismatch;
- out.* = if (value) 1 else 0;
- return .ok;
-}
-export fn arb_to_string(ctx: *ArbCtx, root: Handle, out_ptr: **u8, out_len: *usize) Status {
- const node = ctx.handles.get(root) catch |err| return status(err);
- ctx.prepare() catch |err| return status(err);
- const value = (codecs.toString(&ctx.arena, node) catch |err| return status(err)) orelse return .type_mismatch;
- out_ptr.* = @ptrCast(value.ptr);
- out_len.* = value.len;
- return .ok;
-}
-export fn arb_to_bytes(ctx: *ArbCtx, root: Handle, out_ptr: **u8, out_len: *usize) Status {
- return arb_to_string(ctx, root, out_ptr, out_len);
-}
-export fn arb_unwrap_result(ctx: *ArbCtx, root: Handle, out_ok: *c_int, out_value: *Handle, out_rest: *Handle) Status {
- const node = ctx.handles.get(root) catch |err| return status(err);
- ctx.prepare() catch |err| return status(err);
- const r = (codecs.unwrapResult(&ctx.arena, node) catch |err| return status(err)) orelse return .type_mismatch;
- const result = ctx.publishPair(r.value, r.rest, out_value, out_rest);
- if (result == .ok) out_ok.* = if (r.ok) 1 else 0;
- return result;
-}
-export fn arb_unwrap_host_value(ctx: *ArbCtx, root: Handle, out_tag: *u64, out_payload: *Handle) Status {
- const node = ctx.handles.get(root) catch |err| return status(err);
- ctx.prepare() catch |err| return status(err);
- const hv = (codecs.unwrapHostValue(&ctx.arena, node) catch |err| return status(err)) orelse return .type_mismatch;
- const result = ctx.publish(hv.payload, out_payload);
- if (result == .ok) out_tag.* = hv.tag;
- return result;
-}
-// Explicit version-specific native entrypoint; ABI v3 is NOT an IO selector.
-// No legacy grant struct is accepted. Failed calls never publish an output handle.
-export fn arb_run_io_v2(ctx: *ArbCtx, program: Handle, out: *Handle) Status {
- if (wasm) return .io_error;
- const node = ctx.handles.get(program) catch |err| return status(err);
- ctx.prepare() catch |err| return status(err);
- const result = @import("io_v2_native.zig").run(&ctx.arena, node, std.math.maxInt(u64)) catch |err| return switch (err) {
- error.OutOfMemory => .out_of_memory,
- error.FuelExhausted => .fuel_exhausted,
- else => .io_error,
- };
- return ctx.publish(result, out);
-}
-export fn arb_kernel_root(ctx: *ArbCtx, out: *Handle) Status {
- return ctx.publish(ctx.kernel_root, out);
-}
-export fn arb_load_bundle(ctx: *ArbCtx, bytes: [*]const u8, len: usize, name: [*:0]const u8, out: *Handle) Status {
- return ctx.publish(bundle.loadBundleExport(&ctx.arena, bytes[0..len], std.mem.sliceTo(name, 0)) catch |err| return status(err), out);
-}
-export fn arb_load_bundle_default(ctx: *ArbCtx, bytes: [*]const u8, len: usize, out: *Handle) Status {
- return ctx.publish(bundle.loadBundleDefaultRoot(&ctx.arena, bytes[0..len]) catch |err| return status(err), out);
-}
-
-// WASM-only host scratch buffers; not additions to the native C ABI.
-comptime {
- if (wasm) {
- @export(&hostAlloc, .{ .name = "arb_host_alloc" });
- @export(&hostFree, .{ .name = "arb_host_free" });
- @export(&hostOfText, .{ .name = "arb_host_of_text" });
- @export(&hostToText, .{ .name = "arb_host_to_text" });
- @export(&hostPreview, .{ .name = "arb_host_preview" });
- @export(&hostReduceUnbounded, .{ .name = "arb_host_reduce_unbounded" });
- @export(&hostSnapshotUnbounded, .{ .name = "arb_host_snapshot_unbounded" });
- @export(&hostPreviewUnbounded, .{ .name = "arb_host_preview_unbounded" });
- @export(&hostStats, .{ .name = "arb_host_stats" });
- }
-}
-// 8-byte alignment permits u64 ABI output parameters.
-fn hostAlloc(len: usize) callconv(.c) ?[*]align(8) u8 {
- const allocator = if (wasm) std.heap.wasm_allocator else std.heap.smp_allocator;
- const bytes = allocator.alignedAlloc(u8, .@"8", len) catch return null;
- return bytes.ptr;
-}
-fn hostFree(ptr: [*]align(8) u8, len: usize) callconv(.c) void {
- const allocator = if (wasm) std.heap.wasm_allocator else std.heap.smp_allocator;
- allocator.free(ptr[0..len]);
-}
-
-// Browser text is a list of Unicode scalars, never a list of UTF-8 bytes.
-fn hostOfText(ctx: *ArbCtx, ptr: [*]const u8, len: usize, limit: usize, out: *Handle) callconv(.c) Status {
- const view = std.unicode.Utf8View.init(ptr[0..len]) catch return .invalid_data;
- var iter = view.iterator();
- var count: usize = 0;
- while (iter.nextCodepoint()) |_| {
- if (count == limit) return .invalid_data;
- count += 1;
- }
- return ctx.publish(@import("source_text.zig").fromUtf8(&ctx.arena, ptr[0..len]) catch |err| return status(err), out);
-}
-fn hostToText(ctx: *ArbCtx, root: Handle, limit: usize, out_ptr: **u8, out_len: *usize) callconv(.c) Status {
- const node = ctx.handles.get(root) catch |err| return status(err);
- ctx.prepare() catch |err| return status(err);
- const bytes = @import("source_text.zig").toUtf8UnboundedWork(&ctx.arena, node, limit) catch |err| return status(err);
- out_ptr.* = @ptrCast(bytes.ptr);
- out_len.* = bytes.len;
- return .ok;
-}
-// Diagnostic counts, not byte/RSS limits; useful for repeated-session checks.
-fn hostStats(ctx: *ArbCtx, slots: *u32, owned: *u32, nodes: *u32, live: *u32) callconv(.c) Status {
- slots.* = @intCast(ctx.handles.slots.items.len);
- owned.* = 0;
- for (ctx.handles.slots.items) |slot| {
- if (slot.node != null) owned.* += 1;
- }
- nodes.* = ctx.arena.len();
- live.* = @intCast(ctx.arena.last_live);
- return .ok;
-}
-
-fn hostPreview(ctx: *ArbCtx, root: Handle, limit: u32, fuel: u64, out_ptr: **u8, out_len: *usize) callconv(.c) Status {
- const node = ctx.handles.get(root) catch |err| return status(err);
- ctx.prepare() catch |err| return status(err);
- const bytes = @import("preview.zig").capture(&ctx.arena, node, limit, fuel) catch |err| return status(err);
- out_ptr.* = @ptrCast(bytes.ptr);
- out_len.* = bytes.len;
- return .ok;
-}
-
-// Browser work is cancellable by Worker termination, not an implicit fuel cap.
-fn hostReduceUnbounded(ctx: *ArbCtx, root: Handle, out: *Handle) callconv(.c) Status {
- const node = ctx.handles.get(root) catch |err| return status(err);
- ctx.prepare() catch |err| return status(err);
- const result = reduce.reduceUnbounded(node, &ctx.arena) catch |err| return status(err);
- return ctx.publish(result, out);
-}
-fn hostPreviewUnbounded(ctx: *ArbCtx, root: Handle, limit: u32, out_ptr: **u8, out_len: *usize) callconv(.c) Status {
- const node = ctx.handles.get(root) catch |err| return status(err);
- ctx.prepare() catch |err| return status(err);
- const bytes = @import("preview.zig").captureUnboundedWork(&ctx.arena, node, limit) catch |err| return status(err);
- out_ptr.* = @ptrCast(bytes.ptr);
- out_len.* = bytes.len;
- return .ok;
-}
-
-// Full bounded result transport, distinct from the 256-node preview API.
-fn hostSnapshotUnbounded(ctx: *ArbCtx, root: Handle, limit: u32, out_ptr: **u8, out_len: *usize) callconv(.c) Status {
- const node = ctx.handles.get(root) catch |err| return status(err);
- ctx.prepare() catch |err| return status(err);
- const bytes = @import("preview.zig").snapshotUnboundedWork(&ctx.arena, node, limit) catch |err| return status(err);
- out_ptr.* = @ptrCast(bytes.ptr);
- out_len.* = bytes.len;
- return .ok;
-}
diff --git a/ext/zig/src/codecs.zig b/ext/zig/src/codecs.zig
@@ -1,208 +0,0 @@
-const std = @import("std");
-const Arena = @import("arena.zig").Arena;
-const reduce = @import("reduce.zig");
-
-// ---------------------------------------------------------------------------
-// Number encoding/decoding
-// ---------------------------------------------------------------------------
-
-pub fn ofNumber(arena: *Arena, n: u64) !u32 {
- if (n == 0) {
- return try arena.alloc(.leaf);
- }
- const bit = if (n % 2 == 1) try arena.alloc(.{ .stem = .{ .child = try arena.alloc(.leaf) } }) else try arena.alloc(.leaf);
- const rest = try ofNumber(arena, n / 2);
- return try arena.alloc(.{ .fork = .{ .left = bit, .right = rest } });
-}
-
-pub fn toNumber(arena: *Arena, idx: u32) !?u64 {
- const node = try reduce.reduce(idx, arena, 1_000_000);
- const n = arena.get(node);
- return switch (n.*) {
- .leaf => 0,
- .stem => return null,
- .fork => |f| blk: {
- const bit_node = try reduce.reduce(f.left, arena, 1_000_000);
- const bit = arena.get(bit_node);
- const bit_val: u64 = switch (bit.*) {
- .leaf => 0,
- .stem => |s| if (arena.get(try reduce.reduce(s.child, arena, 1_000_000)).* == .leaf) 1 else return null,
- else => return null,
- };
- const rest = try toNumber(arena, f.right) orelse return null;
- const doubled = std.math.mul(u64, 2, rest) catch return null;
- break :blk std.math.add(u64, bit_val, doubled) catch return null;
- },
- .app => return null,
- };
-}
-
-// ---------------------------------------------------------------------------
-// List encoding/decoding
-// ---------------------------------------------------------------------------
-
-pub fn ofList(arena: *Arena, items: []const u32) !u32 {
- var result = try arena.alloc(.leaf);
- var i: usize = items.len;
- while (i > 0) {
- i -= 1;
- result = try arena.alloc(.{ .fork = .{ .left = items[i], .right = result } });
- }
- return result;
-}
-
-pub fn toList(arena: *Arena, idx: u32) !?std.ArrayList(u32) {
- var result = std.ArrayList(u32).empty;
- errdefer result.deinit(arena.allocator);
-
- var current = idx;
- while (true) {
- const node = try reduce.reduce(current, arena, 1_000_000);
- const n = arena.get(node);
- switch (n.*) {
- .leaf => return result,
- .stem => {
- result.deinit(arena.allocator);
- return null;
- },
- .fork => |f| {
- try result.append(arena.allocator, f.left);
- current = f.right;
- },
- .app => return null,
- }
- }
-}
-
-// ---------------------------------------------------------------------------
-// String / Bytes encoding/decoding
-// Strings are lists of byte values (each character encoded as a number tree).
-// ---------------------------------------------------------------------------
-
-pub fn ofString(arena: *Arena, s: []const u8) !u32 {
- var bytes = try arena.allocator.alloc(u32, s.len);
- defer arena.allocator.free(bytes);
- for (s, 0..) |c, i| {
- bytes[i] = try ofNumber(arena, c);
- }
- return try ofList(arena, bytes);
-}
-
-pub fn toString(arena: *Arena, idx: u32) !?[]u8 {
- var list = try toList(arena, idx) orelse return null;
- defer list.deinit(arena.allocator);
- var result = try arena.allocator.alloc(u8, list.items.len);
- errdefer arena.allocator.free(result);
- for (list.items, 0..) |elem_idx, i| {
- const num = try toNumber(arena, elem_idx) orelse {
- arena.allocator.free(result);
- return null;
- };
- if (num > 255) {
- arena.allocator.free(result);
- return null;
- }
- result[i] = @intCast(num);
- }
- return result;
-}
-
-pub fn ofBytes(arena: *Arena, bytes: []const u8) !u32 {
- return try ofString(arena, bytes);
-}
-
-pub fn toBytes(arena: *Arena, idx: u32) !?[]u8 {
- return try toString(arena, idx);
-}
-
-// ---------------------------------------------------------------------------
-// Result unwrapping (ok/err protocol)
-// ok value rest = pair true (pair value rest)
-// err code rest = pair false (pair code rest)
-// ---------------------------------------------------------------------------
-
-pub const UnwrapResult = struct {
- ok: bool,
- value: u32,
- rest: u32,
-};
-
-pub fn unwrapResult(arena: *Arena, idx: u32) !?UnwrapResult {
- const node = try reduce.reduce(idx, arena, 1_000_000);
- const n = arena.get(node);
- switch (n.*) {
- .fork => |f| {
- const tag = try reduce.reduce(f.left, arena, 1_000_000);
- const rest_pair = try reduce.reduce(f.right, arena, 1_000_000);
- const rp = arena.get(rest_pair);
- switch (rp.*) {
- .fork => |rf| {
- const is_ok = try toBool(arena, tag) orelse return null;
- return UnwrapResult{
- .ok = is_ok,
- .value = rf.left,
- .rest = rf.right,
- };
- },
- else => return null,
- }
- },
- else => return null,
- }
-}
-
-// ---------------------------------------------------------------------------
-// Host ABI value unwrapping
-// A host ABI value is: pair tag payload
-// ---------------------------------------------------------------------------
-
-pub const HostValue = struct {
- tag: u64,
- payload: u32,
-};
-
-pub fn unwrapHostValue(arena: *Arena, idx: u32) !?HostValue {
- const node = try reduce.reduce(idx, arena, 1_000_000);
- const n = arena.get(node);
- switch (n.*) {
- .fork => |f| {
- const tag_num = try toNumber(arena, f.left) orelse return null;
- return HostValue{ .tag = tag_num, .payload = f.right };
- },
- else => return null,
- }
-}
-
-/// Returns true if the tree is a valid boolean (Leaf=false, Stem Leaf=true).
-pub fn isBool(arena: *Arena, idx: u32) !bool {
- const node = try reduce.reduce(idx, arena, 1_000_000);
- const n = arena.get(node);
- return switch (n.*) {
- .leaf => true,
- .stem => |s| arena.get(try reduce.reduce(s.child, arena, 1_000_000)).* == .leaf,
- else => false,
- };
-}
-
-/// Extract the boolean value: false for Leaf, true for Stem Leaf.
-/// Returns null if the tree is not a valid boolean.
-pub fn toBool(arena: *Arena, idx: u32) !?bool {
- const node = try reduce.reduce(idx, arena, 1_000_000);
- const n = arena.get(node);
- return switch (n.*) {
- .leaf => false,
- .stem => |s| if (arena.get(try reduce.reduce(s.child, arena, 1_000_000)).* == .leaf) true else null,
- else => null,
- };
-}
-
-// ---------------------------------------------------------------------------
-// Host ABI tag constants
-// ---------------------------------------------------------------------------
-
-pub const HOST_TREE_TAG: u64 = 0;
-pub const HOST_STRING_TAG: u64 = 1;
-pub const HOST_NUMBER_TAG: u64 = 2;
-pub const HOST_BOOL_TAG: u64 = 3;
-pub const HOST_LIST_TAG: u64 = 4;
-pub const HOST_BYTES_TAG: u64 = 5;
diff --git a/ext/zig/src/handles.zig b/ext/zig/src/handles.zig
@@ -1,58 +0,0 @@
-const std = @import("std");
-const Arena = @import("arena.zig").Arena;
-
-pub const Handle = u64;
-// A process-wide generation prevents cross-context aliases as well as stale
-// slot reuse. Never wrap: exhaustion is an explicit error, not ABA resurrection.
-var generation = std.atomic.Value(u32).init(1);
-fn nextGeneration() !u32 {
- var old = generation.load(.monotonic);
- while (old != std.math.maxInt(i32)) {
- old = generation.cmpxchgWeak(old, old + 1, .monotonic, .monotonic) orelse return old;
- }
- return error.HandleExhausted;
-}
-
-pub const Table = struct {
- const Slot = struct { node: ?u32, generation: u32, next: ?u32 = null };
- slots: std.ArrayList(Slot) = .empty,
- free: ?u32 = null,
-
- pub fn deinit(self: *Table, allocator: std.mem.Allocator) void {
- self.slots.deinit(allocator);
- }
- pub fn create(self: *Table, allocator: std.mem.Allocator, node: u32) !Handle {
- const gen = try nextGeneration();
- const idx = if (self.free) |idx| blk: {
- self.free = self.slots.items[idx].next;
- self.slots.items[idx] = .{ .node = node, .generation = gen };
- break :blk idx;
- } else blk: {
- if (self.slots.items.len >= std.math.maxInt(u32)) return error.HandleExhausted;
- const idx: u32 = @intCast(self.slots.items.len);
- try self.slots.append(allocator, .{ .node = node, .generation = gen });
- break :blk idx;
- };
- return (@as(u64, gen) << 32) | idx;
- }
- pub fn get(self: *const Table, handle: Handle) !u32 {
- const idx: u32 = @truncate(handle);
- const gen: u32 = @intCast(handle >> 32);
- if (idx >= self.slots.items.len) return error.InvalidHandle;
- const slot = self.slots.items[idx];
- if (gen != slot.generation) return error.InvalidHandle;
- return slot.node orelse error.InvalidHandle;
- }
- pub fn release(self: *Table, handle: Handle) !void {
- _ = try self.get(handle);
- const idx: u32 = @truncate(handle);
- self.slots.items[idx].node = null;
- self.slots.items[idx].next = self.free;
- self.free = idx;
- }
- pub fn appendRoots(self: *const Table, arena: *Arena) !void {
- for (self.slots.items) |slot| {
- if (slot.node) |node| try arena.roots.append(arena.allocator, node);
- }
- }
-};
diff --git a/ext/zig/src/indexed.zig b/ext/zig/src/indexed.zig
@@ -1,666 +0,0 @@
-//! Indexed Arboricx codec. No paths, permissions, compiler policy or IO actions.
-//! Physical IDs accelerate traversal; exact constructor/child keys define bytes.
-const std = @import("std");
-const Arena = @import("arena.zig").Arena;
-const Node = @import("tree.zig").Node;
-const reduce = @import("reduce.zig");
-pub const domain = "arboricx.indexed-tree.v1";
-const fields = [_][]const u8{ "arboricx.bundle.manifest.v1", "tree-calculus-executable-object", "tree-calculus.v1", "indexed", "arboricx.indexed.node.v1", "arboricx.indexed.payload.v1", "tree-calculus.v1", "normal-order", "arboricx.abi.tree.v1" };
-const Key = struct { tag: u8, left: u32 = 0, right: u32 = 0 };
-const Writer = struct {
- allocator: std.mem.Allocator,
- bytes: std.ArrayList(u8) = .empty,
- pub fn writeAll(w: *Writer, data: []const u8) !void {
- try w.raw(data);
- }
- fn raw(w: *Writer, data: []const u8) !void {
- try w.bytes.appendSlice(w.allocator, data);
- }
- fn int(w: *Writer, comptime T: type, n: T) !void {
- var data: [@sizeOf(T)]u8 = undefined;
- std.mem.writeInt(T, &data, n, .big);
- try w.raw(&data);
- }
- fn text(w: *Writer, data: []const u8) !void {
- try w.int(u32, @intCast(data.len));
- try w.raw(data);
- }
- fn deinit(w: *Writer) void {
- w.bytes.deinit(w.allocator);
- }
-};
-fn intern(nodes: *std.ArrayList(Key), keys: *std.AutoHashMap(Key, u32), key: Key) !u32 {
- if (keys.get(key)) |id| return id;
- if (nodes.items.len >= std.math.maxInt(u32)) return error.OutOfMemory;
- const id: u32 = @intCast(nodes.items.len);
- try nodes.append(keys.allocator, key);
- try keys.put(key, id);
- return id;
-}
-
-pub fn prepare(arena: *Arena, root: u32) !Plan {
- const allocator = arena.allocator;
- const base = arena.roots.items.len;
- defer arena.roots.shrinkRetainingCapacity(base);
- try arena.roots.append(allocator, root);
- var memo = std.AutoHashMap(u32, ?u32).init(allocator);
- defer memo.deinit();
- var keys = std.AutoHashMap(Key, u32).init(allocator);
- defer keys.deinit();
- var nodes: std.ArrayList(Key) = .empty;
- errdefer nodes.deinit(allocator);
- const Frame = union(enum) { visit: u32, finish: struct { id: u32, node: Node } };
- var stack: std.ArrayList(Frame) = .empty;
- defer stack.deinit(allocator);
- try stack.append(allocator, .{ .visit = root });
- while (stack.pop()) |frame| switch (frame) {
- .visit => |id| {
- if (memo.get(id)) |entry| {
- if (entry == null) return error.CyclicTree;
- continue;
- }
- const forced = try reduce.reduce(id, arena, 1_000_000);
- const node = arena.get(forced).*;
- // Hold memoized IDs through later reductions, even if updates cut
- // old graph edges. Collection must not recycle a memo's identity.
- try arena.roots.appendSlice(allocator, &.{ id, forced });
- try memo.put(id, null);
- try stack.append(allocator, .{ .finish = .{ .id = id, .node = node } });
- switch (node) {
- .leaf => {},
- .stem => |s| try stack.append(allocator, .{ .visit = s.child }),
- .fork => |f| {
- try stack.append(allocator, .{ .visit = f.right });
- try stack.append(allocator, .{ .visit = f.left });
- },
- .app => unreachable,
- }
- },
- .finish => |f| {
- const key: Key = switch (f.node) {
- .leaf => .{ .tag = 0 },
- .stem => |s| .{ .tag = 1, .left = memo.get(s.child).?.? },
- .fork => |p| .{ .tag = 2, .left = memo.get(p.left).?.?, .right = memo.get(p.right).?.? },
- .app => unreachable,
- };
- try memo.put(f.id, try intern(&nodes, &keys, key));
- },
- };
- const root_index = memo.get(root).?.?;
- var manifest = Writer{ .allocator = allocator };
- defer manifest.deinit();
- try manifest.raw("ARBMNFST");
- try manifest.int(u16, 1);
- try manifest.int(u16, 1);
- for (fields) |field| try manifest.text(field);
- try manifest.int(u32, 0);
- try manifest.int(u8, 0);
- try manifest.int(u32, 1);
- try manifest.int(u32, root_index);
- try manifest.text("default");
- try manifest.int(u32, 1);
- try manifest.text("root");
- try manifest.int(u32, root_index);
- try manifest.text("term");
- try manifest.text(fields[8]);
- try manifest.int(u32, 1);
- try manifest.int(u16, 5);
- try manifest.text("arboricx");
- try manifest.int(u32, 0);
- var payload_length: u64 = 8;
- for (nodes.items) |key| payload_length += 5 + 4 * @as(u64, key.tag);
- const manifest_bytes = try manifest.bytes.toOwnedSlice(allocator);
- errdefer allocator.free(manifest_bytes);
- return .{ .allocator = allocator, .nodes = try nodes.toOwnedSlice(allocator), .manifest = manifest_bytes, .payload_length = payload_length };
-}
-
-/// Graph-proportional canonical plan. It contains no encoded payload and no arena
-/// IDs; emission can be repeated without retaining a previous byte stream.
-pub const Plan = struct {
- allocator: std.mem.Allocator,
- nodes: []Key,
- manifest: []u8,
- payload_length: u64,
- pub fn deinit(self: *Plan) void {
- self.allocator.free(self.nodes);
- self.allocator.free(self.manifest);
- self.* = undefined;
- }
- pub fn encodedLength(self: *const Plan) u64 {
- return 96 + @as(u64, self.manifest.len) + self.payload_length;
- }
- /// Sink.writeAll consumes borrowed chunks synchronously; <=4096 bytes each.
- /// A failing sink is never retried by this emitter.
- pub fn emit(self: *const Plan, sink: anytype) !void {
- var output = ChunkWriter(@TypeOf(sink)){ .sink = sink };
- try output.raw("ARBORICX");
- try output.int(u16, 1);
- try output.int(u16, 0);
- try output.int(u32, 2);
- try output.int(u64, 0);
- try output.int(u64, 32);
- var offset: u64 = 96;
- for ([_]u64{ self.manifest.len, self.payload_length }, 1..) |length, kind| {
- try output.int(u32, @intCast(kind));
- try output.int(u16, 1);
- try output.int(u16, 1);
- try output.int(u16, 0);
- try output.int(u16, 0);
- try output.int(u64, offset);
- try output.int(u64, length);
- try output.int(u32, 0);
- offset += length;
- }
- try output.raw(self.manifest);
- try output.int(u64, self.nodes.len);
- for (self.nodes) |key| {
- try output.int(u32, 1 + 4 * @as(u32, key.tag));
- try output.int(u8, key.tag);
- if (key.tag >= 1) try output.int(u32, key.left);
- if (key.tag == 2) try output.int(u32, key.right);
- }
- try output.flush();
- }
- pub fn hash(self: *const Plan) ![64]u8 {
- var sink = HashSink{};
- sink.context.update(domain ++ "\x00");
- try self.emit(&sink);
- var raw: [32]u8 = undefined;
- sink.context.final(&raw);
- return std.fmt.bytesToHex(raw, .lower);
- }
-};
-const HashSink = struct {
- context: std.crypto.hash.sha2.Sha256 = .init(.{}),
- pub fn writeAll(self: *HashSink, bytes: []const u8) !void {
- self.context.update(bytes);
- }
-};
-fn ChunkWriter(comptime Sink: type) type {
- return struct {
- sink: Sink,
- buffer: [4096]u8 = undefined,
- used: usize = 0,
- fn flush(self: *@This()) !void {
- if (self.used == 0) return;
- try self.sink.writeAll(self.buffer[0..self.used]);
- self.used = 0;
- }
- fn raw(self: *@This(), data: []const u8) !void {
- var offset: usize = 0;
- while (offset < data.len) {
- const count = @min(data.len - offset, self.buffer.len - self.used);
- @memcpy(self.buffer[self.used..][0..count], data[offset..][0..count]);
- self.used += count;
- offset += count;
- if (self.used == self.buffer.len) try self.flush();
- }
- }
- fn int(self: *@This(), comptime T: type, value: T) !void {
- var bytes: [@sizeOf(T)]u8 = undefined;
- std.mem.writeInt(T, &bytes, value, .big);
- try self.raw(&bytes);
- }
- };
-}
-
-/// Compatibility whole-value encoder; bounded transports use Plan.emit instead.
-pub fn encode(arena: *Arena, root: u32) ![]u8 {
- var plan = try prepare(arena, root);
- defer plan.deinit();
- var output = Writer{ .allocator = arena.allocator };
- errdefer output.deinit();
- try plan.emit(&output);
- return output.bytes.toOwnedSlice(arena.allocator);
-}
-
-pub fn digest(bytes: []const u8) [64]u8 {
- var hash = std.crypto.hash.sha2.Sha256.init(.{});
- hash.update(domain ++ "\x00");
- hash.update(bytes);
- var raw: [32]u8 = undefined;
- hash.final(&raw);
- return std.fmt.bytesToHex(raw, .lower);
-}
-
-const Reader = struct {
- bytes: []const u8,
- pos: usize = 0,
- fn take(r: *Reader, n: usize) ![]const u8 {
- if (n > r.bytes.len - r.pos) return error.InvalidData;
- const data = r.bytes[r.pos..][0..n];
- r.pos += n;
- return data;
- }
- fn int(r: *Reader, comptime T: type) !T {
- return std.mem.readInt(T, (try r.take(@sizeOf(T)))[0..@sizeOf(T)], .big);
- }
- fn text(r: *Reader) ![]const u8 {
- const data = try r.take(try r.int(u32));
- if (!std.unicode.utf8ValidateSlice(data)) return error.InvalidData;
- return data;
- }
- fn expect(r: *Reader, data: []const u8) !void {
- if (!std.mem.eql(u8, try r.take(data.len), data)) return error.InvalidData;
- }
- fn remaining(r: *const Reader) usize {
- return r.bytes.len - r.pos;
- }
- fn end(r: *Reader) !void {
- if (r.pos != r.bytes.len) return error.InvalidData;
- }
-};
-fn section(bytes: []const u8, start: u64, len: u64) ![]const u8 {
- if (start > bytes.len or len > bytes.len - start) return error.InvalidData;
- return bytes[@intCast(start)..][0..@intCast(len)];
-}
-
-/// General validated bundle reader, also shared by the C ABI. Single-tree IO
-/// additionally requires exactly one root/export named "root". Like Haskell,
-/// read accepts valid noncanonical layouts; only encode defines canonical bytes.
-pub fn load(arena: *Arena, bytes: []const u8, name: ?[]const u8, single: bool) !u32 {
- var header = Reader{ .bytes = bytes };
- try header.expect("ARBORICX");
- if (try header.int(u16) != 1) return error.InvalidData;
- _ = try header.int(u16);
- const count = try header.int(u32);
- _ = try header.int(u64);
- const start = try header.int(u64);
- var directory = Reader{ .bytes = try section(bytes, start, @as(u64, count) * 32) };
- var manifest: ?[]const u8 = null;
- var payload: ?[]const u8 = null;
- for (0..count) |_| {
- const kind = try directory.int(u32);
- _ = try directory.int(u16);
- const flags = try directory.int(u16);
- const compression = try directory.int(u16);
- _ = try directory.int(u16);
- const offset = try directory.int(u64);
- const len = try directory.int(u64);
- _ = try directory.int(u32);
- if (kind == 1 or kind == 2) {
- if (compression != 0) return error.InvalidData;
- const data = try section(bytes, offset, len);
- if (kind == 1) {
- if (manifest != null) return error.InvalidData;
- manifest = data;
- } else {
- if (payload != null) return error.InvalidData;
- payload = data;
- }
- } else if (flags & 1 != 0) return error.InvalidData;
- }
- const selection = try parseManifest(arena, manifest orelse return error.InvalidData, name, single);
- var n = Reader{ .bytes = payload orelse return error.InvalidData };
- return loadNodes(arena, &n, selection);
-}
-const Selection = struct { max_root: u32, selected: ?u32 };
-fn parseManifest(arena: *Arena, bytes: []const u8, name: ?[]const u8, single: bool) !Selection {
- var m = Reader{ .bytes = bytes };
- try m.expect("ARBMNFST");
- if (try m.int(u16) != 1 or try m.int(u16) != 1) return error.InvalidData;
- for (fields) |field| if (!std.mem.eql(u8, try m.text(), field)) return error.InvalidData;
- if (try m.int(u32) != 0 or try m.int(u8) != 0) return error.InvalidData;
- const root_count = try m.int(u32);
- if (root_count == 0 or root_count > (m.bytes.len - m.pos) / 8 or (single and root_count != 1)) return error.InvalidData;
- var max_root: u32 = 0;
- var first: u32 = 0;
- for (0..root_count) |i| {
- const root = try m.int(u32);
- _ = try m.text();
- if (i == 0) first = root;
- max_root = @max(max_root, root);
- }
- const export_count = try m.int(u32);
- if (export_count == 0 or export_count > (m.bytes.len - m.pos) / 16 or (single and export_count != 1)) return error.InvalidData;
- var names = std.StringHashMap(void).init(arena.allocator);
- defer names.deinit();
- var selected: ?u32 = if (name == null) first else null;
- for (0..export_count) |_| {
- const exported = try m.text();
- const root = try m.int(u32);
- if (exported.len == 0 or names.contains(exported)) return error.InvalidData;
- try names.put(exported, {});
- if (!std.mem.eql(u8, try m.text(), "term") or !std.mem.eql(u8, try m.text(), fields[8])) return error.InvalidData;
- if (single and (!std.mem.eql(u8, exported, "root") or root != first)) return error.InvalidData;
- if (name) |requested| {
- if (std.mem.eql(u8, requested, exported)) selected = root;
- }
- max_root = @max(max_root, root);
- }
- var known_metadata: u8 = 0;
- for (0..2) |group| {
- const tlvs = try m.int(u32);
- if (tlvs > (m.bytes.len - m.pos) / 6) return error.InvalidData;
- for (0..tlvs) |_| {
- const tag = try m.int(u16);
- const value = try m.take(try m.int(u32));
- if (group == 0 and tag >= 1 and tag <= 5) {
- const mask = @as(u8, 1) << @as(u3, @intCast(tag));
- if (known_metadata & mask == 0 and !std.unicode.utf8ValidateSlice(value)) return error.InvalidData;
- known_metadata |= mask;
- }
- }
- }
- try m.end();
- return .{ .max_root = max_root, .selected = selected };
-}
-fn loadNodes(arena: *Arena, n: anytype, selection: Selection) !u32 {
- const node_count = try n.int(u64);
- if (node_count == 0 or node_count > std.math.maxInt(u32) or node_count > n.remaining() / 5 or selection.max_root >= node_count) return error.InvalidData;
- const ids = try arena.allocator.alloc(u32, @intCast(node_count));
- defer arena.allocator.free(ids);
- var keys = std.AutoHashMap(Key, void).init(arena.allocator);
- defer keys.deinit();
- for (ids, 0..) |*id, i| {
- const size = try n.int(u32);
- const tag = try n.int(u8);
- if (tag > 2 or size != 1 + 4 * @as(u32, tag)) return error.InvalidData;
- var key = Key{ .tag = tag };
- if (tag >= 1) {
- key.left = try n.int(u32);
- if (key.left >= i) return error.InvalidData;
- }
- if (tag == 2) {
- key.right = try n.int(u32);
- if (key.right >= i) return error.InvalidData;
- }
- if (keys.contains(key)) return error.InvalidData;
- try keys.put(key, {});
- id.* = try arena.alloc(switch (tag) {
- 0 => .leaf,
- 1 => .{ .stem = .{ .child = ids[key.left] } },
- 2 => .{ .fork = .{ .left = ids[key.left], .right = ids[key.right] } },
- else => unreachable,
- });
- }
- try n.end();
- return ids[selection.selected orelse return error.ExportNotFound];
-}
-
-/// Decode only an already-authenticated stable snapshot. The source exposes a
-/// length and exact bounded positional reads; neither original-input rereading
-/// nor whole encoded-payload allocation occurs here. External padding is allowed.
-pub fn loadSnapshot(arena: *Arena, source: anytype) !u32 {
- var header_bytes: [32]u8 = undefined;
- if (source.length < header_bytes.len) return error.InvalidData;
- try source.readAt(0, &header_bytes);
- var header = Reader{ .bytes = &header_bytes };
- try header.expect("ARBORICX");
- if (try header.int(u16) != 1) return error.InvalidData;
- _ = try header.int(u16);
- const count = try header.int(u32);
- if (count > 64) return error.InvalidData;
- _ = try header.int(u64);
- const start = try header.int(u64);
- try snapshotRange(source.length, start, @as(u64, count) * 32);
- const Section = struct { offset: u64, length: u64 };
- var manifest: ?Section = null;
- var payload: ?Section = null;
- for (0..count) |i| {
- var raw: [32]u8 = undefined;
- try source.readAt(start + @as(u64, i) * 32, &raw);
- var entry = Reader{ .bytes = &raw };
- const kind = try entry.int(u32);
- _ = try entry.int(u16);
- const flags = try entry.int(u16);
- const compression = try entry.int(u16);
- _ = try entry.int(u16);
- const offset = try entry.int(u64);
- const length = try entry.int(u64);
- _ = try entry.int(u32);
- if (kind == 1 or kind == 2) {
- if (compression != 0) return error.InvalidData;
- try snapshotRange(source.length, offset, length);
- if (kind == 1) {
- if (manifest != null or length > 65536) return error.InvalidData;
- manifest = .{ .offset = offset, .length = length };
- } else {
- if (payload != null) return error.InvalidData;
- payload = .{ .offset = offset, .length = length };
- }
- } else if (flags & 1 != 0) return error.InvalidData;
- }
- const m = manifest orelse return error.InvalidData;
- const bytes = try arena.allocator.alloc(u8, @intCast(m.length));
- defer arena.allocator.free(bytes);
- var at: usize = 0;
- while (at < bytes.len) {
- const size = @min(4096, bytes.len - at);
- try source.readAt(m.offset + at, bytes[at..][0..size]);
- at += size;
- }
- const selection = try parseManifest(arena, bytes, "root", true);
- const p = payload orelse return error.InvalidData;
- var nodes = SnapshotReader(@TypeOf(source)){ .source = source, .start = p.offset, .length = p.length };
- return loadNodes(arena, &nodes, selection);
-}
-fn snapshotRange(length: u64, offset: u64, count: u64) !void {
- if (offset > length or count > length - offset) return error.InvalidData;
-}
-fn SnapshotReader(comptime Source: type) type {
- return struct {
- source: Source,
- start: u64,
- length: u64,
- pos: u64 = 0,
- cache: [4096]u8 = undefined,
- used: usize = 0,
- available: usize = 0,
- fn remaining(self: *const @This()) u64 {
- return self.length - self.pos;
- }
- fn int(self: *@This(), comptime T: type) !T {
- if (self.remaining() < @sizeOf(T)) return error.InvalidData;
- var bytes: [@sizeOf(T)]u8 = undefined;
- for (&bytes) |*byte| {
- if (self.used == self.available) {
- self.available = @intCast(@min(self.remaining(), self.cache.len));
- try self.source.readAt(self.start + self.pos, self.cache[0..self.available]);
- self.used = 0;
- }
- byte.* = self.cache[self.used];
- self.used += 1;
- self.pos += 1;
- }
- return std.mem.readInt(T, &bytes, .big);
- }
- fn end(self: *const @This()) !void {
- if (self.remaining() != 0) return error.InvalidData;
- }
- };
-}
-
-test "canonical leaf matches independent writer profile and domain digest" {
- var arena = Arena.init(std.testing.allocator);
- defer arena.deinit();
- const bytes = try encode(&arena, try arena.alloc(.leaf));
- defer arena.allocator.free(bytes);
- try std.testing.expectEqual(@as(usize, 431), bytes.len);
- // Independently assembled by test/executable_object.py's wire profile.
- try std.testing.expectEqualStrings("6f93378feaa8bf5ab7cefc071175a654c871bfd53f165d7d96795fb2b38483f1", &digest(bytes));
- const decoded = try load(&arena, bytes, "root", true);
- try std.testing.expect(arena.get(decoded).* == .leaf);
-}
-
-test "physical sharing is only an acceleration hint, and lazy children are serialized" {
- var arena = Arena.init(std.testing.allocator);
- defer arena.deinit();
- const leaf = try arena.alloc(.leaf);
- const child = try arena.alloc(.{ .stem = .{ .child = leaf } });
- const shared = try arena.alloc(.{ .fork = .{ .left = child, .right = child } });
- const other_leaf = try arena.alloc(.leaf);
- const other_child = try arena.alloc(.{ .stem = .{ .child = other_leaf } });
- const constant = try arena.alloc(.{ .fork = .{ .left = other_leaf, .right = other_child } });
- const delayed = try arena.alloc(.{ .app = .{ .func = constant, .arg = other_leaf } });
- const copied = try arena.alloc(.{ .fork = .{ .left = child, .right = delayed } });
- const a = try encode(&arena, shared);
- defer arena.allocator.free(a);
- arena.gc_enabled = true;
- try arena.setGcPolicy(1, 1);
- const b = try encode(&arena, copied);
- defer arena.allocator.free(b);
- try std.testing.expectEqualSlices(u8, a, b);
- try std.testing.expectEqual(@as(usize, 0), arena.roots.items.len);
- const decoded = try load(&arena, a, "root", true);
- try std.testing.expectEqual(arena.get(decoded).fork.left, arena.get(decoded).fork.right);
-}
-
-test "deep exponentially expanded trees use iterative DAG traversal" {
- var arena = Arena.init(std.testing.allocator);
- defer arena.deinit();
- var root = try arena.alloc(.leaf);
- for (0..100000) |_| root = try arena.alloc(.{ .fork = .{ .left = root, .right = root } });
- const bytes = try encode(&arena, root);
- defer arena.allocator.free(bytes);
- try std.testing.expectEqual(@as(usize, 431 + 13 * 100000), bytes.len);
- const copied = try load(&arena, bytes, "root", true);
- const again = try encode(&arena, copied);
- defer arena.allocator.free(again);
- try std.testing.expectEqualSlices(u8, bytes, again);
-}
-
-test "bundle parser rejects every truncated prefix and safely handles byte mutations" {
- var arena = Arena.init(std.testing.allocator);
- defer arena.deinit();
- const bytes = try encode(&arena, try arena.alloc(.leaf));
- defer arena.allocator.free(bytes);
- for (0..bytes.len) |len| try std.testing.expectError(error.InvalidData, load(&arena, bytes[0..len], "root", true));
- for (bytes) |*byte| {
- byte.* ^= 0xff;
- _ = load(&arena, bytes, "root", true) catch |err| switch (err) {
- error.InvalidData, error.ExportNotFound => 0,
- else => return err,
- };
- byte.* ^= 0xff;
- }
- // Section count, offset and size arithmetic must not overflow or slice
- // outside input, even when the supplied bytes themselves are authenticated.
- for ([_]usize{ 24, 44, 52, 76, 84 }) |offset| {
- const old = std.mem.readInt(u64, bytes[offset..][0..8], .big);
- std.mem.writeInt(u64, bytes[offset..][0..8], std.math.maxInt(u64), .big);
- try std.testing.expectError(error.InvalidData, load(&arena, bytes, "root", true));
- std.mem.writeInt(u64, bytes[offset..][0..8], old, .big);
- }
-}
-
-fn allocationProbe(allocator: std.mem.Allocator) !void {
- var arena = Arena.init(allocator);
- defer arena.deinit();
- const leaf = try arena.alloc(.leaf);
- const root = try arena.alloc(.{ .fork = .{ .left = leaf, .right = leaf } });
- const bytes = try encode(&arena, root);
- defer allocator.free(bytes);
- _ = try load(&arena, bytes, "root", true);
-}
-
-test "indexed codec allocation failures unwind all scratch storage" {
- try std.testing.checkAllAllocationFailures(std.testing.allocator, allocationProbe, .{});
-}
-
-test "reader rejects duplicate constructors, forward edges and mismatched single roots" {
- var arena = Arena.init(std.testing.allocator);
- defer arena.deinit();
- const leaf = try arena.alloc(.leaf);
- const root = try arena.alloc(.{ .stem = .{ .child = leaf } });
- const bytes = try encode(&arena, root);
- defer arena.allocator.free(bytes);
- const node_offset: usize = @intCast(std.mem.readInt(u64, bytes[76..84], .big));
- const child_offset = node_offset + 8 + 5 + 5;
- std.mem.writeInt(u32, bytes[child_offset..][0..4], 1, .big);
- try std.testing.expectError(error.InvalidData, load(&arena, bytes, "root", true));
- std.mem.writeInt(u32, bytes[child_offset..][0..4], 0, .big);
- // Valid general bundle with unequal root/export indices is not a valid
- // name-neutral single-tree object, even when both indices are in bounds.
- const export_offset = std.mem.indexOf(u8, bytes, "root").? + 4;
- std.mem.writeInt(u32, bytes[export_offset..][0..4], 0, .big);
- try std.testing.expectError(error.InvalidData, load(&arena, bytes, "root", true));
- _ = try load(&arena, bytes, "root", false);
- const one = try encode(&arena, leaf);
- defer arena.allocator.free(one);
- const duplicate = try arena.allocator.alloc(u8, one.len + 5);
- defer arena.allocator.free(duplicate);
- @memcpy(duplicate[0..one.len], one);
- @memcpy(duplicate[one.len..], "\x00\x00\x00\x01\x00");
- std.mem.writeInt(u64, duplicate[84..92], 18, .big);
- std.mem.writeInt(u64, duplicate[node_offset..][0..8], 2, .big);
- try std.testing.expectError(error.InvalidData, load(&arena, duplicate, "root", true));
-}
-
-test "canonical plan streams bounded repeatable chunks and stops at sink failure" {
- const t = std.testing;
- var arena = Arena.init(t.allocator);
- defer arena.deinit();
- var root = try arena.alloc(.leaf);
- for (0..10000) |_| root = try arena.alloc(.{ .fork = .{ .left = root, .right = root } });
- var plan = try prepare(&arena, root);
- defer plan.deinit();
- try t.expectEqual(@as(u64, 130431), plan.encodedLength());
- const Sink = struct {
- count: u64 = 0,
- calls: usize = 0,
- fail_at: ?usize = null,
- pub fn writeAll(self: *@This(), bytes: []const u8) !void {
- try t.expect(bytes.len > 0 and bytes.len <= 4096);
- self.calls += 1;
- if (self.fail_at == self.calls) return error.InjectedFailure;
- self.count += bytes.len;
- }
- };
- var sink = Sink{};
- try plan.emit(&sink);
- try t.expectEqual(plan.encodedLength(), sink.count);
- const calls = sink.calls;
- for (1..calls + 1) |step| {
- var failing = Sink{ .fail_at = step };
- try t.expectError(error.InjectedFailure, plan.emit(&failing));
- try t.expectEqual(step, failing.calls);
- }
- // Independent Python wire profile: io_v2_indexed_write.dag(10000, True).
- try t.expectEqualStrings("9039b73aff5b088b972a22e41728147bda274d1af9ca01a92afa747a930722b9", &(try plan.hash()));
- const whole = try encode(&arena, root);
- defer t.allocator.free(whole);
- try t.expectEqualSlices(u8, &digest(whole), &(try plan.hash()));
- var second = Writer{ .allocator = t.allocator };
- defer second.deinit();
- try plan.emit(&second);
- try t.expectEqualSlices(u8, whole, second.bytes.items);
- try t.expectEqual(@as(usize, 0), arena.roots.items.len);
-}
-
-const MemorySnapshot = struct {
- bytes: []const u8,
- length: u64,
- pub fn readAt(self: *@This(), offset: u64, target: []u8) !void {
- try std.testing.expect(target.len <= 4096);
- if (offset > self.length or target.len > self.length - offset) return error.InvalidData;
- @memcpy(target, self.bytes[@intCast(offset)..][0..target.len]);
- }
-};
-test "bounded snapshot decoder rejects every truncation and mutation without unchecked ranges" {
- var arena = Arena.init(std.testing.allocator);
- defer arena.deinit();
- const bytes = try encode(&arena, try arena.alloc(.leaf));
- defer arena.allocator.free(bytes);
- for (0..bytes.len) |len| {
- var source = MemorySnapshot{ .bytes = bytes, .length = len };
- try std.testing.expectError(error.InvalidData, loadSnapshot(&arena, &source));
- }
- var source = MemorySnapshot{ .bytes = bytes, .length = bytes.len };
- for (bytes) |*byte| {
- byte.* ^= 0xff;
- _ = loadSnapshot(&arena, &source) catch |err| switch (err) {
- error.InvalidData, error.ExportNotFound => 0,
- else => return err,
- };
- byte.* ^= 0xff;
- }
- const root = try loadSnapshot(&arena, &source);
- try std.testing.expect(arena.get(root).* == .leaf);
- for ([_]usize{ 24, 44, 52, 76, 84 }) |offset| {
- const old = std.mem.readInt(u64, bytes[offset..][0..8], .big);
- std.mem.writeInt(u64, bytes[offset..][0..8], std.math.maxInt(u64), .big);
- try std.testing.expectError(error.InvalidData, loadSnapshot(&arena, &source));
- std.mem.writeInt(u64, bytes[offset..][0..8], old, .big);
- }
-}
diff --git a/ext/zig/src/io_v2_core.zig b/ext/zig/src/io_v2_core.zig
@@ -1,445 +0,0 @@
-//! Private structured core with a synchronous host interface. The default host
-//! rejects all named requests. Optional test hosts provide real resources without
-//! enabling any public v2 entrypoint. Host calls must settle before returning.
-const std = @import("std");
-const Arena = @import("arena.zig").Arena;
-const P = @import("io_v2_protocol.zig");
-const R = @import("io_v2_resources.zig");
-const T = @import("io_v2_tasks.zig");
-const C = @import("codecs.zig");
-pub const Outcome = T.Scheduler(u8, u32).Outcome;
-const Body = struct {
- action: u32,
- frames: std.ArrayList(u32) = .empty,
- mode: enum { runnable, lexical, joining, done } = .runnable,
-};
-fn EngineType(comptime Host: type) type {
- const S = T.Scheduler(Host.Backend, u32);
- return struct {
- const Engine = @This();
- host: *Host,
- cleanup_failure: u32,
- arena: *Arena,
- scheduler: *S,
- bodies: std.AutoHashMapUnmanaged(u64, Body) = .empty,
- leaf: u32,
- fuel: u64,
- cursor: u64 = std.math.maxInt(u64),
- fn deinit(self: *Engine) void {
- var it = self.bodies.valueIterator();
- while (it.next()) |b| b.frames.deinit(self.arena.allocator);
- self.bodies.deinit(self.arena.allocator);
- }
- pub fn reader(self: *Engine) P.Reader {
- return .{ .arena = self.arena, .fuel = self.fuel };
- }
- fn pair(self: *Engine, a: u32, b: u32) !u32 {
- return self.arena.alloc(.{ .fork = .{ .left = a, .right = b } });
- }
- pub fn text(self: *Engine, value: []const u8) !u32 {
- return C.ofString(self.arena, value);
- }
- pub fn list(self: *Engine, values: []const u32) !u32 {
- return C.ofList(self.arena, values);
- }
- pub fn problem(self: *Engine, category: []const u8, op: []const u8, detail: []const u8) !u32 {
- return self.list(&.{ try self.text(category), try self.text(op), try self.text(detail), try self.text("none") });
- }
- pub fn result(self: *Engine, success: bool, value: u32) !u32 {
- return self.pair(if (success) try self.arena.alloc(.{ .stem = .{ .child = self.leaf } }) else self.leaf, try self.pair(value, self.leaf));
- }
- pub fn marker(self: *Engine, name: []const u8, key: u64) !u32 {
- return self.list(&.{ try self.text(name), try C.ofNumber(self.arena, self.scheduler.registry.identity), try C.ofNumber(self.arena, key) });
- }
- fn literal(self: *Engine, id: u32, expected: []const u8) !bool {
- var rest = id;
- for (expected) |c| {
- const cell = try self.reader().pair(rest) orelse return false;
- if ((try self.reader().nat(cell.first, 7)) != @as(?u64, c)) return false;
- rest = cell.second;
- }
- return try self.reader().node(rest) == .leaf;
- }
- pub fn token(self: *Engine, id: u32, name: []const u8) !?R.Token {
- const tag = try self.reader().pair(id) orelse return null;
- const run_field = try self.reader().pair(tag.second) orelse return null;
- const key = try self.reader().pair(run_field.second) orelse return null;
- if (try self.reader().node(key.second) != .leaf or !try self.literal(tag.first, name)) return null;
- return .{ .run = try self.reader().nat(run_field.first, 64) orelse return null, .key = try self.reader().nat(key.first, 64) orelse return null };
- }
- fn outcomeTree(self: *Engine, outcome: S.Outcome) !u32 {
- if (outcome.kind == .completed) return self.list(&.{ try self.text("completed"), outcome.value });
- var diagnostics: [17]u32 = undefined;
- @memcpy(diagnostics[0..outcome.cleanup.len], outcome.cleanup.items[0..outcome.cleanup.len]);
- var len = outcome.cleanup.len;
- if (outcome.cleanup.omitted != 0) {
- var buffer: [20]u8 = undefined;
- diagnostics[len] = try self.problem("cleanup-omitted", "close", try std.fmt.bufPrint(&buffer, "{d}", .{outcome.cleanup.omitted}));
- len += 1;
- }
- return self.list(&.{ try self.text(if (outcome.kind == .failed) "failed" else "cancelled"), outcome.value, try self.list(diagnostics[0..len]) });
- }
- fn done(self: *Engine, scope: u64, outcome: S.Outcome) !void {
- try self.scheduler.bodyDone(scope, outcome);
- const body = self.bodies.getPtr(scope).?;
- body.mode = .done;
- body.action = self.leaf;
- body.frames.clearRetainingCapacity();
- }
- fn resumeValue(self: *Engine, scope: u64, value: u32) !void {
- const body = self.bodies.getPtr(scope).?;
- if (body.frames.pop()) |continuation| {
- body.action = try self.arena.allocApp(continuation, value);
- body.mode = .runnable;
- } else try self.done(scope, .{ .kind = .completed, .value = value });
- }
- fn fault(self: *Engine, scope: u64, detail: []const u8) !void {
- try self.done(scope, .{ .kind = .failed, .value = try self.problem("protocol-error", "core/decode", detail) });
- }
- fn operationError(self: *Engine, scope: u64, op: []const u8, err: anyerror) !void {
- const category = switch (err) {
- error.StaleTask => "stale-task",
- error.JoinCycle => "join-cycle",
- error.OutOfScope => "out-of-scope",
- error.StaleHandle => "stale-handle",
- error.NotOwner => "not-owner",
- error.BorrowedResource => "borrowed-resource",
- error.InternalResource => "internal-resource",
- error.ClosedHandle => "closed-handle",
- error.Busy => "busy",
- error.DuplicateHandle => "duplicate-handle",
- error.ClosedScope => "closed-scope",
- error.InvalidArgument => "invalid-argument",
- else => return err,
- };
- try self.resumeValue(scope, try self.result(false, try self.problem(category, op, "")));
- }
- fn step(self: *Engine, scope: u64) !void {
- const body = self.bodies.get(scope).?;
- const group = self.scheduler.groups.get(scope).?;
- if (body.mode == .done or body.mode == .lexical) return;
- if (group.reason) |reason| return self.done(scope, reason);
- if (body.mode == .joining) {
- if (try self.scheduler.pollJoin(scope)) |outcome| try self.resumeValue(scope, try self.result(true, try self.outcomeTree(outcome)));
- return;
- }
- const action = P.decode(self.arena, body.action, self.fuel) catch |err| return switch (err) {
- error.InvalidComputationShape => self.fault(scope, "invalid computation shape"),
- error.UnknownComputationConstructor => self.fault(scope, "unknown computation constructor"),
- error.InvalidFacilityHeader => self.fault(scope, "invalid facility header"),
- else => err,
- };
- switch (action) {
- .request => |request| {
- return self.resumeValue(scope, try self.host.request(self, scope, request));
- },
- .core => |core| switch (core.code) {
- .pure => try self.resumeValue(scope, core.payload),
- .fail => try self.done(scope, .{ .kind = .failed, .value = core.payload }),
- .cancelled => try self.done(scope, .{ .kind = .cancelled, .value = core.payload }),
- .bind => {
- const parts = try self.reader().pair(core.payload) orelse return self.fault(scope, "invalid v2 computation");
- try self.bodies.getPtr(scope).?.frames.append(self.arena.allocator, parts.second);
- self.bodies.getPtr(scope).?.action = parts.first;
- },
- .current_scope => try self.resumeValue(scope, try self.marker("io.v2.scope", scope)),
- .scope => {
- try self.bodies.ensureUnusedCapacity(self.arena.allocator, 1);
- const child = try self.scheduler.nested(scope);
- const child_action = try self.arena.allocApp(core.payload, try self.marker("io.v2.scope", child));
- self.bodies.putAssumeCapacityNoClobber(child, .{ .action = child_action });
- self.bodies.getPtr(scope).?.action = self.leaf;
- self.bodies.getPtr(scope).?.mode = .lexical;
- },
- .spawn => {
- const supervised = try self.reader().pair(core.payload) orelse return self.operationError(scope, "core/spawn", error.InvalidArgument);
- const keep = try self.reader().pair(supervised.second) orelse return self.operationError(scope, "core/spawn", error.InvalidArgument);
- const handles = try self.reader().pair(keep.second) orelse return self.operationError(scope, "core/spawn", error.InvalidArgument);
- const contained = try self.reader().nat(supervised.first, 1) orelse return self.operationError(scope, "core/spawn", error.InvalidArgument);
- const retained = try self.reader().nat(keep.first, 1) orelse return self.operationError(scope, "core/spawn", error.InvalidArgument);
- var tokens: [R.transfer_limit]R.Token = undefined;
- var count: usize = 0;
- var rest = handles.first;
- while (try self.reader().node(rest) != .leaf) {
- if (count == tokens.len) return self.operationError(scope, "core/spawn", error.InvalidArgument);
- const cell = try self.reader().pair(rest) orelse return self.operationError(scope, "core/spawn", error.InvalidArgument);
- tokens[count] = try self.token(cell.first, "io.v2.resource") orelse return self.operationError(scope, "core/spawn", error.InvalidArgument);
- count += 1;
- rest = cell.second;
- }
- try self.bodies.ensureUnusedCapacity(self.arena.allocator, 1);
- const child = self.scheduler.spawn(scope, contained == 1, retained == 1, tokens[0..count]) catch |err| return self.operationError(scope, "core/spawn", err);
- var fresh: [R.transfer_limit]u32 = undefined;
- for (child.tokens[0..count], 0..) |t, i| fresh[i] = try self.marker("io.v2.resource", t.key);
- const child_action = try self.arena.allocApp(handles.second, try self.list(fresh[0..count]));
- self.bodies.putAssumeCapacityNoClobber(child.scope, .{ .action = child_action });
- try self.resumeValue(scope, try self.result(true, try self.list(&.{ try self.marker("io.v2.task", child.scope), if (retained == 1) try self.marker("io.v2.join", child.scope) else self.leaf })));
- },
- .join, .discard => {
- const op: []const u8 = if (core.code == .join) "core/join" else "core/discard";
- const ref = try self.token(core.payload, "io.v2.join") orelse return self.operationError(scope, op, error.InvalidArgument);
- if (core.code == .discard) {
- self.scheduler.discard(scope, ref) catch |err| return self.operationError(scope, op, err);
- try self.resumeValue(scope, try self.result(true, self.leaf));
- } else {
- self.scheduler.claimJoin(scope, ref) catch |err| return self.operationError(scope, op, err);
- self.bodies.getPtr(scope).?.action = self.leaf;
- self.bodies.getPtr(scope).?.mode = .joining;
- }
- },
- .cancel => {
- const parts = try self.reader().pair(core.payload) orelse return self.operationError(scope, "core/cancel", error.InvalidArgument);
- const ref = try self.token(parts.first, "io.v2.task") orelse return self.operationError(scope, "core/cancel", error.InvalidArgument);
- self.scheduler.cancel(scope, ref, parts.second) catch |err| return self.operationError(scope, "core/cancel", err);
- try self.resumeValue(scope, try self.result(true, self.leaf));
- },
- },
- }
- }
- fn settleOne(self: *Engine) !?S.Outcome {
- var it = self.scheduler.groups.iterator();
- while (it.next()) |entry| {
- if (entry.value_ptr.phase != .joining) continue;
- const scope = entry.key_ptr.*;
- if (!try self.scheduler.beginRelease(scope)) continue;
- const group = self.scheduler.groups.get(scope).?;
- const cleanup = self.releaseScope(scope);
- const outcome = try self.scheduler.settle(scope, cleanup);
- var removed = self.bodies.fetchRemove(scope).?.value;
- removed.frames.deinit(self.arena.allocator);
- if (scope == 0) return outcome;
- if (group.kind == .lexical) {
- const parent = group.parent.?;
- self.bodies.getPtr(parent).?.mode = .runnable;
- if (outcome.kind == .completed) try self.resumeValue(parent, outcome.value) else try self.done(parent, outcome);
- }
- break; // iterator invalidated by settlement
- }
- return null;
- }
- // Synchronous hosts have no outstanding OS work. Abort settles every logical
- // scope/task without allocating, including a child admitted just before its
- // body/continuation allocation failed. Physical release is allocation-free.
- // Asynchronous hosts require a separate cancellation/draining pump.
- fn abort(self: *Engine) void {
- self.scheduler.stop(0, .{ .kind = .cancelled, .value = self.leaf });
- while (self.scheduler.groups.count() != 0) {
- var progressed = false;
- var bodies = self.scheduler.groups.iterator();
- while (bodies.next()) |entry| {
- if (entry.value_ptr.phase != .open or entry.value_ptr.lexical != null) continue;
- self.scheduler.bodyDone(entry.key_ptr.*, entry.value_ptr.reason.?) catch unreachable;
- if (self.bodies.getPtr(entry.key_ptr.*)) |body| {
- body.mode = .done;
- body.action = self.leaf;
- body.frames.clearRetainingCapacity();
- }
- progressed = true;
- }
- var groups = self.scheduler.groups.iterator();
- while (groups.next()) |entry| {
- const scope = entry.key_ptr.*;
- if (entry.value_ptr.phase != .joining) continue;
- if (!(self.scheduler.beginRelease(scope) catch unreachable)) continue;
- const cleanup = self.releaseScope(scope);
- _ = self.scheduler.settle(scope, cleanup) catch unreachable;
- if (self.bodies.fetchRemove(scope)) |removed| {
- var body = removed.value;
- body.frames.deinit(self.arena.allocator);
- }
- progressed = true;
- break;
- }
- std.debug.assert(progressed); // no host callbacks exist in this harness
- }
- std.debug.assert(self.scheduler.tasks.count() == 0);
- std.debug.assert(self.scheduler.registry.scopes.count() == 0);
- std.debug.assert(self.scheduler.registry.resources.count() == 0);
- }
- fn releaseScope(self: *Engine, scope: u64) S.Diagnostics {
- var cleanup: S.Diagnostics = .{};
- while (self.scheduler.registry.newestOwned(scope)) |ref| {
- const resource = (self.scheduler.registry.claimClose(scope, ref, true) catch unreachable).?;
- if (!resource.flags.borrowed) self.host.close(self.scheduler.registry, resource.backend) catch {
- cleanup.append(S.Diagnostics.one(self.cleanup_failure));
- };
- self.scheduler.registry.finishClose(ref) catch unreachable;
- }
- return cleanup;
- }
- fn appendRoot(arena: *Arena, id: u32) !void {
- try arena.roots.append(arena.allocator, id);
- }
- fn roots(self: *Engine, base: usize) !void {
- self.arena.roots.shrinkRetainingCapacity(base);
- try appendRoot(self.arena, self.leaf);
- try appendRoot(self.arena, self.cleanup_failure);
- try self.scheduler.visitPayloads(self.arena, appendRoot);
- var it = self.bodies.valueIterator();
- while (it.next()) |body| {
- try appendRoot(self.arena, body.action);
- try self.arena.roots.appendSlice(self.arena.allocator, body.frames.items);
- }
- }
- fn nextBody(self: *Engine) ?u64 {
- var first: ?u64 = null;
- var after: ?u64 = null;
- var it = self.bodies.iterator();
- while (it.next()) |entry| {
- if (entry.value_ptr.mode == .done or entry.value_ptr.mode == .lexical) continue;
- const key = entry.key_ptr.*;
- if (first == null or key < first.?) first = key;
- if (key > self.cursor and (after == null or key < after.?)) after = key;
- }
- self.cursor = after orelse first orelse return null;
- return self.cursor;
- }
- };
-}
-
-// Caller retains input/output roots. Fatal reducer/allocator errors drain the
-// synchronous host before propagating; host-side cleanup records must not allocate.
-// Host is deliberately SYNCHRONOUS: request and close must return only after
-// their work settles. No pending callbacks, detached work or roots outside the
-// scheduler are supported by this interface. close must not allocate and must
-// retire its physical resource even on error. Async hosts need a different pump.
-pub const UnsupportedHost = struct {
- pub const Backend = u8;
- pub const standard = [3]u8{ 0, 0, 0 };
- pub fn request(_: *@This(), engine: anytype, _: u64, req: anytype) !u32 {
- return unsupported(engine, req);
- }
- pub fn close(_: *@This(), _: anytype, _: u8) error{}!void {
- unreachable;
- }
-};
-pub fn unsupported(engine: anytype, request: anytype) !u32 {
- var operation: [129]u8 = undefined;
- var detail: [40]u8 = undefined;
- const op = try std.fmt.bufPrint(&operation, "{s}/{s}", .{ request.group.slice(), request.operation.slice() });
- const message = try std.fmt.bufPrint(&detail, "group revision {d}", .{request.revision});
- return engine.result(false, try engine.problem("unsupported", op, message));
-}
-pub fn run(arena: *Arena, action: u32, identity: u64, fuel: u64) !Outcome {
- var host: UnsupportedHost = .{};
- return runWithHost(UnsupportedHost, &host, arena, action, identity, fuel);
-}
-pub fn runWithHost(comptime Host: type, host: *Host, arena: *Arena, action: u32, identity: u64, fuel: u64) !T.Scheduler(Host.Backend, u32).Outcome {
- const S = T.Scheduler(Host.Backend, u32);
- const Engine = EngineType(Host);
- const base = arena.roots.items.len;
- defer arena.roots.shrinkRetainingCapacity(base);
- var registry = try S.Registry.init(arena.allocator, identity, Host.standard);
- defer registry.deinit();
- var scheduler = try S.init(arena.allocator, ®istry, try C.ofString(arena, "owning group stopped"), try C.ofString(arena, "task join cycle"));
- defer scheduler.deinit();
- const cleanup_failure = try C.ofList(arena, &.{ try C.ofString(arena, "cleanup-failed"), try C.ofString(arena, "resource/close"), try C.ofString(arena, "host close failed"), try C.ofString(arena, "unknown") });
- var engine: Engine = .{ .host = host, .cleanup_failure = cleanup_failure, .arena = arena, .scheduler = &scheduler, .leaf = try arena.alloc(.leaf), .fuel = fuel };
- defer engine.deinit();
- errdefer engine.abort();
- try engine.bodies.put(arena.allocator, 0, .{ .action = action });
- while (true) {
- try engine.roots(base);
- if (try engine.settleOne()) |outcome| return outcome;
- // Settlement may construct a resumed continuation. Refresh before demand.
- try engine.roots(base);
- if (engine.nextBody()) |scope| try engine.step(scope);
- }
-}
-
-const testing = std.testing;
-fn pair(a: *Arena, x: u32, y: u32) !u32 {
- return a.alloc(.{ .fork = .{ .left = x, .right = y } });
-}
-fn makeAction(a: *Arena, tag: u64, payload: u32) !u32 {
- return pair(a, try C.ofNumber(a, tag), payload);
-}
-fn constant(a: *Arena, value: u32) !u32 {
- return pair(a, try a.alloc(.leaf), value);
-}
-fn poison(a: *Arena) !u32 {
- const p = try a.alloc(.leaf);
- a.get(p).* = .{ .app = .{ .func = p, .arg = p } };
- return p;
-}
-test "real bind and scope continuations with demanded GC and opaque discarded arguments" {
- var a = Arena.init(testing.allocator);
- defer a.deinit();
- const p = try poison(&a);
- var program = try makeAction(&a, 0, try C.ofNumber(&a, 42));
- for (0..100) |_| {
- program = try makeAction(&a, 1, try pair(&a, try makeAction(&a, 0, p), try constant(&a, program)));
- program = try makeAction(&a, 3, try constant(&a, program));
- }
- a.gc_enabled = true;
- try a.setGcPolicy(1, 1);
- const outcome = try run(&a, program, 1, 1000);
- try testing.expectEqual(.completed, outcome.kind);
- // No reduction until the result has an externally retained root.
- try a.roots.append(a.allocator, outcome.value);
- try testing.expectEqual(@as(?u64, 42), try C.toNumber(&a, outcome.value));
- try testing.expect(a.collections > 0);
- a.roots.clearRetainingCapacity();
- try a.collect(&.{});
- try testing.expectEqual(@as(usize, 0), a.last_live);
-}
-test "terminal diagnostics and cancelled reasons are not demanded" {
- for ([_]u64{ 2, 9 }) |tag| {
- var a = Arena.init(testing.allocator);
- defer a.deinit();
- const p = try poison(&a);
- const outcome = try run(&a, try makeAction(&a, tag, p), 1, 0);
- try testing.expectEqual(p, outcome.value);
- try testing.expectEqual(if (tag == 2) @TypeOf(outcome.kind).failed else .cancelled, outcome.kind);
- try testing.expect(a.get(p).* == .app);
- }
-}
-test "unknown valid facility is an ordinary Result without demanding arguments" {
- var a = Arena.init(testing.allocator);
- defer a.deinit();
- const p = try poison(&a);
- const request = try makeAction(&a, 10, try pair(&a, try C.ofString(&a, "future"), try pair(&a, try C.ofNumber(&a, 9), try pair(&a, try C.ofString(&a, "call"), p))));
- const outcome = try run(&a, request, 1, 0);
- try testing.expectEqual(.completed, outcome.kind);
- const result = a.get(outcome.value).fork;
- try testing.expect(a.get(result.left).* == .leaf);
- const problem = a.get(result.right).fork.left;
- const category = a.get(problem).fork.left;
- const name = (try C.toString(&a, category)).?;
- defer testing.allocator.free(name);
- try testing.expectEqualStrings("unsupported", name);
- try testing.expect(a.get(p).* == .app);
-}
-fn allocationProbe(allocator: std.mem.Allocator) !void {
- var a = Arena.init(allocator);
- defer a.deinit();
- const value = try C.ofNumber(&a, 3);
- var program = try makeAction(&a, 0, value);
- for (0..8) |_| program = try makeAction(&a, 3, try constant(&a, program));
- _ = try run(&a, program, 1, 100);
- try testing.expectEqual(@as(usize, 0), a.roots.items.len);
-}
-test "private core allocator failures unwind all host memory and temporary roots" {
- try testing.checkAllAllocationFailures(testing.allocator, allocationProbe, .{});
-}
-
-fn spawnAllocationProbe(allocator: std.mem.Allocator) !void {
- var a = Arena.init(allocator);
- defer a.deinit();
- const leaf = try a.alloc(.leaf);
- const one = try C.ofNumber(&a, 1);
- const child = try constant(&a, try makeAction(&a, 0, try C.ofNumber(&a, 3)));
- const spawned = try makeAction(&a, 5, try pair(&a, one, try pair(&a, one, try pair(&a, leaf, child))));
- // First child in this independent run owns ID4. Constructing the reference
- // independently also checks the exact source token representation.
- const ticket = try C.ofList(&a, &.{ try C.ofString(&a, "io.v2.join"), one, try C.ofNumber(&a, 4) });
- const program = try makeAction(&a, 1, try pair(&a, spawned, try constant(&a, try makeAction(&a, 6, ticket))));
- a.gc_enabled = true;
- try a.setGcPolicy(1, 1);
- const outcome = try run(&a, program, 1, 100);
- try testing.expectEqual(.completed, outcome.kind);
- try testing.expectEqual(@as(usize, 0), a.roots.items.len);
-}
-test "spawn publication and join reply allocation failures settle every admitted child" {
- try testing.checkAllAllocationFailures(testing.allocator, spawnAllocationProbe, .{});
-}
diff --git a/ext/zig/src/io_v2_core_probe.zig b/ext/zig/src/io_v2_core_probe.zig
@@ -1,114 +0,0 @@
-//! Test-only executable: compiled source against the private core, no native IO
-//! facilities. This is not the tricu-zig CLI or a compiler execution backend.
-const std = @import("std");
-const Arena = @import("arena.zig").Arena;
-const P = @import("io_v2_protocol.zig");
-const C = @import("codecs.zig");
-const Core = @import("io_v2_core.zig");
-const bundle = @import("bundle.zig");
-const reduce = @import("reduce.zig");
-pub fn main(init: std.process.Init) !void {
- return mainFor(Core.UnsupportedHost, init);
-}
-pub fn mainFor(comptime Host: type, init: std.process.Init) !void {
- const args = try init.minimal.args.toSlice(init.arena.allocator());
- if (args.len < 3) return error.ExpectedBundleAndExport;
- const bytes = try std.Io.Dir.cwd().readFileAlloc(init.io, args[1], init.gpa, .limited(10 * 1024 * 1024));
- defer init.gpa.free(bytes);
- var arena = Arena.init(init.gpa);
- defer arena.deinit();
- var root = try bundle.loadBundleExport(&arena, bytes, args[2]);
- for (args[3..]) |argument| {
- const value = if (std.mem.startsWith(u8, argument, "tree-file:")) blk: {
- const raw = try std.Io.Dir.cwd().readFileAlloc(init.io, argument[10..], init.gpa, .limited(10 * 1024 * 1024));
- defer init.gpa.free(raw);
- break :blk try parseRaw(&arena, raw);
- } else if (std.mem.startsWith(u8, argument, "text:")) try @import("source_text.zig").fromUtf8(&arena, argument[5..]) else try C.ofNumber(&arena, try std.fmt.parseUnsigned(u64, argument, 10));
- root = try arena.allocApp(root, value);
- }
- try arena.roots.append(init.gpa, root);
- arena.gc_enabled = true;
- try arena.setGcPolicy(128, 1);
- const r: P.Reader = .{ .arena = &arena, .fuel = std.math.maxInt(u64) };
- const envelope = try r.pair(root) orelse return error.InvalidEnvelope;
- var name = envelope.first;
- for ("tricuIO") |ch| {
- const cell = try r.pair(name) orelse return error.InvalidEnvelope;
- if (try r.nat(cell.first, 7) != @as(?u64, ch)) return error.InvalidEnvelope;
- name = cell.second;
- }
- if (try r.node(name) != .leaf) return error.InvalidEnvelope;
- const body = try r.pair(envelope.second) orelse return error.InvalidEnvelope;
- if (try r.nat(body.first, 16) != @as(?u64, 2)) return error.UnsupportedVersion;
- // Drop the input envelope; private run will root the current computation.
- arena.roots.clearRetainingCapacity();
- var host: Host = .{};
- const outcome = try Core.runWithHost(Host, &host, &arena, body.second, 1, std.math.maxInt(u64));
- if (outcome.kind != .completed) {
- var error_output = std.Io.File.stderr().writer(init.io, &.{});
- try error_output.interface.writeAll(if (outcome.kind == .failed) "failed (payload not forced)\n" else "cancelled (reason not forced)\n");
- try error_output.flush();
- std.process.exit(1);
- }
- try arena.roots.append(init.gpa, outcome.value);
- var stack: std.ArrayList(u32) = .empty;
- defer stack.deinit(init.gpa);
- try stack.append(init.gpa, outcome.value);
- var buffer: [4096]u8 = undefined;
- var output = std.Io.File.stdout().writer(init.io, &buffer);
- var count: usize = 0;
- while (stack.pop()) |id| {
- count += 1;
- if (count > 1048576) return error.ResultLimit;
- const node = arena.get(try reduce.reduce(id, &arena, 1_000_000)).*;
- switch (node) {
- .leaf => try output.interface.writeByte('0'),
- .stem => |s| {
- try output.interface.writeByte('1');
- try stack.append(init.gpa, s.child);
- },
- .fork => |f| {
- try output.interface.writeByte('2');
- try stack.appendSlice(init.gpa, &.{ f.right, f.left });
- },
- .app => unreachable,
- }
- }
- try output.interface.writeByte('\n');
- try output.flush();
-}
-
-// Exact raw constructor tree (0/1/2 bytes), parsed without recursion or ignored
-// trailing roots. Private source controls can pass NUL and malformed scalar data.
-fn parseRaw(arena: *Arena, bytes: []const u8) !u32 {
- var stack: std.ArrayList(u32) = .empty;
- defer stack.deinit(arena.allocator);
- var at = bytes.len;
- while (at != 0) {
- at -= 1;
- const node: @import("tree.zig").Node = switch (bytes[at]) {
- 0 => .leaf,
- 1 => .{ .stem = .{ .child = stack.pop() orelse return error.InvalidRawTree } },
- 2 => .{ .fork = .{ .left = stack.pop() orelse return error.InvalidRawTree, .right = stack.pop() orelse return error.InvalidRawTree } },
- else => return error.InvalidRawTree,
- };
- try stack.append(arena.allocator, try arena.alloc(node));
- }
- if (stack.items.len != 1) return error.InvalidRawTree;
- return stack.items[0];
-}
-
-test "private raw argument parser is exact, iterative and preserves constructor order" {
- const t = std.testing;
- var a = Arena.init(t.allocator);
- defer a.deinit();
- const root = try parseRaw(&a, &.{ 2, 1, 0, 0 });
- const node = a.get(root).fork;
- try t.expect(a.get(node.left).* == .stem and a.get(node.right).* == .leaf);
- for ([_][]const u8{ &.{}, &.{ 0, 0 }, &.{1}, &.{ 2, 0 }, &.{3}, "0" }) |raw| try t.expectError(error.InvalidRawTree, parseRaw(&a, raw));
- const deep = try t.allocator.alloc(u8, 100001);
- defer t.allocator.free(deep);
- @memset(deep, 1);
- deep[deep.len - 1] = 0;
- _ = try parseRaw(&a, deep);
-}
diff --git a/ext/zig/src/io_v2_digest.zig b/ext/zig/src/io_v2_digest.zig
@@ -1,41 +0,0 @@
-//! Scoped, incremental SHA-256 state. No chunk/history retention and no writer or
-//! publication policy. The host registry supplies ownership and exclusive leases.
-const std = @import("std");
-pub const Digest = struct {
- state: std.crypto.hash.sha2.Sha256 = std.crypto.hash.sha2.Sha256.init(.{}),
- finished: bool = false,
- pub fn update(self: *Digest, bytes: []const u8) error{ InvalidBound, InvalidState }!void {
- if (bytes.len == 0 or bytes.len > 4096) return error.InvalidBound;
- if (self.finished) return error.InvalidState;
- self.state.update(bytes);
- }
- pub fn finish(self: *Digest) error{InvalidState}![64]u8 {
- if (self.finished) return error.InvalidState;
- var bytes: [32]u8 = undefined;
- self.state.final(&bytes);
- self.finished = true;
- var hex: [64]u8 = undefined;
- for (bytes, 0..) |byte, i| {
- hex[2 * i] = "0123456789abcdef"[byte >> 4];
- hex[2 * i + 1] = "0123456789abcdef"[byte & 15];
- }
- return hex;
- }
-};
-test "independent SHA-256 vectors, partitioning and terminal state" {
- const t = std.testing;
- var empty: Digest = .{};
- try t.expectEqualStrings("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", &(try empty.finish()));
- try t.expectError(error.InvalidState, empty.finish());
- try t.expectError(error.InvalidState, empty.update("x"));
- var abc: Digest = .{};
- try t.expectError(error.InvalidBound, abc.update(""));
- try t.expectError(error.InvalidBound, abc.update(&([_]u8{'x'} ** 4097)));
- try abc.update("a");
- try abc.update("bc");
- try t.expectEqualStrings("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", &(try abc.finish()));
- var million: Digest = .{};
- const block = [_]u8{'a'} ** 1000;
- for (0..1000) |_| try million.update(&block);
- try t.expectEqualStrings("cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0", &(try million.finish()));
-}
diff --git a/ext/zig/src/io_v2_file_host.zig b/ext/zig/src/io_v2_file_host.zig
@@ -1,864 +0,0 @@
-//! Synchronous native compiler/file profile, shared by the CLI and private
-//! integration probes. Explicit capabilities; no standard streams or broad OS parity.
-const std = @import("std");
-const Protocol = @import("io_v2_protocol.zig");
-const Core = @import("io_v2_core.zig");
-const F = @import("io_v2_files.zig");
-const Paths = @import("io_v2_paths.zig");
-const UTF8 = @import("io_v2_utf8.zig");
-const Stage = @import("io_v2_stage.zig");
-const Digest = @import("io_v2_digest.zig").Digest;
-const Snapshot = @import("io_v2_snapshot.zig");
-const Indexed = @import("indexed.zig");
-const C = @import("codecs.zig");
-const Text = @import("source_text.zig");
-pub const Host = struct {
- // Fixed-size host diagnostics remain inspectable even when a reducer/allocator
- // failure prevents producing a tree Result. No allocation or dangling node IDs.
- algorithm_detail: [512]u8 = undefined,
- algorithm_detail_len: usize = 0,
- utf8_detail: [96]u8 = undefined,
- utf8_detail_len: usize = 0,
- snapshot_cleanup_failed: bool = false,
- stage_operation: bool = false,
- stage_status: Stage.Status = .{},
- published: usize = 0,
- opened: usize = 0,
- closed: usize = 0,
- close_errors: [16]F.Status = undefined,
- close_error_count: usize = 0,
- close_errors_omitted: usize = 0,
- fn recordCloseError(self: *Host, status: F.Status) void {
- if (self.close_error_count < self.close_errors.len) {
- self.close_errors[self.close_error_count] = status;
- self.close_error_count += 1;
- } else self.close_errors_omitted += 1;
- }
- pub const Backend = union(enum) { placeholder, file: *F.File, digest: *Digest, stage: *Stage.Stage };
- pub const standard = [3]Backend{ .placeholder, .placeholder, .placeholder };
- fn fileBackend(file: *F.File) Backend {
- return .{ .file = file };
- }
- pub fn close(self: *Host, registry: anytype, backend: Backend) !void {
- var status: F.Status = .{};
- return self.closeWithStatus(registry, backend, &status);
- }
- fn closeWithStatus(self: *Host, registry: anytype, backend: Backend, status: *F.Status) !void {
- defer self.closed += 1;
- switch (backend) {
- .placeholder => unreachable,
- .digest => |digest| registry.allocator.destroy(digest),
- .file => |file| {
- defer registry.allocator.destroy(file);
- file.close(status) catch |err| {
- self.recordCloseError(status.*);
- return err;
- };
- },
- .stage => |stage| {
- defer registry.allocator.destroy(stage);
- defer stage.deinit();
- var released: Stage.Status = .{};
- stage.release(&released) catch |err| {
- status.* = .{ .code = released.code, .cleanup_code = released.secondary };
- self.recordCloseError(status.*);
- return err;
- };
- },
- }
- }
- const Op = enum { open_reader, open_writer, open_appender, open_update, read, write, close, seek, position, size, resize, sync, stat, lstat, create_directory, remove_directory, remove_file, rename_path, sync_directory, open_digest, update_digest, finish_digest, open_stage, seal_stage, publish_stage, encode_utf8, decode_utf8, hash_indexed, write_indexed, verify_indexed, read_indexed, capabilities, inspect };
- const entries = .{
- .{ "runtime", "capabilities", Op.capabilities }, .{ "runtime", "inspect", Op.inspect },
- .{ "indexed-tree", "read", Op.read_indexed }, .{ "indexed-tree", "hash", Op.hash_indexed },
- .{ "indexed-tree", "write", Op.write_indexed }, .{ "indexed-tree", "verify", Op.verify_indexed },
- .{ "utf8", "encode", Op.encode_utf8 }, .{ "utf8", "decode", Op.decode_utf8 },
- .{ "stage", "open", Op.open_stage }, .{ "stage", "seal", Op.seal_stage },
- .{ "stage", "publish", Op.publish_stage }, .{ "digest", "open", Op.open_digest },
- .{ "digest", "update", Op.update_digest }, .{ "digest", "finish", Op.finish_digest },
- .{ "path", "stat", Op.stat }, .{ "path", "lstat", Op.lstat },
- .{ "path", "remove-file", Op.remove_file }, .{ "path", "rename", Op.rename_path },
- .{ "directory", "create", Op.create_directory }, .{ "directory", "remove", Op.remove_directory },
- .{ "directory", "sync", Op.sync_directory }, .{ "file", "open-reader", Op.open_reader },
- .{ "file", "open-writer", Op.open_writer }, .{ "file", "open-appender", Op.open_appender },
- .{ "file", "open-update", Op.open_update }, .{ "stream", "read", Op.read },
- .{ "stream", "write", Op.write }, .{ "resource", "close", Op.close },
- .{ "file", "seek", Op.seek }, .{ "file", "position", Op.position },
- .{ "file", "size", Op.size }, .{ "file", "resize", Op.resize },
- .{ "file", "sync", Op.sync },
- };
- fn resolve(group: []const u8, operation: []const u8) ?Op {
- inline for (entries) |entry| if (std.mem.eql(u8, group, entry[0]) and std.mem.eql(u8, operation, entry[1])) return entry[2];
- return null;
- }
- pub fn request(self: *Host, engine: anytype, scope: u64, req: anytype) !u32 {
- if (req.revision != 1) return Core.unsupported(engine, req);
- const op = resolve(req.group.slice(), req.operation.slice()) orelse return Core.unsupported(engine, req);
- var name_buffer: [129]u8 = undefined;
- const name = try std.fmt.bufPrint(&name_buffer, "{s}/{s}", .{ req.group.slice(), req.operation.slice() });
- var status: F.Status = .{};
- self.stage_operation = false;
- self.snapshot_cleanup_failed = false;
- self.stage_status = .{};
- const value = self.dispatch(engine, scope, op, req.arguments, &status) catch |err| {
- if (err == error.InvalidAlgorithm) return engine.result(false, try engine.list(&.{
- try engine.text("unsupported"), try engine.text(name),
- try Text.fromUtf8(engine.arena, self.algorithm_detail[0..self.algorithm_detail_len]), try engine.text("none"),
- }));
- if (self.stage_operation) status.code = self.stage_status.code;
- const category = switch (err) {
- error.InvalidArgument, error.EmptySnapshotDirectory => "invalid-argument",
- error.InvalidData, error.SnapshotTruncated, error.SnapshotInvalid => "invalid-data",
- error.SnapshotBound => "invalid-bound",
- error.SnapshotDigestMismatch => "integrity-failed",
- error.SnapshotCleanup => "cleanup-failed",
- error.InvalidState => "invalid-state",
- error.InvalidDestination => "invalid-destination",
- error.InvalidBound, error.EncodedLimit => "invalid-bound",
- error.DigestMismatch, error.FrameMismatch => "integrity-failed",
- error.UnsupportedFileType, error.UnsupportedMode => "unsupported",
- error.WrongAccess => "wrong-kind",
- error.NotDirectory => "not-directory",
- error.Closed, error.ClosedHandle => "closed-handle",
- error.StaleHandle => "stale-handle",
- error.OutOfScope => "out-of-scope",
- error.Busy => "busy",
- error.ClosedScope => "closed-scope",
- error.BorrowedResource => "borrowed-resource",
- error.InternalResource => "internal-resource",
- error.HostFailure => F.errorCategory(status.code),
- else => return err,
- };
- const detail = if (err == error.EmptySnapshotDirectory) "empty snapshot directory" else if (err == error.SnapshotBound) "invalid snapshot size limit" else if (err == error.SnapshotTruncated) "incomplete indexed frame" else if (err == error.SnapshotInvalid) "invalid indexed frame" else if (err == error.SnapshotDigestMismatch) "CAS digest mismatch" else if (err == error.SnapshotCleanup) "snapshot release failed" else if (err == error.EncodedLimit) "encoded tree exceeds limit" else if (err == error.DigestMismatch) "CAS digest mismatch" else if (err == error.FrameMismatch) "indexed frame differs from canonical tree" else if (err == error.InvalidData) self.utf8_detail[0..self.utf8_detail_len] else if (err == error.HostFailure) F.errorText(status.code) else if (err == error.UnsupportedFileType) "path-based files support regular files only" else if (err == error.UnsupportedMode) "operation unavailable in this file mode" else if (err == error.NotDirectory) "path is not a directory" else if (err == error.InvalidDestination) "destination must name a file outside the private staging namespace" else if (err == error.InvalidState) (if (self.stage_operation) "stage is not in the required phase" else "digest already finished") else "";
- // Mutating opens can create/truncate before a later failure. Never
- // claim absence of effects; this is not transactional rollback.
- const progress = if (err == error.SnapshotTruncated or err == error.SnapshotInvalid or err == error.SnapshotDigestMismatch) "unknown" else if (err == error.FrameMismatch) "unknown" else if (self.stage_operation) switch (self.stage_status.progress) {
- .none => "none",
- .not_published => "not-published",
- .unknown => "unknown",
- .published => "published",
- } else if (err == error.HostFailure and op != .open_reader and op != .size and op != .position and op != .stat and op != .lstat) "unknown" else "none";
- var detail_buffer: [1024]u8 = undefined;
- const reported = if (self.snapshot_cleanup_failed) try std.fmt.bufPrint(&detail_buffer, "{s}; cleanup: snapshot release failed", .{detail}) else detail;
- return engine.result(false, try engine.list(&.{ try engine.text(category), try engine.text(name), try engine.text(reported), try engine.text(progress) }));
- };
- return engine.result(true, value);
- }
- fn decodeText(engine: anytype, input: u32) ![]u8 {
- var output: std.ArrayList(u8) = .empty;
- errdefer output.deinit(engine.arena.allocator);
- var rest = input;
- const reader = engine.reader();
- while (try reader.node(rest) != .leaf) {
- const cell = try reader.pair(rest) orelse return error.InvalidArgument;
- const scalar = try reader.nat(cell.first, 21) orelse return error.InvalidArgument;
- if (scalar > 0x10ffff or (scalar >= 0xd800 and scalar <= 0xdfff)) return error.InvalidArgument;
- var encoded: [4]u8 = undefined;
- const count = try std.unicode.utf8Encode(@intCast(scalar), &encoded);
- try output.appendSlice(engine.arena.allocator, encoded[0..count]);
- rest = cell.second;
- }
- return output.toOwnedSlice(engine.arena.allocator);
- }
- fn decodeChunk(engine: anytype, input: u32, bytes: *[4096]u8) !usize {
- var count: usize = 0;
- var rest = input;
- const reader = engine.reader();
- while (try reader.node(rest) != .leaf) {
- // Match the protocol decoder (invalid argument), not the lower-level
- // byte-IO bound check. Observe excess spine, never excess head/tail.
- if (count == bytes.len) return error.InvalidArgument;
- const cell = try reader.pair(rest) orelse return error.InvalidArgument;
- bytes[count] = @intCast(try reader.nat(cell.first, 8) orelse return error.InvalidArgument);
- count += 1;
- rest = cell.second;
- }
- return count;
- }
- fn readIndexed(self: *Host, engine: anytype, scope: u64, arguments: u32, status: *F.Status) !u32 {
- const r = engine.reader();
- var fields: [5]u32 = undefined;
- var rest = arguments;
- for (&fields) |*field| {
- const cell = try r.pair(rest) orelse return error.InvalidArgument;
- field.* = cell.first;
- rest = cell.second;
- }
- if (try r.node(rest) != .leaf) return error.InvalidArgument;
- const token = try engine.token(fields[0], "io.v2.resource") orelse return error.InvalidArgument;
- const directory = try decodeText(engine, fields[1]);
- defer engine.arena.allocator.free(directory);
- const path = try engine.arena.allocator.dupeZ(u8, directory);
- defer engine.arena.allocator.free(path);
- const limit = try r.nat(fields[2], 63) orelse return error.InvalidArgument;
- const size = try r.nat(fields[3], 63) orelse return error.InvalidArgument;
- var digest: [64]u8 = undefined;
- rest = fields[4];
- for (&digest) |*byte| {
- const cell = try r.pair(rest) orelse return error.InvalidArgument;
- const scalar = try r.nat(cell.first, 7) orelse return error.InvalidArgument;
- if (!((scalar >= '0' and scalar <= '9') or (scalar >= 'a' and scalar <= 'f'))) return error.InvalidArgument;
- byte.* = @intCast(scalar);
- rest = cell.second;
- }
- if (try r.node(rest) != .leaf) return error.InvalidArgument;
- if (size > limit) return error.SnapshotBound;
- if (directory.len == 0) return error.EmptySnapshotDirectory;
- const registry = engine.scheduler.registry;
- const backend = (try registry.lookup(scope, token)).backend;
- if (backend != .file or (backend.file.mode != .reader and backend.file.mode != .update)) return error.WrongAccess;
- const lease = try registry.beginLease(scope, token, .exclusive, false);
- defer registry.endLease(lease.token) catch unreachable;
- const temporary = blk: {
- const admission = try registry.beginAcquire(scope);
- errdefer registry.abortAcquire(admission) catch unreachable;
- const file = try registry.allocator.create(F.File);
- errdefer registry.allocator.destroy(file);
- file.* = Snapshot.create(registry.allocator, status, path) catch |err| {
- if (status.cleanup_code != 0) self.recordCloseError(.{ .code = status.cleanup_code });
- return err;
- };
- errdefer {
- var cleanup: F.Status = .{};
- file.close(&cleanup) catch self.recordCloseError(cleanup);
- }
- break :blk try registry.publish(admission, .{ .file = file }, .{ .internal = true });
- };
- self.opened += 1;
- const file = (try registry.lookup(scope, temporary)).backend.file;
- const result = blk: {
- Snapshot.authenticate(file, backend.file, status, size, &digest) catch |err| break :blk @as(anyerror!u32, err);
- var snapshot = Snapshot.Snapshot{ .file = file, .status = status, .length = size };
- break :blk Indexed.loadSnapshot(engine.arena, &snapshot) catch |err| switch (err) {
- error.InvalidData, error.ExportNotFound => error.SnapshotInvalid,
- else => err,
- };
- };
- // All host work is synchronous. Retire the internal descriptor even on
- // ordinary Result failure or OOM; cleanup must not replace a primary fault.
- const owned = (try registry.claimClose(scope, temporary, true)).?;
- defer registry.finishClose(temporary) catch unreachable;
- var cleanup: F.Status = .{};
- self.closeWithStatus(registry, owned.backend, &cleanup) catch {
- if (result) |_| return error.SnapshotCleanup else |primary| {
- self.snapshot_cleanup_failed = true;
- return primary;
- }
- };
- return result;
- }
- fn indexed(self: *Host, engine: anytype, scope: u64, op: Op, arguments: u32, status: *F.Status) !u32 {
- const r = engine.reader();
- const first = try r.pair(arguments) orelse return error.InvalidArgument;
- if (op == .hash_indexed) {
- const limit = try r.nat(first.first, 63) orelse return error.InvalidArgument;
- var plan = try Indexed.prepare(engine.arena, first.second);
- defer plan.deinit();
- if (plan.encodedLength() > limit) return error.EncodedLimit;
- const hash = try plan.hash();
- return engine.list(&.{ try C.ofNumber(engine.arena, plan.encodedLength()), try engine.text(&hash) });
- }
- const token = try engine.token(first.first, "io.v2.resource") orelse return error.InvalidArgument;
- const second = try r.pair(first.second) orelse return error.InvalidArgument;
- const limit = try r.nat(second.first, 63) orelse return error.InvalidArgument;
- const third = try r.pair(second.second) orelse return error.InvalidArgument;
- const tree_field = if (op == .write_indexed) try r.pair(third.second) orelse return error.InvalidArgument else third;
- if (try r.node(tree_field.second) != .leaf) return error.InvalidArgument;
- var expected: [64]u8 = undefined;
- if (op == .write_indexed) {
- var rest = third.first;
- for (&expected) |*byte| {
- const cell = try r.pair(rest) orelse return error.InvalidArgument;
- const scalar = try r.nat(cell.first, 7) orelse return error.InvalidArgument;
- if (!((scalar >= '0' and scalar <= '9') or (scalar >= 'a' and scalar <= 'f'))) return error.InvalidArgument;
- byte.* = @intCast(scalar);
- rest = cell.second;
- }
- if (try r.node(rest) != .leaf) return error.InvalidArgument;
- }
- const registry = engine.scheduler.registry;
- const backend = (try registry.lookup(scope, token)).backend;
- if (op == .verify_indexed) {
- if (backend != .file or (backend.file.mode != .reader and backend.file.mode != .update)) return error.WrongAccess;
- } else switch (backend) {
- .file => |file| if (file.mode == .reader) return error.WrongAccess,
- .stage => {},
- else => return error.WrongAccess,
- }
- const lease = try registry.beginLease(scope, token, .exclusive, false);
- defer registry.endLease(lease.token) catch unreachable;
- var plan = try Indexed.prepare(engine.arena, tree_field.first);
- defer plan.deinit();
- if (plan.encodedLength() > limit) return error.EncodedLimit;
- const hash = try plan.hash();
- if (op == .write_indexed and !std.mem.eql(u8, &hash, &expected)) return error.DigestMismatch;
- var sink = IndexedSink{ .host = self, .backend = backend, .status = status, .verify = op == .verify_indexed };
- try plan.emit(&sink);
- return engine.list(&.{ try C.ofNumber(engine.arena, plan.encodedLength()), try engine.text(&hash) });
- }
- const IndexedSink = struct {
- host: *Host,
- backend: Backend,
- status: *F.Status,
- verify: bool,
- pub fn writeAll(self: *@This(), bytes: []const u8) !void {
- var offset: usize = 0;
- while (offset < bytes.len) {
- const count = if (self.verify) blk: {
- var actual: [4096]u8 = undefined;
- const n = try self.backend.file.read(self.status, actual[0 .. bytes.len - offset]);
- if (n == 0 or !std.mem.eql(u8, actual[0..n], bytes[offset..][0..n])) return error.FrameMismatch;
- break :blk n;
- } else switch (self.backend) {
- .file => |file| try file.write(self.status, bytes[offset..]),
- .stage => |stage| blk: {
- self.host.stage_operation = true;
- break :blk try stage.write(&self.host.stage_status, bytes[offset..]);
- },
- else => unreachable,
- };
- std.debug.assert(count > 0 and count <= bytes.len - offset);
- offset += count;
- }
- }
- };
- const core_names = [_][]const u8{ "pure", "bind", "fail", "scope", "current-scope", "spawn", "join", "discard", "cancel", "cancelled" };
- fn strings(engine: anytype, values: []const []const u8) !u32 {
- var ids: std.ArrayList(u32) = .empty;
- defer ids.deinit(engine.arena.allocator);
- for (values) |value| try ids.append(engine.arena.allocator, try engine.text(value));
- return engine.list(ids.items);
- }
- fn row(engine: anytype, key: []const u8, value: u32) !u32 {
- return engine.list(&.{ try engine.text(key), value });
- }
- fn capabilities(engine: anytype) !u32 {
- var groups: std.ArrayList(u32) = .empty;
- defer groups.deinit(engine.arena.allocator);
- inline for (.{ "runtime", "resource", "stream", "file", "path", "directory", "stage", "digest", "indexed-tree", "utf8" }) |group| {
- var operations: std.ArrayList(u32) = .empty;
- defer operations.deinit(engine.arena.allocator);
- inline for (entries) |entry| {
- if (comptime std.mem.eql(u8, group, entry[0])) try operations.append(engine.arena.allocator, try engine.text(entry[1]));
- }
- try groups.append(engine.arena.allocator, try engine.list(&.{ try engine.text(group), try C.ofNumber(engine.arena, 1), try engine.list(operations.items) }));
- }
- const limits = try engine.list(&.{
- try row(engine, "chunk-bytes", try C.ofNumber(engine.arena, 4096)),
- try row(engine, "transfer-handles", try C.ofNumber(engine.arena, 256)),
- try row(engine, "file-offset-max", try C.ofNumber(engine.arena, F.limit)),
- try row(engine, "indexed-sections", try C.ofNumber(engine.arena, 64)),
- try row(engine, "indexed-manifest-bytes", try C.ofNumber(engine.arena, 65536)),
- try row(engine, "utf8-encode-scalars", try C.ofNumber(engine.arena, 1024)),
- });
- return engine.list(&.{
- try row(engine, "schema", try C.ofNumber(engine.arena, 1)),
- try row(engine, "io-version", try C.ofNumber(engine.arena, 2)),
- try row(engine, "backend", try engine.text("zig-native")),
- try row(engine, "authority", try engine.text("os-process")),
- try row(engine, "core", try strings(engine, &core_names)),
- try row(engine, "groups", try engine.list(groups.items)),
- try row(engine, "limits", limits),
- try row(engine, "digest-algorithms", try strings(engine, &.{"sha256"})),
- try row(engine, "restrictions", try strings(engine, &.{
- "action-boundary-cancellation", "no-hard-deadlines", "synchronous-host-calls",
- "path-files-regular-only", "standard-streams-unsupported", "directory-cursors-unsupported",
- "file-single-cursor-lease", "append-no-position-or-resize", "stage-same-filesystem",
- "stage-no-copy-fallback", "stage-private-namespace", "publish-new-requires-hard-links",
- "no-crash-recovery", "indexed-authenticated-snapshot", "indexed-caller-frame-and-disk-limit",
- "indexed-dag-resident", "indexed-writer-preflight", "indexed-budget-not-heap-quota",
- "sync-host-filesystem-guarantees", "utf8-strict-scalars",
- })),
- });
- }
- fn dispatch(self: *Host, engine: anytype, scope: u64, op: Op, arguments: u32, status: *F.Status) !u32 {
- const r = engine.reader();
- const registry = engine.scheduler.registry;
- if (op == .capabilities) {
- if (try r.node(arguments) != .leaf) return error.InvalidArgument;
- return capabilities(engine);
- }
- if (op == .inspect) {
- const action = Protocol.decode(engine.arena, arguments, r.fuel) catch |err| switch (err) {
- error.InvalidComputationShape, error.UnknownComputationConstructor, error.InvalidFacilityHeader => return error.InvalidArgument,
- else => return err,
- };
- return switch (action) {
- .core => |value| engine.list(&.{ try engine.text("core"), try engine.text(core_names[@intFromEnum(value.code)]) }),
- .request => |value| engine.list(&.{ try engine.text("facility"), try engine.text(value.group.slice()), try C.ofNumber(engine.arena, value.revision), try engine.text(value.operation.slice()), try engine.text(if (value.revision == 1 and resolve(value.group.slice(), value.operation.slice()) != null) "available" else "unsupported") }),
- };
- }
- if (op == .read_indexed) return self.readIndexed(engine, scope, arguments, status);
- if (op == .hash_indexed or op == .write_indexed or op == .verify_indexed) return self.indexed(engine, scope, op, arguments, status);
- if (op == .encode_utf8) {
- var bytes: [4096]u8 = undefined;
- var scalars: usize = 0;
- var used: usize = 0;
- var rest = arguments;
- while (try r.node(rest) != .leaf) {
- if (scalars == 1024) return error.InvalidArgument;
- const cell = try r.pair(rest) orelse return error.InvalidArgument;
- const scalar = try r.nat(cell.first, 21) orelse return error.InvalidArgument;
- if (scalar > 0x10ffff or (scalar >= 0xd800 and scalar <= 0xdfff)) return error.InvalidArgument;
- used += try std.unicode.utf8Encode(@intCast(scalar), bytes[used..][0..4]);
- scalars += 1;
- rest = cell.second;
- }
- return C.ofBytes(engine.arena, bytes[0..used]);
- }
- if (op == .decode_utf8) {
- const final = try r.pair(arguments) orelse return error.InvalidArgument;
- const input = try r.pair(final.second) orelse return error.InvalidArgument;
- if (try r.node(input.second) != .leaf) return error.InvalidArgument;
- const ended = try r.nat(final.first, 1) orelse return error.InvalidArgument;
- var bytes: [4096]u8 = undefined;
- const count = try decodeChunk(engine, input.first, &bytes);
- return switch (UTF8.scan(bytes[0..count], ended == 1)) {
- .too_large => unreachable, // argument decoder already bounds it
- .fault => |fault| blk: {
- const why: []const u8 = switch (fault.kind) {
- .leading => "invalid UTF-8 leading byte",
- .sequence => "invalid UTF-8 sequence",
- .incomplete => "incomplete UTF-8 sequence",
- };
- const detail = try std.fmt.bufPrint(&self.utf8_detail, "{s} at byte {d}", .{ why, fault.offset });
- self.utf8_detail_len = detail.len;
- break :blk error.InvalidData;
- },
- .prefix => |prefix| try engine.list(&.{ try Text.fromUtf8(engine.arena, bytes[0..prefix]), try C.ofBytes(engine.arena, bytes[prefix..count]) }),
- };
- }
- switch (op) {
- .stat, .lstat, .create_directory, .remove_directory, .remove_file, .rename_path, .sync_directory => {
- const paths = if (op == .rename_path) try r.pair(arguments) orelse return error.InvalidArgument else null;
- const raw = try decodeText(engine, if (paths) |p| p.first else arguments);
- defer engine.arena.allocator.free(raw);
- const path = try engine.arena.allocator.dupeZ(u8, raw);
- defer engine.arena.allocator.free(path);
- if (op == .stat or op == .lstat) {
- const info = try Paths.stat(status, path, op == .stat);
- return switch (info) {
- .regular => |size| try engine.list(&.{ try engine.text("file"), try C.ofNumber(engine.arena, size) }),
- .directory => try engine.list(&.{try engine.text("directory")}),
- .symlink => try engine.list(&.{try engine.text("symlink")}),
- .other => try engine.list(&.{try engine.text("other")}),
- };
- }
- if (op == .sync_directory) {
- Paths.syncDirectory(status, path) catch |err| {
- if (status.cleanup_code != 0) self.recordCloseError(.{ .code = status.cleanup_code });
- return err;
- };
- return engine.leaf;
- }
- if (op == .rename_path) {
- const other_raw = try decodeText(engine, paths.?.second);
- defer engine.arena.allocator.free(other_raw);
- const other = try engine.arena.allocator.dupeZ(u8, other_raw);
- defer engine.arena.allocator.free(other);
- try Paths.mutate(status, .rename, path, other);
- } else {
- try Paths.mutate(status, switch (op) {
- .create_directory => .create_directory,
- .remove_directory => .remove_directory,
- .remove_file => .remove_file,
- else => unreachable,
- }, path, null);
- }
- return engine.leaf;
- },
- else => {},
- }
- switch (op) {
- .open_reader, .open_writer, .open_appender, .open_update, .open_digest, .open_stage => {
- const pair = try r.pair(arguments) orelse return error.InvalidArgument;
- const owner = try engine.token(pair.first, "io.v2.scope") orelse return error.InvalidArgument;
- if (owner.run != registry.identity or !registry.within(scope, owner.key)) return error.InvalidArgument;
- const text = try decodeText(engine, pair.second);
- defer engine.arena.allocator.free(text);
- const path = try engine.arena.allocator.dupeZ(u8, text);
- defer engine.arena.allocator.free(path);
- if (op == .open_digest) {
- if (!std.mem.eql(u8, text, "sha256")) {
- const view = try std.unicode.Utf8View.init(text);
- var it = view.iterator();
- var scalars: usize = 0;
- while (scalars < 128 and it.nextCodepoint() != null) : (scalars += 1) {}
- self.algorithm_detail_len = it.i;
- @memcpy(self.algorithm_detail[0..it.i], text[0..it.i]);
- return error.InvalidAlgorithm;
- }
- // End acquisition rollback BEFORE encoding the source token.
- // Once published, only scope teardown owns this state, even
- // when reply allocation fails immediately afterwards.
- const token = acquired: {
- const admission = try registry.beginAcquire(owner.key);
- errdefer registry.abortAcquire(admission) catch unreachable;
- const digest = try registry.allocator.create(Digest);
- errdefer registry.allocator.destroy(digest);
- digest.* = .{};
- break :acquired try registry.publish(admission, .{ .digest = digest }, .{});
- };
- self.opened += 1;
- return engine.marker("io.v2.resource", token.key);
- }
- if (op == .open_stage) {
- self.stage_operation = true;
- const token = acquired: {
- const admission = try registry.beginAcquire(owner.key);
- errdefer registry.abortAcquire(admission) catch unreachable;
- const stage = try registry.allocator.create(Stage.Stage);
- errdefer registry.allocator.destroy(stage);
- stage.* = try Stage.Stage.init(registry.allocator, &self.stage_status, path);
- errdefer {
- var released: Stage.Status = .{};
- stage.release(&released) catch self.recordCloseError(.{ .code = released.code, .cleanup_code = released.secondary });
- stage.deinit();
- }
- break :acquired try registry.publish(admission, .{ .stage = stage }, .{});
- };
- self.opened += 1;
- return engine.marker("io.v2.resource", token.key);
- }
- const mode: F.Mode = switch (op) {
- .open_reader => .reader,
- .open_writer => .writer,
- .open_appender => .appender,
- .open_update => .update,
- else => unreachable,
- };
- const token = F.acquireWrapped(registry, owner.key, status, path, mode, fileBackend) catch |err| {
- if (status.cleanup_code != 0) self.recordCloseError(.{ .code = status.cleanup_code });
- return err;
- };
- self.opened += 1;
- return engine.marker("io.v2.resource", token.key);
- },
- else => {},
- }
- const paired = op == .read or op == .write or op == .update_digest or op == .seek or op == .resize or op == .publish_stage;
- const pair = if (paired) try r.pair(arguments) orelse return error.InvalidArgument else null;
- const token = try engine.token(if (pair) |p| p.first else arguments, "io.v2.resource") orelse return error.InvalidArgument;
- var number: u64 = 0;
- var bytes: [4096]u8 = undefined;
- var count: usize = 0;
- switch (op) {
- .read, .seek, .resize => {
- number = try r.nat(pair.?.second, if (op == .read) 16 else 63) orelse return error.InvalidArgument;
- if (op == .read and (number == 0 or number > 4096)) return error.InvalidBound;
- },
- .write, .update_digest => {
- count = try decodeChunk(engine, pair.?.second, &bytes);
- if (count == 0) return error.InvalidBound;
- },
- else => {},
- }
- var destination: ?[:0]u8 = null;
- defer if (destination) |path| engine.arena.allocator.free(path);
- if (op == .publish_stage) {
- const fields = try r.pair(pair.?.second) orelse return error.InvalidArgument;
- number = try r.nat(fields.first, 1) orelse return error.InvalidArgument;
- const raw = try decodeText(engine, fields.second);
- defer engine.arena.allocator.free(raw);
- destination = try engine.arena.allocator.dupeZ(u8, raw);
- }
- if (op == .close) {
- const resource = try registry.claimClose(scope, token, false) orelse return error.Busy;
- defer registry.finishClose(token) catch unreachable;
- try self.closeWithStatus(registry, resource.backend, status);
- return engine.leaf;
- }
- const backend = (try registry.lookup(scope, token)).backend;
- if (op == .seal_stage or op == .publish_stage or (op == .write and backend == .stage)) {
- if (backend != .stage) return error.WrongAccess;
- const lease = try registry.beginLease(scope, token, .exclusive, false);
- defer registry.endLease(lease.token) catch unreachable;
- self.stage_operation = true;
- if (op == .write) return C.ofNumber(engine.arena, try backend.stage.write(&self.stage_status, bytes[0..count]));
- if (op == .seal_stage) {
- try backend.stage.seal(&self.stage_status);
- return engine.leaf;
- }
- const published = try backend.stage.publish(&self.stage_status, number == 1, destination.?);
- if (published == .published) self.published += 1;
- return engine.text(if (published == .published) "published" else "exists");
- }
- if (op == .update_digest or op == .finish_digest) {
- if (backend != .digest) return error.WrongAccess;
- const lease = try registry.beginLease(scope, token, .exclusive, false);
- defer registry.endLease(lease.token) catch unreachable;
- if (op == .update_digest) {
- try backend.digest.update(bytes[0..count]);
- return engine.leaf;
- }
- const hex = try backend.digest.finish();
- return engine.text(&hex);
- }
- if (backend != .file) return error.WrongAccess;
- const file = backend.file;
- const lease = try registry.beginLease(scope, token, .exclusive, false);
- defer registry.endLease(lease.token) catch unreachable;
- return switch (op) {
- .read => blk: {
- const n = try file.read(status, bytes[0..@intCast(number)]);
- const label = try engine.text(if (n == 0) "eof" else "data");
- break :blk try engine.arena.alloc(.{ .fork = .{ .left = label, .right = if (n == 0) engine.leaf else try C.ofBytes(engine.arena, bytes[0..n]) } });
- },
- .write => try C.ofNumber(engine.arena, try file.write(status, bytes[0..count])),
- .seek => blk: {
- try file.seek(number);
- break :blk engine.leaf;
- },
- .resize => blk: {
- try file.resize(status, number);
- break :blk engine.leaf;
- },
- .position => try C.ofNumber(engine.arena, try file.position()),
- .size => try C.ofNumber(engine.arena, try file.size(status)),
- .sync => blk: {
- try file.sync(status);
- break :blk engine.leaf;
- },
- else => unreachable,
- };
- }
-};
-
-fn testProgram(a: *@import("arena.zig").Arena, path: []const u8, terminal: ?u64) !u32 {
- return testOpenProgram(a, path, terminal, "file", "open-reader");
-}
-fn testOpenProgram(a: *@import("arena.zig").Arena, path: []const u8, terminal: ?u64, group: []const u8, operation: []const u8) !u32 {
- const leaf = try a.alloc(.leaf);
- const scope = try C.ofList(a, &.{ try C.ofString(a, "io.v2.scope"), try C.ofNumber(a, 1), leaf });
- const args = try a.alloc(.{ .fork = .{ .left = scope, .right = try Text.fromUtf8(a, path) } });
- const named = try a.alloc(.{ .fork = .{ .left = try C.ofString(a, group), .right = try a.alloc(.{ .fork = .{ .left = try C.ofNumber(a, 1), .right = try a.alloc(.{ .fork = .{ .left = try C.ofString(a, operation), .right = args } }) } }) } });
- const request = try a.alloc(.{ .fork = .{ .left = try C.ofNumber(a, 10), .right = named } });
- if (terminal) |tag| {
- const next = try a.alloc(.{ .fork = .{ .left = try C.ofNumber(a, tag), .right = leaf } });
- const constant = try a.alloc(.{ .fork = .{ .left = leaf, .right = next } });
- return a.alloc(.{ .fork = .{ .left = try C.ofNumber(a, 1), .right = try a.alloc(.{ .fork = .{ .left = request, .right = constant } }) } });
- }
- return request;
-}
-fn failureProbe(allocator: std.mem.Allocator, path: []const u8) !void {
- var a = @import("arena.zig").Arena.init(allocator);
- defer a.deinit();
- const program = try testProgram(&a, path, null);
- a.gc_enabled = true;
- try a.setGcPolicy(1, 1);
- var host: Host = .{};
- defer std.debug.assert(host.opened == host.closed);
- _ = try Core.runWithHost(Host, &host, &a, program, 1, 10000);
- try std.testing.expectEqual(@as(usize, 1), host.opened);
-}
-test "physical resources settle on success, fail, cancellation, protocol fault and allocation failure" {
- const testing = std.testing;
- var tmp = testing.tmpDir(.{});
- defer tmp.cleanup();
- try tmp.dir.writeFile(testing.io, .{ .sub_path = "file", .data = "abc" });
- const path = try tmp.dir.realPathFileAlloc(testing.io, "file", testing.allocator);
- defer testing.allocator.free(path);
- for ([_]u64{ 0, 2, 9, 11 }) |tag| {
- var a = @import("arena.zig").Arena.init(testing.allocator);
- defer a.deinit();
- const program = try testProgram(&a, path, tag);
- var host: Host = .{};
- const outcome = try Core.runWithHost(Host, &host, &a, program, 1, 10000);
- try testing.expectEqual(@as(usize, 1), host.opened);
- try testing.expectEqual(host.opened, host.closed);
- try testing.expectEqual(@as(usize, 0), host.close_error_count);
- const expected: @TypeOf(outcome.kind) = if (tag == 0) .completed else if (tag == 9) .cancelled else .failed;
- try testing.expectEqual(expected, outcome.kind);
- }
- try testing.checkAllAllocationFailures(testing.allocator, failureProbe, .{path});
-}
-
-const CloseFailureHost = struct {
- pub const Backend = Host.Backend;
- pub const standard = Host.standard;
- inner: Host = .{},
- pub fn request(self: *@This(), engine: anytype, scope: u64, req: anytype) !u32 {
- return self.inner.request(engine, scope, req);
- }
- pub fn close(self: *@This(), registry: anytype, backend: Backend) !void {
- // Simulate a close that has released its descriptor but reports failure.
- // No external FD is closed/reused, and every real descriptor is released.
- var original = backend.file.*;
- var status: F.Status = .{};
- try original.close(&status);
- backend.file.fd = -1;
- try self.inner.close(registry, backend);
- }
-};
-test "physical close failures defeat success but retain primary failure and cancellation" {
- const testing = std.testing;
- var tmp = testing.tmpDir(.{});
- defer tmp.cleanup();
- try tmp.dir.writeFile(testing.io, .{ .sub_path = "file", .data = "abc" });
- const path = try tmp.dir.realPathFileAlloc(testing.io, "file", testing.allocator);
- defer testing.allocator.free(path);
- for ([_]u64{ 0, 2, 9 }) |tag| {
- var a = @import("arena.zig").Arena.init(testing.allocator);
- defer a.deinit();
- const program = try testProgram(&a, path, tag);
- var host: CloseFailureHost = .{};
- const outcome = try Core.runWithHost(CloseFailureHost, &host, &a, program, 1, 10000);
- try testing.expectEqual(@as(usize, 1), host.inner.closed);
- try testing.expectEqual(@as(usize, 1), host.inner.close_error_count);
- const expected: @TypeOf(outcome.kind) = if (tag == 9) .cancelled else .failed;
- try testing.expectEqual(expected, outcome.kind);
- if (tag != 0) {
- try testing.expect(a.get(outcome.value).* == .leaf);
- try testing.expectEqual(@as(usize, 1), outcome.cleanup.len);
- }
- }
-}
-
-fn digestAllocationProbe(allocator: std.mem.Allocator) !void {
- var a = @import("arena.zig").Arena.init(allocator);
- defer a.deinit();
- const program = try testOpenProgram(&a, "sha256", null, "digest", "open");
- a.gc_enabled = true;
- try a.setGcPolicy(1, 1);
- var host: Host = .{};
- defer std.debug.assert(host.opened == host.closed);
- _ = try Core.runWithHost(Host, &host, &a, program, 1, 10000);
- try std.testing.expectEqual(@as(usize, 1), host.opened);
-}
-test "digest acquisition and reply allocation failures retire every admitted state" {
- try std.testing.checkAllAllocationFailures(std.testing.allocator, digestAllocationProbe, .{});
-}
-
-fn stageAllocationProbe(allocator: std.mem.Allocator, directory: []const u8, dir: std.Io.Dir) !void {
- var a = @import("arena.zig").Arena.init(allocator);
- defer a.deinit();
- const program = try testOpenProgram(&a, directory, null, "stage", "open");
- a.gc_enabled = true;
- try a.setGcPolicy(1, 1);
- var host: Host = .{};
- defer std.debug.assert(host.opened == host.closed);
- const outcome = Core.runWithHost(Host, &host, &a, program, 1, 10000);
- var iterator = dir.iterate();
- try std.testing.expect((try iterator.next(std.testing.io)) == null);
- _ = try outcome;
- try std.testing.expectEqual(@as(usize, 1), host.opened);
-}
-test "stage acquisition and reply allocation failures remove every private name" {
- const testing = std.testing;
- var tmp = testing.tmpDir(.{ .iterate = true });
- defer tmp.cleanup();
- const directory = try tmp.dir.realPathFileAlloc(testing.io, ".", testing.allocator);
- defer testing.allocator.free(directory);
- try testing.checkAllAllocationFailures(testing.allocator, stageAllocationProbe, .{ directory, tmp.dir });
-}
-
-fn testRequest(a: *@import("arena.zig").Arena, group: []const u8, operation: []const u8, args: u32) !u32 {
- const payload = try a.alloc(.{ .fork = .{ .left = try C.ofString(a, group), .right = try a.alloc(.{ .fork = .{ .left = try C.ofNumber(a, 1), .right = try a.alloc(.{ .fork = .{ .left = try C.ofString(a, operation), .right = args } }) } }) } });
- return a.alloc(.{ .fork = .{ .left = try C.ofNumber(a, 10), .right = payload } });
-}
-fn then(a: *@import("arena.zig").Arena, left: u32, right: u32) !u32 {
- const constant = try a.alloc(.{ .fork = .{ .left = try a.alloc(.leaf), .right = right } });
- return a.alloc(.{ .fork = .{ .left = try C.ofNumber(a, 1), .right = try a.alloc(.{ .fork = .{ .left = left, .right = constant } }) } });
-}
-fn publicationAllocationProbe(allocator: std.mem.Allocator, directory: []const u8, destination: []const u8, dir: std.Io.Dir) !void {
- var a = @import("arena.zig").Arena.init(allocator);
- defer a.deinit();
- const opened = try testOpenProgram(&a, directory, null, "stage", "open");
- const token = try C.ofList(&a, &.{ try C.ofString(&a, "io.v2.resource"), try C.ofNumber(&a, 1), try C.ofNumber(&a, 5) });
- const sealed = try testRequest(&a, "stage", "seal", token);
- const args = try a.alloc(.{ .fork = .{ .left = token, .right = try a.alloc(.{ .fork = .{ .left = try C.ofNumber(&a, 1), .right = try Text.fromUtf8(&a, destination) } }) } });
- const publish = try testRequest(&a, "stage", "publish", args);
- const program = try then(&a, opened, try then(&a, sealed, publish));
- a.gc_enabled = true;
- try a.setGcPolicy(1, 1);
- var host: Host = .{};
- const result = Core.runWithHost(Host, &host, &a, program, 1, 10000);
- try std.testing.expectEqual(host.opened, host.closed);
- var iterator = dir.iterate();
- var visible: usize = 0;
- while (try iterator.next(std.testing.io)) |entry| {
- try std.testing.expectEqualStrings("out", entry.name); // never a private name
- visible += 1;
- }
- try std.testing.expectEqual(host.published, visible); // OOM cannot roll back visibility
- if (visible != 0) try dir.deleteFile(std.testing.io, "out");
- _ = try result;
- try std.testing.expectEqual(@as(usize, 1), host.published);
-}
-test "allocation failures before and after publication clean private names without rollback" {
- const testing = std.testing;
- var tmp = testing.tmpDir(.{ .iterate = true });
- defer tmp.cleanup();
- const directory = try tmp.dir.realPathFileAlloc(testing.io, ".", testing.allocator);
- defer testing.allocator.free(directory);
- const destination = try std.fmt.allocPrint(testing.allocator, "{s}/out", .{directory});
- defer testing.allocator.free(destination);
- try testing.checkAllAllocationFailures(testing.allocator, publicationAllocationProbe, .{ directory, destination, tmp.dir });
-}
-
-fn indexedWriteAllocationProbe(allocator: std.mem.Allocator, path: []const u8, dir: std.Io.Dir) !void {
- defer dir.deleteFile(std.testing.io, "out") catch {};
- var a = @import("arena.zig").Arena.init(allocator);
- defer a.deinit();
- const opened = try testOpenProgram(&a, path, null, "file", "open-writer");
- const token = try C.ofList(&a, &.{ try C.ofString(&a, "io.v2.resource"), try C.ofNumber(&a, 1), try C.ofNumber(&a, 5) });
- const args = try C.ofList(&a, &.{ token, try C.ofNumber(&a, 431), try C.ofString(&a, "6f93378feaa8bf5ab7cefc071175a654c871bfd53f165d7d96795fb2b38483f1"), try a.alloc(.leaf) });
- const program = try then(&a, opened, try testRequest(&a, "indexed-tree", "write", args));
- a.gc_enabled = true;
- try a.setGcPolicy(1, 1);
- var host: Host = .{};
- const outcome = Core.runWithHost(Host, &host, &a, program, 1, 10000);
- try std.testing.expectEqual(host.opened, host.closed);
- if (host.opened != 0) {
- const bytes = try dir.readFileAlloc(std.testing.io, "out", std.testing.allocator, .limited(1024));
- defer std.testing.allocator.free(bytes);
- // Preflight never writes; a failure encoding the reply cannot undo the
- // completed write. There are no allocations inside native emission.
- try std.testing.expect(bytes.len == 0 or bytes.len == 431);
- if (bytes.len != 0) try std.testing.expectEqualStrings("6f93378feaa8bf5ab7cefc071175a654c871bfd53f165d7d96795fb2b38483f1", &Indexed.digest(bytes));
- if (outcome) |_| try std.testing.expectEqual(@as(usize, 431), bytes.len) else |_| {}
- }
- _ = try outcome;
- try std.testing.expectEqual(@as(usize, 1), host.opened);
-}
-test "indexed writer preflight and reply OOM settle leases without rolling back writes" {
- var tmp = std.testing.tmpDir(.{});
- defer tmp.cleanup();
- const directory = try tmp.dir.realPathFileAlloc(std.testing.io, ".", std.testing.allocator);
- defer std.testing.allocator.free(directory);
- const path = try std.fmt.allocPrint(std.testing.allocator, "{s}/out", .{directory});
- defer std.testing.allocator.free(path);
- try std.testing.checkAllAllocationFailures(std.testing.allocator, indexedWriteAllocationProbe, .{ path, tmp.dir });
-}
-
-fn snapshotAllocationProbe(allocator: std.mem.Allocator, path: []const u8, directory: []const u8, dir: std.Io.Dir) !void {
- var a = @import("arena.zig").Arena.init(allocator);
- defer a.deinit();
- const opened = try testOpenProgram(&a, path, null, "file", "open-reader");
- const token = try C.ofList(&a, &.{ try C.ofString(&a, "io.v2.resource"), try C.ofNumber(&a, 1), try C.ofNumber(&a, 5) });
- const args = try C.ofList(&a, &.{ token, try Text.fromUtf8(&a, directory), try C.ofNumber(&a, 431), try C.ofNumber(&a, 431), try C.ofString(&a, "6f93378feaa8bf5ab7cefc071175a654c871bfd53f165d7d96795fb2b38483f1") });
- const program = try then(&a, opened, try testRequest(&a, "indexed-tree", "read", args));
- a.gc_enabled = true;
- try a.setGcPolicy(1, 1);
- var host: Host = .{};
- const outcome = Core.runWithHost(Host, &host, &a, program, 1, 10000);
- try std.testing.expectEqual(host.opened, host.closed);
- var iterator = dir.iterate();
- while (try iterator.next(std.testing.io)) |entry| try std.testing.expectEqualStrings("in", entry.name);
- _ = try outcome;
- try std.testing.expectEqual(@as(usize, 2), host.opened);
-}
-test "snapshot acquisition decode and reply OOM close every file without private names" {
- var tmp = std.testing.tmpDir(.{ .iterate = true });
- defer tmp.cleanup();
- const directory = try tmp.dir.realPathFileAlloc(std.testing.io, ".", std.testing.allocator);
- defer std.testing.allocator.free(directory);
- const path = try std.fmt.allocPrint(std.testing.allocator, "{s}/in", .{directory});
- defer std.testing.allocator.free(path);
- var arena = @import("arena.zig").Arena.init(std.testing.allocator);
- defer arena.deinit();
- const bytes = try Indexed.encode(&arena, try arena.alloc(.leaf));
- defer std.testing.allocator.free(bytes);
- try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "in", .data = bytes });
- try std.testing.checkAllAllocationFailures(std.testing.allocator, snapshotAllocationProbe, .{ path, directory, tmp.dir });
-}
diff --git a/ext/zig/src/io_v2_file_probe.zig b/ext/zig/src/io_v2_file_probe.zig
@@ -1,4 +0,0 @@
-//! Private source integration tool. Does not enable the production IO entrypoints.
-pub fn main(init: @import("std").process.Init) !void {
- return @import("io_v2_core_probe.zig").mainFor(@import("io_v2_file_host.zig").Host, init);
-}
diff --git a/ext/zig/src/io_v2_files.zig b/ext/zig/src/io_v2_files.zig
@@ -1,397 +0,0 @@
-//! Private native regular-file backend, not a v2 facility dispatcher. Single
-//! owner; the embedding supplies scope admission and exclusive registry leases.
-//! These synchronous bounded calls make no cancellation/deadline guarantee.
-//! Never use this for standard streams, FIFOs or devices. No v1 policy/dispatch.
-const std = @import("std");
-const c = @cImport({
- @cUndef("_FORTIFY_SOURCE");
- @cDefine("_FORTIFY_SOURCE", "0");
- @cInclude("uv.h");
- @cInclude("sys/stat.h");
- @cInclude("fcntl.h");
-});
-pub const limit = std.math.maxInt(i64);
-pub const chunk_limit = 4096;
-pub const Mode = enum { reader, writer, appender, update };
-pub const Error = error{ InvalidArgument, InvalidBound, UnsupportedFileType, WrongAccess, UnsupportedMode, Closed, HostFailure };
-// Error codes are copied before request cleanup. A failed acquisition retains its
-// primary error AND any close error; cleanup does not replace the primary fault.
-pub const Status = struct { code: c_int = 0, cleanup_code: c_int = 0 };
-pub const File = struct {
- fd: ?c.uv_file,
- mode: Mode,
- cursor: u64 = 0,
- fn descriptor(self: *const File) Error!c.uv_file {
- return self.fd orelse error.Closed;
- }
- pub fn close(self: *File, status: *Status) Error!void {
- status.* = .{};
- const fd = try self.descriptor();
- self.fd = null; // Never retry an uncertain close against a reused FD.
- var req: c.uv_fs_t = undefined;
- const rc = c.uv_fs_close(null, &req, fd, null);
- c.uv_fs_req_cleanup(&req);
- try check(status, rc);
- }
- pub fn read(self: *File, status: *Status, bytes: []u8) Error!usize {
- status.* = .{};
- if (bytes.len == 0 or bytes.len > chunk_limit) return error.InvalidBound;
- const fd = try self.descriptor();
- if (self.mode != .reader and self.mode != .update) return error.WrongAccess;
- const count = @min(bytes.len, limit - self.cursor);
- if (count == 0) {
- _ = try self.size(status);
- return 0;
- }
- var buffer = c.uv_buf_init(bytes.ptr, @intCast(count));
- var req: c.uv_fs_t = undefined;
- const rc = c.uv_fs_read(null, &req, fd, &buffer, 1, @intCast(self.cursor), null);
- c.uv_fs_req_cleanup(&req);
- try check(status, rc);
- self.cursor += @intCast(rc);
- return @intCast(rc);
- }
- pub fn write(self: *File, status: *Status, bytes: []const u8) Error!usize {
- status.* = .{};
- if (bytes.len == 0 or bytes.len > chunk_limit) return error.InvalidBound;
- const fd = try self.descriptor();
- if (self.mode == .reader) return error.WrongAccess;
- if (self.mode != .appender and bytes.len > limit - self.cursor) return error.InvalidBound;
- var buffer = c.uv_buf_init(@constCast(bytes.ptr), @intCast(bytes.len));
- var req: c.uv_fs_t = undefined;
- const offset: i64 = if (self.mode == .appender) -1 else @intCast(self.cursor);
- const rc = c.uv_fs_write(null, &req, fd, &buffer, 1, offset, null);
- c.uv_fs_req_cleanup(&req);
- try check(status, rc);
- if (rc == 0) {
- status.code = c.UV_EIO;
- return error.HostFailure;
- }
- if (self.mode != .appender) self.cursor += @intCast(rc);
- return @intCast(rc);
- }
- pub fn seek(self: *File, offset: u64) Error!void {
- if (offset > limit) return error.InvalidBound;
- _ = try self.descriptor();
- if (self.mode == .appender) return error.UnsupportedMode;
- self.cursor = offset;
- }
- pub fn position(self: *const File) Error!u64 {
- _ = try self.descriptor();
- if (self.mode == .appender) return error.UnsupportedMode;
- return self.cursor;
- }
- pub fn size(self: *const File, status: *Status) Error!u64 {
- status.* = .{};
- var req: c.uv_fs_t = undefined;
- const rc = c.uv_fs_fstat(null, &req, try self.descriptor(), null);
- defer c.uv_fs_req_cleanup(&req);
- try check(status, rc);
- if (req.statbuf.st_size > limit) return error.InvalidBound;
- return req.statbuf.st_size;
- }
- pub fn resize(self: *File, status: *Status, length: u64) Error!void {
- status.* = .{};
- if (length > limit) return error.InvalidBound;
- const fd = try self.descriptor();
- if (self.mode != .writer and self.mode != .update) return error.UnsupportedMode;
- var req: c.uv_fs_t = undefined;
- const rc = c.uv_fs_ftruncate(null, &req, fd, @intCast(length), null);
- c.uv_fs_req_cleanup(&req);
- try check(status, rc); // Explicit-offset cursor survives resize, even past EOF.
- }
- pub fn sync(self: *const File, status: *Status) Error!void {
- status.* = .{};
- var req: c.uv_fs_t = undefined;
- const rc = c.uv_fs_fsync(null, &req, try self.descriptor(), null);
- c.uv_fs_req_cleanup(&req);
- try check(status, rc);
- }
-};
-fn check(status: *Status, rc: c_int) Error!void {
- if (rc < 0) {
- status.code = rc;
- return error.HostFailure;
- }
-}
-fn regular(mode: u64) bool {
- return mode & c.S_IFMT == c.S_IFREG;
-}
-pub fn open(status: *Status, path: [:0]const u8, mode: Mode) Error!File {
- return openAfterCheck(status, path, mode, null);
-}
-const CheckHook = struct { context: *anyopaque, call: *const fn (*anyopaque) void };
-// Private deterministic namespace-race seam; never a source callback.
-fn openAfterCheck(status: *Status, path: [:0]const u8, mode: Mode, hook: ?CheckHook) Error!File {
- status.* = .{};
- if (path.len == 0 or std.mem.indexOfScalar(u8, path, 0) != null or !std.unicode.utf8ValidateSlice(path)) return error.InvalidArgument;
- // Preflight avoids opening known devices. It is NOT a security check: the
- // namespace can change. The descriptor check below is authoritative, and
- // deliberately precedes truncation. Opening a raced device may have effects.
- var req: c.uv_fs_t = undefined;
- const stat_rc = c.uv_fs_stat(null, &req, path.ptr, null);
- const is_regular = stat_rc >= 0 and regular(req.statbuf.st_mode);
- c.uv_fs_req_cleanup(&req);
- if (stat_rc >= 0 and !is_regular) return error.UnsupportedFileType;
- if (stat_rc < 0 and (stat_rc != c.UV_ENOENT or mode == .reader)) try check(status, stat_rc);
- if (hook) |h| h.call(h.context);
- const flags: c_int = c.O_CLOEXEC | c.O_NONBLOCK | c.O_NOCTTY | switch (mode) {
- .reader => c.O_RDONLY,
- .writer => c.O_WRONLY | c.O_CREAT, // NO O_TRUNC until fstat accepts the FD.
- .appender => c.O_WRONLY | c.O_CREAT | c.O_APPEND,
- .update => c.O_RDWR | c.O_CREAT,
- };
- const fd = c.uv_fs_open(null, &req, path.ptr, flags, 0o666, null);
- c.uv_fs_req_cleanup(&req);
- try check(status, fd);
- var file: File = .{ .fd = fd, .mode = mode };
- errdefer {
- var cleanup: Status = .{};
- file.close(&cleanup) catch {};
- status.cleanup_code = cleanup.code;
- }
- const rc = c.uv_fs_fstat(null, &req, fd, null);
- const accepted = rc >= 0 and regular(req.statbuf.st_mode);
- c.uv_fs_req_cleanup(&req);
- try check(status, rc);
- if (!accepted) return error.UnsupportedFileType;
- if (mode == .writer) try file.resize(status, 0);
- return file;
-}
-
-const testing = std.testing;
-fn testPath(tmp: std.Io.Dir, name: []const u8) ![:0]u8 {
- const directory = try tmp.realPathFileAlloc(testing.io, ".", testing.allocator);
- defer testing.allocator.free(directory);
- return std.fmt.allocPrintSentinel(testing.allocator, "{s}/{s}", .{ directory, name }, 0);
-}
-test "regular file modes, bounded transfers and cursor-preserving resize/sync" {
- var tmp = testing.tmpDir(.{});
- defer tmp.cleanup();
- const path = try testPath(tmp.dir, "é😀");
- defer testing.allocator.free(path);
- var status: Status = .{};
- var file = try open(&status, path, .update);
- defer file.close(&status) catch {};
- try testing.expectEqual(@as(usize, 3), try file.write(&status, "abc"));
- try file.sync(&status);
- try testing.expectEqual(@as(u64, 3), try file.position());
- try file.seek(1);
- var bytes: [4097]u8 = undefined;
- try testing.expectEqual(@as(usize, 2), try file.read(&status, bytes[0..3]));
- try testing.expectEqualStrings("bc", bytes[0..2]);
- try testing.expectEqual(@as(usize, 0), try file.read(&status, bytes[0..3]));
- try file.seek(10);
- try file.resize(&status, 1);
- try testing.expectEqual(@as(u64, 10), try file.position());
- try testing.expectEqual(@as(u64, 1), try file.size(&status));
- try testing.expectError(error.InvalidBound, file.read(&status, &bytes));
- try testing.expectError(error.InvalidBound, file.read(&status, bytes[0..0]));
- try testing.expectError(error.InvalidBound, file.write(&status, &bytes));
- try testing.expectError(error.InvalidBound, file.seek(@as(u64, limit) + 1));
- try testing.expectError(error.InvalidBound, file.resize(&status, @as(u64, limit) + 1));
- try file.close(&status);
- try testing.expectError(error.Closed, file.close(&status));
- try testing.expectError(error.Closed, file.read(&status, bytes[0..1]));
- var writer = try open(&status, path, .writer);
- defer writer.close(&status) catch {};
- try testing.expectEqual(@as(u64, 0), try writer.size(&status));
- try testing.expectError(error.WrongAccess, writer.read(&status, bytes[0..1]));
- _ = try writer.write(&status, "x");
- var appender = try open(&status, path, .appender);
- defer appender.close(&status) catch {};
- _ = try appender.write(&status, "y");
- try testing.expectError(error.UnsupportedMode, appender.seek(0));
- try testing.expectError(error.UnsupportedMode, appender.position());
- try testing.expectError(error.UnsupportedMode, appender.resize(&status, 0));
- try testing.expectError(error.WrongAccess, appender.read(&status, bytes[0..1]));
- var reader = try open(&status, path, .reader);
- defer reader.close(&status) catch {};
- try testing.expectError(error.InvalidBound, reader.write(&status, ""));
- try testing.expectError(error.UnsupportedMode, reader.resize(&status, 0));
- try testing.expectEqual(@as(usize, 2), try reader.read(&status, bytes[0..3]));
- try testing.expectEqualStrings("xy", bytes[0..2]);
- try reader.seek(limit);
- try testing.expectEqual(@as(usize, 0), try reader.read(&status, bytes[0..1]));
-}
-test "reject nonregular paths, invalid text and missing readers; symlinks are not confinement" {
- var tmp = testing.tmpDir(.{});
- defer tmp.cleanup();
- const fifo = try testPath(tmp.dir, "fifo");
- defer testing.allocator.free(fifo);
- try testing.expectEqual(@as(c_int, 0), c.mkfifo(fifo.ptr, 0o600));
- const directory = try testPath(tmp.dir, ".");
- defer testing.allocator.free(directory);
- const missing = try testPath(tmp.dir, "missing");
- defer testing.allocator.free(missing);
- var status: Status = .{};
- for (std.enums.values(Mode)) |mode| {
- try testing.expectError(error.UnsupportedFileType, open(&status, fifo, mode));
- try testing.expectError(error.UnsupportedFileType, open(&status, directory, mode));
- try testing.expectError(error.UnsupportedFileType, open(&status, "/dev/null", mode));
- }
- try testing.expectError(error.HostFailure, open(&status, missing, .reader));
- try testing.expectEqual(@as(c_int, c.UV_ENOENT), status.code);
- try testing.expectError(error.InvalidArgument, open(&status, "", .writer));
- try testing.expectError(error.InvalidArgument, open(&status, "x\x00y", .writer));
- try testing.expectError(error.InvalidArgument, open(&status, "\xff", .writer));
- var file = try open(&status, missing, .writer);
- defer file.close(&status) catch {};
- _ = try file.write(&status, "keep");
- const link = try testPath(tmp.dir, "link");
- defer testing.allocator.free(link);
- var req: c.uv_fs_t = undefined;
- const rc = c.uv_fs_symlink(null, &req, missing.ptr, link.ptr, 0, null);
- c.uv_fs_req_cleanup(&req);
- try testing.expectEqual(@as(c_int, 0), rc);
- var reader = try open(&status, link, .reader);
- defer reader.close(&status) catch {};
- var bytes: [4]u8 = undefined;
- try testing.expectEqual(@as(usize, 4), try reader.read(&status, &bytes));
- try testing.expectEqualStrings("keep", &bytes);
- try testing.expect(c.fcntl(reader.fd.?, c.F_GETFD) & c.FD_CLOEXEC != 0);
-}
-
-// Concrete ownership bridge. Null is reserved for standard placeholders; it is
-// NOT a standard-stream implementation. Real dispatch must check backend kind
-// before leasing. No allocator operation follows successful host acquisition.
-pub const Registry = @import("io_v2_resources.zig").Registry(?*File);
-pub fn acquire(registry: *Registry, owner: u64, status: *Status, path: [:0]const u8, mode: Mode) !@import("io_v2_resources.zig").Token {
- return acquireWrapped(registry, owner, status, path, mode, struct {
- fn wrap(file: *File) ?*File {
- return file;
- }
- }.wrap);
-}
-pub fn acquireWrapped(registry: anytype, owner: u64, status: *Status, path: [:0]const u8, mode: Mode, comptime wrap: anytype) !@import("io_v2_resources.zig").Token {
- status.* = .{};
- const admission = try registry.beginAcquire(owner);
- errdefer registry.abortAcquire(admission) catch unreachable;
- const backend = try registry.allocator.create(File);
- errdefer registry.allocator.destroy(backend);
- backend.* = try open(status, path, mode);
- errdefer {
- var cleanup: Status = .{};
- backend.close(&cleanup) catch {};
- status.cleanup_code = cleanup.code;
- }
- return registry.publish(admission, wrap(backend), .{});
-}
-// Call only for an acquired close claim, after all leases have settled. Registry
-// entry stays present through physical close, even on error. Memory and aliases
-// are retired exactly once; a close error is reported, never retried implicitly.
-pub fn release(registry: *Registry, token: @import("io_v2_resources.zig").Token, backend: *File, status: *Status) Error!void {
- defer registry.finishClose(token) catch unreachable;
- defer registry.allocator.destroy(backend);
- try backend.close(status);
-}
-
-fn acquisitionFailureProbe(allocator: std.mem.Allocator, path: [:0]const u8) !void {
- var registry = try Registry.init(allocator, 91, .{ null, null, null });
- defer registry.deinit();
- var status: Status = .{};
- const token = acquire(®istry, 0, &status, path, .reader) catch |err| {
- try testing.expectEqual(@as(usize, 0), registry.admissions.count());
- try testing.expectEqual(@as(usize, 3), registry.resources.count());
- return err;
- };
- const claim = (try registry.claimClose(0, token, false)).?;
- try release(®istry, token, claim.backend.?, &status);
- try testing.expectEqual(@as(usize, 3), registry.resources.count());
-}
-test "real descriptor admission, lease, handoff, close and allocator failure" {
- const R = @import("io_v2_resources.zig");
- var tmp = testing.tmpDir(.{});
- defer tmp.cleanup();
- try tmp.dir.writeFile(testing.io, .{ .sub_path = "file", .data = "abc" });
- const path = try testPath(tmp.dir, "file");
- defer testing.allocator.free(path);
- try testing.checkAllAllocationFailures(testing.allocator, acquisitionFailureProbe, .{path});
- var registry = try Registry.init(testing.allocator, 92, .{ null, null, null });
- defer registry.deinit();
- var status: Status = .{};
- const token = try acquire(®istry, 0, &status, path, .update);
- const lease = try registry.beginLease(0, token, .exclusive, false);
- const fd = lease.backend.?.fd.?;
- try testing.expectError(error.Busy, registry.reserveChild(0, &.{token}));
- try registry.endLease(lease.token);
- const transfer = try registry.reserveChild(0, &.{token});
- try testing.expectError(error.StaleHandle, registry.lookup(0, token));
- try testing.expectError(error.OutOfScope, registry.lookup(0, transfer.tokens[0]));
- const child_lease = try registry.beginLease(transfer.scope, transfer.tokens[0], .exclusive, false);
- var bytes: [3]u8 = undefined;
- _ = try child_lease.backend.?.read(&status, &bytes);
- try testing.expectEqualStrings("abc", &bytes);
- registry.beginCloseScope(transfer.scope);
- try testing.expect((try registry.claimClose(transfer.scope, transfer.tokens[0], true)) == null);
- try registry.endLease(child_lease.token);
- const claim = (try registry.claimClose(transfer.scope, transfer.tokens[0], true)).?;
- try release(®istry, transfer.tokens[0], claim.backend.?, &status);
- try testing.expectEqual(@as(c_int, -1), c.fcntl(fd, c.F_GETFD));
- try registry.finishScope(transfer.scope);
- // Publication exhaustion can occur AFTER successful open. The descriptor
- // must be closed and acquisition aborted, not silently left ownerless.
- registry.next = std.math.maxInt(u64) - 1;
- try testing.expectError(R.Fault.IdExhausted, acquire(®istry, 0, &status, path, .reader));
- try testing.expectEqual(@as(usize, 0), registry.admissions.count());
- try testing.expectEqual(@as(usize, 3), registry.resources.count());
-}
-
-pub fn errorCategory(code: c_int) []const u8 {
- return switch (code) {
- c.UV_ENOENT => "not-found",
- c.UV_EACCES, c.UV_EPERM => "os-permission-denied",
- c.UV_EEXIST => "already-exists",
- else => "io-error",
- };
-}
-pub fn errorText(code: c_int) []const u8 {
- return std.mem.span(c.uv_strerror(code));
-}
-
-test "descriptor kind check rejects a namespace substitution after preflight without truncation" {
- const Swap = struct {
- from: [:0]const u8,
- to: [:0]const u8,
- fn run(context: *anyopaque) void {
- const self: *@This() = @ptrCast(@alignCast(context));
- var req: c.uv_fs_t = undefined;
- const rc = c.uv_fs_rename(null, &req, self.from.ptr, self.to.ptr, null);
- c.uv_fs_req_cleanup(&req);
- std.debug.assert(rc == 0);
- }
- };
- for ([_]Mode{ .reader, .writer, .appender, .update }) |mode| {
- var tmp = testing.tmpDir(.{});
- defer tmp.cleanup();
- try tmp.dir.writeFile(testing.io, .{ .sub_path = "file", .data = "keep" });
- const path = try testPath(tmp.dir, "file");
- defer testing.allocator.free(path);
- const fifo = try testPath(tmp.dir, "fifo");
- defer testing.allocator.free(fifo);
- const saved = try testPath(tmp.dir, "saved");
- defer testing.allocator.free(saved);
- try testing.expectEqual(@as(c_int, 0), c.mkfifo(fifo.ptr, 0o600));
- var req: c.uv_fs_t = undefined;
- const linked = c.uv_fs_link(null, &req, path.ptr, saved.ptr, null);
- c.uv_fs_req_cleanup(&req);
- try testing.expectEqual(@as(c_int, 0), linked);
- // Keep a reader for writer modes, but no writer: opening the reader case
- // must use NONBLOCK rather than wait for a nonexistent FIFO peer.
- const anchor = c.uv_fs_open(null, &req, fifo.ptr, c.O_RDONLY | c.O_NONBLOCK | c.O_CLOEXEC, 0, null);
- c.uv_fs_req_cleanup(&req);
- try testing.expect(anchor >= 0);
- defer {
- _ = c.uv_fs_close(null, &req, anchor, null);
- c.uv_fs_req_cleanup(&req);
- }
- var swap: Swap = .{ .from = fifo, .to = path };
- var status: Status = .{};
- try testing.expectError(error.UnsupportedFileType, openAfterCheck(&status, path, mode, .{ .context = &swap, .call = Swap.run }));
- var original = try open(&status, saved, .reader);
- defer original.close(&status) catch {};
- var bytes: [4]u8 = undefined;
- try testing.expectEqual(@as(usize, 4), try original.read(&status, &bytes));
- try testing.expectEqualStrings("keep", &bytes);
- }
-}
diff --git a/ext/zig/src/io_v2_native.zig b/ext/zig/src/io_v2_native.zig
@@ -1,81 +0,0 @@
-//! Native v2 entry boundary. Never probes by executing or retries another driver.
-const std = @import("std");
-const Arena = @import("arena.zig").Arena;
-const P = @import("io_v2_protocol.zig");
-const Core = @import("io_v2_core.zig");
-const Host = @import("io_v2_file_host.zig").Host;
-const C = @import("codecs.zig");
-const Identities = struct {
- next: std.atomic.Value(u64) = .init(1),
- fn issue(self: *@This()) !u64 {
- var value = self.next.load(.monotonic);
- while (true) {
- if (value == std.math.maxInt(u64)) return error.RunIdentityExhausted;
- if (self.next.cmpxchgWeak(value, value + 1, .monotonic, .monotonic)) |changed| value = changed else return value;
- }
- }
-};
-var identities: Identities = .{};
-pub const Envelope = struct { version: u64, action: u32 };
-pub fn envelope(arena: *Arena, program: u32, fuel: u64) !Envelope {
- const base = arena.roots.items.len;
- defer arena.roots.shrinkRetainingCapacity(base);
- try arena.roots.append(arena.allocator, program);
- const r = P.Reader{ .arena = arena, .fuel = fuel };
- const outer = try r.pair(program) orelse return error.InvalidIOEnvelope;
- var rest = outer.first;
- for ("tricuIO") |ch| {
- const cell = try r.pair(rest) orelse return error.InvalidIOEnvelope;
- if (try r.nat(cell.first, 7) != @as(?u64, ch)) return error.InvalidIOEnvelope;
- rest = cell.second;
- }
- if (try r.node(rest) != .leaf) return error.InvalidIOEnvelope;
- const inner = try r.pair(outer.second) orelse return error.InvalidIOEnvelope;
- return .{ .version = try r.nat(inner.first, 16) orelse return error.InvalidIOEnvelope, .action = inner.second };
-}
-pub fn run(arena: *Arena, program: u32, fuel: u64) !u32 {
- const decoded = try envelope(arena, program, fuel);
- if (decoded.version != 2) return error.UnsupportedIOVersion;
- var host: Host = .{};
- const outcome = try Core.runWithHost(Host, &host, arena, decoded.action, try identities.issue(), fuel);
- return switch (outcome.kind) {
- .completed => outcome.value,
- .failed => error.IOV2Failed,
- .cancelled => error.IOV2Cancelled,
- };
-}
-test "native run identities never repeat or wrap" {
- var ids: Identities = .{};
- try std.testing.expectEqual(@as(u64, 1), try ids.issue());
- try std.testing.expectEqual(@as(u64, 2), try ids.issue());
- ids.next.store(std.math.maxInt(u64) - 1, .monotonic);
- _ = try ids.issue();
- try std.testing.expectError(error.RunIdentityExhausted, ids.issue());
- try std.testing.expectError(error.RunIdentityExhausted, ids.issue());
-}
-test "retired versions rejected without demanding action" {
- var a = Arena.init(std.testing.allocator);
- defer a.deinit();
- const bad = try a.allocApp(try a.alloc(.leaf), try a.alloc(.leaf));
- for ([_]u64{ 0, 1, 3, 65535 }) |version| {
- const program = try a.alloc(.{ .fork = .{ .left = try C.ofString(&a, "tricuIO"), .right = try a.alloc(.{ .fork = .{ .left = try C.ofNumber(&a, version), .right = bad } }) } });
- try std.testing.expectError(error.UnsupportedIOVersion, run(&a, program, 0));
- }
-}
-
-test "successive native runs produce distinct scope identities" {
- var arena = Arena.init(std.testing.allocator);
- defer arena.deinit();
- const leaf = try arena.alloc(.leaf);
- const action = try arena.alloc(.{ .fork = .{ .left = try C.ofNumber(&arena, 4), .right = leaf } });
- const program = try arena.alloc(.{ .fork = .{ .left = try C.ofString(&arena, "tricuIO"), .right = try arena.alloc(.{ .fork = .{ .left = try C.ofNumber(&arena, 2), .right = action } }) } });
- const first = try run(&arena, program, 10000);
- try arena.roots.append(arena.allocator, first);
- const second = try run(&arena, program, 10000);
- const reader = P.Reader{ .arena = &arena, .fuel = 10000 };
- const first_fields = (try reader.pair(first)).?;
- const second_fields = (try reader.pair(second)).?;
- const first_id = try reader.nat((try reader.pair(first_fields.second)).?.first, 64);
- const second_id = try reader.nat((try reader.pair(second_fields.second)).?.first, 64);
- try std.testing.expect(first_id != null and second_id != null and first_id != second_id);
-}
diff --git a/ext/zig/src/io_v2_paths.zig b/ext/zig/src/io_v2_paths.zig
@@ -1,120 +0,0 @@
-//! Private synchronous path operations. Observations do not pin subsequent path
-//! operations. OS process authority, no grants, sandbox or implicit rollback.
-const std = @import("std");
-const F = @import("io_v2_files.zig");
-const c = @cImport({
- @cUndef("_FORTIFY_SOURCE");
- @cDefine("_FORTIFY_SOURCE", "0");
- @cInclude("uv.h");
- @cInclude("sys/stat.h");
- @cInclude("fcntl.h");
-});
-pub const Error = F.Error || error{NotDirectory};
-pub const Info = union(enum) { regular: u64, directory, symlink, other };
-pub const Mutation = enum { create_directory, remove_directory, remove_file, rename };
-fn valid(path: [:0]const u8) Error!void {
- if (path.len == 0 or std.mem.indexOfScalar(u8, path, 0) != null or !std.unicode.utf8ValidateSlice(path)) return error.InvalidArgument;
-}
-fn checked(status: *F.Status, code: c_int) Error!void {
- if (code < 0) {
- status.code = code;
- return error.HostFailure;
- }
-}
-pub fn stat(status: *F.Status, path: [:0]const u8, follow: bool) Error!Info {
- status.* = .{};
- try valid(path);
- var req: c.uv_fs_t = undefined;
- const rc = if (follow) c.uv_fs_stat(null, &req, path.ptr, null) else c.uv_fs_lstat(null, &req, path.ptr, null);
- defer c.uv_fs_req_cleanup(&req);
- try checked(status, rc);
- return switch (req.statbuf.st_mode & c.S_IFMT) {
- c.S_IFREG => if (req.statbuf.st_size <= F.limit) .{ .regular = req.statbuf.st_size } else error.InvalidBound,
- c.S_IFDIR => .directory,
- c.S_IFLNK => .symlink,
- else => .other,
- };
-}
-pub fn mutate(status: *F.Status, op: Mutation, path: [:0]const u8, destination: ?[:0]const u8) Error!void {
- status.* = .{};
- try valid(path);
- if (op == .rename) try valid(destination orelse return error.InvalidArgument);
- var req: c.uv_fs_t = undefined;
- const rc = switch (op) {
- .create_directory => c.uv_fs_mkdir(null, &req, path.ptr, 0o777, null),
- .remove_directory => c.uv_fs_rmdir(null, &req, path.ptr, null),
- .remove_file => c.uv_fs_unlink(null, &req, path.ptr, null),
- .rename => c.uv_fs_rename(null, &req, path.ptr, destination.?.ptr, null),
- };
- defer c.uv_fs_req_cleanup(&req);
- try checked(status, rc);
-}
-pub fn syncDirectory(status: *F.Status, path: [:0]const u8) Error!void {
- status.* = .{};
- try valid(path);
- var req: c.uv_fs_t = undefined;
- // NONBLOCK plus O_DIRECTORY avoids opening a FIFO while waiting for peers.
- const fd = c.uv_fs_open(null, &req, path.ptr, c.O_RDONLY | c.O_NONBLOCK | c.O_DIRECTORY | c.O_CLOEXEC | c.O_NOCTTY, 0, null);
- c.uv_fs_req_cleanup(&req);
- if (fd == c.UV_ENOTDIR) return error.NotDirectory;
- try checked(status, fd);
- // Close exactly once on every path; retain a primary sync/fstat error.
- const rc = c.uv_fs_fstat(null, &req, fd, null);
- const is_directory = rc >= 0 and req.statbuf.st_mode & c.S_IFMT == c.S_IFDIR;
- c.uv_fs_req_cleanup(&req);
- const synced = if (is_directory) blk: {
- const result = c.uv_fs_fsync(null, &req, fd, null);
- c.uv_fs_req_cleanup(&req);
- break :blk result;
- } else 0;
- const closed = c.uv_fs_close(null, &req, fd, null);
- c.uv_fs_req_cleanup(&req);
- if (closed < 0) status.cleanup_code = closed;
- try checked(status, rc);
- if (!is_directory) return error.NotDirectory;
- try checked(status, synced);
- try checked(status, closed);
-}
-
-test "path observations, explicit symlink following, rename/unlink and directory durability" {
- const t = std.testing;
- var tmp = t.tmpDir(.{});
- defer tmp.cleanup();
- const base = try tmp.dir.realPathFileAlloc(t.io, ".", t.allocator);
- defer t.allocator.free(base);
- const dir = try std.fmt.allocPrintSentinel(t.allocator, "{s}/dir", .{base}, 0);
- defer t.allocator.free(dir);
- const source = try std.fmt.allocPrintSentinel(t.allocator, "{s}/file", .{base}, 0);
- defer t.allocator.free(source);
- const target = try std.fmt.allocPrintSentinel(t.allocator, "{s}/dir/file", .{base}, 0);
- defer t.allocator.free(target);
- const link = try std.fmt.allocPrintSentinel(t.allocator, "{s}/link", .{base}, 0);
- defer t.allocator.free(link);
- var status: F.Status = .{};
- try mutate(&status, .create_directory, dir, null);
- try t.expect(try stat(&status, dir, true) == .directory);
- try t.expectError(error.HostFailure, mutate(&status, .create_directory, dir, null));
- try t.expectEqualStrings("already-exists", F.errorCategory(status.code));
- var file = try F.open(&status, source, .writer);
- defer file.close(&status) catch {};
- _ = try file.write(&status, "abc");
- var req: c.uv_fs_t = undefined;
- const linked = c.uv_fs_symlink(null, &req, source.ptr, link.ptr, 0, null);
- c.uv_fs_req_cleanup(&req);
- try t.expectEqual(@as(c_int, 0), linked);
- try t.expect(try stat(&status, link, false) == .symlink);
- try t.expectEqual(@as(u64, 3), (try stat(&status, link, true)).regular);
- try mutate(&status, .rename, source, target);
- try t.expectError(error.HostFailure, stat(&status, source, true));
- try t.expectEqualStrings("not-found", F.errorCategory(status.code));
- try t.expectEqual(@as(u64, 3), (try stat(&status, target, true)).regular);
- try syncDirectory(&status, dir);
- try t.expectError(error.NotDirectory, syncDirectory(&status, target));
- try t.expectError(error.HostFailure, mutate(&status, .remove_directory, dir, null));
- try mutate(&status, .remove_file, target, null);
- try mutate(&status, .remove_directory, dir, null);
- try mutate(&status, .remove_file, link, null);
- try t.expectError(error.InvalidArgument, mutate(&status, .rename, source, null));
- try t.expectError(error.InvalidArgument, stat(&status, "x\x00y", true));
- try t.expectError(error.InvalidArgument, syncDirectory(&status, ""));
-}
diff --git a/ext/zig/src/io_v2_protocol.zig b/ext/zig/src/io_v2_protocol.zig
@@ -1,253 +0,0 @@
-//! IO v2 wire decoding only. No v1 dispatch, facilities, scheduler or host effects.
-//! Returned node IDs are borrowed: the caller must retain the action's root while
-//! using them. Header decoding temporarily roots its input across reducer GC.
-const std = @import("std");
-const Arena = @import("arena.zig").Arena;
-const Node = @import("tree.zig").Node;
-const reduce = @import("reduce.zig");
-
-pub const Core = enum(u4) { pure, bind, fail, scope, current_scope, spawn, join, discard, cancel, cancelled };
-pub const Name = struct {
- bytes: [64]u8 = undefined,
- len: u7 = 0,
- pub fn slice(self: *const Name) []const u8 {
- return self.bytes[0..self.len];
- }
-};
-pub const Action = union(enum) {
- core: struct { code: Core, payload: u32 },
- request: struct { group: Name, revision: u16, operation: Name, arguments: u32 },
-};
-pub const DecodeError = reduce.ReduceError || error{
- InvalidComputationShape,
- UnknownComputationConstructor,
- InvalidFacilityHeader,
-};
-
-pub const Reader = struct {
- arena: *Arena,
- // Per-demand reducer fuel, not an aggregate request budget or deadline.
- fuel: u64,
- pub fn node(self: Reader, id: u32) reduce.ReduceError!Node {
- // Copy, never hold an arena pointer across another reduction/allocation.
- return self.arena.get(try reduce.reduce(id, self.arena, self.fuel)).*;
- }
- pub const Pair = struct { first: u32, second: u32 };
- pub fn pair(self: Reader, id: u32) reduce.ReduceError!?Pair {
- return switch (try self.node(id)) {
- .fork => |p| .{ .first = p.left, .second = p.right },
- else => null,
- };
- }
- pub fn nat(self: Reader, id: u32, bits: u7) reduce.ReduceError!?u64 {
- std.debug.assert(bits <= 64);
- var rest = id;
- var value: u64 = 0;
- var count: u7 = 0;
- var last: u1 = 0;
- while (true) {
- const n = try self.node(rest);
- if (n == .leaf) return if (count == 0 or last == 1) value else null;
- if (count == bits or n != .fork) return null;
- last = switch (try self.node(n.fork.left)) {
- .leaf => 0,
- .stem => |s| if (try self.node(s.child) == .leaf) 1 else return null,
- else => return null,
- };
- value |= @as(u64, last) << @as(u6, @intCast(count));
- count += 1;
- rest = n.fork.right;
- }
- }
- fn name(self: Reader, id: u32) reduce.ReduceError!?Name {
- var out: Name = .{};
- var rest = id;
- while (true) {
- const n = try self.node(rest);
- if (n == .leaf) return if (out.len == 0) null else out;
- // At the limit observe the spine only, never an excess head/tail.
- if (out.len == 64 or n != .fork) return null;
- const c = try self.nat(n.fork.left, 7) orelse return null;
- const letter = c >= 'a' and c <= 'z';
- if (!letter and (out.len == 0 or !((c >= '0' and c <= '9') or c == '-'))) return null;
- out.bytes[out.len] = @intCast(c);
- out.len += 1;
- rest = n.fork.right;
- }
- }
-};
-
-pub fn decode(arena: *Arena, action: u32, fuel: u64) DecodeError!Action {
- const base = arena.roots.items.len;
- defer arena.roots.shrinkRetainingCapacity(base);
- try arena.roots.append(arena.allocator, action);
- const r: Reader = .{ .arena = arena, .fuel = fuel };
- const outer = try r.pair(action) orelse return error.InvalidComputationShape;
- const tag = try r.nat(outer.first, 8) orelse return error.UnknownComputationConstructor;
- if (tag <= 9) return .{ .core = .{ .code = @enumFromInt(tag), .payload = outer.second } };
- if (tag != 10) return error.UnknownComputationConstructor;
- const group = try r.pair(outer.second) orelse return error.InvalidFacilityHeader;
- const revision = try r.pair(group.second) orelse return error.InvalidFacilityHeader;
- const operation = try r.pair(revision.second) orelse return error.InvalidFacilityHeader;
- const g = try r.name(group.first) orelse return error.InvalidFacilityHeader;
- const v = try r.nat(revision.first, 16) orelse return error.InvalidFacilityHeader;
- if (v == 0) return error.InvalidFacilityHeader;
- const op = try r.name(operation.first) orelse return error.InvalidFacilityHeader;
- return .{ .request = .{ .group = g, .revision = @intCast(v), .operation = op, .arguments = operation.second } };
-}
-
-const testing = std.testing;
-const codecs = @import("codecs.zig");
-fn pair(a: *Arena, left: u32, right: u32) !u32 {
- return a.alloc(.{ .fork = .{ .left = left, .right = right } });
-}
-fn request(a: *Arena, group: u32, revision: u32, operation: u32, arguments: u32) !u32 {
- return pair(a, try codecs.ofNumber(a, 10), try pair(a, group, try pair(a, revision, try pair(a, operation, arguments))));
-}
-fn poison(a: *Arena) !u32 {
- const leaf = try a.alloc(.leaf);
- const p = try a.alloc(.leaf);
- a.get(p).* = .{ .app = .{ .func = p, .arg = leaf } };
- return p;
-}
-
-test "core catalogue and unknown tags never demand payloads" {
- var a = Arena.init(testing.allocator);
- defer a.deinit();
- const p = try poison(&a);
- for (0..10) |tag| {
- const act = try pair(&a, try codecs.ofNumber(&a, tag), p);
- const decoded = try decode(&a, act, 0);
- try testing.expectEqual(tag, @intFromEnum(decoded.core.code));
- try testing.expectEqual(p, decoded.core.payload);
- }
- for ([_]u64{ 11, 20, 255, 256 }) |tag| {
- try testing.expectError(error.UnknownComputationConstructor, decode(&a, try pair(&a, try codecs.ofNumber(&a, tag), p), 0));
- }
- try testing.expectError(error.InvalidComputationShape, decode(&a, try a.alloc(.leaf), 0));
- try testing.expectEqual(@as(usize, 0), a.roots.items.len);
- try testing.expect(a.get(p).* == .app);
-}
-
-test "valid future headers retain arguments without demanding or resolving them" {
- var a = Arena.init(testing.allocator);
- defer a.deinit();
- const p = try poison(&a);
- const act = try request(&a, try codecs.ofString(&a, "future-9"), try codecs.ofNumber(&a, 65535), try codecs.ofString(&a, "unknown"), p);
- const decoded = (try decode(&a, act, 0)).request;
- try testing.expectEqualStrings("future-9", decoded.group.slice());
- try testing.expectEqualStrings("unknown", decoded.operation.slice());
- try testing.expectEqual(@as(u16, 65535), decoded.revision);
- try testing.expectEqual(p, decoded.arguments);
- try testing.expect(a.get(p).* == .app);
-}
-
-test "strict canonical header numerals and identifier grammar" {
- var a = Arena.init(testing.allocator);
- defer a.deinit();
- const p = try poison(&a);
- const leaf = try a.alloc(.leaf);
- const zero = try pair(&a, leaf, leaf); // forbidden redundant zero bit
- const good = try codecs.ofString(&a, "file");
- const one = try codecs.ofNumber(&a, 1);
- try testing.expectError(error.UnknownComputationConstructor, decode(&a, try pair(&a, zero, p), 0));
- for ([_]u32{ leaf, zero, try codecs.ofNumber(&a, 65536) }) |revision| {
- try testing.expectError(error.InvalidFacilityHeader, decode(&a, try request(&a, good, revision, good, p), 0));
- }
- for ([_][]const u8{ "", "-file", "1file", "File", "a_b", "a b", "a\x7f", "a" ** 65 }) |name| {
- const invalid = try codecs.ofString(&a, name);
- try testing.expectError(error.InvalidFacilityHeader, decode(&a, try request(&a, invalid, one, good, p), 0));
- try testing.expectError(error.InvalidFacilityHeader, decode(&a, try request(&a, good, one, invalid, p), 0));
- }
- const longest = try codecs.ofString(&a, "a" ** 64);
- try testing.expectEqual(@as(u7, 64), (try decode(&a, try request(&a, longest, one, longest, p), 0)).request.group.len);
- // Character 97 with a redundant high zero must not be accepted as ASCII a.
- var character = zero;
- for ([_]u1{ 1, 1, 0, 0, 0, 0, 1 }) |bit| {
- character = try pair(&a, if (bit == 0) leaf else try a.alloc(.{ .stem = .{ .child = leaf } }), character);
- }
- const bad_name = try pair(&a, character, leaf);
- try testing.expectError(error.InvalidFacilityHeader, decode(&a, try request(&a, bad_name, one, good, p), 0));
-}
-
-test "oversize names do not demand excess head or tail and evaluation errors stay distinct" {
- var a = Arena.init(testing.allocator);
- defer a.deinit();
- const p = try poison(&a);
- var name = try pair(&a, p, p);
- const ch = try codecs.ofNumber(&a, 'a');
- for (0..64) |_| name = try pair(&a, ch, name);
- const one = try codecs.ofNumber(&a, 1);
- const good = try codecs.ofString(&a, "read");
- try testing.expectError(error.InvalidFacilityHeader, decode(&a, try request(&a, name, one, good, p), 0));
- try testing.expectError(error.FuelExhausted, decode(&a, try pair(&a, p, p), 0));
- try testing.expectEqual(@as(usize, 0), a.roots.items.len);
-}
-
-fn allocationProbe(allocator: std.mem.Allocator) !void {
- var a = Arena.init(allocator);
- defer a.deinit();
- const arg = try poison(&a);
- const action = try request(&a, try codecs.ofString(&a, "file"), try codecs.ofNumber(&a, 1), try codecs.ofString(&a, "open-reader"), arg);
- // An unrelated retained root must survive decoding and all failure unwinds.
- const keep = try a.alloc(.leaf);
- try a.roots.append(allocator, keep);
- a.gc_enabled = true;
- try a.setGcPolicy(1, 1);
- const decoded = decode(&a, action, 0) catch |err| {
- try testing.expectEqual(@as(usize, 1), a.roots.items.len);
- return err;
- };
- try testing.expect(a.collections > 0);
- try testing.expectEqualStrings("open-reader", decoded.request.operation.slice());
- try testing.expect(a.get(decoded.request.arguments).* == .app);
- try testing.expectEqualSlices(u32, &.{keep}, a.roots.items);
- // Decoder roots do not turn into a hidden completion/history archive.
- try a.collect(&.{});
- try testing.expectEqual(@as(usize, 1), a.last_live);
-}
-test "GC roots survive header demands and every allocator failure unwinds" {
- try allocationProbe(testing.allocator);
- try testing.checkAllAllocationFailures(testing.allocator, allocationProbe, .{});
-}
-
-test "malformed facility framing is terminal without examining unrelated arguments" {
- var a = Arena.init(testing.allocator);
- defer a.deinit();
- const p = try poison(&a);
- const leaf = try a.alloc(.leaf);
- const ten = try codecs.ofNumber(&a, 10);
- const group = try codecs.ofString(&a, "file");
- for ([_]u32{ leaf, try pair(&a, group, leaf), try pair(&a, group, try pair(&a, leaf, leaf)) }) |body| {
- try testing.expectError(error.InvalidFacilityHeader, decode(&a, try pair(&a, ten, body), 0));
- }
- // A bad header must not be confused with a valid unsupported operation.
- const operation = try codecs.ofString(&a, "not-implemented");
- const one = try codecs.ofNumber(&a, 1);
- const act = try request(&a, group, one, operation, p);
- try testing.expectEqualStrings("not-implemented", (try decode(&a, act, 0)).request.operation.slice());
- try testing.expect(a.get(p).* == .app);
-}
-
-fn delayed(a: *Arena, value: u32, ignored: u32) !u32 {
- return a.allocApp(try pair(a, try a.alloc(.leaf), value), ignored);
-}
-test "computed headers reduce under the caller budget and preserve borrowed roots through GC" {
- var a = Arena.init(testing.allocator);
- defer a.deinit();
- const p = try poison(&a);
- const group = try delayed(&a, try codecs.ofString(&a, "file"), p);
- const revision = try delayed(&a, try codecs.ofNumber(&a, 1), p);
- const operation = try delayed(&a, try codecs.ofString(&a, "open-reader"), p);
- const act = try delayed(&a, try request(&a, group, revision, operation, p), p);
- try testing.expectError(error.FuelExhausted, decode(&a, act, 0));
- a.gc_enabled = true;
- try a.setGcPolicy(1, 1);
- const decoded = (try decode(&a, act, 1)).request;
- try testing.expectEqualStrings("file", decoded.group.slice());
- try testing.expectEqualStrings("open-reader", decoded.operation.slice());
- try testing.expectEqual(p, decoded.arguments);
- try testing.expect(a.get(p).* == .app);
- try testing.expect(a.collections > 0);
- try testing.expectEqual(@as(usize, 0), a.roots.items.len);
-}
diff --git a/ext/zig/src/io_v2_resources.zig b/ext/zig/src/io_v2_resources.zig
@@ -1,375 +0,0 @@
-//! Single-owner-event-loop registry. This module performs no OS operations and
-//! never invokes user callbacks. Scheduler publication must reserve its own memory
-//! before reserveChild; no fallible work may separate that commit from admission.
-//! Backend values are owned by the embedding. deinit frees registry memory only;
-//! the embedding must drain admitted opens/leases/releases and settle scopes first.
-const std = @import("std");
-pub const Scope = u64;
-pub const Token = struct { run: u64, key: u64 };
-pub const Access = enum { exclusive, read, write };
-pub const Flags = packed struct { borrowed: bool = false, internal: bool = false, duplex: bool = false };
-pub const Fault = error{ ClosedScope, StaleHandle, OutOfScope, DuplicateHandle, NotOwner, BorrowedResource, InternalResource, ClosedHandle, Busy, InvalidAdmission, InvalidLease, InvalidClose, IdExhausted, TransferLimit, OutOfMemory };
-pub const transfer_limit = 256;
-
-pub fn Registry(comptime Backend: type) type {
- return struct {
- const Self = @This();
- const ScopeState = struct { parent: ?Scope, admitting: bool = true, pending: usize = 0, operations: usize = 0, children: usize = 0 };
- pub const Resource = struct { owner: Scope, backend: Backend, flags: Flags, busy: u2 = 0, closing: bool = false, releasing: bool = false };
- const LeaseState = struct { scope: Scope, resource: u64, mask: u2 };
- pub const Lease = struct { token: Token, backend: Backend };
- pub const Transfer = struct { scope: Scope, tokens: [transfer_limit]Token = undefined, len: usize };
- allocator: std.mem.Allocator,
- identity: u64,
- next: u64 = 4, // root=0; standard references are reserved at 1..3
- scopes: std.AutoHashMapUnmanaged(Scope, ScopeState) = .empty,
- resources: std.AutoHashMapUnmanaged(u64, Resource) = .empty,
- admissions: std.AutoHashMapUnmanaged(u64, Scope) = .empty,
- leases: std.AutoHashMapUnmanaged(u64, LeaseState) = .empty,
-
- pub fn init(allocator: std.mem.Allocator, identity: u64, standard: [3]Backend) !Self {
- var self: Self = .{ .allocator = allocator, .identity = identity };
- errdefer self.deinit();
- try self.scopes.put(allocator, 0, .{ .parent = null });
- for (standard, 1..) |backend, standard_key| try self.resources.put(allocator, standard_key, .{ .owner = 0, .backend = backend, .flags = .{ .borrowed = true } });
- return self;
- }
- pub fn deinit(self: *Self) void {
- self.leases.deinit(self.allocator);
- self.admissions.deinit(self.allocator);
- self.resources.deinit(self.allocator);
- self.scopes.deinit(self.allocator);
- }
- fn room(self: *const Self, count: u64) Fault!void {
- _ = std.math.add(u64, self.next, count) catch return error.IdExhausted;
- }
- pub fn within(self: *const Self, current: Scope, owner: Scope) bool {
- var cursor = current;
- while (self.scopes.get(cursor)) |scope| {
- if (cursor == owner) return true;
- cursor = scope.parent orelse return false;
- }
- return false;
- }
- fn openScope(self: *Self, scope: Scope) Fault!*ScopeState {
- const state = self.scopes.getPtr(scope) orelse return error.ClosedScope;
- if (!state.admitting) return error.ClosedScope;
- return state;
- }
- fn key(self: *const Self, token: Token) Fault!u64 {
- if (token.run != self.identity) return error.StaleHandle;
- return token.key;
- }
- pub fn lookup(self: *const Self, current: Scope, token: Token) Fault!Resource {
- const resource = self.resources.get(try self.key(token)) orelse return error.StaleHandle;
- if (!self.within(current, resource.owner)) return error.OutOfScope;
- return resource;
- }
- pub fn beginCloseScope(self: *Self, scope: Scope) void {
- if (self.scopes.getPtr(scope)) |state| state.admitting = false;
- }
- // Admission is counted through host acquisition and publication/abort.
- // Allocate all map capacity BEFORE the host can create a resource.
- pub fn beginAcquire(self: *Self, owner: Scope) Fault!Token {
- _ = try self.openScope(owner);
- try self.room(1);
- try self.admissions.ensureUnusedCapacity(self.allocator, 1);
- try self.resources.ensureTotalCapacity(self.allocator, std.math.add(u32, self.resources.count(), std.math.add(u32, self.admissions.count(), 1) catch return error.OutOfMemory) catch return error.OutOfMemory);
- const id = self.next;
- self.next += 1;
- self.admissions.putAssumeCapacityNoClobber(id, owner);
- self.scopes.getPtr(owner).?.pending += 1;
- return .{ .run = self.identity, .key = id };
- }
- pub fn abortAcquire(self: *Self, admission: Token) Fault!void {
- const removed = self.admissions.fetchRemove(try self.key(admission)) orelse return error.InvalidAdmission;
- self.scopes.getPtr(removed.value).?.pending -= 1;
- }
- // No allocation. A counter-exhaustion failure leaves admission live;
- // the embedding must release its raw backend and abort that admission.
- // IDs reflect publication order, not asynchronous open-start order.
- pub fn publish(self: *Self, admission: Token, backend: Backend, flags: Flags) Fault!Token {
- const id = try self.key(admission);
- const owner = self.admissions.get(id) orelse return error.InvalidAdmission;
- try self.room(1);
- const key_value = self.next;
- self.next += 1;
- self.resources.putAssumeCapacityNoClobber(key_value, .{ .owner = owner, .backend = backend, .flags = flags });
- _ = self.admissions.remove(id);
- self.scopes.getPtr(owner).?.pending -= 1;
- return .{ .run = self.identity, .key = key_value };
- }
- pub fn reserveChild(self: *Self, parent: Scope, tokens: []const Token) Fault!Transfer {
- _ = try self.openScope(parent);
- if (tokens.len > transfer_limit) return error.TransferLimit;
- var sorted: [transfer_limit]u64 = undefined;
- // Validate every token before any mutation. No backend copies escape
- // on failure and no old token is invalidated on partial validation.
- for (tokens, 0..) |token, i| {
- for (tokens[0..i]) |previous| if (std.meta.eql(previous, token)) return error.DuplicateHandle;
- const resource = try self.lookup(parent, token);
- if (resource.owner != parent) return error.NotOwner;
- if (resource.flags.borrowed) return error.BorrowedResource;
- if (resource.flags.internal) return error.InternalResource;
- if (resource.closing) return error.ClosedHandle;
- if (resource.busy != 0) return error.Busy;
- sorted[i] = token.key;
- }
- std.mem.sort(u64, sorted[0..tokens.len], {}, std.sort.asc(u64));
- try self.room(1 + tokens.len);
- try self.scopes.ensureUnusedCapacity(self.allocator, 1);
- var result: Transfer = .{ .scope = self.next, .len = tokens.len };
- self.next += 1 + tokens.len;
- self.scopes.putAssumeCapacityNoClobber(result.scope, .{ .parent = parent });
- self.scopes.getPtr(parent).?.children += 1;
- // Remove/insert each entry without allocation; capacity does not grow.
- // Sorted original IDs preserve acquisition order, not argument order.
- for (sorted[0..tokens.len], 0..) |old, index| {
- var resource = self.resources.fetchRemove(old).?.value;
- resource.owner = result.scope;
- const fresh = result.scope + 1 + index;
- self.resources.putAssumeCapacityNoClobber(fresh, resource);
- for (tokens, 0..) |token, i| if (token.key == old) {
- result.tokens[i] = .{ .run = self.identity, .key = fresh };
- };
- }
- return result;
- }
- pub fn beginLease(self: *Self, current: Scope, token: Token, access: Access, internal: bool) Fault!Lease {
- const resource = try self.lookup(current, token);
- if (resource.closing) return error.ClosedHandle;
- if (resource.flags.internal and !internal) return error.InternalResource;
- const mask: u2 = if (!resource.flags.duplex or access == .exclusive) 3 else if (access == .read) 1 else 2;
- if (resource.busy & mask != 0) return error.Busy;
- try self.room(1);
- try self.leases.ensureUnusedCapacity(self.allocator, 1);
- const id = self.next;
- self.next += 1;
- self.leases.putAssumeCapacityNoClobber(id, .{ .scope = current, .resource = token.key, .mask = mask });
- self.resources.getPtr(token.key).?.busy |= mask;
- self.scopes.getPtr(current).?.operations += 1;
- return .{ .token = .{ .run = self.identity, .key = id }, .backend = resource.backend };
- }
- pub fn endLease(self: *Self, lease: Token) Fault!void {
- const entry = self.leases.fetchRemove(try self.key(lease)) orelse return error.InvalidLease;
- self.resources.getPtr(entry.value.resource).?.busy &= ~entry.value.mask;
- self.scopes.getPtr(entry.value.scope).?.operations -= 1;
- }
- // Mark closing before waiting, blocking handoff and new leases. null
- // means a lease or another close is in flight, not permission to retry IO.
- pub fn claimClose(self: *Self, current: Scope, token: Token, settling: bool) Fault!?Resource {
- const resource = try self.lookup(current, token);
- if (!settling) {
- if (resource.flags.internal) return error.InternalResource;
- if (resource.flags.borrowed) return error.BorrowedResource;
- }
- const entry = self.resources.getPtr(token.key).?;
- entry.closing = true;
- if (entry.busy != 0 or entry.releasing) return null;
- entry.releasing = true;
- return entry.*;
- }
- // Exactly once, including after a host release error. Keep the entry until
- // the callback settles so finishScope cannot outrun asynchronous close.
- pub fn finishClose(self: *Self, token: Token) Fault!void {
- const id = try self.key(token);
- const entry = self.resources.get(id) orelse return error.StaleHandle;
- if (!entry.releasing) return error.InvalidClose;
- _ = self.resources.remove(id);
- }
- pub fn newestOwned(self: *const Self, scope: Scope) ?Token {
- var newest: ?u64 = null;
- var iterator = self.resources.iterator();
- while (iterator.next()) |entry| {
- if (entry.value_ptr.owner == scope and (newest == null or entry.key_ptr.* > newest.?)) newest = entry.key_ptr.*;
- }
- return if (newest) |id| .{ .run = self.identity, .key = id } else null;
- }
- // Scheduler must first settle descendants. No reachability-based lifetime
- // extension, recursive implicit cancellation, or silent orphan removal here.
- pub fn readyForRelease(self: *const Self, scope: Scope) bool {
- const state = self.scopes.get(scope) orelse return false;
- return !state.admitting and state.children == 0 and state.pending == 0 and state.operations == 0;
- }
- pub fn finishScope(self: *Self, scope: Scope) Fault!void {
- const state = self.scopes.get(scope) orelse return error.ClosedScope;
- if (state.admitting or state.children != 0 or state.pending != 0 or state.operations != 0 or self.newestOwned(scope) != null) return error.Busy;
- _ = self.scopes.remove(scope);
- if (state.parent) |parent| self.scopes.getPtr(parent).?.children -= 1;
- }
- };
-}
-
-const testing = std.testing;
-const TestRegistry = Registry(u32);
-fn create(r: *TestRegistry, scope: Scope, value: u32, flags: Flags) !Token {
- return r.publish(try r.beginAcquire(scope), value, flags);
-}
-fn closeScope(r: *TestRegistry, scope: Scope) !void {
- r.beginCloseScope(scope);
- while (r.newestOwned(scope)) |token| {
- _ = (try r.claimClose(scope, token, true)) orelse return error.Busy;
- try r.finishClose(token);
- }
- try r.finishScope(scope);
-}
-test "closing admission preserves existing descendant borrowing and admitted opens" {
- var r = try TestRegistry.init(testing.allocator, 7, .{ 10, 11, 12 });
- defer r.deinit();
- const child = (try r.reserveChild(0, &.{})).scope;
- const admission = try r.beginAcquire(0);
- r.beginCloseScope(0);
- try testing.expectError(error.ClosedScope, r.beginAcquire(0));
- try testing.expectError(error.ClosedScope, r.reserveChild(0, &.{}));
- try testing.expectError(error.Busy, r.finishScope(0));
- const token = try r.publish(admission, 99, .{});
- const lease = try r.beginLease(child, token, .read, false);
- try testing.expectEqual(@as(u32, 99), lease.backend);
- r.beginCloseScope(child);
- try testing.expectError(error.Busy, r.finishScope(child));
- try r.endLease(lease.token);
- try closeScope(&r, child);
- try closeScope(&r, 0);
- try testing.expectEqual(@as(u32, 0), r.scopes.count());
- try testing.expectEqual(@as(u32, 0), r.resources.count());
-}
-test "handoff validates atomically, rekeys aliases, and preserves acquisition order" {
- var r = try TestRegistry.init(testing.allocator, 8, .{ 0, 0, 0 });
- defer r.deinit();
- const a = try create(&r, 0, 101, .{});
- const b = try create(&r, 0, 102, .{});
- const next = r.next;
- try testing.expectError(error.DuplicateHandle, r.reserveChild(0, &.{ a, a }));
- try testing.expectError(error.StaleHandle, r.reserveChild(0, &.{ a, .{ .run = 9, .key = b.key } }));
- try testing.expectError(error.BorrowedResource, r.reserveChild(0, &.{ a, .{ .run = 8, .key = 1 } }));
- try testing.expectEqual(next, r.next);
- const transferred = try r.reserveChild(0, &.{ b, a });
- try testing.expectError(error.StaleHandle, r.lookup(0, a));
- try testing.expectError(error.StaleHandle, r.lookup(transferred.scope, b));
- try testing.expectError(error.OutOfScope, r.lookup(0, transferred.tokens[0]));
- try testing.expectEqual(@as(u32, 102), (try r.lookup(transferred.scope, transferred.tokens[0])).backend);
- try testing.expectEqual(transferred.tokens[0], r.newestOwned(transferred.scope).?);
- const inner = (try r.reserveChild(transferred.scope, &.{})).scope;
- try testing.expectError(error.NotOwner, r.reserveChild(inner, transferred.tokens[1..2]));
- try closeScope(&r, inner);
- try closeScope(&r, transferred.scope);
- try closeScope(&r, 0);
-}
-test "direction leases, close claims and stale completions cannot cross ownership" {
- var r = try TestRegistry.init(testing.allocator, 1, .{ 0, 0, 0 });
- defer r.deinit();
- const token = try create(&r, 0, 5, .{ .duplex = true });
- const read = try r.beginLease(0, token, .read, false);
- const write = try r.beginLease(0, token, .write, false);
- try testing.expectError(error.Busy, r.beginLease(0, token, .read, false));
- try testing.expectError(error.Busy, r.reserveChild(0, &.{token}));
- try r.endLease(read.token);
- const read2 = try r.beginLease(0, token, .read, false);
- try testing.expectError(error.InvalidLease, r.endLease(read.token));
- try testing.expectEqual(@as(?TestRegistry.Resource, null), try r.claimClose(0, token, false));
- try testing.expectError(error.ClosedHandle, r.beginLease(0, token, .read, false));
- try testing.expectError(error.ClosedHandle, r.reserveChild(0, &.{token}));
- try r.endLease(write.token);
- try r.endLease(read2.token);
- _ = (try r.claimClose(0, token, false)).?;
- try testing.expectEqual(@as(?TestRegistry.Resource, null), try r.claimClose(0, token, false));
- r.beginCloseScope(0);
- try testing.expectError(error.Busy, r.finishScope(0));
- try r.finishClose(token); // also required after failed host close
- try testing.expectError(error.StaleHandle, r.finishClose(token));
- try closeScope(&r, 0);
-}
-test "single file cursor, internal resources and borrowed standards stay distinct" {
- var r = try TestRegistry.init(testing.allocator, 1, .{ 0, 0, 0 });
- defer r.deinit();
- const file = try create(&r, 0, 0, .{});
- const reader = try r.beginLease(0, file, .read, false);
- try testing.expectError(error.Busy, r.beginLease(0, file, .write, false));
- try r.endLease(reader.token);
- const hidden = try create(&r, 0, 0, .{ .internal = true });
- try testing.expectError(error.InternalResource, r.beginLease(0, hidden, .read, false));
- try testing.expectError(error.InternalResource, r.claimClose(0, hidden, false));
- try testing.expectError(error.InternalResource, r.reserveChild(0, &.{hidden}));
- const internal = try r.beginLease(0, hidden, .exclusive, true);
- try r.endLease(internal.token);
- try testing.expectError(error.BorrowedResource, r.claimClose(0, .{ .run = 1, .key = 2 }, false));
- try closeScope(&r, 0);
-}
-test "publication order, multiple admitted opens, and explicit abort" {
- var r = try TestRegistry.init(testing.allocator, 1, .{ 0, 0, 0 });
- defer r.deinit();
- var admissions: [64]Token = undefined;
- for (&admissions) |*a| a.* = try r.beginAcquire(0);
- r.beginCloseScope(0);
- var index: usize = admissions.len;
- while (index > 1) {
- index -= 1;
- const token = try r.publish(admissions[index], @intCast(index), .{});
- try testing.expectEqual(token, r.newestOwned(0).?);
- }
- try testing.expectError(error.Busy, r.finishScope(0));
- try r.abortAcquire(admissions[0]);
- try testing.expectError(error.InvalidAdmission, r.abortAcquire(admissions[0]));
- try closeScope(&r, 0);
-}
-test "ID exhaustion is fail closed without mutation or handle reuse" {
- var r = try TestRegistry.init(testing.allocator, 1, .{ 0, 0, 0 });
- defer r.deinit();
- const a = try r.beginAcquire(0);
- r.next = std.math.maxInt(u64);
- try testing.expectError(error.IdExhausted, r.publish(a, 0, .{}));
- try testing.expectError(error.IdExhausted, r.reserveChild(0, &.{}));
- try testing.expectError(error.IdExhausted, r.beginAcquire(0));
- try testing.expectError(error.IdExhausted, r.beginLease(0, .{ .run = 1, .key = 1 }, .read, false));
- try testing.expectEqual(@as(u32, 1), r.admissions.count());
- try r.abortAcquire(a);
- try closeScope(&r, 0);
-}
-fn allocationProbe(allocator: std.mem.Allocator) !void {
- var r = try TestRegistry.init(allocator, 1, .{ 0, 0, 0 });
- defer r.deinit();
- const token = try create(&r, 0, 5, .{});
- // Fill the scope map to its load limit so the tested transfer must grow it.
- while (r.scopes.count() < r.scopes.capacity() * 80 / 100) _ = try r.reserveChild(0, &.{});
- const scope_count = r.scopes.count();
- const count = r.resources.count();
- const next = r.next;
- const child = r.reserveChild(0, &.{token}) catch |err| {
- try testing.expectEqual(next, r.next);
- try testing.expectEqual(scope_count, r.scopes.count());
- try testing.expectEqual(count, r.resources.count());
- try testing.expectEqual(@as(u32, 5), (try r.lookup(0, token)).backend);
- return err;
- };
- const lease = try r.beginLease(child.scope, child.tokens[0], .read, false);
- try r.endLease(lease.token);
- try closeScope(&r, child.scope);
- while (r.scopes.count() > 1) {
- var it = r.scopes.keyIterator();
- while (it.next()) |scope| {
- if (scope.* == 0) continue;
- try closeScope(&r, scope.*);
- break;
- }
- }
- try closeScope(&r, 0);
-}
-test "allocator failure never partially transfers resources" {
- try testing.checkAllAllocationFailures(testing.allocator, allocationProbe, .{});
-}
-test "repeated scopes, acquisitions and leases retain no historical entries" {
- var r = try TestRegistry.init(testing.allocator, 1, .{ 0, 0, 0 });
- defer r.deinit();
- for (0..4096) |_| {
- const child = (try r.reserveChild(0, &.{})).scope;
- const token = try create(&r, child, 0, .{});
- const lease = try r.beginLease(child, token, .exclusive, false);
- try r.endLease(lease.token);
- try closeScope(&r, child);
- try testing.expectEqual(@as(u32, 1), r.scopes.count());
- try testing.expectEqual(@as(u32, 3), r.resources.count());
- try testing.expectEqual(@as(u32, 0), r.admissions.count());
- try testing.expectEqual(@as(u32, 0), r.leases.count());
- }
- try closeScope(&r, 0);
-}
diff --git a/ext/zig/src/io_v2_snapshot.zig b/ext/zig/src/io_v2_snapshot.zig
@@ -1,68 +0,0 @@
-//! Synchronous private anonymous snapshot. The host registers the descriptor
-//! before copy; authentication completes before any format decoding. Not durable
-//! publication, confinement, a deadline, or an aggregate disk/heap quota.
-const std = @import("std");
-const F = @import("io_v2_files.zig");
-const Indexed = @import("indexed.zig");
-const c = @cImport({
- @cUndef("_FORTIFY_SOURCE");
- @cDefine("_FORTIFY_SOURCE", "0");
- @cInclude("uv.h");
-});
-pub fn create(allocator: std.mem.Allocator, status: *F.Status, directory: [:0]const u8) !F.File {
- if (directory.len == 0 or std.mem.indexOfScalar(u8, directory, 0) != null) return error.InvalidArgument;
- const template = try std.fmt.allocPrintSentinel(allocator, "{s}/.tricu-snapshot-XXXXXX", .{directory}, 0);
- defer allocator.free(template);
- var req: c.uv_fs_t = undefined;
- const fd = c.uv_fs_mkstemp(null, &req, template.ptr, null);
- defer c.uv_fs_req_cleanup(&req);
- if (fd < 0) {
- status.code = fd;
- return error.HostFailure;
- }
- var file = F.File{ .fd = fd, .mode = .update };
- var unlink_req: c.uv_fs_t = undefined;
- const removed = c.uv_fs_unlink(null, &unlink_req, req.path, null);
- c.uv_fs_req_cleanup(&unlink_req);
- if (removed < 0) {
- var cleanup: F.Status = .{};
- file.close(&cleanup) catch {};
- status.* = .{ .code = removed, .cleanup_code = cleanup.code };
- return error.HostFailure;
- }
- return file;
-}
-pub const Snapshot = struct {
- file: *F.File,
- status: *F.Status,
- length: u64,
- pub fn readAt(self: *@This(), offset: u64, bytes: []u8) !void {
- if (bytes.len > 4096 or offset > self.length or bytes.len > self.length - offset) return error.InvalidData;
- try self.file.seek(offset);
- var used: usize = 0;
- while (used < bytes.len) {
- const n = try self.file.read(self.status, bytes[used..]);
- if (n == 0) return error.InvalidData;
- used += n;
- }
- }
-};
-pub fn authenticate(file: *F.File, source: *F.File, status: *F.Status, size: u64, expected: *const [64]u8) !void {
- var hash = std.crypto.hash.sha2.Sha256.init(.{});
- hash.update(Indexed.domain ++ "\x00");
- var remaining = size;
- var bytes: [4096]u8 = undefined;
- while (remaining != 0) {
- const wanted: usize = @intCast(@min(remaining, bytes.len));
- const count = try source.read(status, bytes[0..wanted]);
- if (count == 0) return error.SnapshotTruncated;
- var sent: usize = 0;
- while (sent < count) sent += try file.write(status, bytes[sent..count]);
- hash.update(bytes[0..count]);
- remaining -= count;
- }
- var raw: [32]u8 = undefined;
- hash.final(&raw);
- const actual = std.fmt.bytesToHex(raw, .lower);
- if (!std.mem.eql(u8, &actual, expected)) return error.SnapshotDigestMismatch;
-}
diff --git a/ext/zig/src/io_v2_stage.zig b/ext/zig/src/io_v2_stage.zig
@@ -1,208 +0,0 @@
-//! Private native staged files. Scope registry owns the backend; this layer owns
-//! only its descriptor/private name, never the publication destination.
-const std = @import("std");
-const State = @import("io_v2_stage_state.zig");
-const c = @cImport({
- @cUndef("_FORTIFY_SOURCE");
- @cDefine("_FORTIFY_SOURCE", "0");
- @cInclude("uv.h");
- @cInclude("fcntl.h");
- @cInclude("sys/stat.h");
-});
-pub const Status = State.Status;
-pub const Error = State.Error || error{ OutOfMemory, InvalidArgument, InvalidDestination };
-const marker = ".tricu-stage-";
-const Native = struct {
- not_found: i32 = c.UV_ENOENT,
- already_exists: i32 = c.UV_EEXIST,
- io_error: i32 = c.UV_EIO,
- pub fn write(_: *Native, fd: i32, bytes: []const u8) i32 {
- var req: c.uv_fs_t = undefined;
- var buffer = c.uv_buf_init(@constCast(bytes.ptr), @intCast(bytes.len));
- const rc = c.uv_fs_write(null, &req, fd, &buffer, 1, -1, null);
- c.uv_fs_req_cleanup(&req);
- return rc;
- }
- pub fn sync(_: *Native, fd: i32) i32 {
- var req: c.uv_fs_t = undefined;
- const rc = c.uv_fs_fsync(null, &req, fd, null);
- c.uv_fs_req_cleanup(&req);
- return rc;
- }
- pub fn close(_: *Native, fd: i32) i32 {
- var req: c.uv_fs_t = undefined;
- const rc = c.uv_fs_close(null, &req, fd, null);
- c.uv_fs_req_cleanup(&req);
- return rc;
- }
- pub fn unlink(_: *Native, path: [:0]const u8) i32 {
- var req: c.uv_fs_t = undefined;
- const rc = c.uv_fs_unlink(null, &req, path.ptr, null);
- c.uv_fs_req_cleanup(&req);
- return rc;
- }
- pub fn rename(_: *Native, source: [:0]const u8, target: [:0]const u8) i32 {
- var req: c.uv_fs_t = undefined;
- const rc = c.uv_fs_rename(null, &req, source.ptr, target.ptr, null);
- c.uv_fs_req_cleanup(&req);
- return rc;
- }
- pub fn link(_: *Native, source: [:0]const u8, target: [:0]const u8) i32 {
- var req: c.uv_fs_t = undefined;
- const rc = c.uv_fs_link(null, &req, source.ptr, target.ptr, null);
- c.uv_fs_req_cleanup(&req);
- return rc;
- }
- pub fn openDirectory(_: *Native, path: [:0]const u8) i32 {
- var req: c.uv_fs_t = undefined;
- const rc = c.uv_fs_open(null, &req, path.ptr, c.O_RDONLY | c.O_DIRECTORY | c.O_NONBLOCK | c.O_CLOEXEC | c.O_NOCTTY, 0, null);
- c.uv_fs_req_cleanup(&req);
- return rc;
- }
-};
-fn check(status: *Status, rc: i32) Error!void {
- if (rc < 0) {
- status.code = rc;
- return error.HostFailure;
- }
-}
-fn valid(path: [:0]const u8) Error!void {
- if (path.len == 0 or std.mem.indexOfScalar(u8, path, 0) != null or !std.unicode.utf8ValidateSlice(path)) return error.InvalidArgument;
-}
-fn canonical(allocator: std.mem.Allocator, status: *Status, path: [:0]const u8) Error![:0]u8 {
- var req: c.uv_fs_t = undefined;
- const rc = c.uv_fs_realpath(null, &req, path.ptr, null);
- defer c.uv_fs_req_cleanup(&req);
- try check(status, rc);
- return allocator.dupeZ(u8, std.mem.span(@as([*:0]const u8, @ptrCast(req.ptr))));
-}
-pub const Stage = struct {
- allocator: std.mem.Allocator,
- state: State.Stage,
- directory: [:0]u8,
- path: [:0]u8,
- pub fn init(allocator: std.mem.Allocator, status: *Status, directory: [:0]const u8) Error!Stage {
- status.* = .{};
- try valid(directory);
- const dir = try canonical(allocator, status, directory);
- errdefer allocator.free(dir);
- const template = try std.fmt.allocPrintSentinel(allocator, "{s}/{s}XXXXXX", .{ dir, marker }, 0);
- defer allocator.free(template);
- const path = try allocator.allocSentinel(u8, template.len, 0);
- errdefer allocator.free(path);
- var req: c.uv_fs_t = undefined;
- const fd = c.uv_fs_mkstemp(null, &req, template.ptr, null);
- defer c.uv_fs_req_cleanup(&req);
- try check(status, fd);
- // mkstemp replaces exactly six characters. Storage was reserved BEFORE
- // file creation; no fallible allocation separates acquisition/publication.
- @memcpy(path, req.path[0..path.len]);
- return .{ .allocator = allocator, .directory = dir, .path = path, .state = .{ .directory = dir, .path = path, .fd = fd } };
- }
- pub fn deinit(self: *Stage) void {
- self.allocator.free(self.path);
- self.allocator.free(self.directory);
- }
- pub fn write(self: *Stage, status: *Status, bytes: []const u8) Error!usize {
- var ops: Native = .{};
- return self.state.write(&ops, status, bytes);
- }
- pub fn seal(self: *Stage, status: *Status) Error!void {
- var ops: Native = .{};
- try self.state.seal(&ops, status);
- }
- pub fn release(self: *Stage, status: *Status) Error!void {
- var ops: Native = .{};
- try self.state.release(&ops, status);
- }
- pub fn publish(self: *Stage, status: *Status, replace: bool, destination: [:0]const u8) Error!State.Publication {
- status.* = .{};
- try self.state.checkSealed();
- status.progress = .not_published;
- errdefer self.state.preparationFailed();
- if (std.mem.indexOfScalar(u8, destination, 0) != null or !std.unicode.utf8ValidateSlice(destination)) return error.InvalidArgument;
- const slash = std.mem.lastIndexOfScalar(u8, destination, '/');
- const parent = if (slash) |i| (if (i == 0) "/" else destination[0..i]) else ".";
- const name = if (slash) |i| destination[i + 1 ..] else destination;
- const parent_z = try self.allocator.dupeZ(u8, parent);
- defer self.allocator.free(parent_z);
- const target_dir = try canonical(self.allocator, status, parent_z);
- defer self.allocator.free(target_dir);
- if (name.len == 0 or std.mem.eql(u8, name, ".") or std.mem.eql(u8, name, "..") or std.mem.indexOf(u8, name, marker) != null) return error.InvalidDestination;
- const target = try std.fmt.allocPrintSentinel(self.allocator, "{s}/{s}", .{ target_dir, name }, 0);
- defer self.allocator.free(target);
- var ops: Native = .{};
- return self.state.publish(&ops, status, replace, target_dir, target);
- }
-};
-
-const t = std.testing;
-fn testPath(tmp: std.Io.Dir, name: []const u8) ![:0]u8 {
- const dir = try tmp.realPathFileAlloc(t.io, ".", t.allocator);
- defer t.allocator.free(dir);
- return std.fmt.allocPrintSentinel(t.allocator, "{s}/{s}", .{ dir, name }, 0);
-}
-test "native staged cross-directory no-replace, replacement and private cleanup" {
- var tmp = t.tmpDir(.{});
- defer tmp.cleanup();
- try tmp.dir.createDir(t.io, "a", .default_dir);
- try tmp.dir.createDir(t.io, "b", .default_dir);
- const source = try testPath(tmp.dir, "a");
- defer t.allocator.free(source);
- const target = try testPath(tmp.dir, "b/out");
- defer t.allocator.free(target);
- var status: Status = .{};
- var stage = try Stage.init(t.allocator, &status, source);
- defer stage.deinit();
- defer stage.release(&status) catch {};
- _ = try stage.write(&status, "abc");
- try stage.seal(&status);
- try t.expectEqual(State.Publication.published, try stage.publish(&status, false, target));
- var second = try Stage.init(t.allocator, &status, source);
- defer second.deinit();
- defer second.release(&status) catch {};
- _ = try second.write(&status, "new");
- try second.seal(&status);
- try t.expectEqual(State.Publication.exists, try second.publish(&status, false, target));
- var ops: Native = .{};
- // Existing is neither verified identity nor permission to delete that name.
- var req: c.uv_fs_t = undefined;
- const fd = c.uv_fs_open(null, &req, target.ptr, c.O_RDONLY, 0, null);
- c.uv_fs_req_cleanup(&req);
- try t.expect(fd >= 0);
- var bytes: [3]u8 = undefined;
- var buffer = c.uv_buf_init(&bytes, 3);
- const n = c.uv_fs_read(null, &req, fd, &buffer, 1, 0, null);
- c.uv_fs_req_cleanup(&req);
- _ = ops.close(fd);
- try t.expectEqual(@as(c_int, 3), n);
- try t.expectEqualStrings("abc", &bytes);
- try t.expectEqual(State.Publication.published, try second.publish(&status, true, target));
- try second.release(&status);
- try stage.release(&status);
- const output = try tmp.dir.readFileAlloc(t.io, "b/out", t.allocator, .limited(4));
- defer t.allocator.free(output);
- try t.expectEqualStrings("new", output);
-}
-test "invalid destination poisons sealed stage; early release closes and removes private name" {
- var tmp = t.tmpDir(.{});
- defer tmp.cleanup();
- const directory = try testPath(tmp.dir, ".");
- defer t.allocator.free(directory);
- const bad = try testPath(tmp.dir, ".tricu-stage-stolen");
- defer t.allocator.free(bad);
- var status: Status = .{};
- var stage = try Stage.init(t.allocator, &status, directory);
- defer stage.deinit();
- const private = try t.allocator.dupeZ(u8, stage.path);
- defer t.allocator.free(private);
- try stage.seal(&status);
- try t.expectError(error.InvalidDestination, stage.publish(&status, true, bad));
- try t.expectEqual(State.Progress.not_published, status.progress);
- try t.expectError(error.InvalidState, stage.publish(&status, true, bad));
- try stage.release(&status);
- var req: c.uv_fs_t = undefined;
- const rc = c.uv_fs_stat(null, &req, private.ptr, null);
- c.uv_fs_req_cleanup(&req);
- try t.expectEqual(@as(c_int, c.UV_ENOENT), rc);
-}
diff --git a/ext/zig/src/io_v2_stage_state.zig b/ext/zig/src/io_v2_stage_state.zig
@@ -1,229 +0,0 @@
-//! Publication state machine. Ops are synchronous, allocation-free OS hooks;
-//! paths are prepared/owned by the embedding. No source/artifact/digest policy.
-const std = @import("std");
-pub const Error = error{ InvalidState, InvalidBound, HostFailure };
-pub const Phase = enum { writing, sealed, failed, published };
-pub const Progress = enum { none, not_published, unknown, published };
-pub const Status = struct { code: i32 = 0, secondary: i32 = 0, progress: Progress = .none };
-pub const Publication = enum { exists, published };
-pub const Stage = struct {
- directory: [:0]const u8,
- path: [:0]const u8,
- fd: ?i32,
- phase: Phase = .writing,
- private_name: bool = true,
- fn require(self: *Stage, phase: Phase) Error!void {
- if (self.phase != phase) return error.InvalidState;
- }
- fn check(status: *Status, code: i32) Error!void {
- if (code < 0) {
- status.code = code;
- return error.HostFailure;
- }
- }
- fn closeOwned(self: *Stage, ops: anytype) i32 {
- const fd = self.fd orelse return 0;
- self.fd = null; // an uncertain close is never retried against a reused FD
- return ops.close(fd);
- }
- fn removePrivate(self: *Stage, ops: anytype) i32 {
- if (!self.private_name) return 0;
- const rc = ops.unlink(self.path);
- if (rc >= 0 or rc == ops.not_found) {
- self.private_name = false;
- return 0;
- }
- return rc;
- }
- pub fn write(self: *Stage, ops: anytype, status: *Status, bytes: []const u8) Error!usize {
- status.* = .{};
- if (bytes.len == 0 or bytes.len > 4096) return error.InvalidBound;
- try self.require(.writing);
- const rc = ops.write(self.fd.?, bytes);
- if (rc <= 0 or rc > bytes.len) {
- self.phase = .failed;
- status.progress = .unknown;
- status.code = if (rc < 0) rc else ops.io_error;
- return error.HostFailure;
- }
- return @intCast(rc);
- }
- pub fn seal(self: *Stage, ops: anytype, status: *Status) Error!void {
- status.* = .{};
- try self.require(.writing);
- errdefer self.phase = .failed;
- status.progress = .unknown;
- try check(status, ops.sync(self.fd.?));
- try check(status, self.closeOwned(ops));
- self.phase = .sealed;
- }
- pub fn checkSealed(self: *Stage) Error!void {
- try self.require(.sealed);
- }
- // Preparation errors after a valid publish request poison the stage too;
- // argument decoding errors are handled before invoking the backend.
- pub fn preparationFailed(self: *Stage) void {
- self.phase = .failed;
- }
- pub fn publish(self: *Stage, ops: anytype, status: *Status, replace: bool, target_directory: [:0]const u8, target: [:0]const u8) Error!Publication {
- status.* = .{};
- try self.require(.sealed);
- errdefer self.phase = .failed;
- status.progress = .unknown;
- const rc = if (replace) ops.rename(self.path, target) else ops.link(self.path, target);
- if (!replace and rc == ops.already_exists) return .exists;
- try check(status, rc);
- self.phase = .published;
- status.progress = .published;
- if (replace) self.private_name = false else try check(status, self.removePrivate(ops));
- try self.syncParent(ops, status, target_directory);
- if (!std.mem.eql(u8, self.directory, target_directory)) try self.syncParent(ops, status, self.directory);
- return .published;
- }
- fn syncParent(self: *Stage, ops: anytype, status: *Status, path: [:0]const u8) Error!void {
- const fd = ops.openDirectory(path);
- try check(status, fd);
- self.fd = fd;
- try check(status, ops.sync(fd));
- try check(status, self.closeOwned(ops));
- }
- pub fn release(self: *Stage, ops: anytype, status: *Status) Error!void {
- status.* = .{ .progress = .unknown };
- const closed = self.closeOwned(ops);
- const removed = self.removePrivate(ops); // attempted even if close failed
- if (closed < 0) {
- status.code = closed;
- if (removed < 0) status.secondary = removed;
- return error.HostFailure;
- }
- try check(status, removed);
- }
-};
-
-const Mock = struct {
- const Event = enum { write, sync, close, unlink, rename, link, open_source, open_target };
- not_found: i32 = -2,
- already_exists: i32 = -17,
- io_error: i32 = -5,
- events: [32]Event = undefined,
- len: usize = 0,
- fail_at: ?usize = null,
- failure: i32 = -5,
- fn event(self: *Mock, value: Event) i32 {
- self.events[self.len] = value;
- self.len += 1;
- return if (self.fail_at == self.len) self.failure else 0;
- }
- fn write(self: *Mock, _: i32, bytes: []const u8) i32 {
- const rc = self.event(.write);
- return if (rc < 0) rc else @intCast(bytes.len);
- }
- fn sync(self: *Mock, _: i32) i32 {
- return self.event(.sync);
- }
- fn close(self: *Mock, _: i32) i32 {
- return self.event(.close);
- }
- fn unlink(self: *Mock, _: [:0]const u8) i32 {
- return self.event(.unlink);
- }
- fn rename(self: *Mock, _: [:0]const u8, _: [:0]const u8) i32 {
- return self.event(.rename);
- }
- fn link(self: *Mock, _: [:0]const u8, _: [:0]const u8) i32 {
- return self.event(.link);
- }
- fn openDirectory(self: *Mock, path: [:0]const u8) i32 {
- const rc = self.event(if (std.mem.eql(u8, path, "source")) .open_source else .open_target);
- return if (rc < 0) rc else 20;
- }
-};
-fn fresh() Stage {
- return .{ .directory = "source", .path = "source/.tricu-stage-private", .fd = 10 };
-}
-test "stages seal once, publish across directories, sync destination first and retain no destination ownership" {
- const t = std.testing;
- for ([_]bool{ false, true }) |replace| {
- var stage = fresh();
- var ops: Mock = .{};
- var status: Status = .{};
- try t.expectError(error.InvalidState, stage.publish(&ops, &status, replace, "target", "target/file"));
- try t.expectEqual(@as(usize, 0), ops.len);
- _ = try stage.write(&ops, &status, "abc");
- try stage.seal(&ops, &status);
- try t.expect(stage.fd == null);
- try t.expectError(error.InvalidState, stage.write(&ops, &status, "x"));
- try t.expectEqual(Publication.published, try stage.publish(&ops, &status, replace, "target", "target/file"));
- try t.expectEqual(Progress.published, status.progress);
- const expected_link = [_]Mock.Event{ .write, .sync, .close, .link, .unlink, .open_target, .sync, .close, .open_source, .sync, .close };
- const expected_rename = [_]Mock.Event{ .write, .sync, .close, .rename, .open_target, .sync, .close, .open_source, .sync, .close };
- try t.expectEqualSlices(Mock.Event, if (replace) &expected_rename else &expected_link, ops.events[0..ops.len]);
- const before = ops.len;
- try stage.release(&ops, &status);
- try stage.release(&ops, &status);
- try t.expectEqual(before, ops.len);
- }
-}
-test "all publication failures are poisoned, preserve visibility and attempt only private cleanup" {
- const t = std.testing;
- // link, private unlink, target open/sync/close, source open/sync/close.
- for (1..9) |step| {
- var stage = fresh();
- stage.phase = .sealed;
- stage.fd = null;
- var ops: Mock = .{ .fail_at = step };
- var status: Status = .{};
- try t.expectError(error.HostFailure, stage.publish(&ops, &status, false, "target", "target/file"));
- try t.expectEqual(Phase.failed, stage.phase);
- try t.expectEqual(if (step == 1) Progress.unknown else .published, status.progress);
- const before = ops.len;
- try t.expectError(error.InvalidState, stage.publish(&ops, &status, true, "target", "target/file"));
- try t.expectEqual(before, ops.len);
- ops.fail_at = null;
- try stage.release(&ops, &status);
- try t.expect(stage.fd == null and !stage.private_name);
- for (ops.events[before..ops.len]) |event| try t.expect(event == .close or event == .unlink);
- }
-}
-test "exists is not identity or durability; ambiguous EXDEV is not copy or retry permission" {
- const t = std.testing;
- var stage = fresh();
- stage.phase = .sealed;
- stage.fd = null;
- var ops: Mock = .{ .fail_at = 1, .failure = -17 };
- var status: Status = .{};
- try t.expectEqual(Publication.exists, try stage.publish(&ops, &status, false, "target", "target/file"));
- try t.expectEqual(Phase.sealed, stage.phase);
- try t.expectEqual(@as(usize, 1), ops.len);
- ops.fail_at = 2;
- ops.failure = -18;
- try t.expectError(error.HostFailure, stage.publish(&ops, &status, false, "target", "target/file"));
- try t.expectEqual(Progress.unknown, status.progress);
- try t.expectEqual(@as(usize, 2), ops.len);
- ops.fail_at = null;
- try stage.release(&ops, &status);
-}
-test "seal failures retain or retire descriptors exactly once and cleanup still unlinks" {
- const t = std.testing;
- for ([_]usize{ 1, 2 }) |step| {
- var stage = fresh();
- var ops: Mock = .{ .fail_at = step };
- var status: Status = .{};
- try t.expectError(error.HostFailure, stage.seal(&ops, &status));
- try t.expectEqual(Phase.failed, stage.phase);
- try t.expect((stage.fd != null) == (step == 1));
- ops.fail_at = null;
- try stage.release(&ops, &status);
- var closes: usize = 0;
- for (ops.events[0..ops.len]) |event| if (event == .close) {
- closes += 1;
- };
- try t.expectEqual(@as(usize, 1), closes);
- try t.expectEqual(Mock.Event.unlink, ops.events[ops.len - 1]);
- }
- var stage = fresh();
- var ops: Mock = .{ .fail_at = 1 };
- var status: Status = .{};
- try t.expectError(error.HostFailure, stage.release(&ops, &status));
- try t.expectEqualSlices(Mock.Event, &.{ .close, .unlink }, ops.events[0..ops.len]);
-}
diff --git a/ext/zig/src/io_v2_tasks.zig b/ext/zig/src/io_v2_tasks.zig
@@ -1,582 +0,0 @@
-//! Structured task state machine, owned by one event loop. No user code, host IO,
-//! allocation during stop/settlement, or v1 scheduler reuse. The driver owns body
-//! continuations, prepares their storage before admission, and visits every live
-//! Payload here when registering evaluator GC roots.
-const std = @import("std");
-const R = @import("io_v2_resources.zig");
-pub const Phase = enum { open, joining, releasing };
-pub const Kind = enum { root, lexical, spawned };
-pub const Error = R.Fault || error{ Stopped, StaleTask, JoinCycle, InvalidTransition };
-
-pub fn Scheduler(comptime Backend: type, comptime Payload: type) type {
- return struct {
- const Self = @This();
- pub const Registry = R.Registry(Backend);
- pub const Diagnostics = struct {
- items: [16]Payload = undefined,
- len: usize = 0,
- omitted: u64 = 0,
- pub fn append(self: *Diagnostics, other: Diagnostics) void {
- for (other.items[0..other.len]) |item| {
- if (self.len < 16) {
- self.items[self.len] = item;
- self.len += 1;
- } else self.omitted += 1;
- }
- self.omitted += other.omitted;
- }
- pub fn one(value: Payload) Diagnostics {
- var result: Diagnostics = .{};
- result.items[0] = value;
- result.len = 1;
- return result;
- }
- };
- pub const Outcome = struct {
- kind: enum { completed, failed, cancelled },
- value: Payload,
- cleanup: Diagnostics = .{},
- fn primary(self: Outcome) Outcome {
- return .{ .kind = self.kind, .value = self.value };
- }
- };
- pub const Group = struct {
- parent: ?R.Scope,
- task: u64,
- kind: Kind,
- propagate: bool,
- phase: Phase = .open,
- reason: ?Outcome = null,
- body: ?Outcome = null,
- cleanup: Diagnostics = .{},
- lexical: ?R.Scope = null,
- wait: ?u64 = null,
- host_calls: u32 = 0,
- visited: bool = false,
- walked: bool = false,
- };
- const Task = struct {
- owner: R.Scope,
- supervised: bool,
- ticket: bool,
- waiter: ?R.Scope = null,
- outcome: ?Outcome = null,
- };
- allocator: std.mem.Allocator,
- registry: *Registry,
- owner_stopped: Payload,
- cycle: Payload,
- groups: std.AutoHashMapUnmanaged(R.Scope, Group) = .empty,
- tasks: std.AutoHashMapUnmanaged(u64, Task) = .empty,
-
- pub fn init(allocator: std.mem.Allocator, registry: *Registry, owner_stopped: Payload, cycle: Payload) !Self {
- var self: Self = .{ .allocator = allocator, .registry = registry, .owner_stopped = owner_stopped, .cycle = cycle };
- try self.groups.put(allocator, 0, .{ .parent = null, .task = 0, .kind = .root, .propagate = false });
- return self;
- }
- pub fn deinit(self: *Self) void {
- self.tasks.deinit(self.allocator);
- self.groups.deinit(self.allocator);
- }
- pub fn checkpoint(self: *Self, scope: R.Scope) Error!void {
- const group = self.groups.get(scope) orelse return error.ClosedScope;
- if (group.reason != null) return error.Stopped;
- if (group.phase != .open or group.lexical != null or group.wait != null) return error.InvalidTransition;
- }
- pub fn nested(self: *Self, parent: R.Scope) Error!R.Scope {
- try self.checkpoint(parent);
- const task = self.groups.get(parent).?.task;
- try self.groups.ensureUnusedCapacity(self.allocator, 1);
- const child = (try self.registry.reserveChild(parent, &.{})).scope;
- self.groups.putAssumeCapacityNoClobber(child, .{ .parent = parent, .task = task, .kind = .lexical, .propagate = true });
- self.groups.getPtr(parent).?.lexical = child;
- return child;
- }
- pub fn spawn(self: *Self, owner: R.Scope, supervised: bool, keep: bool, tokens: []const R.Token) Error!Registry.Transfer {
- try self.checkpoint(owner);
- try self.groups.ensureUnusedCapacity(self.allocator, 1);
- try self.tasks.ensureUnusedCapacity(self.allocator, 1);
- const child = try self.registry.reserveChild(owner, tokens);
- // Everything following resource commitment is allocation-free.
- self.groups.putAssumeCapacityNoClobber(child.scope, .{ .parent = owner, .task = child.scope, .kind = .spawned, .propagate = !supervised });
- self.tasks.putAssumeCapacityNoClobber(child.scope, .{ .owner = owner, .supervised = supervised, .ticket = keep });
- return child;
- }
- fn recordStop(self: *Self, scope: R.Scope, reason: Outcome) bool {
- const group = self.groups.getPtr(scope) orelse return false;
- if (group.reason != null) return false;
- group.reason = reason.primary();
- self.registry.beginCloseScope(scope);
- return true;
- }
- // No allocation or host interruption may prevent requests reaching the
- // entire active subtree. First recorded reason wins, including while joining.
- pub fn stop(self: *Self, scope: R.Scope, reason: Outcome) void {
- std.debug.assert(reason.kind != .completed);
- if (!self.recordStop(scope, reason)) return;
- var changed = true;
- while (changed) {
- changed = false;
- var it = self.groups.iterator();
- while (it.next()) |entry| {
- const g = entry.value_ptr.*;
- if (g.parent) |parent| {
- if (g.reason) |why| {
- if (why.kind == .failed and g.propagate) changed = self.recordStop(parent, why) or changed;
- }
- if (self.groups.get(parent).?.reason != null) {
- changed = self.recordStop(entry.key_ptr.*, .{ .kind = .cancelled, .value = self.owner_stopped }) or changed;
- }
- }
- }
- }
- }
- fn checked(self: *Self, current: R.Scope, token: R.Token) Error!*Task {
- if (token.run != self.registry.identity) return error.StaleTask;
- const task = self.tasks.getPtr(token.key) orelse return error.StaleTask;
- if (!self.registry.within(current, task.owner)) return error.OutOfScope;
- return task;
- }
- fn markTask(self: *Self, task: u64) bool {
- var changed = false;
- var it = self.groups.valueIterator();
- while (it.next()) |g| if (g.task == task and !g.visited) {
- g.visited = true;
- changed = true;
- };
- return changed;
- }
- // Allocation-free reachability over task identities, not lexical scope
- // IDs. Active implicit joins contribute edges just like explicit tickets.
- fn reaches(self: *Self, start: u64, wanted: u64) bool {
- if (start == wanted) return true;
- var reset = self.groups.valueIterator();
- while (reset.next()) |g| {
- g.visited = false;
- g.walked = false;
- }
- _ = self.markTask(start);
- var changed = true;
- while (changed) {
- changed = false;
- var it = self.groups.iterator();
- while (it.next()) |entry| {
- if (!entry.value_ptr.visited or entry.value_ptr.walked) continue;
- entry.value_ptr.walked = true;
- const g = entry.value_ptr.*;
- if (g.wait) |target| {
- if (target == wanted) return true;
- changed = self.markTask(target) or changed;
- }
- if (g.phase == .joining) {
- var children = self.tasks.iterator();
- while (children.next()) |child| {
- if (child.value_ptr.owner != entry.key_ptr.* or child.value_ptr.outcome != null) continue;
- if (child.key_ptr.* == wanted) return true;
- changed = self.markTask(child.key_ptr.*) or changed;
- }
- }
- }
- }
- return false;
- }
- pub fn claimJoin(self: *Self, current: R.Scope, token: R.Token) Error!void {
- try self.checkpoint(current);
- const task = try self.checked(current, token);
- if (!task.ticket) return error.StaleTask;
- const identity = self.groups.get(current).?.task;
- if (self.registry.within(current, token.key) or self.reaches(token.key, identity)) return error.JoinCycle;
- // Consume before waiting. Cancellation must not restore this ticket.
- task.ticket = false;
- task.waiter = current;
- self.groups.getPtr(current).?.wait = token.key;
- }
- pub fn pollJoin(self: *Self, current: R.Scope) Error!?Outcome {
- const g = self.groups.get(current) orelse return error.ClosedScope;
- const target = g.wait orelse return error.InvalidTransition;
- const task = self.tasks.get(target) orelse return error.StaleTask;
- const outcome = task.outcome orelse return null;
- self.groups.getPtr(current).?.wait = null;
- _ = self.tasks.remove(target);
- return outcome;
- }
- pub fn abandonJoin(self: *Self, current: R.Scope) void {
- const group = self.groups.getPtr(current) orelse return;
- const target = group.wait orelse return;
- group.wait = null;
- const task = self.tasks.getPtr(target).?;
- task.waiter = null;
- if (task.outcome != null) _ = self.tasks.remove(target);
- }
- pub fn discard(self: *Self, current: R.Scope, token: R.Token) Error!void {
- try self.checkpoint(current);
- const task = try self.checked(current, token);
- if (!task.ticket) return error.StaleTask;
- task.ticket = false;
- if (task.outcome != null) _ = self.tasks.remove(token.key);
- }
- pub fn cancel(self: *Self, current: R.Scope, token: R.Token, reason: Payload) Error!void {
- try self.checkpoint(current);
- const task = try self.checked(current, token);
- if (task.outcome != null) return error.StaleTask;
- self.stop(token.key, .{ .kind = .cancelled, .value = reason });
- }
- pub fn bodyDone(self: *Self, scope: R.Scope, outcome: Outcome) Error!void {
- const g = self.groups.get(scope) orelse return error.ClosedScope;
- if (g.phase != .open or g.lexical != null) return error.InvalidTransition;
- self.abandonJoin(scope);
- if (outcome.kind != .completed) self.stop(scope, outcome);
- self.groups.getPtr(scope).?.body = outcome;
- self.registry.beginCloseScope(scope);
- // Detect before adding implicit edges. A cycle is terminal; explicit
- // join cycles instead leave the ticket untouched and return an error.
- var it = self.tasks.iterator();
- while (it.next()) |entry| {
- if (entry.value_ptr.owner == scope and entry.value_ptr.outcome == null and self.reaches(entry.key_ptr.*, g.task)) {
- self.stop(scope, .{ .kind = .failed, .value = self.cycle });
- break;
- }
- }
- self.groups.getPtr(scope).?.phase = .joining;
- }
- // The task owns host calls even when a newly acquired resource belongs to
- // an ancestor. Cancellation never detaches an unfinished callback.
- pub fn beginHost(self: *Self, scope: R.Scope) Error!void {
- try self.checkpoint(scope);
- const group = self.groups.getPtr(scope).?;
- group.host_calls = std.math.add(u32, group.host_calls, 1) catch return error.OutOfMemory;
- }
- pub fn endHost(self: *Self, scope: R.Scope) Error!void {
- const group = self.groups.getPtr(scope) orelse return error.ClosedScope;
- if (group.host_calls == 0) return error.InvalidTransition;
- group.host_calls -= 1;
- }
- pub fn beginRelease(self: *Self, scope: R.Scope) Error!bool {
- const g = self.groups.get(scope) orelse return error.ClosedScope;
- if (g.phase != .joining) return error.InvalidTransition;
- var children = self.tasks.valueIterator();
- while (children.next()) |task| if (task.owner == scope and task.outcome == null) return false;
- if (g.host_calls != 0 or !self.registry.readyForRelease(scope)) return false;
- self.groups.getPtr(scope).?.phase = .releasing;
- return true;
- }
- pub fn settle(self: *Self, scope: R.Scope, own_cleanup: Diagnostics) Error!Outcome {
- const g = self.groups.get(scope) orelse return error.ClosedScope;
- if (g.phase != .releasing) return error.InvalidTransition;
- try self.registry.finishScope(scope);
- var final = (g.reason orelse g.body.?).primary();
- var errors = g.body.?.cleanup;
- errors.append(g.cleanup);
- errors.append(own_cleanup);
- if (final.kind == .completed and errors.len != 0) {
- final = .{ .kind = .failed, .value = errors.items[0] };
- std.mem.copyForwards(Payload, errors.items[0 .. errors.len - 1], errors.items[1..errors.len]);
- errors.len -= 1;
- }
- final.cleanup = errors;
- if (final.kind == .failed) self.stop(scope, final);
- // Drop all unclaimed tickets owned by this scope, including completed
- // results. No iteration pointer survives a removal/restart.
- while (true) {
- var owned: ?u64 = null;
- var it = self.tasks.iterator();
- while (it.next()) |entry| if (entry.value_ptr.owner == scope) {
- owned = entry.key_ptr.*;
- break;
- };
- if (owned) |key| {
- _ = self.tasks.remove(key);
- } else break;
- }
- if (g.parent) |parent| {
- if (g.kind == .lexical) {
- self.groups.getPtr(parent).?.cleanup.append(final.cleanup);
- self.groups.getPtr(parent).?.lexical = null;
- } else {
- const task = self.tasks.getPtr(scope).?;
- if (!task.supervised) self.groups.getPtr(parent).?.cleanup.append(final.cleanup);
- task.outcome = final;
- if (!task.ticket and task.waiter == null) _ = self.tasks.remove(scope);
- }
- }
- _ = self.groups.remove(scope);
- return if (g.kind == .lexical) final.primary() else final;
- }
- // Embeddings must root these plus their body/continuation/host-reply roots.
- pub fn visitPayloads(self: *const Self, context: anytype, visit: anytype) !void {
- try visit(context, self.owner_stopped);
- try visit(context, self.cycle);
- var groups = self.groups.valueIterator();
- while (groups.next()) |g| {
- if (g.reason) |reason| try visit(context, reason.value);
- if (g.body) |body| {
- try visit(context, body.value);
- for (body.cleanup.items[0..body.cleanup.len]) |v| try visit(context, v);
- }
- for (g.cleanup.items[0..g.cleanup.len]) |v| try visit(context, v);
- }
- var tasks = self.tasks.valueIterator();
- while (tasks.next()) |task| if (task.outcome) |outcome| {
- try visit(context, outcome.value);
- for (outcome.cleanup.items[0..outcome.cleanup.len]) |v| try visit(context, v);
- };
- }
- };
-}
-
-const testing = std.testing;
-const S = Scheduler(u32, u32);
-fn taskToken(s: *S, id: u64) R.Token {
- return .{ .run = s.registry.identity, .key = id };
-}
-fn finish(s: *S, scope: R.Scope, body: S.Outcome, cleanup: S.Diagnostics) !S.Outcome {
- try s.bodyDone(scope, body);
- try testing.expect(try s.beginRelease(scope));
- return release(s, scope, cleanup);
-}
-fn release(s: *S, scope: R.Scope, cleanup: S.Diagnostics) !S.Outcome {
- while (s.registry.newestOwned(scope)) |resource| {
- _ = (try s.registry.claimClose(scope, resource, true)).?;
- try s.registry.finishClose(resource);
- }
- return s.settle(scope, cleanup);
-}
-fn ok(value: u32) S.Outcome {
- return .{ .kind = .completed, .value = value };
-}
-fn fail(value: u32) S.Outcome {
- return .{ .kind = .failed, .value = value };
-}
-fn cancelled(value: u32) S.Outcome {
- return .{ .kind = .cancelled, .value = value };
-}
-
-test "normal exit closes admission, joins, remains cancellable and retains no task history" {
- var registry = try S.Registry.init(testing.allocator, 1, .{ 0, 0, 0 });
- defer registry.deinit();
- var s = try S.init(testing.allocator, ®istry, 90, 91);
- defer s.deinit();
- const child = try s.spawn(0, false, false, &.{});
- try s.bodyDone(0, ok(4));
- try testing.expect(!try s.beginRelease(0));
- try testing.expectError(error.ClosedScope, registry.beginAcquire(0));
- s.stop(0, cancelled(77));
- try testing.expectEqual(@as(u32, 90), s.groups.get(child.scope).?.reason.?.value);
- _ = try finish(&s, child.scope, ok(5), .{});
- try testing.expect(try s.beginRelease(0));
- const outcome = try release(&s, 0, .{});
- try testing.expectEqual(.cancelled, outcome.kind);
- try testing.expectEqual(@as(u32, 77), outcome.value);
- try testing.expectEqual(@as(u32, 0), s.tasks.count());
- try testing.expectEqual(@as(u32, 0), s.groups.count());
-}
-test "unsupervised first failure stops siblings before release; supervision contains failure" {
- var registry = try S.Registry.init(testing.allocator, 1, .{ 0, 0, 0 });
- defer registry.deinit();
- var s = try S.init(testing.allocator, ®istry, 90, 91);
- defer s.deinit();
- const contained = (try s.spawn(0, true, true, &.{})).scope;
- _ = try finish(&s, contained, fail(5), .{});
- try testing.expectEqual(@as(?S.Outcome, null), s.groups.get(0).?.reason);
- const first = (try s.spawn(0, false, true, &.{})).scope;
- const second = (try s.spawn(0, false, true, &.{})).scope;
- s.stop(first, fail(7));
- s.stop(second, fail(8));
- try testing.expectEqual(@as(u32, 7), s.groups.get(0).?.reason.?.value);
- try testing.expectEqual(.cancelled, s.groups.get(second).?.reason.?.kind);
- try testing.expectError(error.ClosedScope, registry.beginAcquire(second));
- _ = try finish(&s, first, ok(0), .{});
- _ = try finish(&s, second, ok(0), .{});
- const outcome = try finish(&s, 0, ok(0), .{});
- try testing.expectEqual(@as(u32, 7), outcome.value);
- try testing.expectEqual(@as(u32, 0), s.tasks.count());
-}
-test "consumable join tickets, cancellation and abandoned waits" {
- var registry = try S.Registry.init(testing.allocator, 1, .{ 0, 0, 0 });
- defer registry.deinit();
- var s = try S.init(testing.allocator, ®istry, 90, 91);
- defer s.deinit();
- const child = (try s.spawn(0, true, true, &.{})).scope;
- const other = (try s.spawn(0, true, true, &.{})).scope;
- try s.claimJoin(other, taskToken(&s, child));
- try testing.expectEqual(@as(?S.Outcome, null), try s.pollJoin(other));
- try testing.expectError(error.StaleTask, s.discard(0, taskToken(&s, child)));
- try s.cancel(0, taskToken(&s, other), 13);
- _ = try finish(&s, other, ok(0), .{});
- try testing.expectEqual(@as(?R.Scope, null), s.tasks.get(child).?.waiter);
- _ = try finish(&s, child, ok(123), .{});
- try testing.expect(!s.tasks.contains(child));
- try s.claimJoin(0, taskToken(&s, other));
- const result = (try s.pollJoin(0)).?;
- try testing.expectEqual(.cancelled, result.kind);
- try testing.expectEqual(@as(u32, 13), result.value);
- try testing.expectError(error.StaleTask, s.claimJoin(0, taskToken(&s, other)));
- _ = try finish(&s, 0, ok(0), .{});
-}
-test "explicit and implicit join cycles include lexical task identities" {
- var registry = try S.Registry.init(testing.allocator, 1, .{ 0, 0, 0 });
- defer registry.deinit();
- var s = try S.init(testing.allocator, ®istry, 90, 91);
- defer s.deinit();
- const a = (try s.spawn(0, true, true, &.{})).scope;
- var b = (try s.spawn(0, true, true, &.{})).scope;
- const nested = try s.nested(a);
- try testing.expectError(error.JoinCycle, s.claimJoin(nested, taskToken(&s, a)));
- try s.claimJoin(nested, taskToken(&s, b));
- try testing.expectError(error.JoinCycle, s.claimJoin(b, taskToken(&s, a)));
- try testing.expect(s.tasks.get(a).?.ticket);
- s.abandonJoin(nested);
- _ = try finish(&s, b, ok(0), .{});
- // The abandoned ticket was consumed, so use a fresh task for this cycle.
- b = (try s.spawn(0, true, true, &.{})).scope;
- // b waits for a; a then implicitly joins a nested-scope child waiting on b.
- const c = (try s.spawn(nested, true, true, &.{})).scope;
- try s.claimJoin(b, taskToken(&s, a));
- try s.claimJoin(c, taskToken(&s, b));
- try s.bodyDone(nested, ok(0));
- try testing.expectEqual(@as(u32, 91), s.groups.get(nested).?.reason.?.value);
- try testing.expectEqual(@as(u32, 91), s.groups.get(a).?.reason.?.value);
- _ = try finish(&s, c, ok(0), .{});
- try testing.expect(try s.beginRelease(nested));
- _ = try release(&s, nested, .{});
- _ = try finish(&s, a, ok(0), .{});
- const result = (try s.pollJoin(b)).?;
- try testing.expectEqual(.failed, result.kind);
- try testing.expectEqual(@as(u32, 91), result.value);
- _ = try finish(&s, b, ok(0), .{});
- _ = try finish(&s, 0, ok(0), .{});
-}
-test "cleanup fails success, preserves primary and propagates once with explicit omissions" {
- var registry = try S.Registry.init(testing.allocator, 1, .{ 0, 0, 0 });
- defer registry.deinit();
- var s = try S.init(testing.allocator, ®istry, 90, 91);
- defer s.deinit();
- const inner = try s.nested(0);
- const child = (try s.spawn(inner, false, false, &.{})).scope;
- const child_result = try finish(&s, child, ok(0), S.Diagnostics.one(17));
- try testing.expectEqual(.failed, child_result.kind);
- try testing.expectEqual(@as(u32, 17), child_result.value);
- var cleanup: S.Diagnostics = .{};
- for (0..25) |i| cleanup.append(S.Diagnostics.one(@intCast(100 + i)));
- const inner_result = try finish(&s, inner, ok(0), cleanup);
- try testing.expectEqual(@as(usize, 0), inner_result.cleanup.len); // lexical return omits cleanup
- const result = try finish(&s, 0, fail(999), S.Diagnostics.one(200));
- try testing.expectEqual(@as(u32, 17), result.value); // first stop, not late body fault
- try testing.expectEqual(@as(usize, 16), result.cleanup.len);
- try testing.expectEqual(@as(u64, 10), result.cleanup.omitted);
- try testing.expectEqual(@as(u32, 100), result.cleanup.items[0]);
-}
-test "host callbacks and ancestor-owned acquisition settle before cancelled task retirement" {
- var registry = try S.Registry.init(testing.allocator, 1, .{ 0, 0, 0 });
- defer registry.deinit();
- var s = try S.init(testing.allocator, ®istry, 90, 91);
- defer s.deinit();
- const child = (try s.spawn(0, true, false, &.{})).scope;
- try s.beginHost(child);
- const admission = try registry.beginAcquire(0);
- try s.cancel(0, taskToken(&s, child), 77);
- try s.bodyDone(child, ok(0));
- try testing.expect(!try s.beginRelease(child));
- const resource = try registry.publish(admission, 5, .{});
- try s.endHost(child);
- try testing.expectError(error.InvalidTransition, s.endHost(child));
- try testing.expect(try s.beginRelease(child));
- _ = try release(&s, child, .{});
- try testing.expectEqual(@as(u32, 5), (try registry.lookup(0, resource)).backend);
- _ = try finish(&s, 0, ok(0), .{});
-}
-fn allocationProbe(allocator: std.mem.Allocator) !void {
- var registry = try S.Registry.init(allocator, 1, .{ 0, 0, 0 });
- defer registry.deinit();
- var s = try S.init(allocator, ®istry, 90, 91);
- defer s.deinit();
- for (0..40) |_| {
- const resource = try registry.publish(try registry.beginAcquire(0), 22, .{});
- const before = registry.next;
- const count = s.groups.count();
- const task_count = s.tasks.count();
- _ = s.spawn(0, false, false, &.{resource}) catch |err| {
- try testing.expectEqual(before, registry.next);
- try testing.expectEqual(count, s.groups.count());
- try testing.expectEqual(task_count, s.tasks.count());
- try testing.expectEqual(@as(u32, 22), (try registry.lookup(0, resource)).backend);
- return err;
- };
- }
- s.stop(0, cancelled(8));
- while (s.tasks.count() != 0) {
- var it = s.tasks.keyIterator();
- const child = it.next().?.*;
- _ = try finish(&s, child, ok(0), .{});
- }
- _ = try finish(&s, 0, ok(0), .{});
-}
-test "allocation failures never expose partly published tasks or moved handles" {
- try testing.checkAllAllocationFailures(testing.allocator, allocationProbe, .{});
-}
-fn capture(list: *std.ArrayList(u32), value: u32) !void {
- try list.append(testing.allocator, value);
-}
-test "GC root visitation drops consumed results and repeated tasks retain no history" {
- var registry = try S.Registry.init(testing.allocator, 1, .{ 0, 0, 0 });
- defer registry.deinit();
- var s = try S.init(testing.allocator, ®istry, 90, 91);
- defer s.deinit();
- var roots: std.ArrayList(u32) = .empty;
- defer roots.deinit(testing.allocator);
- for (0..2048) |_| {
- const child = (try s.spawn(0, true, true, &.{})).scope;
- _ = try finish(&s, child, ok(12345), .{});
- roots.clearRetainingCapacity();
- try s.visitPayloads(&roots, capture);
- try testing.expect(std.mem.indexOfScalar(u32, roots.items, 12345) != null);
- try s.claimJoin(0, taskToken(&s, child));
- try testing.expectEqual(@as(u32, 12345), (try s.pollJoin(0)).?.value);
- roots.clearRetainingCapacity();
- try s.visitPayloads(&roots, capture);
- try testing.expectEqualSlices(u32, &.{ 90, 91 }, roots.items);
- try testing.expectEqual(@as(u32, 0), s.tasks.count());
- try testing.expectEqual(@as(u32, 1), s.groups.count());
- }
- _ = try finish(&s, 0, ok(0), .{});
-}
-
-test "cancelled primaries survive cleanup and supervised discarded cleanup stays contained" {
- var registry = try S.Registry.init(testing.allocator, 1, .{ 0, 0, 0 });
- defer registry.deinit();
- var s = try S.init(testing.allocator, ®istry, 90, 91);
- defer s.deinit();
- const discarded = (try s.spawn(0, true, false, &.{})).scope;
- _ = try finish(&s, discarded, ok(3), S.Diagnostics.one(8));
- try testing.expect(s.groups.get(0).?.reason == null);
- try testing.expectEqual(@as(usize, 0), s.groups.get(0).?.cleanup.len);
- const outcome = try finish(&s, 0, cancelled(19), S.Diagnostics.one(20));
- try testing.expectEqual(.cancelled, outcome.kind);
- try testing.expectEqual(@as(u32, 19), outcome.value);
- try testing.expectEqualSlices(u32, &.{20}, outcome.cleanup.items[0..outcome.cleanup.len]);
-}
-test "stop, host settlement, and teardown need no allocation" {
- var allocator = std.testing.FailingAllocator.init(testing.allocator, .{});
- var registry = try S.Registry.init(allocator.allocator(), 1, .{ 0, 0, 0 });
- defer registry.deinit();
- var s = try S.init(allocator.allocator(), ®istry, 90, 91);
- defer s.deinit();
- const first = (try s.spawn(0, false, true, &.{})).scope;
- const lexical = try s.nested(first);
- const child = (try s.spawn(lexical, false, true, &.{})).scope;
- try s.beginHost(child);
- allocator.fail_index = allocator.alloc_index;
- s.stop(0, cancelled(4));
- try s.bodyDone(child, ok(1));
- try testing.expect(!try s.beginRelease(child));
- try s.endHost(child);
- try testing.expect(try s.beginRelease(child));
- _ = try release(&s, child, S.Diagnostics.one(7));
- _ = try finish(&s, lexical, ok(1), .{});
- _ = try finish(&s, first, ok(1), .{});
- const outcome = try finish(&s, 0, ok(1), .{});
- try testing.expectEqual(.cancelled, outcome.kind);
- try testing.expectEqualSlices(u32, &.{7}, outcome.cleanup.items[0..outcome.cleanup.len]);
- try testing.expectEqual(@as(u32, 0), registry.resources.count());
- try testing.expectEqual(@as(u32, 0), registry.scopes.count());
-}
diff --git a/ext/zig/src/io_v2_utf8.zig b/ext/zig/src/io_v2_utf8.zig
@@ -1,63 +0,0 @@
-//! Stateless strict UTF-8 framing. A successful nonfinal scan returns the complete
-//! prefix length; the remaining <=3 bytes are the caller-owned carry. Invalid
-//! available continuation bytes fail immediately, not after an eventual EOF.
-const std = @import("std");
-pub const Fault = struct { offset: usize, kind: enum { leading, sequence, incomplete } };
-pub const Scan = union(enum) { prefix: usize, fault: Fault, too_large };
-pub fn scan(bytes: []const u8, final: bool) Scan {
- if (bytes.len > 4096) return .too_large;
- var offset: usize = 0;
- while (offset < bytes.len) {
- const lead = bytes[offset];
- if (lead <= 0x7f) {
- offset += 1;
- continue;
- }
- var length: usize = undefined;
- var low: u8 = 0x80;
- var high: u8 = 0xbf;
- if (lead >= 0xc2 and lead <= 0xdf) length = 2 else if (lead >= 0xe0 and lead <= 0xef) {
- length = 3;
- if (lead == 0xe0) low = 0xa0;
- if (lead == 0xed) high = 0x9f;
- } else if (lead >= 0xf0 and lead <= 0xf4) {
- length = 4;
- if (lead == 0xf0) low = 0x90;
- if (lead == 0xf4) high = 0x8f;
- } else return .{ .fault = .{ .offset = offset, .kind = .leading } };
- const available = @min(length - 1, bytes.len - offset - 1);
- for (1..available + 1) |i| {
- const byte = bytes[offset + i];
- if (byte < (if (i == 1) low else 0x80) or byte > (if (i == 1) high else 0xbf))
- return .{ .fault = .{ .offset = offset, .kind = .sequence } };
- }
- if (bytes.len - offset < length) return if (final) .{ .fault = .{ .offset = offset, .kind = .incomplete } } else .{ .prefix = offset };
- offset += length;
- }
- return .{ .prefix = offset };
-}
-test "every Unicode scalar and every valid incomplete prefix" {
- for (0..0x110000) |number| {
- if (number >= 0xd800 and number <= 0xdfff) continue;
- var bytes: [4]u8 = undefined;
- const n = try std.unicode.utf8Encode(@intCast(number), &bytes);
- try std.testing.expectEqual(@as(usize, n), scan(bytes[0..n], true).prefix);
- for (1..n) |prefix| {
- try std.testing.expectEqual(@as(usize, 0), scan(bytes[0..prefix], false).prefix);
- try std.testing.expectEqual(Fault{ .offset = 0, .kind = .incomplete }, scan(bytes[0..prefix], true).fault);
- }
- }
-}
-test "strict malformed prefix classification, byte offsets and bounded scan" {
- const t = std.testing;
- for ([_][]const u8{ "\xc0", "\xc1", "\x80", "\xf5", "\xff" }) |bytes| {
- try t.expectEqual(Fault{ .offset = 0, .kind = .leading }, scan(bytes, false).fault);
- }
- for ([_][]const u8{ "\xe0\x80", "\xed\xa0", "\xf0\x80", "\xf4\x90", "\xc2A", "\xe1\x80A" }) |bytes| {
- try t.expectEqual(Fault{ .offset = 0, .kind = .sequence }, scan(bytes, false).fault);
- }
- try t.expectEqual(Fault{ .offset = 3, .kind = .leading }, scan("abc\xff", true).fault);
- try t.expectEqual(@as(usize, 4), scan("abc\x00\xf0\x90", false).prefix);
- try t.expectEqual(@as(usize, 0), scan("", true).prefix);
- try t.expect(scan(&([_]u8{0} ** 4097), false) == .too_large);
-}
diff --git a/ext/zig/src/kernel.zig b/ext/zig/src/kernel.zig
@@ -1,22 +0,0 @@
-const std = @import("std");
-const tree = @import("tree.zig");
-const Arena = @import("arena.zig").Arena;
-const embed = @import("kernel_embed");
-
-/// Copy the embedded kernel into an arena, returning the new root index.
-/// This allows the kernel to be used in App nodes alongside application terms.
-pub fn loadKernel(arena: *Arena) !u32 {
- var mapping = try arena.allocator.alloc(u32, embed.kernel_nodes.len);
- defer arena.allocator.free(mapping);
-
- for (embed.kernel_nodes, 0..) |node, i| {
- const idx: u32 = @intCast(i);
- mapping[idx] = switch (node) {
- .leaf => try arena.alloc(.leaf),
- .stem => |s| try arena.alloc(.{ .stem = .{ .child = mapping[s.child] } }),
- .fork => |f| try arena.alloc(.{ .fork = .{ .left = mapping[f.left], .right = mapping[f.right] } }),
- };
- }
-
- return mapping[embed.kernel_root];
-}
diff --git a/ext/zig/src/main.zig b/ext/zig/src/main.zig
@@ -1,273 +0,0 @@
-const std = @import("std");
-const tree = @import("tree.zig");
-const Arena = @import("arena.zig").Arena;
-const reduce = @import("reduce.zig");
-const codecs = @import("codecs.zig");
-const kernel = @import("kernel.zig");
-const bundle = @import("bundle.zig");
-const io_v2 = @import("io_v2_native.zig");
-
-fn writeStdout(bytes: []const u8) !void {
- var offset: usize = 0;
- while (offset < bytes.len) {
- const n = std.c.write(1, bytes[offset..].ptr, bytes.len - offset);
- if (n <= 0) return error.WriteFailed;
- offset += @intCast(n);
- }
-}
-
-// IO putStr uses libc writes. Use the same file position for the final
-// result, rather than a separate buffered writer that can overwrite it.
-fn printNode(arena: *Arena, tag: u64, node: u32, io: std.Io) !void {
- _ = io;
- var output: std.Io.Writer.Allocating = .init(arena.allocator);
- defer output.deinit();
- const writer = &output.writer;
- switch (tag) {
- codecs.HOST_STRING_TAG => {
- const s = try codecs.toString(arena, node) orelse {
- try writeStdout("Error: failed to decode string result\n");
- return error.DecodeFailed;
- };
- defer arena.allocator.free(s);
- try writer.writeAll(s);
- try writer.writeAll("\n");
- },
- codecs.HOST_NUMBER_TAG => {
- const n = try codecs.toNumber(arena, node) orelse 0;
- try writer.print("{d}\n", .{n});
- },
- codecs.HOST_BOOL_TAG => {
- const b = try codecs.toBool(arena, node) orelse {
- try writeStdout("Error: failed to decode bool result\n");
- return error.DecodeFailed;
- };
- try writer.writeAll(if (b) "true\n" else "false\n");
- },
- codecs.HOST_TREE_TAG => {
- try tree.formatTree(writer, arena, node, 0);
- try writer.writeAll("\n");
- },
- else => {
- try writer.print("(tag={d}, payload=", .{tag});
- try tree.formatTree(writer, arena, node, 0);
- try writer.writeAll(")\n");
- },
- }
- try writeStdout(output.written());
-}
-
-fn runNative(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []const []const u8, fuel: u64, io: std.Io) !void {
- const term = try bundle.loadBundleDefaultRoot(arena, bundle_bytes);
-
- var current = term;
- for (args_raw) |arg| {
- const arg_tree = try parseArg(arena, io, arg);
- current = try arena.alloc(.{ .app = .{ .func = current, .arg = arg_tree } });
- }
-
- const base = arena.roots.items.len;
- defer arena.roots.shrinkRetainingCapacity(base);
- try arena.roots.append(arena.allocator, current);
- arena.gc_enabled = true;
- defer arena.gc_enabled = false;
- const result = try reduce.reduce(current, arena, fuel);
- try printNode(arena, tag, result, io);
-}
-
-fn runIO(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []const []const u8, fuel: u64, io: std.Io) !void {
- const term = try bundle.loadBundleDefaultRoot(arena, bundle_bytes);
-
- var current = term;
- for (args_raw) |arg| {
- const arg_tree = try parseArg(arena, io, arg);
- current = try arena.alloc(.{ .app = .{ .func = current, .arg = arg_tree } });
- }
-
- const base = arena.roots.items.len;
- defer arena.roots.shrinkRetainingCapacity(base);
- try arena.roots.append(arena.allocator, current);
- arena.gc_enabled = true;
- defer arena.gc_enabled = false;
- const reduced = try reduce.reduce(current, arena, fuel);
-
- const result = try io_v2.run(arena, reduced, fuel);
- try arena.roots.append(arena.allocator, result);
- try printNode(arena, tag, result, io);
-}
-
-fn runBundle(arena: *Arena, tag: u64, bundle_bytes: []const u8, args_raw: []const []const u8, fuel: u64, io: std.Io) !void {
- const kernel_root = try kernel.loadKernel(arena);
-
- const tag_tree = try codecs.ofNumber(arena, tag);
- const bundle_tree = try codecs.ofBytes(arena, bundle_bytes);
-
- var arg_items = try arena.allocator.alloc(u32, args_raw.len);
- defer arena.allocator.free(arg_items);
- for (args_raw, 0..) |arg, i| {
- arg_items[i] = try parseArg(arena, io, arg);
- }
- const args_tree = try codecs.ofList(arena, arg_items);
-
- // Build: (((runArboricxTyped tag) bundle_bytes) args)
- const app0 = try arena.alloc(.{ .app = .{ .func = kernel_root, .arg = tag_tree } });
- const app1 = try arena.alloc(.{ .app = .{ .func = app0, .arg = bundle_tree } });
- const app2 = try arena.alloc(.{ .app = .{ .func = app1, .arg = args_tree } });
-
- const base = arena.roots.items.len;
- defer arena.roots.shrinkRetainingCapacity(base);
- try arena.roots.append(arena.allocator, app2);
- arena.gc_enabled = true;
- defer arena.gc_enabled = false;
- const result = try reduce.reduce(app2, arena, fuel);
-
- const unwrapped = try codecs.unwrapResult(arena, result) orelse {
- var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
- try stderr.interface.writeAll("Error: result is not a valid ok/err pair\n");
- try stderr.flush();
- return error.InvalidResult;
- };
-
- if (!unwrapped.ok) {
- var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
- const code = try codecs.toNumber(arena, unwrapped.value) orelse 0;
- try stderr.interface.print("Error: kernel returned err, code={d}\n", .{code});
- try stderr.flush();
- return error.KernelError;
- }
-
- const hv = try codecs.unwrapHostValue(arena, unwrapped.value) orelse {
- var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
- try stderr.interface.writeAll("Error: result is not a valid host ABI value\n");
- try stderr.flush();
- return error.InvalidHostValue;
- };
-
- try printNode(arena, hv.tag, hv.payload, io);
-}
-
-fn parseArg(arena: *Arena, io: std.Io, s: []const u8) !u32 {
- if (std.mem.startsWith(u8, s, "text:")) return @import("source_text.zig").fromUtf8(arena, s[5..]);
- if (std.mem.endsWith(u8, s, ".arboricx")) {
- const bundle_bytes = try std.Io.Dir.cwd().readFileAlloc(io, s, arena.allocator, .limited(10 * 1024 * 1024));
- defer arena.allocator.free(bundle_bytes);
- return try bundle.loadBundleDefaultRoot(arena, bundle_bytes);
- }
-
- if (std.fmt.parseInt(u64, s, 10)) |n| {
- return try codecs.ofNumber(arena, n);
- } else |_| {}
-
- if (s.len >= 2 and s[0] == '"' and s[s.len - 1] == '"') {
- return try codecs.ofString(arena, s[1 .. s.len - 1]);
- }
-
- return try codecs.ofString(arena, s);
-}
-
-pub fn main(init: std.process.Init) !void {
- const gpa = init.gpa;
- const io = init.io;
-
- const args = try init.minimal.args.toSlice(init.arena.allocator());
- if (args.len < 2) {
- var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
- try stderr.interface.writeAll("Usage: tricu-zig [--type TYPE] [--kernel] [--io] [--fuel N] <bundle.arboricx> [arg1 arg2 ...]\n");
- try stderr.flush();
- std.process.exit(1);
- }
-
- // Parse options before bundle path
- var tag = codecs.HOST_STRING_TAG;
- var bundle_idx: usize = 1;
- var arg_start: usize = 2;
-
- var use_kernel = false;
- var use_io = false;
- var fuel: u64 = std.math.maxInt(u64);
-
- var i: usize = 1;
- while (i < args.len) : (i += 1) {
- if (std.mem.eql(u8, args[i], "--type")) {
- if (i + 1 >= args.len) {
- var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
- try stderr.interface.writeAll("Usage: tricu-zig --type <tree|number|bool|string|list|bytes> [--io] [--fuel N] <bundle> [args...]\n");
- try stderr.flush();
- std.process.exit(1);
- }
- const type_str = args[i + 1];
- tag = if (std.mem.eql(u8, type_str, "tree")) codecs.HOST_TREE_TAG else if (std.mem.eql(u8, type_str, "number")) codecs.HOST_NUMBER_TAG else if (std.mem.eql(u8, type_str, "bool")) codecs.HOST_BOOL_TAG else if (std.mem.eql(u8, type_str, "string")) codecs.HOST_STRING_TAG else if (std.mem.eql(u8, type_str, "list")) codecs.HOST_LIST_TAG else if (std.mem.eql(u8, type_str, "bytes")) codecs.HOST_BYTES_TAG else blk: {
- var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
- try stderr.interface.print("Unknown type: {s}\n", .{type_str});
- try stderr.flush();
- std.process.exit(1);
- break :blk codecs.HOST_STRING_TAG;
- };
- i += 1;
- } else if (std.mem.eql(u8, args[i], "--kernel")) {
- use_kernel = true;
- } else if (std.mem.eql(u8, args[i], "--io")) {
- use_io = true;
- } else if (std.mem.eql(u8, args[i], "--unsafe-io")) {
- return error.RetiredIOGrants;
- } else if (std.mem.eql(u8, args[i], "--fuel")) {
- if (i + 1 >= args.len) {
- var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
- try stderr.interface.writeAll("Usage: tricu-zig --fuel <N> [--io] <bundle> [args...]\n");
- try stderr.flush();
- std.process.exit(1);
- }
- const n = std.fmt.parseInt(u64, args[i + 1], 10) catch {
- var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
- try stderr.interface.print("Invalid fuel: {s}\n", .{args[i + 1]});
- try stderr.flush();
- std.process.exit(1);
- };
- fuel = std.math.mul(u64, n, 1_000_000) catch std.math.maxInt(u64);
- i += 1;
- } else {
- bundle_idx = i;
- arg_start = i + 1;
- break;
- }
- }
-
- if (bundle_idx >= args.len) {
- var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
- try stderr.interface.writeAll("Usage: tricu-zig [--type TYPE] [--kernel] [--io] [--fuel N] <bundle.arboricx> [arg1 arg2 ...]\n");
- try stderr.flush();
- std.process.exit(1);
- }
-
- const bundle_path = args[bundle_idx];
- const bundle_bytes = try std.Io.Dir.cwd().readFileAlloc(io, bundle_path, gpa, .limited(10 * 1024 * 1024));
- defer gpa.free(bundle_bytes);
-
- var arena = Arena.init(gpa);
- defer arena.deinit();
-
- const call_args = if (arg_start < args.len) args[arg_start..] else &[_][]const u8{};
-
- if (use_io) {
- runIO(&arena, tag, bundle_bytes, call_args, fuel, io) catch |err| {
- var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
- try stderr.interface.print("Execution failed: {s}\n", .{@errorName(err)});
- try stderr.flush();
- std.process.exit(1);
- };
- } else if (use_kernel) {
- runBundle(&arena, tag, bundle_bytes, call_args, fuel, io) catch |err| {
- var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
- try stderr.interface.print("Execution failed: {s}\n", .{@errorName(err)});
- try stderr.flush();
- std.process.exit(1);
- };
- } else {
- runNative(&arena, tag, bundle_bytes, call_args, fuel, io) catch |err| {
- var stderr = std.Io.File.stderr().writer(io, &[_]u8{});
- try stderr.interface.print("Execution failed: {s}\n", .{@errorName(err)});
- try stderr.flush();
- std.process.exit(1);
- };
- }
-}
diff --git a/ext/zig/src/preview.zig b/ext/zig/src/preview.zig
@@ -1,173 +0,0 @@
-//! Bounded constructor snapshots. Labels belong to one snapshot, never the arena
-//! or the owned-handle API. Frontier children remain undemanded at the node cap.
-const std = @import("std");
-const Arena = @import("arena.zig").Arena;
-const reduce = @import("reduce.zig");
-
-const Record = struct { label: u32, kind: u32, left: u32 = 0, right: u32 = 0 };
-const Labels = struct {
- ids: [513]u32 = undefined,
- len: usize = 0,
- fn intern(self: *Labels, id: u32) u32 {
- if (std.mem.indexOfScalar(u32, self.ids[0..self.len], id)) |index| return @intCast(index);
- const index = self.len;
- self.ids[index] = id;
- self.len += 1;
- return @intCast(index);
- }
-};
-
-// Binary v1: little-endian u32 version, root label, record count, truncated;
-// followed by [label, constructor arity (0/1/2), left, right] records.
-// Fuel is PER constructor demand, not a whole-snapshot fuel budget.
-pub fn capture(arena: *Arena, root: u32, limit: u32, fuel: u64) ![]u8 {
- return captureWithFuel(arena, root, limit, fuel);
-}
-pub fn captureUnboundedWork(arena: *Arena, root: u32, limit: u32) ![]u8 {
- return captureWithFuel(arena, root, limit, null);
-}
-fn captureWithFuel(arena: *Arena, root: u32, limit: u32, fuel: ?u64) ![]u8 {
- if (limit == 0 or limit > 256) return error.InvalidData;
- const base = arena.roots.items.len;
- try arena.roots.append(arena.allocator, root);
- defer arena.roots.shrinkRetainingCapacity(base);
- var labels: Labels = .{};
- var pending: [513]u32 = undefined;
- pending[0] = labels.intern(root);
- var pending_len: usize = 1;
- var seen = [_]bool{false} ** 513;
- var records: [256]Record = undefined;
- var count: usize = 0;
- var truncated = false;
- while (pending_len > 0) {
- pending_len -= 1;
- const label = pending[pending_len];
- if (seen[label]) continue;
- if (count == limit) {
- truncated = true;
- break;
- }
- const normal = if (fuel) |budget| try reduce.reduce(labels.ids[label], arena, budget) else try reduce.reduceUnbounded(labels.ids[label], arena);
- const node = arena.get(normal).*; // Never retain a pointer over allocation/GC.
- seen[label] = true;
- var record = Record{ .label = label, .kind = 0 };
- switch (node) {
- .leaf => {},
- .stem => |s| {
- record.kind = 1;
- record.left = labels.intern(s.child);
- pending[pending_len] = record.left;
- pending_len += 1;
- },
- .fork => |f| {
- record.kind = 2;
- record.left = labels.intern(f.left);
- record.right = labels.intern(f.right);
- pending[pending_len] = record.right;
- pending[pending_len + 1] = record.left;
- pending_len += 2;
- },
- .app => unreachable,
- }
- records[count] = record;
- count += 1;
- }
- const bytes = try arena.allocator.alloc(u8, 16 + count * 16);
- const header = [_]u32{ 1, 0, @intCast(count), @intFromBool(truncated) };
- for (header, 0..) |word, i| std.mem.writeInt(u32, bytes[i * 4 ..][0..4], word, .little);
- for (records[0..count], 0..) |record, i| {
- const words = [_]u32{ record.label, record.kind, record.left, record.right };
- for (words, 0..) |word, j| std.mem.writeInt(u32, bytes[16 + i * 16 + j * 4 ..][0..4], word, .little);
- }
- return bytes;
-}
-
-fn allocationProbe(allocator: std.mem.Allocator) !void {
- var arena = Arena.init(allocator);
- defer arena.deinit();
- const leaf = try arena.alloc(.leaf);
- var root = leaf;
- for (0..31) |_| root = try arena.alloc(.{ .fork = .{ .left = root, .right = root } });
- arena.gc_enabled = true;
- try arena.setGcPolicy(1, 1);
- const bytes = try captureUnboundedWork(&arena, root, 32);
- defer allocator.free(bytes);
- try std.testing.expectEqual(@as(usize, 16 + 32 * 16), bytes.len);
- try std.testing.expectEqual(@as(u32, 0), std.mem.readInt(u32, bytes[12..16], .little));
- try std.testing.expectEqual(@as(usize, 0), arena.roots.items.len);
-}
-
-test "preview preserves sharing, survives GC, and cleans every allocation failure" {
- try std.testing.checkAllAllocationFailures(std.testing.allocator, allocationProbe, .{});
-}
-
-// Result serialization has a larger, explicitly bounded transport profile.
-// It does not relax the interactive preview's limit or bound reduction work.
-pub fn snapshotUnboundedWork(arena: *Arena, root: u32, limit: u32) ![]u8 {
- if (limit == 0 or limit > 100000) return error.InvalidData;
- const allocator = arena.allocator;
- const base = arena.roots.items.len;
- try arena.roots.append(allocator, root);
- defer arena.roots.shrinkRetainingCapacity(base);
- var ids: std.AutoArrayHashMapUnmanaged(u32, void) = .empty;
- defer ids.deinit(allocator);
- var seen: std.AutoHashMapUnmanaged(u32, void) = .empty;
- defer seen.deinit(allocator);
- var pending: std.ArrayList(u32) = .empty;
- defer pending.deinit(allocator);
- var records: std.ArrayList(Record) = .empty;
- defer records.deinit(allocator);
- try ids.put(allocator, root, {});
- try pending.append(allocator, 0);
- var truncated = false;
- while (pending.pop()) |label| {
- if (seen.contains(label)) continue;
- if (records.items.len == limit) { truncated = true; break; }
- const normal = try reduce.reduceUnbounded(ids.keys()[label], arena);
- const node = arena.get(normal).*;
- try seen.put(allocator, label, {});
- var record = Record{ .label = label, .kind = 0 };
- var children: [2]u32 = undefined;
- const count: usize = switch (node) {
- .leaf => 0,
- .stem => |s| blk: { children[0] = s.child; break :blk 1; },
- .fork => |f| blk: { children = .{ f.left, f.right }; break :blk 2; },
- .app => unreachable,
- };
- record.kind = @intCast(count);
- for (children[0..count], 0..) |child, i| {
- try ids.put(allocator, child, {});
- const key: u32 = @intCast(ids.getIndex(child).?);
- if (i == 0) record.left = key else record.right = key;
- }
- if (count == 2) try pending.append(allocator, record.right);
- if (count >= 1) try pending.append(allocator, record.left);
- try records.append(allocator, record);
- }
- const bytes = try allocator.alloc(u8, 16 + records.items.len * 16);
- const header = [_]u32{ 1, 0, @intCast(records.items.len), @intFromBool(truncated) };
- for (header, 0..) |word, i| std.mem.writeInt(u32, bytes[i * 4 ..][0..4], word, .little);
- for (records.items, 0..) |record, i| {
- const words = [_]u32{ record.label, record.kind, record.left, record.right };
- for (words, 0..) |word, j| std.mem.writeInt(u32, bytes[16 + i * 16 + j * 4 ..][0..4], word, .little);
- }
- return bytes;
-}
-
-fn snapshotAllocationProbe(allocator: std.mem.Allocator) !void {
- var arena = Arena.init(allocator);
- defer arena.deinit();
- var root = try arena.alloc(.leaf);
- for (0..600) |_| root = try arena.alloc(.{ .fork = .{ .left = root, .right = root } });
- const bytes = try snapshotUnboundedWork(&arena, root, 100000);
- defer allocator.free(bytes);
- try std.testing.expectEqual(@as(u32, 601), std.mem.readInt(u32, bytes[8..12], .little));
- try std.testing.expectEqual(@as(u32, 0), std.mem.readInt(u32, bytes[12..16], .little));
- const bounded = try snapshotUnboundedWork(&arena, root, 256);
- defer allocator.free(bounded);
- try std.testing.expectEqual(@as(u32, 1), std.mem.readInt(u32, bounded[12..16], .little));
- try std.testing.expectEqual(@as(usize, 0), arena.roots.items.len);
-}
-test "complete snapshot preserves sharing, bounds transport, and cleans allocation failures" {
- try std.testing.checkAllAllocationFailures(std.testing.allocator, snapshotAllocationProbe, .{});
-}
diff --git a/ext/zig/src/reduce.zig b/ext/zig/src/reduce.zig
@@ -1,235 +0,0 @@
-const Arena = @import("arena.zig").Arena;
-
-pub const ReduceError = error{ FuelExhausted, InvalidApply, OutOfMemory };
-
-const Frame = @import("arena.zig").ReductionFrame;
-
-/// Call-by-need WHNF machine. Every suspended demand lives on the heap, and
-/// every update targets the original application (including shared redexes).
-/// Fuel counts application entries, not observations or continuation dispatch.
-/// Collection is opt-in: callers must register all externally held node IDs.
-pub fn reduce(root: u32, arena: *Arena, fuel: u64) ReduceError!u32 {
- return reduceWithLimit(root, arena, fuel);
-}
-
-/// No work ceiling. Host cancellation must dispose the owning Worker.
-pub fn reduceUnbounded(root: u32, arena: *Arena) ReduceError!u32 {
- return reduceWithLimit(root, arena, null);
-}
-
-fn reduceWithLimit(root: u32, arena: *Arena, fuel: ?u64) ReduceError!u32 {
- // Codecs repeatedly observe already-normal constructors. Do not borrow the
- // continuation buffers for those calls, but never skip a due collection.
- if (!arena.collectionDue() and arena.get(root).* != .app) return root;
- var remaining = fuel orelse 0;
- const bounded = fuel != null;
- // Reuse scratch capacity across codec/IO demands. Move it out while active;
- // no stale redex IDs survive a return, including fuel/allocation failures.
- var frames = arena.reduction_frames;
- arena.reduction_frames = .empty;
- defer {
- frames.clearRetainingCapacity();
- arena.reduction_frames = frames;
- }
- var roots = arena.reduction_roots;
- arena.reduction_roots = .empty;
- defer {
- roots.clearRetainingCapacity();
- arena.reduction_roots = roots;
- }
- var current = root;
-
- while (true) {
- // The only GC safepoint: no unlinked allocation or raw Node pointer is
- // held here. Suspended redexes retain their function and argument trees.
- if (arena.collectionDue()) {
- roots.clearRetainingCapacity();
- try roots.append(arena.allocator, root);
- try roots.append(arena.allocator, current);
- for (frames.items) |frame| try roots.append(arena.allocator, frame.redex);
- try arena.collect(roots.items);
- }
- var frame: Frame = undefined;
- if (arena.get(current).* == .app) {
- if (bounded and remaining == 0) return error.FuelExhausted;
- if (bounded) remaining -= 1;
- frame = .{ .kind = .function, .redex = current };
- current = arena.get(current).app.func;
- } else {
- frame = frames.pop() orelse return current;
- }
-
- // Constructor demands need no suspension or extra machine-loop turn.
- // Only an unevaluated application needs a continuation on the stack.
- // Allocating rules always return to the safepoint before further work.
- demand: while (arena.get(current).* != .app) {
- const orig = frame.redex;
- if (frame.kind == .update) {
- arena.get(orig).* = arena.get(current).*;
- current = orig;
- break :demand;
- }
- const app = arena.get(orig).app;
- switch (frame.kind) {
- .function => switch (arena.get(current).*) {
- .leaf => {
- arena.get(orig).* = .{ .stem = .{ .child = app.arg } };
- current = orig;
- break :demand;
- },
- .stem => |s| {
- arena.get(orig).* = .{ .fork = .{ .left = s.child, .right = app.arg } };
- current = orig;
- break :demand;
- },
- .fork => |f| {
- frame.kind = .discriminator;
- current = f.left;
- },
- .app => unreachable,
- },
- .discriminator => {
- const f = arena.get(app.func).fork;
- switch (arena.get(current).*) {
- .leaf => {
- frame.kind = .update;
- current = f.right;
- },
- .stem => |s| {
- // The generated left application is demanded immediately.
- // Fuse its constructor-only reduction when two further
- // application entries fit in the caller's fuel budget.
- if (!bounded or remaining >= 2) {
- switch (arena.get(s.child).*) {
- .leaf => {
- const b = try arena.allocApp(f.right, app.arg);
- if (bounded) remaining -= 2;
- arena.get(orig).* = .{ .fork = .{ .left = app.arg, .right = b } };
- current = orig;
- break :demand;
- },
- .stem => |k| if (arena.get(k.child).* == .leaf) {
- if (bounded) remaining -= 2;
- frame.kind = .update;
- current = app.arg;
- continue :demand;
- },
- .fork => |k| if (arena.get(k.left).* == .leaf) {
- // (K value arg) is the immediately demanded
- // function of the outer application. Its
- // private thunk need not be allocated.
- const value = arena.get(k.right).*;
- if (value == .fork and arena.get(value.fork.left).* == .leaf) {
- if (bounded) remaining -= 2;
- frame.kind = .update;
- current = value.fork.right;
- continue :demand;
- }
- const b = try arena.allocApp(f.right, app.arg);
- switch (value) {
- .leaf => {
- if (bounded) remaining -= 2;
- arena.get(orig).* = .{ .stem = .{ .child = b } };
- },
- .stem => |v| {
- if (bounded) remaining -= 2;
- arena.get(orig).* = .{ .fork = .{ .left = v.child, .right = b } };
- },
- else => {
- if (bounded) remaining -= 1;
- arena.get(orig).* = .{ .app = .{ .func = k.right, .arg = b } };
- },
- }
- current = orig;
- break :demand;
- },
- else => {},
- }
- }
- const a = try arena.allocApp(s.child, app.arg);
- const b = try arena.allocApp(f.right, app.arg);
- arena.get(orig).* = .{ .app = .{ .func = a, .arg = b } };
- current = orig;
- break :demand;
- },
- .fork => {
- frame.kind = .argument;
- current = app.arg;
- },
- .app => unreachable,
- }
- },
- .argument => {
- const f = arena.get(app.func).fork;
- const branches = arena.get(f.left).fork;
- switch (arena.get(current).*) {
- .leaf => {
- frame.kind = .update;
- current = branches.left;
- },
- .stem => |s| {
- arena.get(orig).* = .{ .app = .{ .func = branches.right, .arg = s.child } };
- current = orig;
- break :demand;
- },
- .fork => |arg| {
- // Triage produces (handler left) right. The inner
- // application is private and immediately demanded;
- // preserve both application-entry charges when fused.
- if (!bounded or remaining >= 2) {
- switch (arena.get(f.right).*) {
- .leaf => {
- if (bounded) remaining -= 2;
- arena.get(orig).* = .{ .fork = .{ .left = arg.left, .right = arg.right } };
- current = orig;
- break :demand;
- },
- .stem => |k| if (arena.get(k.child).* == .leaf) {
- if (bounded) remaining -= 2;
- frame.kind = .update;
- current = arg.left;
- continue :demand;
- },
- .fork => |k| if (arena.get(k.left).* == .leaf) {
- const value = arena.get(k.right).*;
- if (value == .fork and arena.get(value.fork.left).* == .leaf) {
- if (bounded) remaining -= 2;
- frame.kind = .update;
- current = value.fork.right;
- continue :demand;
- }
- switch (value) {
- .leaf => {
- if (bounded) remaining -= 2;
- arena.get(orig).* = .{ .stem = .{ .child = arg.right } };
- },
- .stem => |v| {
- if (bounded) remaining -= 2;
- arena.get(orig).* = .{ .fork = .{ .left = v.child, .right = arg.right } };
- },
- else => {
- if (bounded) remaining -= 1;
- arena.get(orig).* = .{ .app = .{ .func = k.right, .arg = arg.right } };
- },
- }
- current = orig;
- break :demand;
- },
- else => {},
- }
- }
- const inner = try arena.allocApp(f.right, arg.left);
- arena.get(orig).* = .{ .app = .{ .func = inner, .arg = arg.right } };
- current = orig;
- break :demand;
- },
- .app => unreachable,
- }
- },
- .update => unreachable,
- }
- } else {
- try frames.append(arena.allocator, frame);
- }
- }
-}
diff --git a/ext/zig/src/reduce_test.zig b/ext/zig/src/reduce_test.zig
@@ -1,644 +0,0 @@
-const std = @import("std");
-const testing = std.testing;
-const Arena = @import("arena.zig").Arena;
-const tree = @import("tree.zig");
-const machine = @import("reduce.zig");
-const codecs = @import("codecs.zig");
-
-fn stem(a: *Arena, x: u32) !u32 {
- return a.alloc(.{ .stem = .{ .child = x } });
-}
-fn fork(a: *Arena, x: u32, y: u32) !u32 {
- return a.alloc(.{ .fork = .{ .left = x, .right = y } });
-}
-fn app(a: *Arena, x: u32, y: u32) !u32 {
- return a.alloc(.{ .app = .{ .func = x, .arg = y } });
-}
-fn force(a: *Arena, x: u32) !u32 {
- return machine.reduce(x, a, 10_000_000);
-}
-fn equal(a: *Arena, x: u32, y: u32) !void {
- const nx = a.get(try force(a, x)).*;
- const ny = a.get(try force(a, y)).*;
- try testing.expectEqual(std.meta.activeTag(nx), std.meta.activeTag(ny));
- switch (nx) {
- .leaf => {},
- .stem => |s| try equal(a, s.child, ny.stem.child),
- .fork => |f| {
- try equal(a, f.left, ny.fork.left);
- try equal(a, f.right, ny.fork.right);
- },
- .app => unreachable,
- }
-}
-
-test "kernel rules, including lazy distribution and all triage arms" {
- var a = Arena.init(testing.allocator);
- defer a.deinit();
- const t = try a.alloc(.leaf);
- const u = try stem(&a, t);
- const v = try fork(&a, u, t);
- try equal(&a, try app(&a, t, v), try stem(&a, v));
- try equal(&a, try app(&a, u, v), try fork(&a, t, v));
- try equal(&a, try app(&a, try fork(&a, t, v), u), v);
- const s = try fork(&a, try stem(&a, u), t);
- try equal(&a, try app(&a, s, v), try app(&a, try app(&a, u, v), try app(&a, t, v)));
- const f = try fork(&a, try fork(&a, v, u), t);
- try equal(&a, try app(&a, f, t), v);
- try equal(&a, try app(&a, f, u), try app(&a, u, t));
- try equal(&a, try app(&a, f, v), try app(&a, try app(&a, t, u), t));
-}
-
-test "discarded work and constructor children are never demanded" {
- var a = Arena.init(testing.allocator);
- defer a.deinit();
- const t = try a.alloc(.leaf);
- const poison = try app(&a, t, t);
- a.get(poison).* = .{ .app = .{ .func = poison, .arg = t } };
- const k = try fork(&a, t, t);
- const result = try app(&a, k, poison);
- try testing.expectEqual(tree.NodeTag.leaf, std.meta.activeTag(a.get(try machine.reduce(result, &a, 1)).*));
- const constructor = try app(&a, t, poison);
- const r = try machine.reduce(constructor, &a, 1);
- try testing.expectEqual(poison, a.get(r).stem.child);
- try testing.expectError(error.FuelExhausted, machine.reduce(poison, &a, 100));
-}
-
-test "shared redex updated once and fuel failure can be resumed" {
- var a = Arena.init(testing.allocator);
- defer a.deinit();
- const t = try a.alloc(.leaf);
- const shared = try app(&a, t, t);
- try testing.expectError(error.FuelExhausted, machine.reduce(shared, &a, 0));
- _ = try machine.reduce(shared, &a, 1);
- _ = try machine.reduce(shared, &a, 0);
- try testing.expectEqual(t, a.get(shared).stem.child);
-}
-
-test "100000 nested function demands and selected-result updates use heap frames" {
- var a = Arena.init(testing.allocator);
- defer a.deinit();
- const t = try a.alloc(.leaf);
- const identity = try fork(&a, t, t);
- var x = t;
- for (0..100000) |_| x = try app(&a, try fork(&a, t, x), t);
- try testing.expect(a.get(try force(&a, x)).* == .leaf);
- // Left-associated applications of a constant returning itself would be
- // cyclic; use alternating constructor applications and constant selection.
- x = identity;
- for (0..100000) |_| x = try app(&a, x, identity);
- _ = try force(&a, x);
-}
-
-test "non-moving collection preserves explicit roots and reuses dead slots" {
- var a = Arena.init(testing.allocator);
- defer a.deinit();
- const t = try a.alloc(.leaf);
- const kept = try stem(&a, t);
- try a.roots.append(a.allocator, kept);
- for (0..1000) |_| _ = try fork(&a, t, t);
- const before = a.len();
- try a.collect(&.{});
- try testing.expectEqual(@as(usize, 0), a.free.items.len);
- for (0..1000) |_| _ = try stem(&a, t);
- try testing.expectEqual(before, a.len());
- try testing.expectEqual(t, a.get(kept).stem.child);
-}
-
-fn gcWorkload(allocator: std.mem.Allocator) !void {
- var a = Arena.init(allocator);
- defer a.deinit();
- const t = try a.alloc(.leaf);
- const k = try stem(&a, t);
- const direct_id = try fork(&a, try stem(&a, k), k);
- // S (K K) I is extensionally K, but not a constructor shortcut. Keep
- // real distribution allocations here so GC/failure stress cannot disappear
- // when the direct S K b identity is fused by the evaluator.
- const extended_k = try fork(&a, try stem(&a, try fork(&a, t, k)), direct_id);
- const id = try fork(&a, try stem(&a, extended_k), k);
- var x = t;
- for (0..100) |_| x = try app(&a, id, x);
- a.gc_enabled = true;
- try a.setGcPolicy(8, 1);
- try testing.expect(a.get(try force(&a, x)).* == .leaf);
- try testing.expect(a.collections > 1);
- try a.collect(&.{x});
- try testing.expectEqual(@as(usize, a.len() - 1), a.free.items.len);
-}
-
-test "GC during distribution preserves pending demands" {
- try gcWorkload(testing.allocator);
-}
-
-test "allocation failures unwind machine and collector without leaking" {
- try testing.checkAllAllocationFailures(testing.allocator, gcWorkload, .{});
-}
-
-test "deep shared DAG copying uses a growable work stack" {
- var a = Arena.init(testing.allocator);
- defer a.deinit();
- var b = Arena.init(testing.allocator);
- defer b.deinit();
- var x = try a.alloc(.leaf);
- for (0..10000) |_| x = try fork(&a, x, x);
- const copied = try tree.copyTree(a.nodes.items, &b, x);
- try testing.expectEqual(a.len(), b.len());
- try testing.expectEqual(b.get(copied).fork.left, b.get(copied).fork.right);
-}
-
-test "boolean observation forces its child and rejects malformed booleans" {
- var a = Arena.init(testing.allocator);
- defer a.deinit();
- const t = try a.alloc(.leaf);
- const delayed = try app(&a, try fork(&a, t, t), t);
- try testing.expectEqual(true, (try codecs.toBool(&a, try stem(&a, delayed))).?);
- try testing.expectEqual(@as(?bool, null), try codecs.toBool(&a, try stem(&a, try stem(&a, t))));
-}
-
-const handles = @import("handles.zig");
-fn handleWorkload(allocator: std.mem.Allocator) !void {
- var a = Arena.init(allocator);
- defer a.deinit();
- var table: handles.Table = .{};
- defer table.deinit(allocator);
- const t = try a.alloc(.leaf);
- const handle = try table.create(allocator, t);
- try table.release(handle);
- const replacement = try table.create(allocator, t);
- try testing.expectError(error.InvalidHandle, table.get(handle));
- try testing.expectEqual(t, try table.get(replacement));
- for (0..1000) |_| {
- const node = try stem(&a, t);
- const h = try table.create(allocator, node);
- try table.release(h);
- }
- a.roots.clearRetainingCapacity();
- try table.appendRoots(&a);
- try a.collect(&.{});
- try testing.expectEqual(@as(usize, 0), a.free.items.len);
- try testing.expectEqual(@as(usize, 2), table.slots.items.len);
-}
-
-test "handle table failure cleanup and reclamation" {
- try testing.checkAllAllocationFailures(testing.allocator, handleWorkload, .{});
-}
-
-// Independent eager reference on finite, already-normal inputs. No application
-// nodes, mutation or continuation/update machinery are shared with the machine.
-fn reference(a: *Arena, f: u32, x: u32, fuel: *usize) error{ OutOfMemory, FuelExhausted }!u32 {
- if (fuel.* == 0) return error.FuelExhausted;
- fuel.* -= 1;
- return switch (a.get(f).*) {
- .leaf => stem(a, x),
- .stem => |s| fork(a, s.child, x),
- .fork => |b| switch (a.get(b.left).*) {
- .leaf => b.right,
- .stem => |s| reference(a, try reference(a, s.child, x, fuel), try reference(a, b.right, x, fuel), fuel),
- .fork => |branches| switch (a.get(x).*) {
- .leaf => branches.left,
- .stem => |s| reference(a, branches.right, s.child, fuel),
- .fork => |arg| reference(a, try reference(a, b.right, arg.left, fuel), arg.right, fuel),
- .app => unreachable,
- },
- .app => unreachable,
- },
- .app => unreachable,
- };
-}
-
-test "all pairs of depth-two values agree with an independent reference" {
- var a = Arena.init(testing.allocator);
- defer a.deinit();
- const t = try a.alloc(.leaf);
- const small = [_]u32{ t, try stem(&a, t), try fork(&a, t, t) };
- var values: std.ArrayList(u32) = .empty;
- defer values.deinit(a.allocator);
- try values.append(a.allocator, t);
- for (small) |x| {
- try values.append(a.allocator, try stem(&a, x));
- for (small) |y| try values.append(a.allocator, try fork(&a, x, y));
- }
- for (values.items) |f| for (values.items) |x| {
- var fuel: usize = 10000;
- const expected = try reference(&a, f, x, &fuel);
- try equal(&a, try app(&a, f, x), expected);
- try equal(&a, try machine.reduceUnbounded(try app(&a, f, x), &a), expected);
- };
-}
-
-test "result tags are demanded booleans, not arbitrary non-true error tags" {
- var a = Arena.init(testing.allocator);
- defer a.deinit();
- const t = try a.alloc(.leaf);
- const delayed = try app(&a, try fork(&a, t, t), t);
- const payload = try fork(&a, t, t);
- const good = try fork(&a, try stem(&a, delayed), payload);
- try testing.expect((try codecs.unwrapResult(&a, good)).?.ok);
- const malformed = try fork(&a, try fork(&a, t, t), payload);
- try testing.expectEqual(@as(?codecs.UnwrapResult, null), try codecs.unwrapResult(&a, malformed));
-}
-
-test "oversized host numbers return a decode failure rather than trapping" {
- var a = Arena.init(testing.allocator);
- defer a.deinit();
- const max = try codecs.ofNumber(&a, std.math.maxInt(u64));
- try testing.expectEqual(std.math.maxInt(u64), (try codecs.toNumber(&a, max)).?);
- const overflow = try fork(&a, try a.alloc(.leaf), max);
- try testing.expectEqual(@as(?u64, null), try codecs.toNumber(&a, overflow));
-}
-
-test "headroom policy can shrink after a live peak and rejects zero atomically" {
- var a = Arena.init(testing.allocator);
- defer a.deinit();
- try a.setGcPolicy(8, 4);
- var root = try a.alloc(.leaf);
- for (0..100) |_| root = try stem(&a, root);
- try a.collect(&.{root});
- try testing.expectEqual(@as(usize, 404), a.gc_interval);
- try testing.expectError(error.InvalidData, a.setGcPolicy(0, 2));
- try testing.expectError(error.InvalidData, a.setGcPolicy(2, 0));
- try testing.expectEqual(@as(usize, 8), a.gc_min_interval);
- try testing.expectEqual(@as(usize, 4), a.gc_live_multiplier);
- try testing.expectEqual(@as(usize, 404), a.gc_interval);
- a.get(root).* = .leaf;
- try a.collect(&.{root});
- try testing.expectEqual(@as(usize, 8), a.gc_interval);
- try a.setGcPolicy(1, std.math.maxInt(usize));
- try testing.expectEqual(std.math.maxInt(usize), a.gc_interval);
- // Two live nodes exercise saturating multiplication, including on 32-bit.
- const next = try stem(&a, root);
- try a.collect(&.{next});
- try testing.expectEqual(std.math.maxInt(usize), a.gc_interval);
-}
-
-test "collection trims only dead tails and reuses interior holes without moving roots" {
- var a = Arena.init(testing.allocator);
- defer a.deinit();
- const t = try a.alloc(.leaf);
- const hole = try stem(&a, t);
- const root = try fork(&a, t, t);
- for (0..1000) |_| _ = try a.alloc(.leaf);
- const capacity = a.nodes.capacity;
- try a.collect(&.{root});
- try testing.expectEqual(root + 1, a.len());
- try testing.expectEqual(capacity, a.nodes.capacity);
- try testing.expectEqualSlices(u32, &.{hole}, a.free.items);
- try testing.expectEqual(hole, try a.alloc(.leaf));
- try testing.expectEqual(root + 1, try a.alloc(.leaf));
- try testing.expectEqual(t, a.get(root).fork.left);
- try a.collect(&.{});
- try testing.expectEqual(@as(u32, 0), a.len());
- try testing.expectEqual(@as(usize, 0), a.free.items.len);
- try testing.expectEqual(@as(u32, 0), try a.alloc(.leaf));
-}
-
-test "constructor fast return still honors collection with zero fuel" {
- var a = Arena.init(testing.allocator);
- defer a.deinit();
- const t = try a.alloc(.leaf);
- const root = try stem(&a, t);
- for (0..1000) |_| _ = try a.alloc(.leaf);
- try a.setGcPolicy(1, 1);
- a.gc_enabled = true;
- try testing.expectEqual(root, try machine.reduce(root, &a, 0));
- try testing.expectEqual(@as(usize, 1), a.collections);
- try testing.expectEqual(t, a.get(root).stem.child);
- try testing.expectEqual(@as(u32, 2), a.len());
-}
-
-test "suspended demands preserve fuel boundaries and clear retained scratch on failure" {
- var a = Arena.init(testing.allocator);
- defer a.deinit();
- const t = try a.alloc(.leaf);
- const delayed_function = try app(&a, t, t);
- const root = try app(&a, delayed_function, t);
- try testing.expectError(error.FuelExhausted, machine.reduce(root, &a, 1));
- try testing.expectEqual(@as(usize, 0), a.reduction_frames.items.len);
- try testing.expectEqual(@as(usize, 0), a.reduction_roots.items.len);
- try testing.expect(a.reduction_frames.capacity > 0);
- _ = try machine.reduce(root, &a, 2);
- try testing.expectEqual(t, a.get(root).fork.left);
- // A delayed discriminator and a divergent, discarded argument.
- const delayed_leaf = try app(&a, try fork(&a, t, t), t);
- const poison = try app(&a, t, t);
- a.get(poison).* = .{ .app = .{ .func = poison, .arg = t } };
- const selected = try app(&a, try fork(&a, delayed_leaf, t), poison);
- _ = try machine.reduce(selected, &a, 2);
- try testing.expect(a.get(selected).* == .leaf);
- // A delayed selected result takes a second application entry.
- const delayed_result = try app(&a, t, t);
- const select = try app(&a, try fork(&a, t, delayed_result), poison);
- try testing.expectError(error.FuelExhausted, machine.reduce(select, &a, 1));
- try testing.expectEqual(@as(usize, 0), a.reduction_frames.items.len);
- _ = try machine.reduce(select, &a, 2);
- try testing.expectEqual(t, a.get(select).stem.child);
- // Delayed triage argument, then the stem arm creates another application.
- const triage = try fork(&a, try fork(&a, t, t), t);
- const argument = try app(&a, t, t);
- const inspect = try app(&a, triage, argument);
- try testing.expectError(error.FuelExhausted, machine.reduce(inspect, &a, 2));
- _ = try machine.reduce(inspect, &a, 1);
- try testing.expectEqual(t, a.get(inspect).stem.child);
-}
-
-test "collector allocation failure is retryable without losing live nodes or free slots" {
- // Fail each allocation/growth in the first collection, then retry in the
- // same arena. This also exercises dirty marks/grey left by failed marking.
- for (0..12) |offset| {
- var failing = testing.FailingAllocator.init(testing.allocator, .{});
- var a = Arena.init(failing.allocator());
- defer a.deinit();
- const t = try a.alloc(.leaf);
- _ = try a.alloc(.leaf); // interior hole
- var root = t;
- // Traversing right leaves many left demands on the grey stack;
- // each stem also shares t, exercising mark-on-enqueue deduplication.
- for (0..100) |_| root = try fork(&a, try stem(&a, t), root);
- for (0..100) |_| _ = try a.alloc(.leaf);
- const before = a.len();
- failing.fail_index = failing.alloc_index + offset;
- failing.resize_fail_index = failing.resize_index;
- a.collect(&.{root}) catch |err| {
- try testing.expectEqual(error.OutOfMemory, err);
- try testing.expectEqual(before, a.len());
- try testing.expectEqual(@as(usize, 0), a.free.items.len);
- try testing.expectEqual(@as(usize, 0), a.collections);
- };
- failing.fail_index = std.math.maxInt(usize);
- failing.resize_fail_index = std.math.maxInt(usize);
- try a.collect(&.{root});
- try testing.expectEqual(@as(usize, 201), a.last_live);
- try testing.expectEqualSlices(u32, &.{1}, a.free.items);
- var cursor = root;
- for (0..100) |_| {
- try testing.expectEqual(t, a.get(a.get(cursor).fork.left).stem.child);
- cursor = a.get(cursor).fork.right;
- }
- try testing.expectEqual(t, cursor);
- }
-}
-
-test "continuation allocation failure returns reusable scratch and evaluation can resume" {
- var failing = testing.FailingAllocator.init(testing.allocator, .{});
- var a = Arena.init(failing.allocator());
- defer a.deinit();
- const t = try a.alloc(.leaf);
- var root = t;
- for (0..1000) |_| root = try app(&a, try fork(&a, t, root), t);
- failing.fail_index = failing.alloc_index + 1;
- failing.resize_fail_index = failing.resize_index;
- try testing.expectError(error.OutOfMemory, force(&a, root));
- try testing.expectEqual(@as(usize, 0), a.reduction_frames.items.len);
- try testing.expectEqual(@as(usize, 0), a.reduction_roots.items.len);
- failing.fail_index = std.math.maxInt(usize);
- failing.resize_fail_index = std.math.maxInt(usize);
- try testing.expect(a.get(try force(&a, root)).* == .leaf);
- try testing.expectEqual(@as(usize, 0), a.reduction_frames.items.len);
-}
-
-// Small recursive oracle: the literal unfused rules, with one fuel charge for
-// each application entry. Test inputs are shallow; this is not a runtime path.
-fn unfused(root: u32, a: *Arena, fuel: *u64) machine.ReduceError!u32 {
- const node = a.get(root).*;
- if (node != .app) return root;
- if (fuel.* == 0) return error.FuelExhausted;
- fuel.* -= 1;
- const function = a.get(try unfused(node.app.func, a, fuel)).*;
- switch (function) {
- .leaf => a.get(root).* = .{ .stem = .{ .child = node.app.arg } },
- .stem => |s| a.get(root).* = .{ .fork = .{ .left = s.child, .right = node.app.arg } },
- .fork => |f| {
- const discriminator = a.get(try unfused(f.left, a, fuel)).*;
- switch (discriminator) {
- .leaf => {
- const value = try unfused(f.right, a, fuel);
- a.get(root).* = a.get(value).*;
- },
- .stem => |s| {
- const left = try app(a, s.child, node.app.arg);
- const right = try app(a, f.right, node.app.arg);
- a.get(root).* = .{ .app = .{ .func = left, .arg = right } };
- return unfused(root, a, fuel);
- },
- .fork => |branches| {
- const argument = a.get(try unfused(node.app.arg, a, fuel)).*;
- switch (argument) {
- .leaf => {
- const value = try unfused(branches.left, a, fuel);
- a.get(root).* = a.get(value).*;
- },
- .stem => |s| {
- a.get(root).* = .{ .app = .{ .func = branches.right, .arg = s.child } };
- return unfused(root, a, fuel);
- },
- .fork => |v| {
- const inner = try app(a, f.right, v.left);
- a.get(root).* = .{ .app = .{ .func = inner, .arg = v.right } };
- return unfused(root, a, fuel);
- },
- .app => unreachable,
- }
- },
- .app => unreachable,
- }
- },
- .app => unreachable,
- }
- return root;
-}
-
-fn equalUnfused(a: *Arena, x: u32, b: *Arena, y: u32) !void {
- var fuel: u64 = 10000;
- const expected = a.get(try unfused(x, a, &fuel)).*;
- const actual = b.get(try machine.reduce(y, b, 10000)).*;
- try testing.expectEqual(std.meta.activeTag(expected), std.meta.activeTag(actual));
- switch (expected) {
- .leaf => {},
- .stem => |s| try equalUnfused(a, s.child, b, actual.stem.child),
- .fork => |f| {
- try equalUnfused(a, f.left, b, actual.fork.left);
- try equalUnfused(a, f.right, b, actual.fork.right);
- },
- .app => unreachable,
- }
-}
-
-fn fusionFixture(a: *Arena, variant: usize, argument: usize, triage: bool) !u32 {
- const t = try a.alloc(.leaf);
- const k = try stem(a, t);
- const payload = try fork(a, k, t);
- const value = switch (variant) {
- 0 => t,
- 1 => k,
- 2 => try fork(a, t, t),
- 3 => try fork(a, t, k),
- 4 => try fork(a, t, try fork(a, t, payload)),
- 5 => try fork(a, t, try app(a, try fork(a, t, k), t)),
- // Negative controls: delayed discriminator, non-K stem, non-K fork.
- 6 => try fork(a, try app(a, try fork(a, t, t), t), k),
- 7 => try stem(a, k),
- 8 => try fork(a, t, try fork(a, k, t)),
- else => unreachable,
- };
- const x = switch (argument) {
- 0 => t,
- 1 => k,
- 2 => payload,
- 3 => try app(a, try fork(a, t, payload), t),
- else => unreachable,
- };
- if (triage) {
- const pair = try fork(a, x, k);
- const input = if (argument == 3) try app(a, try fork(a, t, pair), t) else pair;
- return app(a, try fork(a, try fork(a, k, t), value), input);
- }
- return app(a, try fork(a, try stem(a, value), t), x);
-}
-
-test "fused distribution and triage agree with literal rules at every small fuel boundary" {
- for ([_]bool{ false, true }) |triage| for (0..9) |variant| for (0..4) |argument| for (0..20) |budget| {
- var expected = Arena.init(testing.allocator);
- defer expected.deinit();
- var actual = Arena.init(testing.allocator);
- defer actual.deinit();
- const x = try fusionFixture(&expected, variant, argument, triage);
- const y = try fusionFixture(&actual, variant, argument, triage);
- var fuel: u64 = budget;
- const reference_result = unfused(x, &expected, &fuel);
- if (reference_result) |_| {
- _ = try machine.reduce(y, &actual, budget);
- try equalUnfused(&expected, x, &actual, y);
- } else |err| {
- try testing.expectEqual(error.FuelExhausted, err);
- try testing.expectError(error.FuelExhausted, machine.reduce(y, &actual, budget));
- // Partial graph shapes can differ; both must remain resumable.
- try equalUnfused(&expected, x, &actual, y);
- }
- };
-}
-
-test "fused identity and constant selection discard divergent branches" {
- var a = Arena.init(testing.allocator);
- defer a.deinit();
- const t = try a.alloc(.leaf);
- const poison = try app(&a, t, t);
- a.get(poison).* = .{ .app = .{ .func = poison, .arg = poison } };
- const k = try stem(&a, t);
- const identity = try fork(&a, try stem(&a, k), poison);
- const result = try app(&a, identity, t);
- const before = a.allocations;
- _ = try machine.reduce(result, &a, 3);
- try testing.expect(a.get(result).* == .leaf);
- try testing.expectEqual(before, a.allocations);
- const constant = try fork(&a, try stem(&a, try fork(&a, t, try fork(&a, t, k))), poison);
- const discard = try app(&a, constant, poison);
- const allocated = a.allocations;
- _ = try machine.reduce(discard, &a, 3);
- try testing.expectEqual(t, a.get(discard).stem.child);
- try testing.expectEqual(allocated, a.allocations);
- // Selecting divergence must still demand it, rather than return a thunk.
- const demand = try app(&a, identity, poison);
- try testing.expectError(error.FuelExhausted, machine.reduce(demand, &a, 100));
-}
-
-test "fused selection updates shared demanded payloads and survives collection" {
- var a = Arena.init(testing.allocator);
- defer a.deinit();
- const t = try a.alloc(.leaf);
- const payload = try app(&a, try fork(&a, t, t), t);
- const poison = try app(&a, t, t);
- a.get(poison).* = .{ .app = .{ .func = poison, .arg = poison } };
- const constant = try fork(&a, t, try fork(&a, t, payload));
- const root = try app(&a, try fork(&a, try stem(&a, constant), poison), poison);
- try a.roots.append(a.allocator, payload); // independently retained alias
- a.gc_enabled = true;
- try a.setGcPolicy(1, 1);
- _ = try machine.reduce(root, &a, 4);
- try testing.expect(a.get(root).* == .leaf);
- try testing.expect(a.get(payload).* == .leaf);
- try a.collect(&.{root});
- try testing.expect(a.get(root).* == .leaf);
- try testing.expect(a.get(payload).* == .leaf);
-}
-
-test "scalar application allocation initializes dirty recycled slots completely" {
- var a = Arena.init(testing.allocator);
- defer a.deinit();
- const t = try a.alloc(.leaf);
- const hole = try app(&a, t, t);
- a.get(hole).* = .{ .app = .{ .func = hole, .arg = hole } };
- const kept = try stem(&a, t);
- try a.collect(&.{kept});
- try testing.expectEqual(hole, try a.allocApp(kept, t));
- try testing.expectEqual(kept, a.get(hole).app.func);
- try testing.expectEqual(t, a.get(hole).app.arg);
- _ = try machine.reduce(hole, &a, 1);
- try testing.expectEqual(t, a.get(hole).fork.left);
- try testing.expectEqual(t, a.get(hole).fork.right);
- try a.collect(&.{kept});
- try testing.expectEqual(hole, try a.alloc(.leaf));
- try testing.expect(a.get(hole).* == .leaf);
-}
-
-test "fused triage preserves lazy children and shared updates with forced GC" {
- for ([_]bool{ false, true }) |gc| for (0..5) |variant| {
- var a = Arena.init(testing.allocator);
- defer a.deinit();
- const t = try a.alloc(.leaf);
- const k = try stem(&a, t);
- const poison = try app(&a, t, t);
- a.get(poison).* = .{ .app = .{ .func = poison, .arg = poison } };
- const payload = try app(&a, try fork(&a, t, t), t);
- const handler = switch (variant) {
- 0 => t,
- 1 => k,
- 2 => try fork(&a, t, t),
- 3 => try fork(&a, t, k),
- 4 => try fork(&a, t, try fork(&a, t, payload)),
- else => unreachable,
- };
- const left = if (variant == 1) payload else poison;
- const pair = try fork(&a, left, poison);
- const root = try app(&a, try fork(&a, try fork(&a, poison, poison), handler), pair);
- try a.roots.append(a.allocator, payload);
- a.gc_enabled = gc;
- try a.setGcPolicy(1, 1);
- const before = a.allocations;
- _ = try machine.reduce(root, &a, if (variant == 1 or variant == 4) 4 else 3);
- if (gc) {
- try testing.expect(a.collections > 0);
- try a.collect(&.{root});
- } else try testing.expectEqual(before, a.allocations);
- switch (variant) {
- 0 => {
- try testing.expectEqual(poison, a.get(root).fork.left);
- try testing.expectEqual(poison, a.get(root).fork.right);
- },
- 1, 4 => {
- try testing.expect(a.get(root).* == .leaf);
- try testing.expect(a.get(payload).* == .leaf);
- },
- 2 => try testing.expectEqual(poison, a.get(root).stem.child),
- 3 => {
- try testing.expect(a.get(a.get(root).fork.left).* == .leaf);
- try testing.expectEqual(poison, a.get(root).fork.right);
- },
- else => unreachable,
- }
- if (variant != 1 and variant != 4) try testing.expect(a.get(payload).* == .app);
- };
-}
-
-test "fused triage still demands a selected divergent child" {
- var a = Arena.init(testing.allocator);
- defer a.deinit();
- const t = try a.alloc(.leaf);
- const poison = try app(&a, t, t);
- a.get(poison).* = .{ .app = .{ .func = poison, .arg = poison } };
- const handler = try stem(&a, t);
- const root = try app(&a, try fork(&a, try fork(&a, t, t), handler), try fork(&a, poison, t));
- a.gc_enabled = true;
- try a.setGcPolicy(1, 1);
- try testing.expectError(error.FuelExhausted, machine.reduce(root, &a, 100));
- try testing.expectError(error.FuelExhausted, machine.reduce(root, &a, 100));
-}
diff --git a/ext/zig/src/source_text.zig b/ext/zig/src/source_text.zig
@@ -1,176 +0,0 @@
-//! Unicode scalar boundary for source IO and the browser adapter. Binary/string ABI codecs remain byte-oriented.
-const std = @import("std");
-const Arena = @import("arena.zig").Arena;
-const codecs = @import("codecs.zig");
-const reduce = @import("reduce.zig");
-
-pub fn fromUtf8(arena: *Arena, bytes: []const u8) !u32 {
- const view = std.unicode.Utf8View.init(bytes) catch return error.InvalidUtf8;
- var iter = view.iterator();
- var scalars: std.ArrayList(u32) = .empty;
- defer scalars.deinit(arena.allocator);
- while (iter.nextCodepoint()) |scalar| {
- try scalars.append(arena.allocator, try codecs.ofNumber(arena, scalar));
- }
- return codecs.ofList(arena, scalars.items);
-}
-
-// Match the Haskell source boundary's canonical-tree check, not merely its
-// numeric interpretation. Reject redundant high zero bits, non-Boolean bits,
-// overlong integers, surrogates and values beyond the Unicode scalar range.
-fn scalarNumber(arena: *Arena, input: u32, unbounded: bool) !u21 {
- var current = input;
- var value: u32 = 0;
- var bits: u5 = 0;
- var high_bit: u32 = 0;
- while (true) {
- const node = arena.get(try observe(arena, current, unbounded)).*;
- switch (node) {
- .leaf => {
- if (bits != 0 and high_bit == 0) return error.InvalidScalarList;
- if (value > 0x10ffff or (value >= 0xd800 and value <= 0xdfff)) return error.InvalidScalarList;
- return @intCast(value);
- },
- .fork => |pair| {
- if (bits == 21) return error.InvalidScalarList;
- const bit = arena.get(try observe(arena, pair.left, unbounded)).*;
- high_bit = switch (bit) {
- .leaf => 0,
- .stem => |s| if (arena.get(try observe(arena, s.child, unbounded)).* == .leaf) 1 else return error.InvalidScalarList,
- else => return error.InvalidScalarList,
- };
- value |= high_bit << bits;
- bits += 1;
- current = pair.right;
- },
- else => return error.InvalidScalarList,
- }
- }
-}
-
-pub fn toUtf8(arena: *Arena, input: u32) ![]u8 {
- return toUtf8Bounded(arena, input, std.math.maxInt(usize));
-}
-
-// A scalar-count ceiling, not a fuel/deadline limit. Do not demand elements
-// beyond the ceiling; still inspect the spine to distinguish exact length.
-fn observe(arena: *Arena, input: u32, unbounded: bool) !u32 {
- return if (unbounded) reduce.reduceUnbounded(input, arena) else reduce.reduce(input, arena, 1_000_000);
-}
-
-pub fn toUtf8UnboundedWork(arena: *Arena, input: u32, limit: usize) ![]u8 {
- return decode(arena, input, limit, true);
-}
-
-pub fn toUtf8Bounded(arena: *Arena, input: u32, limit: usize) ![]u8 {
- return decode(arena, input, limit, false);
-}
-
-fn decode(arena: *Arena, input: u32, limit: usize, unbounded: bool) ![]u8 {
- const base = arena.roots.items.len;
- defer arena.roots.shrinkRetainingCapacity(base);
- try arena.roots.append(arena.allocator, input);
- var bytes: std.ArrayList(u8) = .empty;
- errdefer bytes.deinit(arena.allocator);
- var current = input;
- var count: usize = 0;
- while (true) {
- const node = arena.get(try observe(arena, current, unbounded)).*;
- switch (node) {
- .leaf => return bytes.toOwnedSlice(arena.allocator),
- .fork => |pair| {
- if (count == limit) return error.ScalarLimit;
- count += 1;
- const scalar = try scalarNumber(arena, pair.left, unbounded);
- var encoded: [4]u8 = undefined;
- const len = try std.unicode.utf8Encode(scalar, &encoded);
- try bytes.appendSlice(arena.allocator, encoded[0..len]);
- current = pair.right;
- },
- else => return error.InvalidScalarList,
- }
- }
-}
-
-test "source scalar codec preserves exact UTF-8 including NUL, CRLF and noncharacters" {
- var arena = Arena.init(std.testing.allocator);
- defer arena.deinit();
- const bytes = "\x00a\r\né😀\u{ffff}\u{10ffff}\u{feff}";
- const scalars = try fromUtf8(&arena, bytes);
- var list = (try codecs.toList(&arena, scalars)).?;
- defer list.deinit(arena.allocator);
- const expected = [_]u64{ 0, 0x61, 13, 10, 0xe9, 0x1f600, 0xffff, 0x10ffff, 0xfeff };
- try std.testing.expectEqual(expected.len, list.items.len);
- for (list.items, expected) |item, n| try std.testing.expectEqual(n, (try codecs.toNumber(&arena, item)).?);
- arena.gc_enabled = true;
- try arena.setGcPolicy(1, 1);
- const result = try toUtf8(&arena, scalars);
- defer arena.allocator.free(result);
- try std.testing.expectEqualStrings(bytes, result);
- try std.testing.expect(arena.collections > 0);
- try std.testing.expectEqual(@as(usize, 0), arena.roots.items.len);
-}
-
-test "source scalar codec rejects malformed UTF-8 and scalar trees" {
- var arena = Arena.init(std.testing.allocator);
- defer arena.deinit();
- for ([_][]const u8{ "\x80", "\xc0\xaf", "\xe2\x82", "\xed\xa0\x80", "\xf4\x90\x80\x80", "\xff" }) |bytes| {
- try std.testing.expectError(error.InvalidUtf8, fromUtf8(&arena, bytes));
- }
- for ([_]u64{ 0xd800, 0xdfff, 0x110000, std.math.maxInt(u64) }) |n| {
- const list = try codecs.ofList(&arena, &.{try codecs.ofNumber(&arena, n)});
- try std.testing.expectError(error.InvalidScalarList, toUtf8(&arena, list));
- }
- const leaf = try arena.alloc(.leaf);
- const zero_with_bit = try arena.alloc(.{ .fork = .{ .left = leaf, .right = leaf } });
- const bad_bit = try arena.alloc(.{ .stem = .{ .child = zero_with_bit } });
- const malformed = try arena.alloc(.{ .fork = .{ .left = bad_bit, .right = leaf } });
- for ([_]u32{ zero_with_bit, malformed, bad_bit }) |n| {
- const list = try codecs.ofList(&arena, &.{n});
- try std.testing.expectError(error.InvalidScalarList, toUtf8(&arena, list));
- }
- try std.testing.expectError(error.InvalidScalarList, toUtf8(&arena, bad_bit));
- // Failure after successful UTF-8 output must free the partial buffer too.
- const partial = try codecs.ofList(&arena, &.{ try codecs.ofNumber(&arena, 65), try codecs.ofNumber(&arena, 0xd800) });
- try std.testing.expectError(error.InvalidScalarList, toUtf8(&arena, partial));
-}
-
-test "source scalar codec accepts lazy scalar numerals" {
- var arena = Arena.init(std.testing.allocator);
- defer arena.deinit();
- const leaf = try arena.alloc(.leaf);
- const scalar = try codecs.ofNumber(&arena, 0x1f600);
- const constant = try arena.alloc(.{ .fork = .{ .left = leaf, .right = scalar } });
- const delayed = try arena.alloc(.{ .app = .{ .func = constant, .arg = leaf } });
- const list = try codecs.ofList(&arena, &.{delayed});
- arena.gc_enabled = true;
- try arena.setGcPolicy(1, 1);
- const bytes = try toUtf8(&arena, list);
- defer arena.allocator.free(bytes);
- try std.testing.expectEqualStrings("😀", bytes);
-}
-
-fn allocationProbe(allocator: std.mem.Allocator) !void {
- var arena = Arena.init(allocator);
- defer arena.deinit();
- const scalars = try fromUtf8(&arena, "aé😀");
- const bytes = try toUtf8(&arena, scalars);
- defer allocator.free(bytes);
- try std.testing.expectEqualStrings("aé😀", bytes);
-}
-
-test "source scalar codec cleans up on every allocation failure" {
- try std.testing.checkAllAllocationFailures(std.testing.allocator, allocationProbe, .{});
-}
-
-
-test "bounded scalar encoding handles exact limits and unwinds partial output" {
- var arena = Arena.init(std.testing.allocator);
- defer arena.deinit();
- const input = try fromUtf8(&arena, "é😀");
- const bytes = try toUtf8Bounded(&arena, input, 2);
- defer arena.allocator.free(bytes);
- try std.testing.expectEqualStrings("é😀", bytes);
- try std.testing.expectError(error.ScalarLimit, toUtf8Bounded(&arena, input, 1));
- try std.testing.expectEqual(@as(usize, 0), arena.roots.items.len);
-}
diff --git a/ext/zig/src/ternary.zig b/ext/zig/src/ternary.zig
@@ -1,27 +0,0 @@
-const std = @import("std");
-const tree = @import("tree.zig");
-const Arena = @import("arena.zig").Arena;
-
-pub fn parseTernary(source: []const u8, arena: *Arena) !u32 {
- var pos: usize = 0;
- return try parseTernaryRec(source, &pos, arena);
-}
-
-fn parseTernaryRec(source: []const u8, pos: *usize, arena: *Arena) !u32 {
- if (pos.* >= source.len) return error.UnexpectedEnd;
- const ch = source[pos.*];
- pos.* += 1;
- return switch (ch) {
- '0' => try arena.alloc(.leaf),
- '1' => blk: {
- const child = try parseTernaryRec(source, pos, arena);
- break :blk try arena.alloc(.{ .stem = .{ .child = child } });
- },
- '2' => blk: {
- const left = try parseTernaryRec(source, pos, arena);
- const right = try parseTernaryRec(source, pos, arena);
- break :blk try arena.alloc(.{ .fork = .{ .left = left, .right = right } });
- },
- else => error.InvalidChar,
- };
-}
diff --git a/ext/zig/src/tree.zig b/ext/zig/src/tree.zig
@@ -1,181 +0,0 @@
-const std = @import("std");
-
-pub const NodeTag = enum(u8) {
- leaf = 0,
- stem = 1,
- fork = 2,
- app = 3,
-};
-
-pub const Node = union(NodeTag) {
- leaf,
- stem: struct { child: u32 },
- fork: struct { left: u32, right: u32 },
- app: struct { func: u32, arg: u32 },
-
- pub fn leafNode() Node {
- return .leaf;
- }
-
- pub fn stemNode(child: u32) Node {
- return .{ .stem = .{ .child = child } };
- }
-
- pub fn forkNode(left: u32, right: u32) Node {
- return .{ .fork = .{ .left = left, .right = right } };
- }
-
- pub fn appNode(func: u32, arg: u32) Node {
- return .{ .app = .{ .func = func, .arg = arg } };
- }
-};
-
-pub const NodePool = struct {
- allocator: std.mem.Allocator,
- nodes: std.ArrayList(Node),
-
- pub fn init(allocator: std.mem.Allocator) NodePool {
- return .{
- .allocator = allocator,
- .nodes = .empty,
- };
- }
-
- pub fn deinit(self: *NodePool) void {
- self.nodes.deinit(self.allocator);
- }
-
- pub fn push(self: *NodePool, node: Node) !u32 {
- const idx: u32 = @intCast(self.nodes.items.len);
- try self.nodes.append(self.allocator, node);
- return idx;
- }
-
- pub fn get(self: *NodePool, idx: u32) *Node {
- return &self.nodes.items[idx];
- }
-
- pub fn len(self: *const NodePool) u32 {
- return @intCast(self.nodes.items.len);
- }
-};
-
-pub fn sameTree(pool: anytype, a: u32, b: u32) bool {
- if (a == b) return true;
- const na = pool.nodes.items[a];
- const nb = pool.nodes.items[b];
- if (@intFromEnum(na) != @intFromEnum(nb)) return false;
- return switch (na) {
- .leaf => true,
- .stem => |sa| sameTree(pool, sa.child, nb.stem.child),
- .fork => |fa| sameTree(pool, fa.left, nb.fork.left) and sameTree(pool, fa.right, nb.fork.right),
- .app => |aa| sameTree(pool, aa.func, nb.app.func) and sameTree(pool, aa.arg, nb.app.arg),
- };
-}
-
-const DstArena = @import("arena.zig").Arena;
-
-/// Iterative deep-copy of a DAG from `src` into `dst`. Uses an explicit
-/// heap-allocated stack so that very deep (e.g. long list) trees do not
-/// blow the native C stack. Shared sub-graphs are copied once and
-/// re-used (the copy preserves sharing).
-pub fn copyTree(src: []const Node, dst: *DstArena, root: u32) !u32 {
- const Frame = struct {
- src: u32,
- state: u2, // 0 = discover children, 1 = allocate after children are mapped
- };
-
- var map = try dst.allocator.alloc(u32, src.len);
- defer dst.allocator.free(map);
- @memset(std.mem.sliceAsBytes(map), 0xFF);
-
- var stack: std.ArrayList(Frame) = .empty;
- defer stack.deinit(dst.allocator);
- try stack.append(dst.allocator, .{ .src = root, .state = 0 });
-
- while (stack.items.len > 0) {
- const frame = &stack.items[stack.items.len - 1];
- const src_idx = frame.src;
-
- if (map[src_idx] != 0xFFFFFFFF) {
- _ = stack.pop();
- continue;
- }
-
- if (frame.state == 0) {
- frame.state = 1;
- const node = src[src_idx];
- switch (node) {
- .leaf => {}, // no children, fall through to allocation next iteration
- .stem => |s| {
- if (map[s.child] == 0xFFFFFFFF) {
- try stack.append(dst.allocator, .{ .src = s.child, .state = 0 });
- }
- },
- .fork => |f| {
- const need_left = map[f.left] == 0xFFFFFFFF;
- const need_right = map[f.right] == 0xFFFFFFFF;
- if (need_right) {
- try stack.append(dst.allocator, .{ .src = f.right, .state = 0 });
- }
- if (need_left) {
- try stack.append(dst.allocator, .{ .src = f.left, .state = 0 });
- }
- },
- .app => |a| {
- const need_func = map[a.func] == 0xFFFFFFFF;
- const need_arg = map[a.arg] == 0xFFFFFFFF;
- if (need_arg) {
- try stack.append(dst.allocator, .{ .src = a.arg, .state = 0 });
- }
- if (need_func) {
- try stack.append(dst.allocator, .{ .src = a.func, .state = 0 });
- }
- },
- }
- } else {
- // All children mapped; allocate this node in dst.
- const node = src[src_idx];
- const dst_idx = switch (node) {
- .leaf => try dst.alloc(.leaf),
- .stem => |s| try dst.alloc(.{ .stem = .{ .child = map[s.child] } }),
- .fork => |f| try dst.alloc(.{ .fork = .{ .left = map[f.left], .right = map[f.right] } }),
- .app => |a| try dst.alloc(.{ .app = .{ .func = map[a.func], .arg = map[a.arg] } }),
- };
- map[src_idx] = dst_idx;
- _ = stack.pop();
- }
- }
-
- return map[root];
-}
-
-pub fn formatTree(writer: anytype, pool: anytype, idx: u32, depth: usize) !void {
- if (depth > 200) {
- try writer.writeAll("...");
- return;
- }
- const node = pool.nodes.items[idx];
- switch (node) {
- .leaf => try writer.writeAll("Leaf"),
- .stem => |s| {
- try writer.writeAll("Stem(");
- try formatTree(writer, pool, s.child, depth + 1);
- try writer.writeAll(")");
- },
- .fork => |f| {
- try writer.writeAll("Fork(");
- try formatTree(writer, pool, f.left, depth + 1);
- try writer.writeAll(", ");
- try formatTree(writer, pool, f.right, depth + 1);
- try writer.writeAll(")");
- },
- .app => |a| {
- try writer.writeAll("App(");
- try formatTree(writer, pool, a.func, depth + 1);
- try writer.writeAll(", ");
- try formatTree(writer, pool, a.arg, depth + 1);
- try writer.writeAll(")");
- },
- }
-}
diff --git a/ext/zig/tests/c_abi_append_test.c b/ext/zig/tests/c_abi_append_test.c
@@ -1,86 +0,0 @@
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <time.h>
-#include "helpers.h"
-
-static uint8_t *read_file(const char *path, size_t *out_len) {
- FILE *f = fopen(path, "rb");
- if (!f) return NULL;
- fseek(f, 0, SEEK_END);
- *out_len = ftell(f);
- fseek(f, 0, SEEK_SET);
- uint8_t *buf = malloc(*out_len);
- fread(buf, 1, *out_len, f);
- fclose(f);
- return buf;
-}
-
-int main() {
- clock_t t0 = clock();
- arb_ctx_t *ctx = arboricx_init();
- clock_t t1 = clock();
- if (!ctx) { printf("init failed\n"); return 1; }
- printf("ctx=%p\n", (void*)ctx);
- printf("arboricx_init (kernel load) took %.3f ms\n", (double)(t1 - t0) * 1000.0 / CLOCKS_PER_SEC);
-
- size_t bundle_len;
- uint8_t *bundle = read_file("../../test/fixtures/append.arboricx", &bundle_len);
- if (!bundle) { printf("bundle not found\n"); return 1; }
- printf("bundle size=%zu\n", bundle_len);
-
- arb_handle_t bundle_tree = checked_arb_of_bytes(ctx, bundle, bundle_len);
- printf("bundle_tree=%" PRIu64 "\n", bundle_tree);
-
- arb_handle_t tag = checked_arb_of_number(ctx, 1);
- printf("tag=%" PRIu64 "\n", tag);
-
- arb_handle_t arg1 = checked_arb_of_string(ctx, "Hello, ");
- arb_handle_t arg2 = checked_arb_of_string(ctx, "world!");
- printf("arg1=%" PRIu64 " arg2=%" PRIu64 "\n", arg1, arg2);
-
- arb_handle_t list_tail = checked_arb_fork(ctx, arg2, checked_arb_leaf(ctx));
- arb_handle_t args_list = checked_arb_fork(ctx, arg1, list_tail);
- printf("args_list=%" PRIu64 "\n", args_list);
-
- arb_handle_t app0 = checked_arb_app(ctx, checked_arb_kernel_root(ctx), tag);
- arb_handle_t app1 = checked_arb_app(ctx, app0, bundle_tree);
- arb_handle_t app2 = checked_arb_app(ctx, app1, args_list);
- printf("app2=%" PRIu64 "\n", app2);
-
- printf("reducing...\n");
- clock_t t2 = clock();
- arb_handle_t result = checked_arb_reduce(ctx, app2, 1000000000ULL);
- clock_t t3 = clock();
- printf("arb_reduce took %.3f ms, result=%" PRIu64 "\n", (double)(t3 - t2) * 1000.0 / CLOCKS_PER_SEC, result);
-
- int ok;
- arb_handle_t value, rest;
- if (!(arb_unwrap_result(ctx, result, &ok, &value, &rest) == ARB_OK)) {
- printf("unwrap_result failed\n");
- return 1;
- }
- printf("ok=%d value=%" PRIu64 "\n", ok, value);
-
- uint64_t htag;
- arb_handle_t payload;
- if (!(arb_unwrap_host_value(ctx, value, &htag, &payload) == ARB_OK)) {
- printf("unwrap_host_value failed\n");
- return 1;
- }
- printf("htag=%lu payload=%" PRIu64 "\n", htag, payload);
-
- uint8_t *str_ptr;
- size_t str_len;
- if (!(arb_to_string(ctx, payload, &str_ptr, &str_len) == ARB_OK)) {
- printf("to_string failed\n");
- return 1;
- }
- printf("RESULT: %.*s\n", (int)str_len, str_ptr);
- arboricx_free_buf(ctx, str_ptr, str_len);
-
- free(bundle);
- arboricx_free(ctx);
- printf("done\n");
- return 0;
-}
diff --git a/ext/zig/tests/c_abi_test.c b/ext/zig/tests/c_abi_test.c
@@ -1,120 +0,0 @@
-#include <stdio.h>
-#include <string.h>
-#include "helpers.h"
-
-int main(void) {
- arb_ctx_t* ctx = arboricx_init();
- if (!ctx) {
- fprintf(stderr, "Failed to initialize Arboricx context\n");
- return 1;
- }
-
- /* Test: Leaf @ Leaf -> Stem */
- arb_handle_t leaf = checked_arb_leaf(ctx);
- arb_handle_t app = checked_arb_app(ctx, leaf, leaf);
- arb_handle_t result = checked_arb_reduce(ctx, app, 10000);
- arb_handle_t stem = checked_arb_stem(ctx, leaf);
-
- /* Build expected Stem(Leaf) and compare */
- assert(same_value(ctx, result, stem));
- printf("PASS: reduce Leaf@Leaf\n");
-
- /* Test: number codec roundtrip */
- arb_handle_t num_tree = checked_arb_of_number(ctx, 42);
- uint64_t decoded_num;
- if (!(arb_to_number(ctx, num_tree, &decoded_num) == ARB_OK) || decoded_num != 42) {
- fprintf(stderr, "FAIL: number roundtrip\n");
- arboricx_free(ctx);
- return 1;
- }
- printf("PASS: number roundtrip 42\n");
-
- /* Test: string codec roundtrip */
- arb_handle_t str_tree = checked_arb_of_string(ctx, "hello");
- uint8_t* decoded_str;
- size_t decoded_len;
- if (!(arb_to_string(ctx, str_tree, &decoded_str, &decoded_len) == ARB_OK) ||
- decoded_len != 5 || memcmp(decoded_str, "hello", 5) != 0) {
- fprintf(stderr, "FAIL: string roundtrip\n");
- arboricx_free(ctx);
- return 1;
- }
- arboricx_free_buf(ctx, decoded_str, decoded_len);
- printf("PASS: string roundtrip \"hello\"\n");
-
- /* Test: kernel loaded */
- arb_handle_t kernel_root = checked_arb_kernel_root(ctx);
- if (kernel_root == 0) {
- fprintf(stderr, "FAIL: kernel not loaded\n");
- arboricx_free(ctx);
- return 1;
- }
- printf("PASS: kernel loaded (root=%" PRIu64 ")\n", kernel_root);
-
- /* Test: tree inspection primitives */
- arb_handle_t l = checked_arb_leaf(ctx);
- arb_handle_t s = checked_arb_stem(ctx, l);
- arb_handle_t f = checked_arb_fork(ctx, s, l);
- arb_handle_t a = checked_arb_app(ctx, f, s);
-
- if (!checked_arb_is_leaf(ctx, l)) {
- fprintf(stderr, "FAIL: is_leaf on leaf\n");
- arboricx_free(ctx);
- return 1;
- }
- if (checked_arb_is_leaf(ctx, s)) {
- fprintf(stderr, "FAIL: is_leaf on stem should be false\n");
- arboricx_free(ctx);
- return 1;
- }
- if (!checked_arb_is_stem(ctx, s)) {
- fprintf(stderr, "FAIL: is_stem on stem\n");
- arboricx_free(ctx);
- return 1;
- }
- if (!checked_arb_is_fork(ctx, f)) {
- fprintf(stderr, "FAIL: is_fork on fork\n");
- arboricx_free(ctx);
- return 1;
- }
- if (!checked_arb_is_app(ctx, a)) {
- fprintf(stderr, "FAIL: is_app on app\n");
- arboricx_free(ctx);
- return 1;
- }
-
- arb_handle_t child;
- if (arb_get_stem_child(ctx, s, &child) != ARB_OK || !same_value(ctx, child, l)) {
- fprintf(stderr, "FAIL: get_stem_child\n");
- arboricx_free(ctx);
- return 1;
- }
-
- arb_handle_t left, right;
- if (!(arb_get_fork_children(ctx, f, &left, &right) == ARB_OK) || !same_value(ctx, left, s) || !same_value(ctx, right, l)) {
- fprintf(stderr, "FAIL: get_fork_children\n");
- arboricx_free(ctx);
- return 1;
- }
-
- arb_handle_t func, arg;
- if (!(arb_get_app_func_arg(ctx, a, &func, &arg) == ARB_OK) || !same_value(ctx, func, f) || !same_value(ctx, arg, s)) {
- fprintf(stderr, "FAIL: get_app_func_arg\n");
- arboricx_free(ctx);
- return 1;
- }
-
- /* Invalid index should return 0 */
- int invalid_out = 42;
- if (arb_is_leaf(ctx, 999999, &invalid_out) != ARB_INVALID_HANDLE || invalid_out != 42) {
- fprintf(stderr, "FAIL: is_leaf on invalid index should be false\n");
- arboricx_free(ctx);
- return 1;
- }
-
- printf("PASS: tree inspection primitives\n");
-
- arboricx_free(ctx);
- printf("\nAll C ABI tests passed.\n");
- return 0;
-}
diff --git a/ext/zig/tests/handles_test.c b/ext/zig/tests/handles_test.c
@@ -1,84 +0,0 @@
-#include "arboricx.h"
-#include <assert.h>
-#include <stdio.h>
-#include <string.h>
-
-#define OK(call) assert((call) == ARB_OK)
-
-int main(void) {
- assert(arb_abi_version() == 3);
- arb_ctx_t *ctx = arboricx_init(), *other = arboricx_init();
- assert(ctx && other);
- assert(arb_set_gc_policy(ctx, 0, 1) == ARB_INVALID_DATA);
- assert(arb_set_gc_policy(ctx, 1, 0) == ARB_INVALID_DATA);
- OK(arb_set_gc_policy(ctx, 1, 1)); /* stress safepoints, including codecs */
- arb_handle_t leaf, alias, stem, child, app, result;
- OK(arb_leaf(ctx, &leaf));
- OK(arb_retain(ctx, leaf, &alias));
- assert(leaf != alias);
- OK(arb_stem(ctx, leaf, &stem));
- OK(arb_release(ctx, leaf));
- assert(arb_release(ctx, leaf) == ARB_INVALID_HANDLE);
- arb_handle_t untouched = 123;
- assert(arb_stem(ctx, leaf, &untouched) == ARB_INVALID_HANDLE);
- assert(untouched == 123);
- assert(arb_retain(other, alias, &untouched) == ARB_INVALID_HANDLE);
- assert(untouched == 123);
- OK(arb_collect(ctx));
- int yes;
- OK(arb_is_leaf(ctx, alias, &yes)); assert(yes);
- OK(arb_get_stem_child(ctx, stem, &child));
- OK(arb_release(ctx, stem));
- OK(arb_release(ctx, alias));
- OK(arb_collect(ctx));
- OK(arb_is_leaf(ctx, child, &yes)); assert(yes);
-
- OK(arb_app(ctx, child, child, &app));
- assert(arb_reduce(ctx, app, 0, &untouched) == ARB_FUEL_EXHAUSTED);
- assert(untouched == 123);
- OK(arb_reduce(ctx, app, 1, &result));
- OK(arb_is_stem(ctx, app, &yes)); assert(yes); /* update visible to alias */
- OK(arb_release(ctx, app));
- OK(arb_collect(ctx));
- OK(arb_is_stem(ctx, result, &yes)); assert(yes);
- arb_handle_t a = 123, b = 456;
- assert(arb_get_fork_children(ctx, result, &a, &b) == ARB_TYPE_MISMATCH);
- assert(a == 123 && b == 456);
- OK(arb_release(ctx, result));
- OK(arb_release(ctx, child));
-
- /* Released slot reuse must never resurrect a stale generation. */
- for (int i = 0; i < 10000; ++i) {
- OK(arb_leaf(ctx, &a));
- assert(a != leaf);
- assert(arb_retain(ctx, leaf, &untouched) == ARB_INVALID_HANDLE);
- OK(arb_release(ctx, a));
- }
- OK(arb_collect(ctx));
-
- /* A nested selected result retains only its live spine; releasing inputs
- and collecting repeatedly must not damage the eventual result. */
- OK(arb_leaf(ctx, &leaf));
- OK(arb_of_string(ctx, "survives collection", &a));
- for (int i = 0; i < 10000; ++i) {
- OK(arb_fork(ctx, leaf, a, &b));
- OK(arb_release(ctx, a));
- OK(arb_app(ctx, b, leaf, &a));
- OK(arb_release(ctx, b));
- if (i % 1000 == 0) OK(arb_collect(ctx));
- }
- OK(arb_reduce(ctx, a, 100000, &result));
- OK(arb_release(ctx, a));
- OK(arb_collect(ctx));
- uint8_t *bytes; size_t len;
- OK(arb_to_string(ctx, result, &bytes, &len));
- assert(len == strlen("survives collection"));
- assert(memcmp(bytes, "survives collection", len) == 0);
- arboricx_free_buf(ctx, bytes, len);
- OK(arb_release(ctx, result));
- OK(arb_release(ctx, leaf));
- OK(arb_collect(ctx));
- arboricx_free(other);
- arboricx_free(ctx);
- puts("Owned handles, stale/cross-context rejection, in-place evaluation and GC passed");
-}
diff --git a/ext/zig/tests/helpers.h b/ext/zig/tests/helpers.h
@@ -1,111 +0,0 @@
-/* Test-only checked constructors. Each returned handle is owned until released
- * or context teardown. Production callers use the status/out-parameter API. */
-#ifndef ARBORICX_TEST_HELPERS_H
-#define ARBORICX_TEST_HELPERS_H
-#include "arboricx.h"
-#include <assert.h>
-#include <stdlib.h>
-#include <inttypes.h>
-static inline arb_handle_t checked_arb_leaf(arb_ctx_t* ctx) {
- arb_handle_t out;
- arb_status_t status = arb_leaf(ctx, &out);
- if (status != ARB_OK) abort();
- return out;
-}
-static inline arb_handle_t checked_arb_stem(arb_ctx_t* ctx, arb_handle_t child) {
- arb_handle_t out;
- arb_status_t status = arb_stem(ctx, child, &out);
- if (status != ARB_OK) abort();
- return out;
-}
-static inline arb_handle_t checked_arb_fork(arb_ctx_t* ctx, arb_handle_t left, arb_handle_t right) {
- arb_handle_t out;
- arb_status_t status = arb_fork(ctx, left, right, &out);
- if (status != ARB_OK) abort();
- return out;
-}
-static inline arb_handle_t checked_arb_app(arb_ctx_t* ctx, arb_handle_t func, arb_handle_t arg) {
- arb_handle_t out;
- arb_status_t status = arb_app(ctx, func, arg, &out);
- if (status != ARB_OK) abort();
- return out;
-}
-static inline arb_handle_t checked_arb_reduce(arb_ctx_t* ctx, arb_handle_t root, uint64_t fuel) {
- arb_handle_t out;
- arb_status_t status = arb_reduce(ctx, root, fuel, &out);
- if (status != ARB_OK) abort();
- return out;
-}
-static inline arb_handle_t checked_arb_of_number(arb_ctx_t* ctx, uint64_t n) {
- arb_handle_t out;
- arb_status_t status = arb_of_number(ctx, n, &out);
- if (status != ARB_OK) abort();
- return out;
-}
-static inline arb_handle_t checked_arb_of_string(arb_ctx_t* ctx, const char* s) {
- arb_handle_t out;
- arb_status_t status = arb_of_string(ctx, s, &out);
- if (status != ARB_OK) abort();
- return out;
-}
-static inline arb_handle_t checked_arb_of_bytes(arb_ctx_t* ctx, const uint8_t* bytes, size_t len) {
- arb_handle_t out;
- arb_status_t status = arb_of_bytes(ctx, bytes, len, &out);
- if (status != ARB_OK) abort();
- return out;
-}
-static inline arb_handle_t checked_arb_of_list(arb_ctx_t* ctx, const arb_handle_t* items, size_t len) {
- arb_handle_t out;
- arb_status_t status = arb_of_list(ctx, items, len, &out);
- if (status != ARB_OK) abort();
- return out;
-}
-static inline arb_handle_t checked_arb_get_stem_child(arb_ctx_t* ctx, arb_handle_t root) {
- arb_handle_t out;
- arb_status_t status = arb_get_stem_child(ctx, root, &out);
- if (status != ARB_OK) abort();
- return out;
-}
-static inline arb_handle_t checked_arb_kernel_root(arb_ctx_t* ctx) {
- arb_handle_t out;
- arb_status_t status = arb_kernel_root(ctx, &out);
- if (status != ARB_OK) abort();
- return out;
-}
-static inline arb_handle_t checked_arb_load_bundle(arb_ctx_t* ctx, const uint8_t* bytes, size_t len, const char* name) {
- arb_handle_t out;
- arb_status_t status = arb_load_bundle(ctx, bytes, len, name, &out);
- if (status != ARB_OK) abort();
- return out;
-}
-static inline arb_handle_t checked_arb_load_bundle_default(arb_ctx_t* ctx, const uint8_t* bytes, size_t len) {
- arb_handle_t out;
- arb_status_t status = arb_load_bundle_default(ctx, bytes, len, &out);
- if (status != ARB_OK) abort();
- return out;
-}
-static inline int checked_arb_is_leaf(arb_ctx_t* ctx, arb_handle_t h) { int out; if (arb_is_leaf(ctx, h, &out) != ARB_OK) abort(); return out; }
-static inline int checked_arb_is_stem(arb_ctx_t* ctx, arb_handle_t h) { int out; if (arb_is_stem(ctx, h, &out) != ARB_OK) abort(); return out; }
-static inline int checked_arb_is_fork(arb_ctx_t* ctx, arb_handle_t h) { int out; if (arb_is_fork(ctx, h, &out) != ARB_OK) abort(); return out; }
-static inline int checked_arb_is_app(arb_ctx_t* ctx, arb_handle_t h) { int out; if (arb_is_app(ctx, h, &out) != ARB_OK) abort(); return out; }
-static inline int same_value(arb_ctx_t* ctx, arb_handle_t a, arb_handle_t b) {
- if (checked_arb_is_leaf(ctx, a)) return checked_arb_is_leaf(ctx, b);
- arb_handle_t al, ar, bl, br;
- if (checked_arb_is_stem(ctx, a)) {
- if (!checked_arb_is_stem(ctx, b)) return 0;
- assert(arb_get_stem_child(ctx, a, &al) == ARB_OK);
- assert(arb_get_stem_child(ctx, b, &bl) == ARB_OK);
- int same = same_value(ctx, al, bl);
- assert(arb_release(ctx, al) == ARB_OK); assert(arb_release(ctx, bl) == ARB_OK);
- return same;
- }
- int fork = checked_arb_is_fork(ctx, a);
- if (fork != checked_arb_is_fork(ctx, b)) return 0;
- assert((fork ? arb_get_fork_children(ctx, a, &al, &ar) : arb_get_app_func_arg(ctx, a, &al, &ar)) == ARB_OK);
- assert((fork ? arb_get_fork_children(ctx, b, &bl, &br) : arb_get_app_func_arg(ctx, b, &bl, &br)) == ARB_OK);
- int same = same_value(ctx, al, bl) && same_value(ctx, ar, br);
- assert(arb_release(ctx, al) == ARB_OK); assert(arb_release(ctx, ar) == ARB_OK);
- assert(arb_release(ctx, bl) == ARB_OK); assert(arb_release(ctx, br) == ARB_OK);
- return same;
-}
-#endif
diff --git a/ext/zig/tests/io_protocol_test.c b/ext/zig/tests/io_protocol_test.c
@@ -1,223 +0,0 @@
-#include <stdio.h>
-#include <string.h>
-#include "helpers.h"
-
-int main(void) {
- arb_ctx_t* ctx = arboricx_init();
- if (!ctx) {
- fprintf(stderr, "Failed to initialize Arboricx context\n");
- return 1;
- }
-
- /* Test: construct and verify pure action = Fork 0 Leaf */
- arb_handle_t leaf = checked_arb_leaf(ctx);
- arb_handle_t zero = checked_arb_of_number(ctx, 0);
- arb_handle_t pure_action = checked_arb_fork(ctx, zero, leaf);
-
- if (!checked_arb_is_fork(ctx, pure_action)) {
- fprintf(stderr, "FAIL: pure action should be fork\n");
- arboricx_free(ctx);
- return 1;
- }
-
- arb_handle_t tag, payload;
- if (!(arb_get_fork_children(ctx, pure_action, &tag, &payload) == ARB_OK) ||
- !same_value(ctx, tag, zero) || !same_value(ctx, payload, leaf)) {
- fprintf(stderr, "FAIL: pure action children mismatch\n");
- arboricx_free(ctx);
- return 1;
- }
-
- uint64_t tag_num;
- if (!(arb_to_number(ctx, tag, &tag_num) == ARB_OK) || tag_num != 0) {
- fprintf(stderr, "FAIL: pure action tag should be 0\n");
- arboricx_free(ctx);
- return 1;
- }
- printf("PASS: pure action shape\n");
-
- /* Test: construct and verify bind action = Fork 1 (Fork left k) */
- arb_handle_t one = checked_arb_of_number(ctx, 1);
- arb_handle_t left = checked_arb_fork(ctx, zero, leaf); /* pure Leaf */
- arb_handle_t k = checked_arb_fork(ctx, leaf, leaf); /* identity as Fork Leaf Leaf */
- arb_handle_t bind_pair = checked_arb_fork(ctx, left, k);
- arb_handle_t bind_action = checked_arb_fork(ctx, one, bind_pair);
-
- if (!(arb_get_fork_children(ctx, bind_action, &tag, &payload) == ARB_OK) ||
- !(arb_to_number(ctx, tag, &tag_num) == ARB_OK) || tag_num != 1) {
- fprintf(stderr, "FAIL: bind action tag should be 1\n");
- arboricx_free(ctx);
- return 1;
- }
-
- arb_handle_t bind_left, bind_k;
- if (!(arb_get_fork_children(ctx, payload, &bind_left, &bind_k) == ARB_OK) ||
- !same_value(ctx, bind_left, left) || !same_value(ctx, bind_k, k)) {
- fprintf(stderr, "FAIL: bind payload should be Fork left k\n");
- arboricx_free(ctx);
- return 1;
- }
- printf("PASS: bind action shape\n");
-
- /* Test: construct and verify IO sentinel = Fork "tricuIO" (Fork 1 action) */
- arb_handle_t sentinel_str = checked_arb_of_string(ctx, "tricuIO");
- arb_handle_t version = checked_arb_of_number(ctx, 1);
- arb_handle_t version_action_pair = checked_arb_fork(ctx, version, pure_action);
- arb_handle_t io_sentinel = checked_arb_fork(ctx, sentinel_str, version_action_pair);
-
- if (!checked_arb_is_fork(ctx, io_sentinel)) {
- fprintf(stderr, "FAIL: IO sentinel should be fork\n");
- arboricx_free(ctx);
- return 1;
- }
-
- arb_handle_t sent_left, sent_right;
- if (!(arb_get_fork_children(ctx, io_sentinel, &sent_left, &sent_right) == ARB_OK)) {
- fprintf(stderr, "FAIL: get_fork_children on IO sentinel\n");
- arboricx_free(ctx);
- return 1;
- }
-
- /* Verify sentinel string */
- uint8_t* decoded_sentinel;
- size_t decoded_len;
- if (!(arb_to_string(ctx, sent_left, &decoded_sentinel, &decoded_len) == ARB_OK) ||
- decoded_len != 7 || memcmp(decoded_sentinel, "tricuIO", 7) != 0) {
- fprintf(stderr, "FAIL: IO sentinel string mismatch\n");
- arboricx_free(ctx);
- return 1;
- }
- arboricx_free_buf(ctx, decoded_sentinel, decoded_len);
-
- /* Verify version = 1 and action = pure */
- arb_handle_t ver, act;
- if (!(arb_get_fork_children(ctx, sent_right, &ver, &act) == ARB_OK) ||
- !(arb_to_number(ctx, ver, &tag_num) == ARB_OK) || tag_num != 1 ||
- !same_value(ctx, act, pure_action)) {
- fprintf(stderr, "FAIL: IO sentinel version/action mismatch\n");
- arboricx_free(ctx);
- return 1;
- }
- printf("PASS: IO sentinel shape\n");
-
- /* Test: putStr action = Fork 10 string */
- arb_handle_t ten = checked_arb_of_number(ctx, 10);
- arb_handle_t msg = checked_arb_of_string(ctx, "hello");
- arb_handle_t putStr_action = checked_arb_fork(ctx, ten, msg);
-
- if (!(arb_get_fork_children(ctx, putStr_action, &tag, &payload) == ARB_OK) ||
- !(arb_to_number(ctx, tag, &tag_num) == ARB_OK) || tag_num != 10) {
- fprintf(stderr, "FAIL: putStr tag should be 10\n");
- arboricx_free(ctx);
- return 1;
- }
- printf("PASS: putStr action shape\n");
-
- /* Test: getLine action = Fork 11 Leaf */
- arb_handle_t eleven = checked_arb_of_number(ctx, 11);
- arb_handle_t getLine_action = checked_arb_fork(ctx, eleven, leaf);
-
- if (!(arb_get_fork_children(ctx, getLine_action, &tag, &payload) == ARB_OK) ||
- !(arb_to_number(ctx, tag, &tag_num) == ARB_OK) || tag_num != 11 ||
- !same_value(ctx, payload, leaf)) {
- fprintf(stderr, "FAIL: getLine tag should be 11 with Leaf payload\n");
- arboricx_free(ctx);
- return 1;
- }
- printf("PASS: getLine action shape\n");
-
- /* Test: readFile action = Fork 20 path */
- arb_handle_t twenty = checked_arb_of_number(ctx, 20);
- arb_handle_t path = checked_arb_of_string(ctx, "/tmp/test.txt");
- arb_handle_t readFile_action = checked_arb_fork(ctx, twenty, path);
-
- if (!(arb_get_fork_children(ctx, readFile_action, &tag, &payload) == ARB_OK) ||
- !(arb_to_number(ctx, tag, &tag_num) == ARB_OK) || tag_num != 20) {
- fprintf(stderr, "FAIL: readFile tag should be 20\n");
- arboricx_free(ctx);
- return 1;
- }
- printf("PASS: readFile action shape\n");
-
- /* Test: writeFile action = Fork 21 (Fork path contents) */
- arb_handle_t twenty_one = checked_arb_of_number(ctx, 21);
- arb_handle_t contents = checked_arb_of_string(ctx, "data");
- arb_handle_t write_pair = checked_arb_fork(ctx, path, contents);
- arb_handle_t writeFile_action = checked_arb_fork(ctx, twenty_one, write_pair);
-
- if (!(arb_get_fork_children(ctx, writeFile_action, &tag, &payload) == ARB_OK) ||
- !(arb_to_number(ctx, tag, &tag_num) == ARB_OK) || tag_num != 21) {
- fprintf(stderr, "FAIL: writeFile tag should be 21\n");
- arboricx_free(ctx);
- return 1;
- }
-
- arb_handle_t wf_path, wf_contents;
- if (!(arb_get_fork_children(ctx, payload, &wf_path, &wf_contents) == ARB_OK) ||
- !same_value(ctx, wf_path, path) || !same_value(ctx, wf_contents, contents)) {
- fprintf(stderr, "FAIL: writeFile payload should be Fork path contents\n");
- arboricx_free(ctx);
- return 1;
- }
- printf("PASS: writeFile action shape\n");
-
- /* Test: ok result = Fork (Stem Leaf) (Fork val Leaf) */
- arb_handle_t stem_leaf = checked_arb_stem(ctx, leaf);
- arb_handle_t val_pair = checked_arb_fork(ctx, msg, leaf);
- arb_handle_t ok_result = checked_arb_fork(ctx, stem_leaf, val_pair);
-
- if (!checked_arb_is_fork(ctx, ok_result)) {
- fprintf(stderr, "FAIL: ok result should be fork\n");
- arboricx_free(ctx);
- return 1;
- }
-
- arb_handle_t ok_tag, ok_rest;
- if (!(arb_get_fork_children(ctx, ok_result, &ok_tag, &ok_rest) == ARB_OK) ||
- !checked_arb_is_stem(ctx, ok_tag)) {
- fprintf(stderr, "FAIL: ok result left should be stem\n");
- arboricx_free(ctx);
- return 1;
- }
-
- arb_handle_t ok_val, ok_leaf;
- if (!(arb_get_fork_children(ctx, ok_rest, &ok_val, &ok_leaf) == ARB_OK) ||
- !same_value(ctx, ok_val, msg) || !same_value(ctx, ok_leaf, leaf)) {
- fprintf(stderr, "FAIL: ok result right should be Fork val Leaf\n");
- arboricx_free(ctx);
- return 1;
- }
- printf("PASS: ok result shape\n");
-
- /* Test: err result = Fork Leaf (Fork code Leaf) */
- arb_handle_t err_code = checked_arb_of_number(ctx, 42);
- arb_handle_t err_pair = checked_arb_fork(ctx, err_code, leaf);
- arb_handle_t err_result = checked_arb_fork(ctx, leaf, err_pair);
-
- if (!checked_arb_is_fork(ctx, err_result)) {
- fprintf(stderr, "FAIL: err result should be fork\n");
- arboricx_free(ctx);
- return 1;
- }
-
- arb_handle_t err_tag, err_rest;
- if (!(arb_get_fork_children(ctx, err_result, &err_tag, &err_rest) == ARB_OK) ||
- !checked_arb_is_leaf(ctx, err_tag)) {
- fprintf(stderr, "FAIL: err result left should be leaf\n");
- arboricx_free(ctx);
- return 1;
- }
-
- arb_handle_t err_c, err_l;
- if (!(arb_get_fork_children(ctx, err_rest, &err_c, &err_l) == ARB_OK) ||
- !same_value(ctx, err_c, err_code) || !same_value(ctx, err_l, leaf)) {
- fprintf(stderr, "FAIL: err result right should be Fork code Leaf\n");
- arboricx_free(ctx);
- return 1;
- }
- printf("PASS: err result shape\n");
-
- arboricx_free(ctx);
- printf("\nAll IO protocol tests passed.\n");
- return 0;
-}
diff --git a/ext/zig/tests/io_run_test.c b/ext/zig/tests/io_run_test.c
@@ -1,63 +0,0 @@
-#include <stdio.h>
-#include <string.h>
-#include "helpers.h"
-
-static arb_handle_t make_pure(arb_ctx_t* ctx, arb_handle_t val) {
- arb_handle_t zero = checked_arb_of_number(ctx, 0);
- return checked_arb_fork(ctx, zero, val);
-}
-
-static arb_handle_t make_io_sentinel(arb_ctx_t* ctx, arb_handle_t action) {
- arb_handle_t sentinel = checked_arb_of_string(ctx, "tricuIO");
- arb_handle_t version = checked_arb_of_number(ctx, 1);
- arb_handle_t version_action = checked_arb_fork(ctx, version, action);
- return checked_arb_fork(ctx, sentinel, version_action);
-}
-
-int main(void) {
- arb_ctx_t* ctx = arboricx_init();
- if (!ctx) {
- fprintf(stderr, "Failed to initialize Arboricx context\n");
- return 1;
- }
-
- /* Version-specific v2 execution; output handles remain untouched on errors. */
- {
- arb_handle_t payload = checked_arb_of_number(ctx, 42);
- arb_handle_t action = make_pure(ctx, payload);
- arb_handle_t v1 = make_io_sentinel(ctx, action);
- arb_handle_t v2 = checked_arb_fork(ctx, checked_arb_of_string(ctx, "tricuIO"),
- checked_arb_fork(ctx, checked_arb_of_number(ctx, 2), action));
- arb_handle_t out = 123;
- uint64_t value = 0;
- if (arb_run_io_v2(ctx,v2,&out) != ARB_OK || arb_to_number(ctx,out,&value) != ARB_OK || value != 42) return 1;
- if (arb_release(ctx,out) != ARB_OK) return 1;
- out = 123;
- if (arb_run_io_v2(ctx,v1,&out) != ARB_IO_ERROR || out != 123) return 1;
- if (arb_run_io_v2(ctx,0,&out) != ARB_INVALID_HANDLE || out != 123) return 1;
- arb_handle_t malformed = checked_arb_fork(ctx, checked_arb_of_string(ctx, "notIO"),
- checked_arb_fork(ctx, checked_arb_of_number(ctx, 2), action));
- if (arb_run_io_v2(ctx,malformed,&out) != ARB_IO_ERROR || out != 123) return 1;
- for (uint64_t tag = 0; tag <= 100; tag++) {
- arb_handle_t retired = make_io_sentinel(ctx,
- checked_arb_fork(ctx, checked_arb_of_number(ctx, tag), payload));
- if (arb_run_io_v2(ctx,retired,&out) != ARB_IO_ERROR || out != 123) return 1;
- }
- /* Non-normal terminal payloads must not become successful Results. */
- arb_handle_t inert = checked_arb_app(ctx, checked_arb_leaf(ctx), checked_arb_leaf(ctx));
- for (uint64_t tag = 2; tag <= 9; tag += 7) {
- arb_handle_t terminal = checked_arb_fork(ctx, checked_arb_of_string(ctx, "tricuIO"),
- checked_arb_fork(ctx, checked_arb_of_number(ctx, 2),
- checked_arb_fork(ctx, checked_arb_of_number(ctx, tag), inert)));
- if (arb_run_io_v2(ctx,terminal,&out) != ARB_IO_ERROR || out != 123) return 1;
- }
- /* A failed run does not poison subsequent valid runs in this context. */
- if (arb_run_io_v2(ctx,v2,&out) != ARB_OK || arb_to_number(ctx,out,&value) != ARB_OK || value != 42) return 1;
- if (arb_release(ctx,out) != ARB_OK) return 1;
- printf("PASS: explicit v2 ABI and no fallback\n");
- }
-
- arboricx_free(ctx);
- printf("\nAll IO run tests passed.\n");
- return 0;
-}
diff --git a/ext/zig/tests/native_bundle_append_test.c b/ext/zig/tests/native_bundle_append_test.c
@@ -1,84 +0,0 @@
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <time.h>
-#include "helpers.h"
-
-static uint8_t *read_file(const char *path, size_t *out_len) {
- FILE *f = fopen(path, "rb");
- if (!f) return NULL;
- fseek(f, 0, SEEK_END);
- *out_len = ftell(f);
- fseek(f, 0, SEEK_SET);
- uint8_t *buf = malloc(*out_len);
- fread(buf, 1, *out_len, f);
- fclose(f);
- return buf;
-}
-
-int main() {
- arb_ctx_t *ctx = arboricx_init();
- if (!ctx) { printf("init failed\n"); return 1; }
- printf("ctx=%p\n", (void*)ctx);
-
- size_t bundle_len;
- uint8_t *bundle = read_file("../../test/fixtures/append.arboricx", &bundle_len);
- if (!bundle) { printf("bundle not found\n"); return 1; }
- printf("bundle size=%zu\n", bundle_len);
-
- clock_t t0 = clock();
- arb_handle_t term = checked_arb_load_bundle(ctx, bundle, bundle_len, "append");
- clock_t t1 = clock();
- printf("load_bundle took %.3f ms, term=%" PRIu64 "\n", (double)(t1 - t0) * 1000.0 / CLOCKS_PER_SEC, term);
- if (term == 0) {
- printf("load_bundle failed\n");
- return 1;
- }
-
- arb_handle_t arg1 = checked_arb_of_string(ctx, "Hello, ");
- arb_handle_t arg2 = checked_arb_of_string(ctx, "world!");
- printf("arg1=%" PRIu64 " arg2=%" PRIu64 "\n", arg1, arg2);
-
- arb_handle_t app0 = checked_arb_app(ctx, term, arg1);
- arb_handle_t app1 = checked_arb_app(ctx, app0, arg2);
- printf("app1=%" PRIu64 "\n", app1);
-
- printf("reducing...\n");
- clock_t t2 = clock();
- arb_handle_t result = checked_arb_reduce(ctx, app1, 1000000000ULL);
- clock_t t3 = clock();
- printf("reduce took %.3f ms, result=%" PRIu64 "\n", (double)(t3 - t2) * 1000.0 / CLOCKS_PER_SEC, result);
-
- /* Try decoding as a plain string first (direct call, no kernel wrapper) */
- uint8_t *str_ptr;
- size_t str_len;
- if ((arb_to_string(ctx, result, &str_ptr, &str_len) == ARB_OK)) {
- printf("RESULT: %.*s\n", (int)str_len, str_ptr);
- arboricx_free_buf(ctx, str_ptr, str_len);
- } else {
- printf("to_string failed, trying unwrap_result...\n");
- int ok;
- arb_handle_t value, rest;
- if (!(arb_unwrap_result(ctx, result, &ok, &value, &rest) == ARB_OK)) {
- printf("unwrap_result also failed\n");
- return 1;
- }
- printf("unwrap_result: ok=%d value=%" PRIu64 "\n", ok, value);
- uint64_t htag;
- arb_handle_t payload;
- if (!(arb_unwrap_host_value(ctx, value, &htag, &payload) == ARB_OK)) {
- printf("unwrap_host_value failed\n");
- return 1;
- }
- printf("htag=%lu payload=%" PRIu64 "\n", htag, payload);
- if ((arb_to_string(ctx, payload, &str_ptr, &str_len) == ARB_OK)) {
- printf("RESULT: %.*s\n", (int)str_len, str_ptr);
- arboricx_free_buf(ctx, str_ptr, str_len);
- }
- }
-
- free(bundle);
- arboricx_free(ctx);
- printf("done\n");
- return 0;
-}
diff --git a/ext/zig/tests/native_bundle_bools_test.c b/ext/zig/tests/native_bundle_bools_test.c
@@ -1,60 +0,0 @@
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <time.h>
-#include "helpers.h"
-
-static uint8_t *read_file(const char *path, size_t *out_len) {
- FILE *f = fopen(path, "rb");
- if (!f) return NULL;
- fseek(f, 0, SEEK_END);
- *out_len = ftell(f);
- fseek(f, 0, SEEK_SET);
- uint8_t *buf = malloc(*out_len);
- fread(buf, 1, *out_len, f);
- fclose(f);
- return buf;
-}
-
-int test_bundle(arb_ctx_t *ctx, const char *path, const char *name, int expect_val) {
- size_t bundle_len;
- uint8_t *bundle = read_file(path, &bundle_len);
- if (!bundle) { printf("bundle not found: %s\n", path); return 1; }
-
- arb_handle_t term = checked_arb_load_bundle(ctx, bundle, bundle_len, name);
- if (term == 0) {
- printf("load_bundle failed for %s\n", path);
- free(bundle);
- return 1;
- }
-
- arb_handle_t result = checked_arb_reduce(ctx, term, 1000000000ULL);
-
- int b;
- if (!(arb_to_bool(ctx, result, &b) == ARB_OK)) {
- printf("to_bool failed for %s\n", path);
- free(bundle);
- return 1;
- }
- printf("%s result bool=%d (expected %d)\n", path, b, expect_val);
- if (b != expect_val) {
- printf("MISMATCH!\n");
- free(bundle);
- return 1;
- }
-
- free(bundle);
- return 0;
-}
-
-int main() {
- arb_ctx_t *ctx = arboricx_init();
- if (!ctx) { printf("init failed\n"); return 1; }
-
- if (test_bundle(ctx, "../../test/fixtures/true.arboricx", "true", 1) != 0) return 1;
- if (test_bundle(ctx, "../../test/fixtures/false.arboricx", "false", 0) != 0) return 1;
-
- arboricx_free(ctx);
- printf("All bool tests passed.\n");
- return 0;
-}
diff --git a/ext/zig/tests/native_bundle_id_test.c b/ext/zig/tests/native_bundle_id_test.c
@@ -1,60 +0,0 @@
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <time.h>
-#include "helpers.h"
-
-static uint8_t *read_file(const char *path, size_t *out_len) {
- FILE *f = fopen(path, "rb");
- if (!f) return NULL;
- fseek(f, 0, SEEK_END);
- *out_len = ftell(f);
- fseek(f, 0, SEEK_SET);
- uint8_t *buf = malloc(*out_len);
- fread(buf, 1, *out_len, f);
- fclose(f);
- return buf;
-}
-
-int main() {
- arb_ctx_t *ctx = arboricx_init();
- if (!ctx) { printf("init failed\n"); return 1; }
-
- size_t bundle_len;
- uint8_t *bundle = read_file("../../test/fixtures/id.arboricx", &bundle_len);
- if (!bundle) { printf("bundle not found\n"); return 1; }
- printf("bundle size=%zu\n", bundle_len);
-
- clock_t t0 = clock();
- arb_handle_t term = checked_arb_load_bundle(ctx, bundle, bundle_len, "id");
- clock_t t1 = clock();
- printf("load_bundle took %.3f ms, term=%" PRIu64 "\n", (double)(t1 - t0) * 1000.0 / CLOCKS_PER_SEC, term);
- if (term == 0) {
- printf("load_bundle failed\n");
- return 1;
- }
-
- arb_handle_t arg1 = checked_arb_of_string(ctx, "hello");
- arb_handle_t app0 = checked_arb_app(ctx, term, arg1);
-
- printf("reducing...\n");
- clock_t t2 = clock();
- arb_handle_t result = checked_arb_reduce(ctx, app0, 1000000000ULL);
- clock_t t3 = clock();
- printf("reduce took %.3f ms, result=%" PRIu64 "\n", (double)(t3 - t2) * 1000.0 / CLOCKS_PER_SEC, result);
-
- uint8_t *str_ptr;
- size_t str_len;
- if ((arb_to_string(ctx, result, &str_ptr, &str_len) == ARB_OK)) {
- printf("RESULT: %.*s\n", (int)str_len, str_ptr);
- arboricx_free_buf(ctx, str_ptr, str_len);
- } else {
- printf("to_string failed\n");
- return 1;
- }
-
- free(bundle);
- arboricx_free(ctx);
- printf("done\n");
- return 0;
-}
diff --git a/ext/zig/tests/performance_smoke.c b/ext/zig/tests/performance_smoke.c
@@ -1,155 +0,0 @@
-/* Correctness-checked ABI v3 performance smoke. No compiler execution.
- * Times exclude context/kernel loading; process peak RSS includes everything.
- * The library's optimization mode is specified by the Nix package, not gcc. */
-#define _POSIX_C_SOURCE 200809L
-#ifndef ARB_HOST_MODE
-#error "Set ARB_HOST_MODE to the linked runtime's optimization mode"
-#endif
-#include "arboricx.h"
-#include <stdio.h>
-#include <stdlib.h>
-#include <stdint.h>
-#include <string.h>
-#include <time.h>
-#include <sys/resource.h>
-#include <unistd.h>
-
-#define CHECK(x) do { if (!(x)) { fprintf(stderr, "failed: %s:%d: %s\n", __FILE__, __LINE__, #x); exit(1); } } while (0)
-#define OK(x) CHECK((x) == ARB_OK)
-static double now(void) {
- struct timespec t; CHECK(clock_gettime(CLOCK_MONOTONIC, &t) == 0);
- return (double)t.tv_sec + (double)t.tv_nsec / 1e9;
-}
-static long rss_kib(void) {
- FILE *f = fopen("/proc/self/statm", "r");
- if (!f) return -1;
- long pages = 0, resident = 0;
- CHECK(fscanf(f, "%ld %ld", &pages, &resident) == 2);
- fclose(f);
- return resident * sysconf(_SC_PAGESIZE) / 1024;
-}
-static long peak_rss_kib(void) {
- // VmHWM belongs to this executable's address space. ru_maxrss can include
- // the launcher's pre-exec address space, obscuring small-workload peaks.
- FILE *f = fopen("/proc/self/status", "r"); CHECK(f);
- char line[256]; long peak = -1;
- while (fgets(line, sizeof line, f)) {
- if (sscanf(line, "VmHWM: %ld kB", &peak) == 1) break;
- }
- fclose(f); CHECK(peak >= 0); return peak;
-}
-static arb_handle_t load(arb_ctx_t *ctx, const char *path) {
- FILE *f = fopen(path, "rb"); CHECK(f);
- CHECK(fseek(f, 0, SEEK_END) == 0); long n = ftell(f); CHECK(n > 0);
- rewind(f); uint8_t *buf = malloc((size_t)n); CHECK(buf);
- CHECK(fread(buf, 1, (size_t)n, f) == (size_t)n); fclose(f);
- arb_handle_t h; OK(arb_load_bundle_default(ctx, buf, (size_t)n, &h));
- free(buf); return h;
-}
-int main(int argc, char **argv) {
- CHECK(argc == 5); CHECK(arb_abi_version() == 3);
- // Apply limits before runtime initialization without a Python preexec hook.
- struct rlimit memory = {512 * 1024 * 1024, 512 * 1024 * 1024};
- struct rlimit stack = {8 * 1024 * 1024, 8 * 1024 * 1024};
- struct rlimit cpu = {60, 60}, core = {0, 0};
- CHECK(setrlimit(RLIMIT_AS, &memory) == 0);
- CHECK(setrlimit(RLIMIT_STACK, &stack) == 0);
- CHECK(setrlimit(RLIMIT_CPU, &cpu) == 0);
- CHECK(setrlimit(RLIMIT_CORE, &core) == 0);
- const char *kind = argv[1];
- size_t n = strtoul(argv[2], NULL, 10), rounds = strtoul(argv[3], NULL, 10);
- CHECK(n > 0 && rounds > 0);
- int strings = strcmp(kind, "id") == 0 || strcmp(kind, "append") == 0;
- int append = strcmp(kind, "append") == 0;
- int distribution = strcmp(kind, "distribution") == 0;
- CHECK(strings || distribution || strcmp(kind, "deep") == 0);
- double start = now();
- arb_ctx_t *ctx = arboricx_init(); CHECK(ctx);
- arb_handle_t leaf, function;
- OK(arb_leaf(ctx, &leaf));
- if (strings) {
- function = load(ctx, argv[4]);
- } else if (distribution) {
- // I = Fork (Stem K) K, K = Stem Leaf; I x distributes before returning x.
- arb_handle_t k, sk;
- OK(arb_stem(ctx, leaf, &k)); OK(arb_stem(ctx, k, &sk));
- OK(arb_fork(ctx, sk, k, &function));
- OK(arb_release(ctx, sk)); OK(arb_release(ctx, k));
- } else {
- OK(arb_retain(ctx, leaf, &function));
- }
- double init_s = now() - start;
- uint8_t *input = malloc(n), *second = malloc(n); CHECK(input && second);
- double build_s = 0, reduce_s = 0, observe_s = 0, gc_s = 0;
- long rss_mid = -1, rss_end = -1;
- for (size_t r = 0; r < rounds; ++r) {
- /* Fresh, round-dependent bytes also prevent cross-host harnesses from
- * accidentally sharing one already-evaluated Haskell result. */
- if (strings) for (size_t j = 0; j < n; ++j) {
- input[j] = (uint8_t)('a' + (j + r) % 26);
- second[j] = (uint8_t)('A' + (j + r) % 26);
- }
- start = now();
- arb_handle_t term, result;
- if (strings) {
- arb_handle_t arg;
- OK(arb_of_bytes(ctx, input, n, &arg));
- OK(arb_app(ctx, function, arg, &term));
- if (append) {
- arb_handle_t next, arg2;
- OK(arb_of_bytes(ctx, second, n, &arg2));
- OK(arb_app(ctx, term, arg2, &next));
- OK(arb_release(ctx, arg2));
- OK(arb_release(ctx, term)); term = next;
- }
- OK(arb_release(ctx, arg));
- } else {
- OK(arb_retain(ctx, leaf, &term));
- for (size_t j = 0; j < n; ++j) {
- arb_handle_t next;
- if (distribution) {
- OK(arb_app(ctx, function, term, &next));
- } else {
- arb_handle_t k;
- OK(arb_fork(ctx, leaf, term, &k));
- OK(arb_app(ctx, k, leaf, &next));
- OK(arb_release(ctx, k));
- }
- OK(arb_release(ctx, term)); term = next;
- }
- }
- build_s += now() - start;
- start = now();
- OK(arb_reduce(ctx, term, 1000000000, &result));
- reduce_s += now() - start;
- OK(arb_release(ctx, term));
- start = now();
- if (strings) {
- uint8_t *out; size_t len;
- OK(arb_to_bytes(ctx, result, &out, &len));
- CHECK(len == n * (append ? 2 : 1));
- CHECK(memcmp(out, input, n) == 0);
- if (append) CHECK(memcmp(out + n, second, n) == 0);
- arboricx_free_buf(ctx, out, len);
- } else {
- int is_leaf; OK(arb_is_leaf(ctx, result, &is_leaf)); CHECK(is_leaf);
- }
- observe_s += now() - start;
- OK(arb_release(ctx, result));
- // Batch reclamation rather than forcing one collection per tiny call.
- if ((r + 1) % 16 == 0 || r + 1 == rounds || r + 1 == rounds / 2) {
- start = now(); OK(arb_collect(ctx)); gc_s += now() - start;
- if (r + 1 == rounds / 2) rss_mid = rss_kib();
- if (r + 1 == rounds) rss_end = rss_kib();
- }
- }
- long peak = peak_rss_kib();
- printf("{\"host_mode\":\"%s\",\"kind\":\"%s\",\"n\":%zu,\"rounds\":%zu,\"init_s\":%.9f,"
- "\"build_s\":%.9f,\"reduce_s\":%.9f,\"observe_s\":%.9f,\"gc_s\":%.9f,"
- "\"peak_rss_kib\":%ld,\"mid_rss_kib\":%ld,\"end_rss_kib\":%ld}\n",
- ARB_HOST_MODE, kind, n, rounds, init_s, build_s, reduce_s, observe_s, gc_s,
- peak, rss_mid, rss_end);
- free(input); free(second); OK(arb_release(ctx, function)); OK(arb_release(ctx, leaf));
- arboricx_free(ctx);
- return 0;
-}
diff --git a/ext/zig/tests/performance_smoke.py b/ext/zig/tests/performance_smoke.py
@@ -1,63 +0,0 @@
-#!/usr/bin/env python3
-"""Run each host smoke in a fresh, resource-limited process.
-
-Three samples are descriptive, not a statistically controlled benchmark.
-Every child validates its complete result before reporting a timing.
-"""
-import hashlib
-import json
-import os
-from pathlib import Path
-import platform
-import statistics
-import subprocess
-import sys
-import time
-
-runner, fixtures, output = sys.argv[1:]
-fixtures = Path(fixtures)
-
-
-# The C executable applies limits before initializing the runtime.
-
-cases = [(kind, n, 5) for kind in ("id", "append") for n in (256, 4096, 16384)]
-cases += [(kind, n, 1) for kind in ("deep", "distribution") for n in (1000, 10000, 100000)]
-cases += [("append", 4096, 256)]
-report = {
- "platform": platform.platform(),
- "machine": platform.machine(), "cpu_count": os.cpu_count(),
- "cpu_model": next((line.split(":", 1)[1].strip() for line in
- Path("/proc/cpuinfo").read_text().splitlines()
- if line.startswith("model name")), "unknown"),
- "rss_measurement": "Linux /proc/self/status VmHWM (peak), /proc/self/statm (mid/end)",
- "limits": {"address_space_mib": 512, "stack_mib": 8, "wall_seconds": 65, "cpu_seconds": 60},
- "samples_per_case": 3,
- "runner": runner,
- "fixture_sha256": {name: hashlib.sha256((fixtures / name).read_bytes()).hexdigest()
- for name in ("id.arboricx", "append.arboricx")},
- "cases": [],
-}
-for kind, n, rounds in cases:
- samples = []
- for _ in range(3):
- fixture = fixtures / ("append.arboricx" if kind == "append" else "id.arboricx")
- started = time.monotonic()
- completed = subprocess.run([runner, kind, str(n), str(rounds), str(fixture)],
- text=True, capture_output=True, timeout=65)
- if completed.returncode:
- raise RuntimeError(f"{kind}/{n}/{rounds}: exit {completed.returncode}\n{completed.stderr}")
- sample = json.loads(completed.stdout)
- if "host_mode" not in report:
- report["host_mode"] = sample["host_mode"]
- if sample["host_mode"] != report["host_mode"]:
- raise RuntimeError("mixed runtime optimization modes")
- sample["process_wall_s"] = time.monotonic() - started
- samples.append(sample)
- entry = {"kind": kind, "n": n, "rounds": rounds, "samples": samples}
- entry["median_ms_per_iteration"] = {
- field: statistics.median(s[field + "_s"] for s in samples) * 1000 / rounds
- for field in ("build", "reduce", "observe", "gc")}
- entry["max_peak_rss_mib"] = max(s["peak_rss_kib"] for s in samples) / 1024
- report["cases"].append(entry)
- print(f"{kind}/{n} x{rounds}: {entry['median_ms_per_iteration']} peak={entry['max_peak_rss_mib']:.1f} MiB", flush=True)
-Path(output).write_text(json.dumps(report, indent=2) + "\n")
diff --git a/ext/zig/tests/python_ffi_test.py b/ext/zig/tests/python_ffi_test.py
@@ -1,383 +0,0 @@
-#!/usr/bin/env python3
-"""Python FFI tests for the Arboricx C ABI.
-
-Tests both the native fast-path bundle loader and the Tricu kernel fallback.
-"""
-import ctypes
-import os
-import sys
-import time
-
-SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
-ZIG_DIR = os.path.dirname(SCRIPT_DIR)
-lib_path = os.environ.get(
- "ARBORICX_LIB",
- os.path.join(ZIG_DIR, "zig-out", "lib", "libarboricx.so"),
-)
-lib = ctypes.CDLL(lib_path)
-
-# --- Lifecycle ---
-lib.arboricx_init.restype = ctypes.c_void_p
-lib.arboricx_free.argtypes = [ctypes.c_void_p]
-
-# --- Tree construction ---
-lib.arb_leaf.argtypes = [ctypes.c_void_p]
-lib.arb_leaf.argtypes += [ctypes.POINTER(ctypes.c_uint64)]
-lib.arb_leaf.restype = ctypes.c_int
-lib.arb_stem.argtypes = [ctypes.c_void_p, ctypes.c_uint64]
-lib.arb_stem.argtypes += [ctypes.POINTER(ctypes.c_uint64)]
-lib.arb_stem.restype = ctypes.c_int
-lib.arb_fork.argtypes = [ctypes.c_void_p, ctypes.c_uint64, ctypes.c_uint64]
-lib.arb_fork.argtypes += [ctypes.POINTER(ctypes.c_uint64)]
-lib.arb_fork.restype = ctypes.c_int
-lib.arb_app.argtypes = [ctypes.c_void_p, ctypes.c_uint64, ctypes.c_uint64]
-lib.arb_app.argtypes += [ctypes.POINTER(ctypes.c_uint64)]
-lib.arb_app.restype = ctypes.c_int
-
-# --- Reduction ---
-lib.arb_reduce.argtypes = [ctypes.c_void_p, ctypes.c_uint64, ctypes.c_uint64]
-lib.arb_reduce.argtypes += [ctypes.POINTER(ctypes.c_uint64)]
-lib.arb_reduce.restype = ctypes.c_int
-
-# --- Codecs ---
-lib.arb_of_number.argtypes = [ctypes.c_void_p, ctypes.c_uint64]
-lib.arb_of_number.argtypes += [ctypes.POINTER(ctypes.c_uint64)]
-lib.arb_of_number.restype = ctypes.c_int
-lib.arb_of_string.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
-lib.arb_of_string.argtypes += [ctypes.POINTER(ctypes.c_uint64)]
-lib.arb_of_string.restype = ctypes.c_int
-lib.arb_of_bytes.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t]
-lib.arb_of_bytes.argtypes += [ctypes.POINTER(ctypes.c_uint64)]
-lib.arb_of_bytes.restype = ctypes.c_int
-lib.arb_of_list.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint64), ctypes.c_size_t]
-lib.arb_of_list.argtypes += [ctypes.POINTER(ctypes.c_uint64)]
-lib.arb_of_list.restype = ctypes.c_int
-lib.arb_to_number.argtypes = [ctypes.c_void_p, ctypes.c_uint64, ctypes.POINTER(ctypes.c_uint64)]
-lib.arb_to_number.restype = ctypes.c_int
-lib.arb_to_string.argtypes = [ctypes.c_void_p, ctypes.c_uint64, ctypes.POINTER(ctypes.POINTER(ctypes.c_uint8)), ctypes.POINTER(ctypes.c_size_t)]
-lib.arb_to_string.restype = ctypes.c_int
-lib.arb_to_bool.argtypes = [ctypes.c_void_p, ctypes.c_uint64, ctypes.POINTER(ctypes.c_int)]
-lib.arb_to_bool.restype = ctypes.c_int
-lib.arboricx_free_buf.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t]
-
-# --- Result unwrapping ---
-lib.arb_unwrap_result.argtypes = [ctypes.c_void_p, ctypes.c_uint64, ctypes.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_uint64), ctypes.POINTER(ctypes.c_uint64)]
-lib.arb_unwrap_result.restype = ctypes.c_int
-lib.arb_unwrap_host_value.argtypes = [ctypes.c_void_p, ctypes.c_uint64, ctypes.POINTER(ctypes.c_uint64), ctypes.POINTER(ctypes.c_uint64)]
-lib.arb_unwrap_host_value.restype = ctypes.c_int
-
-# --- Kernel ---
-lib.arb_kernel_root.argtypes = [ctypes.c_void_p]
-lib.arb_kernel_root.argtypes += [ctypes.POINTER(ctypes.c_uint64)]
-lib.arb_kernel_root.restype = ctypes.c_int
-
-# --- Native bundle loading ---
-lib.arb_load_bundle.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t, ctypes.c_char_p]
-lib.arb_load_bundle.argtypes += [ctypes.POINTER(ctypes.c_uint64)]
-lib.arb_load_bundle.restype = ctypes.c_int
-lib.arb_load_bundle_default.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t]
-lib.arb_load_bundle_default.argtypes += [ctypes.POINTER(ctypes.c_uint64)]
-lib.arb_load_bundle_default.restype = ctypes.c_int
-
-
-lib.arb_release.argtypes = [ctypes.c_void_p, ctypes.c_uint64]
-lib.arb_release.restype = ctypes.c_int
-lib.arb_collect.argtypes = [ctypes.c_void_p]
-lib.arb_collect.restype = ctypes.c_int
-lib.arb_abi_version.restype = ctypes.c_uint32
-assert lib.arb_abi_version() == 3
-assert not hasattr(lib, "arb_run_io"), "retired v1 entrypoint remains exported"
-
-def owned(name, *args):
- out = ctypes.c_uint64()
- status = getattr(lib, name)(*args, ctypes.byref(out))
- if status != 0:
- raise RuntimeError(f"{name}: status {status}")
- return out.value
-
-ctx = lib.arboricx_init()
-print("ctx init ok")
-
-fixtures = os.path.join(ZIG_DIR, "..", "..", "test", "fixtures")
-
-
-def read_bundle(name):
- path = os.path.join(fixtures, name)
- with open(path, "rb") as f:
- return f.read()
-
-
-def c_bytes(py_bytes):
- arr = (ctypes.c_uint8 * len(py_bytes))(*py_bytes)
- return arr
-
-
-def to_string(ctx, root):
- ptr = ctypes.POINTER(ctypes.c_uint8)()
- length = ctypes.c_size_t()
- if lib.arb_to_string(ctx, root, ctypes.byref(ptr), ctypes.byref(length)) != 0:
- raise RuntimeError("to_string failed")
- result = bytes(ptr[i] for i in range(length.value))
- lib.arboricx_free_buf(ctx, ptr, length.value)
- return result.decode("utf-8")
-
-
-def to_number(ctx, root):
- out = ctypes.c_uint64()
- if lib.arb_to_number(ctx, root, ctypes.byref(out)) != 0:
- raise RuntimeError("to_number failed")
- return out.value
-
-
-def to_bool(ctx, root):
- out = ctypes.c_int()
- if lib.arb_to_bool(ctx, root, ctypes.byref(out)) != 0:
- raise RuntimeError("to_bool failed")
- return bool(out.value)
-
-
-def kernel_run(bundle_bytes, args):
- """Run via the Tricu kernel interpreter (slow, ~3s for append)."""
- buf = c_bytes(bundle_bytes)
- bundle_tree = owned("arb_of_bytes", ctx, buf, len(bundle_bytes))
- tag = owned("arb_of_number", ctx, 1)
- arg_items = []
- for a in args:
- arg_items.append(owned("arb_of_string", ctx, a.encode("utf-8")))
- current = owned("arb_leaf", ctx)
- for item in reversed(arg_items):
- current = owned("arb_fork", ctx, item, current)
- app0 = owned("arb_app", ctx, owned("arb_kernel_root", ctx), tag)
- app1 = owned("arb_app", ctx, app0, bundle_tree)
- app2 = owned("arb_app", ctx, app1, current)
- result = owned("arb_reduce", ctx, app2, 1_000_000_000)
- ok = ctypes.c_int()
- value = ctypes.c_uint64()
- rest = ctypes.c_uint64()
- if lib.arb_unwrap_result(ctx, result, ctypes.byref(ok), ctypes.byref(value), ctypes.byref(rest)) != 0:
- raise RuntimeError("unwrap_result failed")
- tag_num = ctypes.c_uint64()
- payload = ctypes.c_uint64()
- if lib.arb_unwrap_host_value(ctx, value.value, ctypes.byref(tag_num), ctypes.byref(payload)) != 0:
- raise RuntimeError("unwrap_host_value failed")
- return to_string(ctx, payload.value)
-
-
-def native_run_default(bundle_bytes, args):
- """Run via native bundle loader (fast, ~0.01s)."""
- buf = c_bytes(bundle_bytes)
- term = owned("arb_load_bundle_default", ctx, buf, len(bundle_bytes))
- if term == 0:
- raise RuntimeError("load_bundle_default failed")
- current = term
- for a in args:
- arg_tree = owned("arb_of_string", ctx, a.encode("utf-8"))
- current = owned("arb_app", ctx, current, arg_tree)
- result = owned("arb_reduce", ctx, current, 1_000_000_000)
- return to_string(ctx, result)
-
-
-def native_run_named(bundle_bytes, name, args):
- """Run via native bundle loader with named export (fast)."""
- buf = c_bytes(bundle_bytes)
- term = owned("arb_load_bundle", ctx, buf, len(bundle_bytes), name.encode("utf-8"))
- if term == 0:
- raise RuntimeError(f"load_bundle({name!r}) failed")
- current = term
- for a in args:
- arg_tree = owned("arb_of_string", ctx, a.encode("utf-8"))
- current = owned("arb_app", ctx, current, arg_tree)
- result = owned("arb_reduce", ctx, current, 1_000_000_000)
- return to_string(ctx, result)
-
-
-# ============================================================================
-# Tests
-# ============================================================================
-
-all_ok = True
-
-
-def check(label, got, want):
- global all_ok
- if got != want:
- print(f"FAIL {label}: got {got!r}, want {want!r}")
- all_ok = False
- else:
- print(f"PASS {label}: {got!r}")
-
-
-# Test 1: id via kernel
-print("\n--- Test 1: id (kernel path) ---")
-bundle = read_bundle("id.arboricx")
-t0 = time.time()
-result = kernel_run(bundle, ["hello"])
-t1 = time.time()
-check("id kernel", result, "hello")
-print(f" time: {(t1 - t0) * 1000:.1f} ms")
-
-# Test 2: id via native
-print("\n--- Test 2: id (native path) ---")
-t0 = time.time()
-result = native_run_default(bundle, ["hello"])
-t1 = time.time()
-check("id native", result, "hello")
-print(f" time: {(t1 - t0) * 1000:.1f} ms")
-
-# Test 3: append via kernel
-print("\n--- Test 3: append (kernel path) ---")
-bundle = read_bundle("append.arboricx")
-t0 = time.time()
-result = kernel_run(bundle, ["Hello, ", "world!"])
-t1 = time.time()
-check("append kernel", result, "Hello, world!")
-print(f" time: {(t1 - t0) * 1000:.1f} ms")
-
-# Test 4: append via native
-print("\n--- Test 4: append (native path) ---")
-t0 = time.time()
-result = native_run_default(bundle, ["Hello, ", "world!"])
-t1 = time.time()
-check("append native", result, "Hello, world!")
-print(f" time: {(t1 - t0) * 1000:.1f} ms")
-
-# Test 5: append via native named export
-print("\n--- Test 5: append via named export 'root' ---")
-t0 = time.time()
-result = native_run_named(bundle, "append", ["Hello, ", "world!"])
-t1 = time.time()
-check("append named", result, "Hello, world!")
-print(f" time: {(t1 - t0) * 1000:.1f} ms")
-
-# Test 6: true / false via native
-print("\n--- Test 6: true / false (native path) ---")
-for name, expected in [("true.arboricx", True), ("false.arboricx", False)]:
- bundle = read_bundle(name)
- buf = c_bytes(bundle)
- term = owned("arb_load_bundle_default", ctx, buf, len(bundle))
- result = owned("arb_reduce", ctx, term, 1_000_000_000)
- check(f"{name} bool", to_bool(ctx, result), expected)
-
-# Test 7: number roundtrip
-print("\n--- Test 7: number roundtrip ---")
-num_tree = owned("arb_of_number", ctx, 42)
-check("number 42", to_number(ctx, num_tree), 42)
-
-# Test 8: string roundtrip
-print("\n--- Test 8: string roundtrip ---")
-str_tree = owned("arb_of_string", ctx, b"hello")
-check("string hello", to_string(ctx, str_tree), "hello")
-
-# Independent canonical writer oracle, exercised through the public IO ABI.
-# Build runtime graphs separately so shared/unshared equal children cannot be
-# hidden by a matching bug in the native loader and serializer.
-import hashlib
-import random
-import struct
-import tempfile
-from pathlib import Path
-sys.path.insert(0, os.path.join(ZIG_DIR, "..", "..", "test"))
-from executable_object import indexed_manifest, verify_payload, INDEXED
-
-lib.arb_run_io_v2.argtypes = [ctypes.c_void_p, ctypes.c_uint64, ctypes.POINTER(ctypes.c_uint64)]
-lib.arb_run_io_v2.restype = ctypes.c_int
-lib.arb_get_fork_children.argtypes = [ctypes.c_void_p, ctypes.c_uint64,
- ctypes.POINTER(ctypes.c_uint64), ctypes.POINTER(ctypes.c_uint64)]
-lib.arb_get_fork_children.restype = ctypes.c_int
-
-def pair(a, b):
- return owned("arb_fork", ctx, a, b)
-
-def fork_children(value):
- a, b = ctypes.c_uint64(), ctypes.c_uint64()
- assert lib.arb_get_fork_children(ctx, value, ctypes.byref(a), ctypes.byref(b)) == 0
- return a.value, b.value
-
-def execute(action):
- program = pair(owned("arb_of_string", ctx, b"tricuIO"),
- pair(owned("arb_of_number", ctx, 2), action))
- result = owned("arb_run_io_v2", ctx, program)
- ok, value, rest = ctypes.c_int(), ctypes.c_uint64(), ctypes.c_uint64()
- assert lib.arb_unwrap_result(ctx, result, ctypes.byref(ok), ctypes.byref(value), ctypes.byref(rest)) == 0
- assert ok.value, "v2 operation failed"
- return value.value
-
-# Only the scoped storage composition comes from the authenticated compiler
-# library. The canonical DAG and expected bytes/digest below remain independent.
-fixture = (Path(ZIG_DIR).parent.parent / 'test/fixtures/compiler-library.arboricx').read_bytes()
-def export(name):
- return owned('arb_load_bundle', ctx, c_bytes(fixture), len(fixture), name.encode())
-publish_tree = export('io_v2_storage.publishTree')
-read_tree = export('io_v2_storage.readTree')
-def call(function, *arguments):
- for argument in arguments:
- function = owned('arb_app', ctx, function, argument)
- return execute(function)
-limit = owned('arb_of_number', ctx, 1048576)
-def hash_tree(value):
- # Independently encode indexed-tree revision 1 hash request.
- request = pair(owned('arb_of_string', ctx, b'indexed-tree'),
- pair(owned('arb_of_number', ctx, 1), pair(owned('arb_of_string', ctx, b'hash'), pair(limit, value))))
- info = execute(pair(owned('arb_of_number', ctx, 10), request))
- size, tail = fork_children(info)
- digest, _ = fork_children(tail)
- return to_number(ctx, size), to_string(ctx, digest)
-
-rng = random.Random(510052)
-with tempfile.TemporaryDirectory() as directory:
- for case in range(24):
- model, handles = [(0,)], [owned("arb_leaf", ctx)]
- for i in range(1, 40):
- tag = rng.randrange(3)
- children = tuple(rng.randrange(i) for _ in range(tag))
- model.append((tag, *children))
- handles.append(owned(("arb_leaf", "arb_stem", "arb_fork")[tag], ctx,
- *(handles[c] for c in children)))
- canonical, by_key, memo = [], {}, {}
- def visit(i):
- if i in memo:
- return memo[i]
- key = (model[i][0], *(visit(c) for c in model[i][1:]))
- if key not in by_key:
- by_key[key] = len(canonical)
- canonical.append(key)
- memo[i] = by_key[key]
- return memo[i]
- root = visit(len(model)-1)
- manifest = indexed_manifest(root)
- nodes = struct.pack('>Q', len(canonical))
- for tag, *children in canonical:
- nodes += struct.pack('>IB', 1 + 4*tag, tag) + b''.join(struct.pack('>I', c) for c in children)
- expected = b'ARBORICX' + struct.pack('>HHIQQ', 1, 0, 2, 0, 32)
- offset = 96
- for kind, data in [(1, manifest), (2, nodes)]:
- expected += struct.pack('>IHHHHQQI', kind, 1, 1, 0, 0, offset, len(data), 0)
- offset += len(data)
- expected += manifest + nodes
- verify_payload(INDEXED, expected)
- digest = hashlib.sha256(INDEXED.encode() + b'\0' + expected).hexdigest()
- check(f"indexed independent hash {case}", hash_tree(handles[-1])[1], digest)
- path = Path(directory) / str(case)
- path_tree = owned("arb_of_string", ctx, str(path).encode())
- hash_tree_ = owned("arb_of_string", ctx, digest.encode())
- directory_tree = owned('arb_of_string', ctx, directory.encode())
- assert hash_tree(handles[-1])[0] == len(expected)
- assert to_bool(ctx, call(publish_tree, directory_tree, path_tree, limit, hash_tree_, handles[-1])) is True
- assert path.read_bytes() == expected
- assert to_bool(ctx, call(publish_tree, directory_tree, path_tree, limit, hash_tree_, handles[-1])) is False
- decoded = call(read_tree, directory_tree, path_tree, limit, hash_tree_)
- assert hash_tree(decoded) == (len(expected), digest)
- # Malformed bundle failure must not publish or clobber the output handle.
- bad = expected[:31]
- out = ctypes.c_uint64(123456)
- assert lib.arb_load_bundle_default(ctx, c_bytes(bad), len(bad), ctypes.byref(out)) == 5
- assert out.value == 123456
-
-lib.arboricx_free(ctx)
-
-if all_ok:
- print("\nAll tests passed!")
- sys.exit(0)
-else:
- print("\nSome tests failed!")
- sys.exit(1)
diff --git a/ext/zig/tools/gen_kernel.zig b/ext/zig/tools/gen_kernel.zig
@@ -1,92 +0,0 @@
-const std = @import("std");
-
-// Minimal Node definition for the DAG format (no App variant for kernels)
-const Node = union(enum(u8)) {
- leaf,
- stem: struct { child: u32 },
- fork: struct { left: u32, right: u32 },
-};
-
-fn parseLine(line: []const u8) !Node {
- var it = std.mem.splitScalar(u8, std.mem.trim(u8, line, " \t\n\r"), ' ');
- const tag = it.next() orelse return error.EmptyLine;
- if (std.mem.eql(u8, tag, "leaf")) {
- return .leaf;
- } else if (std.mem.eql(u8, tag, "stem")) {
- const child_str = it.next() orelse return error.MissingChild;
- const child = try std.fmt.parseInt(u32, child_str, 10);
- return .{ .stem = .{ .child = child } };
- } else if (std.mem.eql(u8, tag, "fork")) {
- const left_str = it.next() orelse return error.MissingLeft;
- const right_str = it.next() orelse return error.MissingRight;
- const left = try std.fmt.parseInt(u32, left_str, 10);
- const right = try std.fmt.parseInt(u32, right_str, 10);
- return .{ .fork = .{ .left = left, .right = right } };
- } else {
- return error.UnknownTag;
- }
-}
-
-pub fn main(init: std.process.Init) !void {
- const gpa = init.gpa;
- const io = init.io;
-
- const args = try init.minimal.args.toSlice(init.arena.allocator());
- if (args.len != 3) {
- std.debug.print("Usage: gen_kernel <input.dag> <output.zig>\n", .{});
- std.process.exit(1);
- }
-
- const input_path = args[1];
- const output_path = args[2];
-
- const source = try std.Io.Dir.cwd().readFileAlloc(io, input_path, gpa, .limited(10 * 1024 * 1024));
- defer gpa.free(source);
-
- var nodes = std.ArrayList(Node).empty;
- defer nodes.deinit(gpa);
-
- var it = std.mem.splitScalar(u8, source, '\n');
- const root_line = it.next() orelse return error.EmptyFile;
- const root = try std.fmt.parseInt(u32, std.mem.trim(u8, root_line, " \t\n\r"), 10);
-
- while (it.next()) |line| {
- const trimmed = std.mem.trim(u8, line, " \t\n\r");
- if (trimmed.len == 0) continue;
- const node = try parseLine(trimmed);
- try nodes.append(gpa, node);
- }
-
- const file = try std.Io.Dir.cwd().createFile(io, output_path, .{});
- defer file.close(io);
-
- var buf: [4096]u8 = undefined;
- var writer = file.writer(io, &buf);
-
- try writer.interface.writeAll("// Auto-generated from ");
- try writer.interface.writeAll(input_path);
- try writer.interface.writeAll("\n// Do not edit manually.\n\n");
-
- try writer.interface.writeAll("pub const NodeTag = enum(u8) { leaf = 0, stem = 1, fork = 2 };\n\n");
- try writer.interface.writeAll("pub const Node = union(NodeTag) {\n");
- try writer.interface.writeAll(" leaf,\n");
- try writer.interface.writeAll(" stem: struct { child: u32 },\n");
- try writer.interface.writeAll(" fork: struct { left: u32, right: u32 },\n");
- try writer.interface.writeAll("};\n\n");
-
- try writer.interface.print("pub const kernel_root: u32 = {d};\n\n", .{root});
- try writer.interface.writeAll("pub const kernel_nodes = [_]Node{\n");
-
- for (nodes.items) |node| {
- switch (node) {
- .leaf => try writer.interface.writeAll(" .leaf,\n"),
- .stem => |s| try writer.interface.print(" .{{ .stem = .{{ .child = {d} }} }},\n", .{s.child}),
- .fork => |f| try writer.interface.print(" .{{ .fork = .{{ .left = {d}, .right = {d} }} }},\n", .{f.left, f.right}),
- }
- }
-
- try writer.interface.writeAll("};\n");
- try writer.flush();
-
- std.debug.print("Generated {d} kernel nodes, root={d} -> {s}\n", .{ nodes.items.len, root, output_path });
-}
diff --git a/flake.nix b/flake.nix
@@ -12,7 +12,7 @@
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
- siteRelease = import ./ext/demo-site/release.nix { inherit pkgs; zigRuntime = self.packages.${system}.tricu-wasm; haskellRuntime = self.packages.${system}.haskellWasmBrowser; };
+ siteRelease = import ./ext/demo-site/release.nix { inherit pkgs; haskellRuntime = self.packages.${system}.haskellWasmBrowser; };
packageName = "tricu";
containerPackageName = "${packageName}-container";
@@ -188,178 +188,8 @@
sha256sum $out/workers.arboricx | cut -d ' ' -f 1 > $out/sha256
'';
- # ------------------------------------------------------------------
- # Zig Arboricx host
- # ------------------------------------------------------------------
- tricuZig = pkgs.stdenv.mkDerivation {
- pname = "tricu-zig";
- version = "0.1.0";
- src = ./ext/zig;
- passthru.optimizationMode = "ReleaseSafe";
- nativeBuildInputs = [ pkgs.zig pkgs.pkg-config ];
- buildInputs = [ pkgs.libuv ];
- buildPhase = ''
- export ZIG_GLOBAL_CACHE_DIR=$TMPDIR/zig-cache
- zig build -Doptimize=ReleaseSafe
- zig build test -Doptimize=ReleaseSafe
- zig build test -Doptimize=Debug
- '';
- installPhase = ''
- mkdir -p $out/bin $out/lib $out/include
- cp zig-out/bin/* $out/bin/ 2>/dev/null || true
- cp zig-out/lib/* $out/lib/ 2>/dev/null || true
- cp include/arboricx.h $out/include/
- '';
- };
-
- tricuWasm = pkgs.stdenv.mkDerivation {
- pname = "tricu-wasm";
- version = "0.1.0";
- src = ./ext/zig;
- nativeBuildInputs = [ pkgs.zig ];
- buildPhase = ''
- export ZIG_GLOBAL_CACHE_DIR=$TMPDIR/zig-cache
- zig build wasm
- '';
- installPhase = ''
- mkdir -p $out/lib
- cp zig-out/bin/arboricx.wasm $out/lib/
- '';
- };
-
siteJavaScript = import ./ext/demo-site/javascript.nix { inherit pkgs; };
- tricuWasmTests = pkgs.runCommand "tricu-wasm-tests" {
- nativeBuildInputs = [ pkgs.nodejs pkgs.typescript ];
- } ''
- cp -r ${tricuSource} source
- chmod -R u+w source
- cd source
- cp -r ${siteJavaScript}/. ext/demo-site/dist/
- timeout 120 node ext/wasm/check.mjs ${tricuWasm}/lib/arboricx.wasm
- timeout 120 node ext/wasm/backend-check.mjs ${tricuWasm}/lib/arboricx.wasm
- mkdir -p $out
- '';
-
- tricuWasmBrowser = pkgs.runCommand "tricu-wasm-browser" {
- nativeBuildInputs = [ pkgs.nodejs pkgs.typescript pkgs.chromium ];
- } ''
- export HOME=$TMPDIR/home
- mkdir -p "$HOME" $out
- cp -r ${tricuSource} source
- chmod -R u+w source
- cd source
- cp -r ${siteJavaScript}/. ext/demo-site/dist/
- timeout 240 node ext/wasm/browser.mjs ${tricuWasm}/lib/arboricx.wasm chromium $out/report.json
- '';
-
- tricuWasmComparison = pkgs.runCommand "tricu-wasm-comparison" {
- nativeBuildInputs = [ pkgs.nodejs pkgs.typescript ];
- } ''
- cp -r ${tricuSource} source
- chmod -R u+w source
- cd source
- cp -r ${siteJavaScript}/. ext/demo-site/dist/
- mkdir -p $out
- node ext/wasm/compare.mjs ${tricuWasm}/lib/arboricx.wasm $out/report.json
- node ext/wasm/preview-compare.mjs ${tricuWasm}/lib/arboricx.wasm $out/preview-report.json
- '';
-
- # Explicit, Linux-only smoke measurements; not a timing threshold gate.
- tricuZigPerformanceSmoke = pkgs.stdenv.mkDerivation {
- pname = "tricu-zig-performance-smoke";
- version = "0.1.0";
- src = ./.;
- nativeBuildInputs = [ pkgs.gcc pkgs.python3 ];
- buildPhase = ''
- gcc -O2 -Wall -Wextra -Werror ext/zig/tests/performance_smoke.c \
- -DARB_HOST_MODE='"${tricuZig.optimizationMode}"' \
- -I ${tricuZig}/include -L ${tricuZig}/lib -larboricx \
- -Wl,-rpath,${tricuZig}/lib -o performance-smoke
- '';
- installPhase = ''
- mkdir -p $out/bin
- cp performance-smoke $out/bin/
- python3 ext/zig/tests/performance_smoke.py \
- $out/bin/performance-smoke test/fixtures $out/report.json
- '';
- };
-
- tricuZigTests = pkgs.stdenv.mkDerivation {
- pname = "tricu-zig-tests";
- version = "0.1.0";
- src = ./.;
- nativeBuildInputs = [ pkgs.gcc pkgs.python3 tricuZig ];
- buildInputs = [ pkgs.libuv ];
- buildPhase = "true";
- doCheck = true;
- checkPhase = ''
- export LD_LIBRARY_PATH=${tricuZig}/lib:$LD_LIBRARY_PATH
- ulimit -s 32768
-
- cd ext/zig
-
- gcc -Wall -Wextra -Werror -o /tmp/handles_test tests/handles_test.c \
- -I ${tricuZig}/include -L ${tricuZig}/lib -larboricx \
- -Wl,-rpath,${tricuZig}/lib
- /tmp/handles_test
-
- test "$(tricu-zig --type string ../../test/fixtures/id.arboricx hello)" = hello
- test "$(tricu-zig --type string --kernel ../../test/fixtures/append.arboricx 'Hello, ' 'world!')" = 'Hello, world!'
-
- # Retired grants fail before even trying to open a bundle.
- if tricu-zig --unsafe-io /definitely-missing >grant.log 2>&1; then exit 1; fi
- grep -q RetiredIOGrants grant.log
-
- # C ABI smoke test
- gcc -o /tmp/c_abi_test tests/c_abi_test.c \
- -I ${tricuZig}/include -L ${tricuZig}/lib -larboricx \
- -Wl,-rpath,${tricuZig}/lib
- /tmp/c_abi_test
-
- # IO protocol shape test
- gcc -o /tmp/io_protocol_test tests/io_protocol_test.c \
- -I ${tricuZig}/include -L ${tricuZig}/lib -larboricx \
- -Wl,-rpath,${tricuZig}/lib
- /tmp/io_protocol_test
-
- # IO run test (synchronous driver)
- gcc -o /tmp/io_run_test tests/io_run_test.c \
- -I ${tricuZig}/include -L ${tricuZig}/lib -larboricx \
- -Wl,-rpath,${tricuZig}/lib
- /tmp/io_run_test
-
- # Kernel path append test
- gcc -o /tmp/c_abi_append_test tests/c_abi_append_test.c \
- -I ${tricuZig}/include -L ${tricuZig}/lib -larboricx \
- -Wl,-rpath,${tricuZig}/lib
- /tmp/c_abi_append_test
-
- # Native bundle tests
- gcc -o /tmp/native_bundle_append_test tests/native_bundle_append_test.c \
- -I ${tricuZig}/include -L ${tricuZig}/lib -larboricx \
- -Wl,-rpath,${tricuZig}/lib
- /tmp/native_bundle_append_test
-
- gcc -o /tmp/native_bundle_id_test tests/native_bundle_id_test.c \
- -I ${tricuZig}/include -L ${tricuZig}/lib -larboricx \
- -Wl,-rpath,${tricuZig}/lib
- /tmp/native_bundle_id_test
-
- gcc -o /tmp/native_bundle_bools_test tests/native_bundle_bools_test.c \
- -I ${tricuZig}/include -L ${tricuZig}/lib -larboricx \
- -Wl,-rpath,${tricuZig}/lib
- /tmp/native_bundle_bools_test
-
- # Python FFI test
- ARBORICX_LIB=${tricuZig}/lib/libarboricx.so \
- python3 tests/python_ffi_test.py
-
- mkdir -p $out
- echo "All Zig tests passed" > $out/result
- '';
- };
-
tricuHaskell = import ./ext/haskell/build.nix {
inherit pkgs haskellPackages;
source = tricuSource;
@@ -536,6 +366,25 @@
packages.${packageName} = tricuPackage;
packages.default = tricuPackage;
packages.tricu-haskell-ffi = tricuHaskell;
+ packages.tricu-generation-ffi-test = pkgs.runCommand "tricu-generation-ffi-test" {
+ nativeBuildInputs = [ pkgs.python3 ];
+ } ''
+ export PYTHONDONTWRITEBYTECODE=1
+ mkdir -p $out
+ python3 ${tricuSource}/test/shared_library_generation.py \
+ --tricu ${tricuPackage}/bin/tricu \
+ --library ${tricuHaskell}/lib/libtricu-haskell${pkgs.stdenv.hostPlatform.extensions.sharedLibrary} \
+ --directory $out/compatibility --compatibility
+ python3 ${tricuSource}/test/shared_library_smoke.py \
+ --tricu ${tricuPackage}/bin/tricu \
+ --library ${tricuHaskell}/lib/libtricu-haskell${pkgs.stdenv.hostPlatform.extensions.sharedLibrary} \
+ --directory $out/programs
+ python3 ${tricuSource}/test/io_v2_generation.py ${tricuPackage}/bin/tricu \
+ --kit ${tricuSource}/bootstrap/rebuild \
+ --haskell-library ${tricuHaskell}/lib/libtricu-haskell${pkgs.stdenv.hostPlatform.extensions.sharedLibrary} \
+ --directory $out/scoped
+ '';
+
packages.tricu-core = tricuCore;
packages.tricu-core-archive = tricuCoreArchive;
packages.tricu-core-linkage-smoke = pkgs.runCommand "tricu-core-linkage-smoke" {
@@ -626,25 +475,18 @@
--report $out/report.json
'';
- # Independent Haskell oracle for the opt-in published-library consumer.
- # Opt-in Linux paired runtime measurements. Build first; timing runs
- # explicitly outside the Nix build, never during the fast check gate.
- packages.reducerComparison = pkgs.stdenv.mkDerivation {
- pname = "tricu-reducer-comparison";
- version = "0.1.0";
- src = ./.;
- nativeBuildInputs = [ pkgs.gcc pkgs.makeWrapper
- (haskellPackages.ghcWithPackages (p: [
- p.cryptonite p.memory p.base16-bytestring p.vector
- ])) ];
+ # Native reducer smoke preserves full-result and wrong-worker controls.
+ packages.reducerSmoke = pkgs.stdenv.mkDerivation {
+ pname = "tricu-reducer-smoke";
+ version = "2026.10.24";
+ src = tricuSource;
+ nativeBuildInputs = [ (haskellPackages.ghcWithPackages (p: [
+ p.cryptonite p.memory p.base16-bytestring p.vector
+ ])) ];
buildPhase = ''
ghc -O2 -threaded -rtsopts -XOverloadedStrings -XScopedTypeVariables \
-XMultiWayIf -XLambdaCase -isrc -outputdir objects \
bench/ReducerSmoke.hs -o haskell-reducer-smoke
- gcc -O2 -Wall -Wextra -Werror ext/zig/tests/performance_smoke.c \
- -DARB_HOST_MODE='"${tricuZig.optimizationMode}"' \
- -I ${tricuZig}/include -L ${tricuZig}/lib -larboricx \
- -Wl,-rpath,${tricuZig}/lib -o zig-reducer-smoke
'';
doCheck = true;
checkPhase = ''
@@ -652,25 +494,17 @@
fixture=test/fixtures/id.arboricx
if [ "$kind" = append ]; then fixture=test/fixtures/append.arboricx; fi
./haskell-reducer-smoke "$kind" 64 2 "$fixture" +RTS -N1 -M512m -K8m -RTS
- ./zig-reducer-smoke "$kind" 64 2 "$fixture"
done
- # A real but wrong worker must fail complete-result validation.
if ./haskell-reducer-smoke id 256 1 test/fixtures/append.arboricx \
- +RTS -N1 -M512m -K8m -RTS >bad-haskell.log 2>&1; then exit 1; fi
- grep -q 'incorrect complete result' bad-haskell.log
- if ./zig-reducer-smoke id 256 1 test/fixtures/append.arboricx \
- >bad-zig.log 2>&1; then exit 1; fi
- grep -q 'failed:' bad-zig.log
+ +RTS -N1 -M512m -K8m -RTS >bad-worker.log 2>&1; then exit 1; fi
+ grep -q 'incorrect complete result' bad-worker.log
'';
installPhase = ''
- mkdir -p $out/bin $out/libexec
- cp haskell-reducer-smoke zig-reducer-smoke $out/bin/
- cp test/reducer_comparison.py test/process_rss.py $out/libexec/
+ mkdir -p $out/bin
+ cp haskell-reducer-smoke $out/bin/
ghc --version > $out/compiler-version
- makeWrapper ${pkgs.python3}/bin/python3 $out/bin/compare-reducers \
- --add-flags "$out/libexec/reducer_comparison.py $out/bin/haskell-reducer-smoke $out/bin/zig-reducer-smoke ${./test/fixtures}"
'';
- meta.mainProgram = "compare-reducers";
+ meta.mainProgram = "haskell-reducer-smoke";
};
packages.inspectorOracle = pkgs.runCommand "arboricx-inspector-oracle" {
nativeBuildInputs = [ (haskellPackages.ghcWithPackages (p: [
@@ -733,18 +567,18 @@
--directory $out/cost > $out/cost.log
'';
- packages.tricu-zig = tricuZig;
- packages.tricu-wasm = tricuWasm;
- packages.tricu-wasm-tests = tricuWasmTests;
- packages.tricu-wasm-browser = tricuWasmBrowser;
- packages.tricu-wasm-comparison = tricuWasmComparison;
- packages.tricu-zig-tests = tricuZigTests;
- packages.tricu-zig-performance-smoke = tricuZigPerformanceSmoke;
packages.tricu-php = tricuPhp;
packages.tricu-php-tests = tricuPhpTests;
packages.tricu-js = tricuJs;
packages.tricu-js-tests = tricuJsTests;
+ checks.haskellRuntimeSelection = pkgs.runCommand "tricu-haskell-runtime-selection" {} ''
+ test ! -e ${tricuSource}/ext/zig
+ test ! -e ${tricuSource}/ext/wasm
+ test '${builtins.toJSON (builtins.filter (name: pkgs.lib.hasInfix "zig" name || pkgs.lib.hasPrefix "tricu-wasm" name) (builtins.attrNames self.packages.${system}))}' = '[]'
+ mkdir -p $out
+ '';
+
checks.runtimeBoundary = pkgs.runCommand "tricu-runtime-source-boundary" {} ''
test -f ${runtimeSource}/src/Runtime/Tree.hs
for path in Research.hs Eval.hs FileEval.hs Lexer.hs Parser.hs \
@@ -826,7 +660,6 @@
inputsFrom = [
tricuPackage
- tricuZig
tricuPhp
];
};
diff --git a/test/bootstrap_acceptance.py b/test/bootstrap_acceptance.py
@@ -44,7 +44,7 @@ def verify_generation(run, runtime, source_root=None, execution_host='haskell'):
produced_adapter(run, inputs)
invocation = json.loads((run / 'invocation.json').read_text())
check_invocation(inputs, invocation, execution_host)
- if execution_host == 'zig':
+ if execution_host == 'haskell-ffi':
request = json.loads((run / 'execution-request.json').read_text())
if version(request.get('execution_io_version', 1)) != execution_io:
raise ValueError('execution request IO version mismatch')
diff --git a/test/bootstrap_cross_host.py b/test/bootstrap_cross_host.py
@@ -1,281 +0,0 @@
-#!/usr/bin/env python3
-"""Opt-in isolated H1 -> H2 -> {H3,Z3} reproduction, or bounded native_math probe.
-
-Every job sees only /source, /kit, /work, /nix/store and ephemeral /tmp,/proc,/dev.
-The generation supervisor runs inside isolation and monitors the execution
-child. Haskell supplies common artifact transport and packaging.
-"""
-import argparse
-import json
-from pathlib import Path
-import shutil
-import subprocess
-import sys
-import tarfile
-import time
-
-from bootstrap_acceptance import ARTIFACTS, sha, verify, verify_generation
-from bootstrap_generations import source_map, snapshot
-from bootstrap_inputs import load_kit, kit_io_version
-from generation_io import versions, produced_adapter, cli_version, version
-from bootstrap_retention_lock import protect_generations
-from process_rss import run_guarded
-
-ROOT = Path(__file__).resolve().parent.parent
-
-
-def save(path, value):
- path.write_text(json.dumps(value, indent=2)+'\n')
-
-
-def inventory(run):
- return {p.name:sha(p) for p in (run/'cas/objects').glob('*/*')}
-
-
-def verify_cross(first, second, control, zig, runtime, library, source):
- if len({p.resolve() for p in (first,second,control,zig)}) != 4:
- raise ValueError('four distinct fresh generations required')
- pair = verify(first, second, runtime, source)
- control_pair = verify(second, control, runtime, source)
- zi, zs, audit = verify_generation(zig, library, source, 'zig')
- hi = control_pair['inputs'][1]
- if versions(zi) != versions(hi) or produced_adapter(zig,zi) != produced_adapter(control,hi):
- raise ValueError('cross-host IO framing or produced adapter mismatch')
- for key in ('sources','map_sha256','backend','root','adapter_sha256',
- 'compiler_bundle_sha256','compiler_tree','compiler_tree_kind',
- 'worker_export','backend_options'):
- if zi[key] != hi[key]:
- raise ValueError('cross-host input mismatch: '+key)
- if zi.get('transport_runtime_sha256') != sha(runtime) or zi.get('transport_host') != 'haskell':
- raise ValueError('cross-host transport identity mismatch')
- if zi.get('executor_sha256') != sha(source/'test/zig_generation_executor.py'):
- raise ValueError('execution launcher identity mismatch')
- request = json.loads((zig/'execution-request.json').read_text())
- expected_request = dict(worker_export=zi['worker_export'],
- compiler_bundle_sha256=zi['compiler_bundle_sha256'],
- adapter_sha256=zi['adapter_sha256'],
- arguments=['/source','/work/generation/overlay','/work/generation/cas',
- zi['compiler_bundle_sha256'],(zig/'source-map.v1').read_text(),'source_map'])
- if 'execution_io_version' in request:
- expected_request['execution_io_version'] = versions(zi)[0]
- if request != expected_request:
- raise ValueError('isolated Zig execution request mismatch')
- for name in ARTIFACTS:
- if sha(second/name) != sha(zig/name):
- raise ValueError('cross-host packaged artifact mismatch: '+name)
- # Byte-identical full inventories include manifests, receipts and every
- # published indexed object, not just a subset of selected export hashes.
- expected = inventory(second)
- for run in (control,zig):
- if inventory(run) != expected:
- raise ValueError('complete canonical object/receipt inventory differs: '+str(run))
- for alias in hi['sources']:
- if (second/'cas/cache/source-build'/alias).read_bytes() != (run/'cas/cache/source-build'/alias).read_bytes():
- raise ValueError('complete checkpoint mismatch: '+alias)
- return dict(accepted=True, scope='cross-host-fixed-point-not-retention',
- generations=list(map(str,(first,second,control,zig))),
- haskell_pair=pair, haskell_control=control_pair,
- zig_inputs=zi, zig_audit=audit, objects=expected,
- artifacts={name:sha(zig/name) for name in ARTIFACTS})
-
-
-def map_name(io_version):
- return 'bootstrap/compiler-source-map' + ('.io-v2' if version(io_version)==2 else '') + '.v1'
-
-
-def freeze_kit(selected, destination, io_version):
- raw,payloads,_=load_kit(selected,'stable')
- if kit_io_version(json.loads(raw)) != version(io_version):
- raise ValueError('selected kit IO version mismatch')
- if destination.exists():
- retained,contents,_=load_kit(destination,'stable')
- if retained!=raw or contents!=payloads:
- raise ValueError('frozen seed kit changed')
- else:
- destination.mkdir()
- (destination/'bootstrap-kit.json').write_bytes(raw)
- for role,name in [('compiler','rebuilt-compiler.arboricx'),('adapter','io-adapter.arboricx')]:
- (destination/name).write_bytes(payloads[role])
- return sha(destination/'bootstrap-kit.json')
-
-
-def kit_from_generation(run,kit):
- record=json.loads((run/'inputs.json').read_text())
- output=versions(record)[1]
- adapter=produced_adapter(run,record)
- compiler=(run/'rebuilt-compiler.arboricx').read_bytes()
- if kit.exists():
- raw,payloads,_=load_kit(kit,'stable')
- if kit_io_version(json.loads(raw))!=output or payloads!={'compiler':compiler,'adapter':adapter}:
- raise ValueError('retained kit differs from parent generation')
- return kit
- kit.mkdir()
- artifacts={}
- for role,name,export,payload in [('compiler','rebuilt-compiler.arboricx','sourceMapBuildStable',compiler),
- ('adapter','io-adapter.arboricx','main',adapter)]:
- (kit/name).write_bytes(payload)
- artifacts[role]=dict(file=name,export=export,sha256=sha(kit/name))
- save(kit/'bootstrap-kit.json',dict(schema='tricu.bootstrap-kit.v2',backend='stable',io_version=output,
- parent_generation=str(run),artifacts=artifacts))
- return kit
-
-
-def sandbox_command(source, kit, stage, runtime, library, host, root, memory, seconds, io_version=1):
- command = ['unshare','--user','--map-root-user','--mount','--pid','--fork',
- '--kill-child','--mount-proc','--','bwrap','--die-with-parent','--unshare-all',
- '--ro-bind','/nix/store','/nix/store','--proc','/proc','--dev','/dev',
- '--tmpfs','/tmp','--ro-bind',str(source),'/source',
- '--ro-bind',str(kit),'/kit','--bind',str(stage),'/work',
- '--chdir','/source','--setenv','HOME','/tmp','--setenv','TMPDIR','/tmp',
- '--setenv','PYTHONDONTWRITEBYTECODE','1',
- sys.executable,'/source/test/bootstrap_generations.py','/work/generation',
- '--bootstrap-kit','/kit','--map','/source/'+map_name(io_version),
- '--source-root','/source','--root',root,'--backend','stable','--fresh',
- '--tricu',str(runtime),'--execution-host',host,'--capabilities','8',
- '--memory-mib',str(memory),'--wall-seconds',str(seconds),
- '--initial-seconds',str(min(600,seconds)),
- '--extended-seconds',str(min(1800,seconds))]
- if host=='zig': command += ['--zig-library',str(library)]
- return command
-
-
-def main():
- p = argparse.ArgumentParser(description=__doc__)
- p.add_argument('--directory',type=Path,required=True)
- p.add_argument('--source-commit',required=True)
- p.add_argument('--tricu',type=Path,required=True)
- p.add_argument('--zig-library',type=Path,required=True)
- p.add_argument('--probe',action='store_true',help='native_math on both hosts, 1 GiB/120s per job')
- p.add_argument('--verify-only',action='store_true')
- p.add_argument('--continue-experiment',action='store_true',help='re-audit completed jobs and retry pre-execution launch failures')
- p.add_argument('--io-version',type=cli_version,default=1,help='explicit cohort framing; default historical v1')
- p.add_argument('--bootstrap-kit',type=Path,help='explicit trusted seed; required for a new v2 experiment')
- args=p.parse_args()
- directory=args.directory.resolve()
- runtime,library=args.tricu.resolve(),args.zig_library.resolve()
- source=directory/'source'
- locks=protect_generations([directory])
- if args.verify_only:
- evidence=verify_cross(*(directory/label/'generation' for label in ('H1','H2','H3','Z3')),
- runtime,library,source)
- print(json.dumps(evidence,indent=2))
- return
- commit=subprocess.check_output(['git','rev-parse',args.source_commit+'^{commit}'],cwd=ROOT,text=True).strip()
- archive=directory/'source.tar'
- if args.io_version==2 and not args.bootstrap_kit and not args.continue_experiment:
- p.error('new v2 experiments require --bootstrap-kit')
- if args.bootstrap_kit:
- raw,_,_=load_kit(args.bootstrap_kit,'stable')
- if kit_io_version(json.loads(raw)) != args.io_version:
- raise ValueError('selected kit IO version mismatch')
- if args.continue_experiment:
- original=json.loads((directory/'experiment.json').read_text())
- if (args.probe or original.get('io_version',1)!=args.io_version or original['source_commit']!=commit or original['source_archive_sha256']!=sha(archive)
- or original['runtime']!=dict(path=str(runtime),sha256=sha(runtime))
- or original['zig_library']!=dict(path=str(library),sha256=sha(library))):
- raise ValueError('continuation requires identical frozen sources and runtimes')
- with tarfile.open(archive) as tar:
- for member in tar:
- if member.isfile() and (source/member.name).read_bytes()!=tar.extractfile(member).read():
- raise ValueError('frozen source changed: '+member.name)
- # Preserve the failed attempt, including its exact original tooling.
- shutil.copyfile(directory/'report.json',directory/('report-before-'+str(time.time_ns())+'.json'))
- else:
- directory.mkdir(parents=True,exist_ok=False)
- with archive.open('wb') as out:
- subprocess.run(['git','archive',commit],cwd=ROOT,stdout=out,check=True)
- source.mkdir()
- with tarfile.open(archive) as tar:
- # Historical tracked build-output symlink, excluded by recovery
- # archives too. Keep all other extraction safety checks.
- tar.extractall(source,members=(m for m in tar if m.name!='ext/zig/result'),filter='data')
- # The sandbox also mounts this snapshot read-only; never use live sources.
- selected_seed=args.bootstrap_kit or (directory/'seed-kit' if args.continue_experiment and 'seed_manifest_sha256' in original else source/'bootstrap/rebuild')
- seed_pin=freeze_kit(selected_seed,directory/'seed-kit',args.io_version)
- if args.continue_experiment and seed_pin!=original.get('seed_manifest_sha256',seed_pin):
- raise ValueError('continuation seed identity mismatch')
- map_text,entries,required=source_map(source/map_name(args.io_version),source,'source_map')
- attempt='continuation-'+str(time.time_ns()) if args.continue_experiment else 'harness'
- harness=directory/attempt/'test'
- harness.mkdir(parents=True)
- for path in (ROOT/'test').glob('*.py'): shutil.copyfile(path,harness/path.name)
- tooling={path.name:sha(path) for path in harness.glob('*.py')}
- memory,seconds=(1024,120) if args.probe else (32768,3600)
- definition=dict(schema='tricu.cross-host-experiment.v1',source_commit=commit,
- io_version=args.io_version,source_map=map_name(args.io_version),seed_manifest_sha256=seed_pin,
- source_archive_sha256=sha(archive), excluded_source_paths=['ext/zig/result'],
- runtime=dict(path=str(runtime),sha256=sha(runtime)),
- zig_library=dict(path=str(library),sha256=sha(library)),
- python=dict(path=sys.executable,sha256=sha(Path(sys.executable))),
- harness_sha256=sha(Path(__file__)), tooling_sha256=tooling,
- isolation_tools={name:dict(path=str(Path(shutil.which(name)).resolve()),sha256=sha(Path(shutil.which(name)))) for name in ('unshare','bwrap')},
- limits=dict(memory_mib=memory,wall_seconds=seconds,capabilities=8),
- expected='H2 = H3 = Z3: complete objects, receipts, checkpoints and three bundles',
- comparison_exclusions=['runtime/transport provenance','timings','logs','filesystem locations','invocation records'],
- **snapshot(map_text,entries,source))
- save(directory/(attempt+'-definition.json' if args.continue_experiment else 'experiment.json'),definition)
- report=dict(complete=False,scope='probe' if args.probe else 'full',stages=[])
- save(directory/'report.json',report)
- def job(label,kit,host):
- # Verify the kit before crossing the sandbox boundary; the shared runner
- # independently captures/authenticates it again inside.
- raw, _, _ = load_kit(kit,'stable')
- if kit_io_version(json.loads(raw)) != args.io_version:
- raise ValueError('cross-host kit IO version mismatch')
- stage=directory/label
- if args.continue_experiment and stage.exists():
- if (stage/'generation/status.json').is_file():
- inputs,_,_=verify_generation(stage/'generation',runtime if host=='haskell' else library,source,host)
- if inputs['compiler_bundle_sha256']!=sha(kit/'rebuilt-compiler.arboricx') or inputs['adapter_sha256']!=sha(kit/'io-adapter.arboricx'):
- raise ValueError('completed job used a different kit')
- report['stages'].append(label)
- save(directory/'report.json',report)
- print('Re-audited completed '+label,flush=True)
- return stage/'generation'
- if (stage/'generation').exists():
- raise ValueError('cannot resume a partially executed generation; select a fresh experiment')
- stage.rename(directory/(label+'-launch-failed-'+str(time.time_ns())))
- stage.mkdir()
- command=sandbox_command(source,kit,stage,runtime,library,host,
- 'native_math' if args.probe else 'source_map',memory,seconds,args.io_version)
- save(stage/'sandbox-command.json',command)
- try:
- reply,_=run_guarded(command,memory_mib=memory,seconds=seconds+30)
- except Exception as error:
- (stage/'supervisor.log').write_text(str(getattr(error,'output',''))+'\n'+str(error))
- report['failure']=label
- save(directory/'report.json',report)
- raise
- (stage/'supervisor.log').write_text(reply)
- report['stages'].append(label)
- save(directory/'report.json',report)
- print('Completed isolated '+label,flush=True)
- return stage/'generation'
- def kit_from(run,label):
- return kit_from_generation(run,directory/(label+'-kit'))
- first=job('H1',directory/'seed-kit','haskell')
- if args.probe:
- zig=job('Z3',directory/'seed-kit','zig')
- if inventory(first)!=inventory(zig): raise ValueError('probe complete object inventories differ')
- else:
- second=job('H2',kit_from(first,'H1'),'haskell')
- accepted=verify(first,second,runtime,source)
- if (directory/'accepted-pair.json').exists():
- if json.loads((directory/'accepted-pair.json').read_text())!=accepted:
- raise ValueError('existing pair acceptance changed')
- else:
- save(directory/'accepted-pair.json',accepted)
- kit=kit_from(second,'H2')
- control=job('H3',kit,'haskell')
- zig=job('Z3',kit,'zig')
- save(directory/'accepted-cross-host.json',verify_cross(first,second,control,zig,runtime,library,source))
- if tooling != {name:sha(ROOT/'test'/name) for name in tooling}:
- raise ValueError('experiment tooling changed during execution')
- report['complete']=True
- save(directory/'report.json',report)
- label='Isolated mapped-library probe' if args.probe else 'Cross-runtime compiler reproduction'
- print(label+' verified: '+str(directory))
-
-
-if __name__=='__main__':
- main()
diff --git a/test/bootstrap_cross_host_controls.py b/test/bootstrap_cross_host_controls.py
@@ -1,80 +0,0 @@
-#!/usr/bin/env python3
-"""Check verifier rejection of corrupted copies of a completed experiment.
-Restore the copies and verify acceptance again.
-"""
-import argparse
-import hashlib
-import json
-from pathlib import Path
-import shutil
-import subprocess
-from bootstrap_acceptance import sha
-from bootstrap_cross_host import verify_cross
-from native_indexed_store import indexed_bytes
-from base_workloads import encode
-
-
-def main():
- p=argparse.ArgumentParser(description=__doc__)
- p.add_argument('experiment',type=Path)
- p.add_argument('--directory',type=Path,required=True)
- a=p.parse_args(); a.directory.mkdir(parents=True,exist_ok=False)
- e=a.experiment.resolve(); definition=json.loads((e/'experiment.json').read_text())
- runtime=Path(definition['runtime']['path']); library=Path(definition['zig_library']['path'])
- first,second,control,original=(e/name/'generation' for name in ('H1','H2','H3','Z3'))
- source=e/'source'; copy=a.directory/'zig-copy'
- shutil.copytree(original,copy)
- report=dict(complete=False,rejected=[],acceptance_sha256=sha(e/'accepted-cross-host.json'))
- def reject(label,expected,zig=copy):
- try: verify_cross(first,second,control,zig,runtime,library,source)
- except (ValueError,AssertionError,subprocess.CalledProcessError) as error:
- if expected and expected not in str(error): raise
- report['rejected'].append(label)
- (a.directory/(label+'.log')).write_text(str(error)+'\n'+str(getattr(error,'output','')))
- else: raise AssertionError('invalid experiment accepted: '+label)
- try:
- reject('same-directory','four distinct',zig=control)
- for key,value,message in [('execution_host','haskell','execution host mismatch'),
- ('runtime_sha256','0'*64,'runtime identity mismatch'),
- ('fresh_store',False,'fresh-store execution evidence'),
- ('compiler_bundle_sha256','0'*64,'execution request identity mismatch')]:
- saved={name:(copy/name).read_bytes() for name in ('inputs.json','status.json')}
- for name,payload in saved.items():
- record=json.loads(payload); record[key]=value
- (copy/name).write_text(json.dumps(record))
- reject(key,message)
- for name,payload in saved.items(): (copy/name).write_bytes(payload)
- request_path=copy/'execution-request.json'
- request_bytes=request_path.read_bytes()
- request=json.loads(request_bytes)
- request['execution_io_version']=1 if request.get('execution_io_version',1)==2 else 2
- request_path.write_text(json.dumps(request))
- reject('request-io-version','execution request IO version mismatch')
- request_path.write_bytes(request_bytes)
- status=json.loads((copy/'status.json').read_text())
- digest=status['artifacts']['build-stable.arboricx']['tree']
- obj=copy/'cas/objects'/digest[:3]/digest
- original_object=obj.read_bytes(); obj.write_bytes(b'corrupt')
- reject('corrupt-object',None)
- obj.write_bytes(original_object)
- package=copy/'build-stable.arboricx'; original_package=package.read_bytes()
- package.write_bytes(original_package[:-1]+bytes([original_package[-1]^1]))
- status['artifacts']['build-stable.arboricx']['sha256']=sha(package)
- (copy/'status.json').write_text(json.dumps(status))
- reject('different-authenticated-package','cross-host packaged artifact mismatch')
- package.write_bytes(original_package)
- shutil.copyfile(original/'status.json',copy/'status.json')
- extra=indexed_bytes(encode(987654321))
- digest=hashlib.sha256(b'arboricx.indexed-tree.v1\0'+extra).hexdigest()
- obj=copy/'cas/objects'/digest[:3]/digest
- assert not obj.exists(); obj.parent.mkdir(exist_ok=True); obj.write_bytes(extra)
- reject('additional-valid-object','complete canonical object/receipt inventory differs')
- obj.unlink()
- # The restored copy must pass, so failure is not caused merely by relocation.
- verify_cross(first,second,control,copy,runtime,library,source)
- report.update(complete=True,restored_copy_passes=True)
- print('PASS: '+str(len(report['rejected']))+' cross-host rejection controls; restored copy accepted')
- finally: (a.directory/'report.json').write_text(json.dumps(report,indent=2)+'\n')
-
-
-if __name__=='__main__': main()
diff --git a/test/bootstrap_cross_host_smoke.py b/test/bootstrap_cross_host_smoke.py
@@ -1,54 +0,0 @@
-#!/usr/bin/env python3
-"""Observe the post-generation program on Zig against independent complete trees.
-Requires the Haskell post-acceptance smoke and cross-host reproduction evidence.
-Compilation of this program was Haskell-executed; compiler bundles are bit-identical.
-"""
-import argparse
-import hashlib
-import json
-from pathlib import Path
-import sys
-from bootstrap_smoke import smoke_cases
-from bootstrap_acceptance import sha
-from process_rss import run_guarded
-
-ROOT=Path(__file__).resolve().parent.parent
-
-
-def main():
- p=argparse.ArgumentParser(description=__doc__)
- p.add_argument('experiment',type=Path)
- p.add_argument('--library',type=Path,required=True)
- p.add_argument('--directory',type=Path,required=True)
- a=p.parse_args(); a.directory.mkdir(parents=True,exist_ok=False)
- accepted=json.loads((a.experiment/'accepted-cross-host.json').read_text())
- hs=a.experiment/'haskell-smoke'
- previous=json.loads((hs/'report.json').read_text())
- assert accepted['accepted'] and previous['complete']
- assert sha(a.library)==accepted['zig_inputs']['runtime_sha256']
- assert previous['compiler_sha256']==accepted['artifacts']['build-stable.arboricx']
- assert sha(hs/'program.arboricx')==previous['bundle_sha256']
- report=dict(complete=False,compiler_sha256=previous['compiler_sha256'],
- bundle_sha256=previous['bundle_sha256'],runtime_sha256=sha(a.library),
- acceptance_sha256=sha(a.experiment/'accepted-cross-host.json'),
- compilation_host='haskell',execution_host='zig',
- observer_sha256=sha(ROOT/'test/zig_observe.py'),cases=[])
- try:
- for i,(export,argument,expected) in enumerate(smoke_cases()):
- input_path=a.directory/(str(i)+'.input.tree'); input_path.write_bytes(argument)
- output=a.directory/(str(i)+'.output.tree')
- reply,peak=run_guarded([sys.executable,str(ROOT/'test/zig_observe.py'),
- '--library',str(a.library),'--bundle',str(hs/'program.arboricx'),
- '--export',export,'--argument',str(input_path),'--output',str(output)],
- memory_mib=512,seconds=65)
- (a.directory/(str(i)+'.log')).write_text(reply)
- assert output.read_bytes()==expected, (export,i)
- assert sha(output)==previous['cases'][i]['output_sha256']
- report['cases'].append(dict(export=export,input_sha256=sha(input_path),output_sha256=sha(output),peak_kib=peak))
- report.update(complete=True,wrong_output_rejected=(output.read_bytes()!=b'\0'))
- assert report['wrong_output_rejected']
- print('PASS: 12 complete results on Zig, including recursive lists and invoice totals/text')
- finally: (a.directory/'report.json').write_text(json.dumps(report,indent=2)+'\n')
-
-
-if __name__=='__main__': main()
diff --git a/test/bootstrap_generations.py b/test/bootstrap_generations.py
@@ -84,10 +84,8 @@ def main():
parser.add_argument('previous', type=Path, nargs='?', help='completed generation; omit when selecting --bootstrap-kit')
parser.add_argument('output', type=Path)
parser.add_argument('--tricu', default='result/bin/tricu')
- parser.add_argument('--execution-host', choices=('haskell', 'zig'), default='haskell')
- parser.add_argument('--zig-library', type=Path, help='explicit Nix-built owned-ABI library; required for Zig execution')
- parser.add_argument('--zig-gc-policy', type=int, nargs=2, metavar=('MIN_ALLOCATIONS', 'LIVE_MULTIPLIER'),
- help='explicit positive u32 allocation headroom (not an RSS limit); Zig only')
+ parser.add_argument('--execution-host', choices=('haskell', 'haskell-ffi'), default='haskell')
+ parser.add_argument('--haskell-library', type=Path, help='explicit full Haskell embedding library; required for haskell-ffi execution')
parser.add_argument('--backend', choices=BACKENDS, default=DEFAULT_BACKEND,
help='generation lowering backend (default: %(default)s); never inferred from previous')
parser.add_argument('--worker-bundle', type=Path, help='explicit pinned transition worker exporting the selected backend entrypoint; no fallback')
@@ -111,13 +109,10 @@ def main():
args = parser.parse_args()
if not 0 < args.memory_mib <= 65536 or args.capabilities <= 0 or not 0 < args.wall_seconds <= 7200 or not 0 < args.initial_seconds <= args.extended_seconds:
parser.error('invalid resource boundaries')
- if (args.execution_host == 'zig') != (args.zig_library is not None):
- parser.error('--zig-library is required exactly when --execution-host zig is selected')
- if args.zig_library and not args.zig_library.is_file():
- parser.error('selected Zig library is unavailable; no fallback')
- if args.zig_gc_policy and (args.execution_host != 'zig' or
- any(not 0 < n <= 0xffffffff for n in args.zig_gc_policy)):
- parser.error('--zig-gc-policy requires Zig and two positive u32 values')
+ if (args.execution_host == 'haskell-ffi') != (args.haskell_library is not None):
+ parser.error('--haskell-library is required exactly when --execution-host haskell-ffi is selected')
+ if args.haskell_library and not args.haskell_library.is_file():
+ parser.error('selected Haskell embedding library is unavailable; no fallback')
previous = args.previous.resolve() if args.previous else None
output = args.output.resolve()
from bootstrap_retention_lock import protect_generations
@@ -179,9 +174,9 @@ def main():
parser.error('transition execution version requires an explicit matching adapter')
produced_io = args.produced_io_version if args.produced_io_version is not None else execution_io
grants = execution_flags(execution_io, args.execution_host)
- if args.execution_host == 'zig':
- from zig_generation_executor import validate_library
- validate_library(args.zig_library.resolve(),execution_io)
+ if args.execution_host == 'haskell-ffi':
+ from haskell_generation_executor import validate_library
+ validate_library(args.haskell_library.resolve(),execution_io)
if produced_io != execution_io and not args.produced_adapter:
parser.error('changed produced IO version requires --produced-adapter')
produced_adapter_bytes = args.produced_adapter.resolve().read_bytes() if args.produced_adapter else adapter_bytes
@@ -256,14 +251,14 @@ def main():
compiler_bundle_sha256=compiler, compiler_manifest=compiler_manifest,
adapter_sha256=hashlib.sha256(adapter_bytes).hexdigest(),
execution_host=args.execution_host,
- runtime_path=str(args.zig_library.resolve()) if args.zig_library else tricu,
- runtime_sha256=hashlib.sha256((args.zig_library or Path(tricu)).read_bytes()).hexdigest(),
+ runtime_path=str(args.haskell_library.resolve()) if args.haskell_library else tricu,
+ runtime_sha256=hashlib.sha256((args.haskell_library or Path(tricu)).read_bytes()).hexdigest(),
transport_host='haskell', transport_runtime_path=tricu,
transport_runtime_sha256=hashlib.sha256(Path(tricu).read_bytes()).hexdigest(),
- executor_sha256=hashlib.sha256((REPO / 'test/zig_generation_executor.py').read_bytes()).hexdigest() if args.zig_library else None,
+ execution_capabilities=1 if args.haskell_library else args.capabilities,
+ binding_sha256=hashlib.sha256((REPO / 'ext/haskell/python/tricu.py').read_bytes()).hexdigest() if args.haskell_library else None,
+ executor_sha256=hashlib.sha256((REPO / 'test/haskell_generation_executor.py').read_bytes()).hexdigest() if args.haskell_library else None,
**inputs)
- if args.zig_gc_policy:
- provenance['zig_gc_policy'] = args.zig_gc_policy
(output / 'inputs.json').write_text(json.dumps(provenance, indent=2) + '\n')
(output / 'source-map.v1').write_text(map_text)
cmd = [tricu, 'arboricx', 'run', '--store', str(output / 'compiler-cas'),
@@ -273,18 +268,16 @@ def main():
'--arg', compiler, '--arg', map_text, '--arg', args.root,
'--io', *grants, '--result', '--format', 'decode',
'+RTS', '-N' + str(args.capabilities), '-M' + str(args.memory_mib) + 'm', '-T', '-s', '-RTS']
- if args.execution_host == 'zig':
+ if args.execution_host == 'haskell-ffi':
request = dict(execution_io_version=execution_io, worker_export=worker_export, compiler_bundle_sha256=compiler,
adapter_sha256=provenance['adapter_sha256'],
arguments=[str(source_root), str(output / 'overlay'), str(output / 'cas'),
compiler, map_text, args.root])
(output / 'execution-request.json').write_text(json.dumps(request, indent=2) + '\n')
import math
- cmd = [sys.executable, str(REPO / 'test/zig_generation_executor.py'),
- '--library', str(args.zig_library.resolve()), '--output', str(output),
+ cmd = [sys.executable, str(REPO / 'test/haskell_generation_executor.py'),
+ '--library', str(args.haskell_library.resolve()), '--output', str(output),
'--memory-mib', str(args.memory_mib), '--seconds', str(math.ceil(remaining()))]
- if args.zig_gc_policy:
- cmd += ['--gc-policy', *map(str, args.zig_gc_policy)]
(output / 'invocation.json').write_text(json.dumps(cmd, indent=2))
log = output / f'native-{time.time_ns()}.log'
(output / 'status.json').write_text(json.dumps(dict(complete=False,
@@ -293,7 +286,8 @@ def main():
seen, object_count, peak = {}, 0, 0
extended, reason = False, 'completed'
with log.open('w') as stdout:
- proc = subprocess.Popen(cmd, stdout=stdout, stderr=stdout, start_new_session=True)
+ environment = dict(os.environ, MALLOC_ARENA_MAX='2') if args.haskell_library else None
+ proc = subprocess.Popen(cmd, stdout=stdout, stderr=stdout, start_new_session=True, env=environment)
try:
monitor = ChildRSS(proc)
while proc.poll() is None:
diff --git a/test/bootstrap_store_check.py b/test/bootstrap_store_check.py
@@ -112,6 +112,9 @@ for name, (key, manifest, receipt) in indices.items():
map_receipts = []
for obj in (cas / 'objects').glob('*/*'):
+ # Forced process termination can leave a private, unpublished stage.
+ if '.tricu-stage-' in obj.name:
+ continue
with obj.open('rb') as handle:
if not handle.read(64).startswith(b'arboricx.source-map-build.v1\n'):
continue
diff --git a/test/cross_host_generation.py b/test/cross_host_generation.py
@@ -1,194 +0,0 @@
-#!/usr/bin/env python3
-"""Bounded compiler compatibility checks through the shared generation runner.
-Haskell supplies transport and result execution; each selected execution host
-compiles and publishes the test modules. Independent audits verify the stores.
-"""
-import argparse
-import hashlib
-import json
-from pathlib import Path
-import sys
-import subprocess
-from process_rss import run_guarded
-
-ROOT = Path(__file__).resolve().parent.parent
-
-
-def main():
- p = argparse.ArgumentParser(description=__doc__)
- p.add_argument('--tricu', type=Path, required=True)
- p.add_argument('--library', type=Path, required=True)
- p.add_argument('--directory', type=Path, required=True)
- p.add_argument('--zig-gc-policy', type=int, nargs=2, metavar=('MIN_ALLOCATIONS', 'LIVE_MULTIPLIER'))
- p.add_argument('--compatibility', action='store_true', help='also check broader syntax, invalidation and negative controls')
- args = p.parse_args()
- if args.zig_gc_policy and any(not 0 < n <= 0xffffffff for n in args.zig_gc_policy):
- p.error('GC policy requires positive u32 values')
- directory = args.directory.resolve()
- directory.mkdir(parents=True, exist_ok=False)
- source = directory / 'source'
- source.mkdir()
- (source / 'dep.tri').write_text('!export answer\nanswer = 41\n')
- (source / 'app.tri').write_text('!import dep as D\n!export main\nmain = D.answer\n')
- source_map = directory / 'source-map.v1'
- source_map.write_text('arboricx.source-map.v1\nnative\tdep\tdep\tdep.tri\t-\nnative\tapp\tapp\tapp.tri\tdep\n')
- report = dict(complete=False, stages=[])
- def save():
- (directory / 'report.json').write_text(json.dumps(report, indent=2)+'\n')
- def run(label, command, success=True):
- try:
- output, peak = run_guarded(list(map(str,command)), memory_mib=1024, seconds=150)
- if not success:
- raise AssertionError('unexpected success: '+label)
- except subprocess.CalledProcessError as error:
- (directory / (label+'.log')).write_text(error.output)
- if success:
- report['failure'] = label
- save()
- raise
- report['stages'].append(dict(label=label, expected_failure=True))
- save()
- return error.output
- except Exception as error:
- (directory / (label+'.log')).write_text(str(getattr(error,'output',''))+'\n'+str(error))
- report['failure'] = label
- save()
- raise
- (directory / (label+'.log')).write_text(output)
- report['stages'].append(dict(label=label,peak_kib=peak))
- save()
- return output
- def checkpoints(host):
- return {name:(directory/host/'cas/cache/source-build'/name).read_bytes() for name in ('dep','app')}
- save()
- commands = {}
- for host in ('haskell','zig'):
- output = directory / host
- command = [sys.executable, ROOT/'test/bootstrap_generations.py', output,
- '--bootstrap-kit', ROOT/'bootstrap/rebuild', '--tricu',args.tricu.resolve(),
- '--execution-host',host,'--source-root',source,'--map',source_map,'--root','app',
- '--backend','stable','--memory-mib','1024','--wall-seconds','120',
- '--initial-seconds','60','--extended-seconds','100']
- if host == 'zig':
- command += ['--zig-library',args.library.resolve()]
- if args.zig_gc_policy:
- command += ['--zig-gc-policy', *map(str, args.zig_gc_policy)]
- commands[host] = command
- run(host+'-cold',command+['--fresh'])
- status = json.loads((output/'status.json').read_text())
- assert status['complete'] and status['execution_host']==host and status['fresh_store']
- assert status['transport_host']=='haskell'
- if host == 'zig' and args.zig_gc_policy:
- assert status['zig_gc_policy'] == args.zig_gc_policy
- assert json.loads((output/'execution.json').read_text())['gc_policy'] == args.zig_gc_policy
- runtime = args.tricu if host == 'haskell' else args.library
- assert status['runtime_sha256']==hashlib.sha256(runtime.read_bytes()).hexdigest()
- invocation = json.loads((output/'invocation.json').read_text())
- assert ('+RTS' in invocation) == (host == 'haskell')
- cold = checkpoints(host)
- run(host+'-warm',command)
- assert cold == checkpoints(host)
- pin = cold['app'].decode().split('\t')[2]
- answer = run(host+'-result',[args.tricu,'arboricx','run','--store',output/'cas',
- '--module',pin,'--name','main','--format','number',
- '+RTS','-N1','-M1024m','-RTS'])
- assert answer.strip()=='41', answer
- assert checkpoints('haskell') == checkpoints('zig'), 'complete module receipts differ'
- # Exact independently audited object sets, not just root result equality.
- objects = lambda host: {p.name:p.read_bytes() for p in (directory/host/'cas/objects').glob('*/*')}
- assert objects('haskell') == objects('zig'), 'published object sets differ'
- if args.compatibility:
- def same_objects():
- assert objects('haskell') == objects('zig'), 'published object sets differ'
- def stage(label, success=True, diagnostic=None):
- for host, command in commands.items():
- reply = run(host+'-'+label, command, success=success)
- status = json.loads((directory/host/'status.json').read_text())
- assert status['complete'] == success
- if diagnostic:
- assert diagnostic in reply, (host,label,reply[-3000:])
- same_objects()
- def result(host, name, expected):
- pin = (directory/host/'cas/cache/source-build/app').read_text().split('\t')[2]
- reply = run(host+'-result-'+name,[args.tricu,'arboricx','run','--store',directory/host/'cas',
- '--module',pin,'--name',name,'--format','number',
- '+RTS','-N1','-M1024m','-RTS'])
- assert reply.strip()==str(expected), reply
- dep = '!export answer\n!export extra\nanswer = 42\nextra = 99\n-- preserved UTF-8: λ 😀\n'
- (source/'dep.tri').write_text(dep)
- (source/'bridge.tri').write_text('!export * from dep\n')
- source_map.write_text('arboricx.source-map.v1\nnative\tdep\tdep\tdep.tri\t-\n'
- 'native\tbridge\tbridge\tbridge.tri\tdep\n'
- 'native\tapp\tapp\tapp.tri\tbridge\n')
- app = '!import bridge as D\n!export main\n!export local\n!export pinned\n!export sequenced\n!export lazy\n!export partial\nwalk t = D.answer\nwalk (t x) = walk x\nwalk _ = 0\nmain = walk (t (t t))\nlocal = (let rec\n copy [] = []\n copy [x | xs] = t x (copy xs)\n copy _ = []\n in (x : case x of\n [head | _] = head\n _ = 0) (copy [42 8]))\nchoose ^D.extra = 42\nchoose _ = 0\npinned = choose 99\npass value next = next value\nsequenced = (do pass\n value <- 42\n (x : x) value)\nlazy = (x : 42) ((x : x x) (x : x x))\nfirst x y = x\npartial = first 42 0\n'
- (source/'app.tri').write_text(app)
- stage('features-and-change')
- for host in commands:
- for name in ('main','local','pinned','sequenced','lazy','partial'):
- result(host,name,42)
- before = {host:checkpoints(host) for host in commands}
- stage('features-warm')
- assert before == {host:checkpoints(host) for host in commands}
- # Invalid source, not merely a supervisor preflight error.
- (source/'app.tri').write_text('!import bridge as D\n!export main\nmain = missingName\n')
- stage('undefined-name',success=False)
- assert json.loads((directory/'zig/execution.json').read_text())['diagnostic']=={'first_number':20}
- assert 'error: [20, "missingName"' in (directory/'haskell-undefined-name.log').read_text()
- (source/'app.tri').write_text(app)
- stage('restore-source')
- assert before == {host:checkpoints(host) for host in commands}
- # A matching corrupt local checkpoint must fail closed, not recompile.
- saved = {}
- for host in commands:
- path = directory/host/'cas/cache/source-build/app'
- saved[host] = path.read_bytes()
- path.write_text('corrupt')
- stage('corrupt-checkpoint',success=False)
- for host in commands:
- (directory/host/'cas/cache/source-build/app').write_bytes(saved[host])
- # Corrupt a referenced executable while keeping its digest/path.
- for host in commands:
- pin = saved[host].decode().split('\t')[2]
- manifest = (directory/host/'cas/objects'/pin[:3]/pin).read_text()
- digest = next(row.split('\t')[3] for row in manifest.splitlines()[1:] if row.split('\t')[1]=='main')
- path = directory/host/'cas/objects'/digest[:3]/digest
- saved[host] = (path,path.read_bytes())
- path.write_bytes(b'corrupt')
- stage('corrupt-executable',success=False)
- for path,payload in saved.values(): path.write_bytes(payload)
- stage('restore-object')
- # An interrupted preparation must invalidate success; completed dependency
- # publications survive and the next invocation reconstructs the root.
- for host,command in commands.items():
- (directory/host/'cas/cache/source-build/app').unlink()
- run(host+'-resource-stop',command+['--memory-mib','1'],success=False)
- assert not json.loads((directory/host/'status.json').read_text())['complete']
- # Also stop a running Zig invocation, not only Haskell transport setup.
- run('zig-execution-deadline',commands['zig']+['--wall-seconds','0.5'],success=False)
- stopped = json.loads((directory/'zig/status.json').read_text())
- assert not stopped['complete'] and stopped['reason']=='whole-job wall-time guard', stopped
- assert 'code' in stopped, 'guard must reach the execution subprocess'
- stage('restart')
- assert before == {host:checkpoints(host) for host in commands}
- # Header preflight rejects initial BOM before an invocation starts;
- # existing status remains historical, not evidence of this failed attempt.
- (source/'app.tri').write_bytes(b'\xef\xbb\xbf'+app.encode())
- for host, command in commands.items():
- reply = run(host+'-source-bom',command,success=False)
- assert 'header mismatch' in reply
- (source/'app.tri').write_text(app)
- stage('final-restored')
- for host,command in commands.items():
- reply = run(host+'-missing-dependency',command+['--root','absent'],success=False)
- assert 'undeclared map dependency' in reply
- reply = run('zig-missing-library',commands['zig']+['--zig-library',str(directory/'missing.so')],success=False)
- assert 'no fallback' in reply
- report['compatibility'] = True
- report['complete'] = True
- save()
- print('Shared-runner cold/warm parity and exact receipts/objects passed; compatibility='
- +str(args.compatibility)+': '+str(directory))
-
-
-if __name__ == '__main__':
- main()
diff --git a/test/generation_io.py b/test/generation_io.py
@@ -49,8 +49,10 @@ def produced_adapter(directory, record):
def execution_flags(io_version, host='haskell'):
value = version(io_version)
- if host not in ('haskell', 'zig'):
+ if host not in ('haskell', 'haskell-ffi'):
raise ValueError('selected execution host does not support IO v2')
+ if host == 'haskell-ffi' and value != 2:
+ raise ValueError('Haskell embedding requires IO version 2')
return ['--unsafe-io'] if value == 1 else []
@@ -68,10 +70,10 @@ def check_pair(first, second):
def check_invocation(record, command, host='haskell'):
execution, _ = versions(record)
execution_flags(execution, host)
- if host != 'haskell':
- return
if not isinstance(command, list) or not all(isinstance(arg, str) for arg in command):
raise ValueError('invalid generation invocation')
+ if host != 'haskell':
+ return
grants = {'--unsafe-io', '--allow-read', '--allow-write', '--allow-read-all', '--allow-write-all'}
if execution == 2 and any(arg.split('=', 1)[0] in grants for arg in command):
raise ValueError('invocation IO authority mismatch')
diff --git a/test/haskell_generation_executor.py b/test/haskell_generation_executor.py
@@ -0,0 +1,146 @@
+#!/usr/bin/env python3
+"""Fresh-process Haskell C-ABI executor for captured IO-v2 generation requests.
+
+The supervisor owns transport, process guards and independent store audits.
+This child authenticates the complete captured request before executing effects.
+"""
+import argparse
+import hashlib
+import json
+from pathlib import Path
+import resource
+import subprocess
+import sys
+import time
+
+ROOT = Path(__file__).resolve().parent.parent
+BINDING = ROOT / 'ext/haskell/python/tricu.py'
+sys.path.insert(0, str(BINDING.parent))
+from tricu import Library, TricuError
+from generation_io import version, versions
+
+
+def sha(path):
+ return hashlib.sha256(path.read_bytes()).hexdigest()
+
+
+def validate_library(path, io_version):
+ if version(io_version) != 2:
+ raise ValueError('Haskell embedding requires IO version 2; select a retained runtime for v1')
+ # The supervisor never initializes an RTS. Validation and actual execution
+ # have separate, freshly exec'd processes, with no inherited GHC contexts.
+ result = subprocess.run([sys.executable, str(Path(__file__).resolve()), '--library', str(path),
+ '--validate-only'], capture_output=True, text=True, timeout=30)
+ if result.returncode:
+ raise ValueError('Haskell embedding library unavailable or incompatible; no fallback: ' + result.stderr[-2000:])
+
+
+def captured_request(output, library):
+ recorded = json.loads((output / 'inputs.json').read_text())
+ request = json.loads((output / 'execution-request.json').read_text())
+ execution = version(request.get('execution_io_version'))
+ if execution != 2 or execution != versions(recorded)[0]:
+ raise ValueError('execution request IO version mismatch')
+ if (recorded.get('execution_host') != 'haskell-ffi'
+ or recorded.get('runtime_sha256') != sha(library)
+ or recorded.get('executor_sha256') != sha(Path(__file__).resolve())
+ or recorded.get('binding_sha256') != sha(BINDING)):
+ raise ValueError('execution runtime/launcher/binding identity mismatch')
+ if sha(output / 'source-map.v1') != recorded.get('map_sha256'):
+ raise ValueError('execution map identity mismatch')
+ expected = dict(execution_io_version=execution,
+ worker_export=recorded['worker_export'],
+ compiler_bundle_sha256=recorded['compiler_bundle_sha256'],
+ adapter_sha256=recorded['adapter_sha256'],
+ arguments=[recorded['source_root'], str(output / 'overlay'), str(output / 'cas'),
+ recorded['compiler_bundle_sha256'], (output / 'source-map.v1').read_text(), recorded['root']])
+ if request != expected:
+ raise ValueError('execution request identity/arguments mismatch')
+ if any(not isinstance(text, str) or '\0' in text for text in request['arguments']):
+ raise ValueError('invalid compiler arguments')
+ compiler = (output / 'compiler.arboricx').read_bytes()
+ adapter = (output / 'io-adapter.arboricx').read_bytes()
+ if (hashlib.sha256(compiler).hexdigest() != request['compiler_bundle_sha256']
+ or hashlib.sha256(adapter).hexdigest() != request['adapter_sha256']):
+ raise ValueError('captured executable identity mismatch')
+ return request, compiler, adapter
+
+
+def diagnostic(ctx, roots, value):
+ # Observe only a bounded prefix of an ordinary compiler error. Terminal task
+ # diagnostics never enter this function and remain opaque.
+ characters, first = [], None
+ try:
+ while ctx.kind(value) == 2 and len(characters) < 240:
+ head = roots.child(value, 0)
+ number = ctx.to_number(head)
+ if first is None:
+ first = number
+ if number > 0x10ffff or 0xd800 <= number <= 0xdfff:
+ break
+ characters.append(chr(number))
+ value = roots.child(value, 1)
+ if ctx.kind(value) == 0:
+ return ''.join(characters)
+ if len(characters) == 240:
+ return ''.join(characters) + '…'
+ except TricuError:
+ pass
+ return {'first_number': first} if first is not None else 'opaque compiler error'
+
+
+def child(args):
+ resource.setrlimit(resource.RLIMIT_AS, (args.memory_mib * 1024**2,) * 2)
+ resource.setrlimit(resource.RLIMIT_STACK, (8 * 1024**2,) * 2)
+ resource.setrlimit(resource.RLIMIT_CPU, (args.seconds,) * 2)
+ resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
+ started = time.monotonic()
+ request, compiler, adapter = captured_request(args.output, args.library)
+ library = Library(args.library)
+ with library.context() as ctx, ctx.scope() as roots:
+ worker = roots.load_bundle(compiler, request['worker_export'])
+ wrapper = roots.load_bundle(adapter, 'main')
+ current = roots.apply_many(wrapper, worker, *(roots.string(text) for text in request['arguments']))
+ prepared = time.monotonic()
+ program = roots.reduce(current)
+ forced = time.monotonic()
+ outcome, result, cleanup = roots.run_io(program)
+ ran = time.monotonic()
+ message, ok = None, False
+ if outcome != 0:
+ message = 'opaque terminal task failure/cancellation'
+ else:
+ if ctx.kind(cleanup) != 0 or ctx.kind(result) != 2:
+ raise ValueError('invalid compiler Result/cleanup shape')
+ ok = ctx.to_bool(roots.child(result, 0))
+ payload = roots.child(result, 1)
+ if ctx.kind(payload) != 2 or ctx.kind(roots.child(payload, 1)) != 0:
+ raise ValueError('invalid compiler Result remainder')
+ if not ok:
+ message = diagnostic(ctx, roots, roots.child(payload, 0))
+ record = dict(ok=ok, diagnostic=message, task_outcome=outcome, execution_io_version=2,
+ prepare_seconds=prepared-started, initial_reduce_seconds=forced-prepared,
+ io_seconds=ran-forced, total_seconds=time.monotonic()-started)
+ (args.output / 'execution.json').write_text(json.dumps(record, indent=2) + '\n')
+ if not ok:
+ raise RuntimeError(message)
+ print('ok: Haskell shared-library map-builder completed', flush=True)
+ print(json.dumps(record), flush=True)
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--library', type=Path, required=True)
+ parser.add_argument('--output', type=Path)
+ parser.add_argument('--validate-only', action='store_true')
+ parser.add_argument('--memory-mib', type=int, default=1024)
+ parser.add_argument('--seconds', type=int, default=30)
+ args = parser.parse_args()
+ if not 0 < args.memory_mib <= 65536 or not 0 < args.seconds <= 7200:
+ parser.error('invalid resource boundaries')
+ if args.validate_only:
+ Library(args.library)
+ elif args.output is None:
+ parser.error('--output is required for execution')
+ else:
+ child(args)
diff --git a/test/haskell_observe.py b/test/haskell_observe.py
@@ -0,0 +1,52 @@
+#!/usr/bin/env python3
+"""Bounded whole-tree observation of an explicit bundle export via Haskell ABI."""
+import argparse
+from pathlib import Path
+import resource
+import sys
+from executable_object import FLAT, verify_payload
+
+sys.path.insert(0, str(Path(__file__).resolve().parent.parent / 'ext/haskell/python'))
+from tricu import Library
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ for key in ('library', 'bundle', 'argument', 'output'):
+ parser.add_argument('--' + key, type=Path, required=True)
+ parser.add_argument('--export', required=True)
+ args = parser.parse_args()
+ # RTS virtual reservation is distinct from the supervisor's 512-MiB RSS cap.
+ for kind, limit in [(resource.RLIMIT_AS, 1024 * 1024**2),
+ (resource.RLIMIT_STACK, 8 * 1024**2),
+ (resource.RLIMIT_CPU, 60), (resource.RLIMIT_CORE, 0)]:
+ resource.setrlimit(kind, (limit, limit))
+ data = args.argument.read_bytes()
+ verify_payload(FLAT, data)
+ with Library(args.library).context() as ctx, ctx.scope() as roots:
+ stack = []
+ for tag in reversed(data):
+ children = [stack.pop() for _ in range(tag)]
+ value = (roots.leaf, roots.stem, roots.fork)[tag](*children)
+ for child in children:
+ roots.release(child)
+ stack.append(value)
+ assert len(stack) == 1
+ worker = roots.load_bundle(args.bundle.read_bytes(), args.export)
+ current = roots.apply(worker, stack[0])
+ todo, output = [current], bytearray()
+ while todo:
+ value = todo.pop()
+ tag = ctx.kind(value)
+ output.append(tag)
+ children = [roots.child(value, i) for i in range(tag)]
+ todo.extend(reversed(children))
+ roots.release(value)
+ if len(output) > 1024 * 1024:
+ raise ValueError('test output boundary')
+ verify_payload(FLAT, output)
+ args.output.write_bytes(output)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/io-v2/README.md b/test/io-v2/README.md
@@ -3,7 +3,7 @@
Current hosts execute canonical IO v2 envelopes. Native execution uses OS process
authority; retired grant arguments and unsupported envelope versions fail before
effects. Haskell provides filesystem, TCP/resolver, process/context and clock
-facilities. Zig provides the compiler/file profile; browser hosts provide
+facilities. The Haskell browser host provides
structured core/task execution and terminal streams. Capability queries report
the selected backend's facilities. TLS, UDP and hard deadlines require additional
facilities. Historical v1 execution requires a retained runtime.
@@ -691,7 +691,7 @@ Implemented group revisions are all **1**:
`digest-algorithms`, and `restrictions`. It describes this backend's implementation,
not OS permissions, resource availability or a guarantee that a future call works.
For example, no-replace publication still needs filesystem hard-link support.
-No browser/Zig v2 capabilities are claimed by the native report.
+The native report covers native Linux capabilities.
A well-formed named request with an unavailable group, revision or operation
returns `unsupported` **without demanding its arguments**. Unknown names do not
@@ -1258,7 +1258,7 @@ complete binary outputs through 1 MiB, including simultaneously full stdout/stde
normal joining without an explicit ticket wait, literal arguments and nonzero
status, and malformed bounds. Runtime RSS/maximum-live guards exclude child-process
memory and are regression guards, not capacity or throughput promises. This slice
-is validated on native Linux; it does not add Zig/browser process support or change
+is validated on native Linux; browser process support and scheduling changes require separate validation; it does not change
compiler fixtures, deployment defaults, or accepted compiler generations.
### Bounded, incremental line reading
@@ -1433,80 +1433,43 @@ The larger native HTTP/streaming controls remain the service's independent cover
### Validated program/backend boundary
-IO envelope versions, compiler entrypoints and owned-handle ABI versions are
-separate contracts. The current Zig owned-handle ABI is **3** and IO protocol is **2**.
+IO envelope versions, compiler entrypoints and native embedding ABI versions are
+separate contracts. Native embedding uses Haskell ABI 1 and IO protocol 2.
-| Program/operation | Haskell native (validated Linux) | Zig native | Haskell browser / Zig browser |
-| --- | --- | --- | --- |
-| Pure tree evaluation / stored definitions | Yes | Yes | Yes |
-| Pure stable module-compiler entrypoint | Yes | Yes | Yes, independently pinned browser compiler |
-| Filesystem compiler workflow | IO v2 | IO v2 compiler/file profile | Use pure compilation over supplied sources |
-| Historical IO v1 | Retained runtime required | Retained ABI2 runtime required | Inert inspection only |
-| IO v2 envelope/core | Structured core | Structured core | Structured core and terminal profile |
-| Named facilities | Query `IO.capabilities` | Compiler/file catalogue | Runtime, standard streams, strict UTF-8 |
-| Applications | Files, pipelines, tasks, processes and registry | Scoped regular files/tasks and compiler workflows | Structured tasks and bounded terminal pipelines |
-
-Browser terminal input is encoded as UTF-8 bytes through the session's line/EOF
-controls. Stop requests structured cancellation and waits for admitted terminal
-operations, tasks and leases to settle; compiled definitions remain usable.
-Emergency teardown disposes the Worker without acknowledging resource settlement.
-A non-returning native reduction can delay structured cancellation indefinitely.
-Current execution accepts canonical v2 framing before observing the action.
-Historical trees remain inspectable data. The JS reference graph supports pure
-evaluation; IO runs through native browser adapters.
-See [public browser integration](../../ext/demo-site/README.md#io-version-boundary)
-for terminal byte preservation, capabilities, stepping, result views and acceptance.
-
-Browser compilation invokes `source_unit.compileStableModuleDefinitions` as a pure
-function from its independently authenticated compiler library. Browser execution
-provides the terminal profile; native filesystem/network/process workflows use the
-corresponding native hosts. Published browser assets retain their own source and
-compiler provenance.
-
-Reproduce the boundary controls without changing release assets or defaults:
+| Program/operation | Haskell native (validated Linux) | Haskell browser |
+| --- | --- | --- |
+| Pure tree evaluation / stored definitions | Yes | Yes |
+| Pure stable module-compiler entrypoint | Yes | Independently pinned browser compiler |
+| Filesystem compiler workflow | IO v2 | Pure compilation over supplied sources |
+| Historical IO v1 | Retained runtime required | Inert inspection only |
+| IO v2 envelope/core | Structured core | Structured core and terminal profile |
+| Named facilities | Query `IO.capabilities` | Runtime, standard streams, strict UTF-8 |
+| Applications | Files, pipelines, tasks, processes and registry | Structured tasks and terminal pipelines |
+
+Browser terminal input uses UTF-8 line/EOF messages. Stop requests structured
+cancellation and waits for admitted operations, tasks and leases to settle.
+Emergency disposal terminates the Worker without promising resource settlement.
+A non-returning reduction can delay structured cancellation; external supervision
+bounds native work. Canonical v2 framing is checked before action demand. The JS
+reference graph supplies independent pure conformance checks.
+
+Browser compilation invokes `source_unit.compileStableModuleDefinitions` from
+its independently authenticated compiler library. Published assets retain their
+own source and compiler provenance. Reproduce native and browser boundaries with:
```sh
-nix build .#tricu-zig .#tricu-zig-tests .#tricu-wasm-tests .#arboricxSite --no-link
+nix build .#tricu-native-bindings-integration .#arboricxSite --no-link
nix shell --inputs-from . nixpkgs#nodejs -c \
node --test ext/demo-site/test/io-version-boundary.test.mjs
SITE=$(nix path-info .#arboricxSite)
-for BACKEND in haskell-wasm zig-wasm; do
- TEST_IO_VERSION=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"
-done
+TEST_IO_VERSION=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
```
-Boundary controls reject retired v1 and unsupported versions before demanding
-poisoned action payloads or invoking callbacks. Both native browser adapters also
-exercise current v2 terminal sessions and independently pinned pure compilation.
-Full native/browser suites and cross-host compiler reproduction supply their
-respective application and generation acceptance.
-
-### Private Zig structured-core validation
-
-`test/io_v2_zig_core.py` compiles the actual task/handler controls and checks both
-Haskell and `io-v2-core-probe` against independent complete-tree expectations. The
-additional core corpus covers cancellation, scope identity, malformed flags and
-transfers, consumed tickets, lexical self-join cycles (including a deliberately
-wrong expectation), ordinary Results, non-demand of failure/cancellation payloads,
-and unsupported requests. A v1 envelope is rejected before its divergent action.
-The existing Haskell task suite retains its independent expectations and separate
-fresh-host-byte retention workloads; the private Zig probe cannot run those file
-operations yet.
-
-`core-retention.tri` adds distinct, forced 4096-cell child results at16/128/512
-iterations for both consumed joins and discarded tickets. The private probe runs
-with frequent GC and a128-MiB process RSS guard. This is bounded-history evidence
-for this workload, not an OS-streaming or general memory-quota claim. See the
-[Zig guide](../../ext/zig/README.md#io-protocol-boundary) for the Nix/probe command.
-Production Zig v2 and cross-host compiler reproduction have separate acceptance
-suites; this private probe isolates structured-core behavior.
-
-`test/io_v2_zig_files.py` validates the separate private native regular-file probe:
-49 exact-byte copy/cancellation cases through1MiB, five path-failure/type controls,
-position/update/append/close controls, failure after a committed write, and
-non-demand of an unknown revision. Fourteen selected compositions/Unicode-path/mode/failure
-cases also run on Haskell against independent expectations. This probe exercises its selected regular-file subset. The production compiler/file
-profile has separate staging, digest and codec controls. See the Zig guide for
-current facilities and synchronous-call limitations.
+Independent Haskell task/resource/file/indexed/text controls retain malformed
+framing, non-demand, cancellation/cleanup, exact byte and publication oracles.
+Shared-library generation tests compare complete inventories with the CLI; full
+compiler-pair acceptance and retention remain separate. See the
+[browser IO contract](../../ext/demo-site/README.md#io-version-boundary) and
+[compiler rebuilding](../../docs/compiler-rebuilding.md#execution-interface-selection).
diff --git a/test/io-v2/native-cli.tri b/test/io-v2/native-cli.tri
@@ -1,54 +0,0 @@
-!import base as B
-!import io_v2 as IO
-!export compile
-!export pure
-!export write
-!export unsupported
-!export inspect
-!export capabilities
-!export groups
-!export failed
-!export cancelled
-!export divergent
-!export legacy
-!export future
-
-pure = IO.io (IO.pure 42)
-write path = IO.io <| IO.scope (owner : IO.require (IO.openWriter owner path) (writer :
- IO.require (IO.writeSome writer [0 255 65]) IO.pure))
-diverge seed = diverge seed
-unsupported seed = IO.io <| IO.bind (IO.request "tcp" 1 "connect" (diverge seed)) (reply : case reply of
- (t, (["unsupported" "tcp/connect" _ "none"], t)) = IO.pure 1
- _ = IO.fail "invalid unsupported reply")
-inspect seed = IO.io <| IO.require (IO.inspect (IO.request "file" 1 "open-reader" (diverge seed))) (reply : case reply of
- ["facility" "file" 1 "open-reader" "available"] = IO.pure 1
- _ = IO.fail "invalid inspection")
-find key rows = case rows of
- [[name value] | rest] = case name of
- ^key = value
- _ = find key rest
- _ = t
-capabilities = IO.io <| IO.require IO.capabilities (rows : case [(find "backend" rows) (find "authority" rows) (find "io-version" rows)] of
- ["zig-native" "os-process" 2] = IO.pure 1
- _ = IO.fail "invalid capabilities")
-failed seed = IO.io (IO.fail (diverge seed))
-cancelled seed = IO.io (IO.cancelled (diverge seed))
-divergent seed = IO.io (diverge seed)
-legacy = B.pair "tricuIO" (B.pair 1 (B.pair 0 23))
-future seed = B.pair "tricuIO" (B.pair 3 (diverge seed))
-
-compile worker workspace root identity name = IO.io <| IO.require (worker workspace root identity name)
- (built : IO.pure (B.fst built))
-
-groups = IO.io <| IO.require IO.capabilities (rows : case find "groups" rows of
- [["runtime" 1 ["capabilities" "inspect"]]
- ["resource" 1 ["close"]]
- ["stream" 1 ["read" "write"]]
- ["file" 1 ["open-reader" "open-writer" "open-appender" "open-update" "seek" "position" "size" "resize" "sync"]]
- ["path" 1 ["stat" "lstat" "remove-file" "rename"]]
- ["directory" 1 ["create" "remove" "sync"]]
- ["stage" 1 ["open" "seal" "publish"]]
- ["digest" 1 ["open" "update" "finish"]]
- ["indexed-tree" 1 ["read" "hash" "write" "verify"]]
- ["utf8" 1 ["encode" "decode"]]] = IO.pure 1
- _ = IO.fail "incorrect advertised groups")
diff --git a/test/io-v2/zig-utf8.tri b/test/io-v2/utf8-boundaries.tri
diff --git a/test/io-v2/zig-digest.tri b/test/io-v2/zig-digest.tri
@@ -1,27 +0,0 @@
-!import io_v2 as IO
-!export hash
-!export lifecycle
-!export unsupported
-!export transferred
-
-hash bytes = IO.io <| IO.scope (owner : do IO.require
- digest <- IO.openDigest owner "sha256"
- IO.updateDigest digest bytes
- IO.require (IO.finishDigest digest) IO.pure)
-lifecycle = IO.io <| IO.scope (owner : do IO.require
- digest <- IO.openDigest owner "sha256"
- value <- IO.finishDigest digest
- IO.bind (IO.finishDigest digest) (again :
- IO.bind (IO.updateDigest digest [0]) (update :
- IO.pure [value again update])))
-unsupported algorithm = IO.io <| IO.scope (owner : IO.openDigest owner algorithm)
-child [digest] = do IO.require
- IO.updateDigest digest [97 98 99]
- IO.require (IO.finishDigest digest) IO.pure
-child _ = IO.fail "invalid transfer"
-transferred = IO.io <| IO.scope (owner : do IO.require
- digest <- IO.openDigest owner "sha256"
- spawned <- IO.spawn 0 1 [digest] child
- case spawned of
- [task ticket] = IO.require (IO.join ticket) IO.pure
- _ = IO.fail "invalid spawn")
diff --git a/test/io-v2/zig-files.tri b/test/io-v2/zig-files.tri
@@ -1,40 +0,0 @@
-!import base as B
-!import io_v2 as IO
-!export positioned
-!export appended
-!export emptyWrite
-!export closed
-!export readerWrite
-!export failedAfterWrite
-!export unknownWithPoison
-
-positioned path = IO.io <| IO.scope (owner : do IO.require
- file <- IO.openUpdate owner path
- IO.writeSome file [97 98 99]
- IO.seekFile file 1
- content <- IO.readSome file 3
- length <- IO.fileSize file
- IO.seekFile file 10
- IO.resizeFile file 1
- IO.syncFile file
- position <- IO.filePosition file
- eof <- IO.readSome file 1
- IO.pure [content length position eof])
-appended path = IO.io <| IO.scope (owner : do IO.require
- file <- IO.openAppender owner path
- IO.writeSome file [120 121]
- IO.syncFile file
- length <- IO.fileSize file
- IO.bind (IO.seekFile file 0) (bad : IO.pure [length bad]))
-emptyWrite path = IO.io <| IO.scope (owner : IO.require (IO.openWriter owner path) (file : IO.writeSome file []))
-closed path = IO.io <| IO.scope (owner : do IO.require
- file <- IO.openReader owner path
- IO.close file
- IO.close file)
-readerWrite path = IO.io <| IO.scope (owner : IO.require (IO.openReader owner path) (file : IO.writeSome file [0]))
-failedAfterWrite path = IO.io <| IO.scope (owner : do IO.require
- file <- IO.openWriter owner path
- IO.writeSome file [42]
- IO.fail "preserve bytes, release descriptor")
-diverge seed = diverge seed
-unknownWithPoison seed = IO.io (IO.request "file" 99 "open-reader" (diverge seed))
diff --git a/test/io-v2/zig-paths.tri b/test/io-v2/zig-paths.tri
@@ -1,28 +0,0 @@
-!import io_v2 as IO
-!export paths
-!export observe
-!export badDirectory
-!export failedRename
-
-category (t, ([name _ _ _], t)) = name
-category _ = "unexpected"
-paths directory source target = IO.io <| IO.scope (owner : do IO.require
- IO.createDirectory directory
- file <- IO.openWriter owner source
- IO.writeSome file [97 98 99]
- IO.close file
- before <- IO.statPath source
- dir <- IO.statPath directory
- IO.renamePath source target
- after <- IO.lstatPath target
- IO.syncDirectory directory
- IO.bind (IO.statPath source) (missing : do IO.require
- IO.removeFile target
- IO.removeDirectory directory
- IO.pure [before dir after (category missing)]))
-observe path = IO.io <| do IO.require
- link <- IO.lstatPath path
- followed <- IO.statPath path
- IO.pure [link followed]
-badDirectory path = IO.io <| IO.bind (IO.syncDirectory path) (reply : IO.pure (category reply))
-failedRename source target = IO.io <| IO.bind (IO.renamePath source target) (reply : IO.pure (category reply))
diff --git a/test/io-v2/zig-stages.tri b/test/io-v2/zig-stages.tri
@@ -1,24 +0,0 @@
-!import io_v2 as IO
-!export publish
-!export failureAfterPublication
-!export invalidDestination
-!export crossDevice
-
-prepare owner directory = IO.require (IO.openStage owner directory) (stage : do IO.require
- IO.writeAll stage [97 98 99]
- IO.sealStage stage
- IO.pure stage)
-publish directory destination replace = IO.io <| IO.scope (owner : IO.bind (prepare owner directory) (stage : case replace of
- 0 = IO.publishNew stage destination
- _ = IO.publishReplace stage destination))
-failureAfterPublication directory destination = IO.io <| IO.scope (owner : IO.bind (prepare owner directory) (stage : do IO.require
- IO.publishReplace stage destination
- IO.fail "do not roll back publication"))
-invalidDestination directory destination = IO.io <| IO.scope (owner : IO.bind (prepare owner directory) (stage :
- IO.bind (IO.publishNew stage destination) (first :
- IO.bind (IO.publishNew stage destination) (second : IO.pure [first second]))))
-problem (t, ([category _ _ progress], t)) = [category progress]
-problem _ = ["unexpected"]
-crossDevice directory destination = IO.io <| IO.scope (owner : IO.bind (prepare owner directory) (stage :
- IO.bind (IO.publishNew stage destination) (first :
- IO.bind (IO.publishNew stage destination) (second : IO.pure [(problem first) (problem second)]))))
diff --git a/test/io_v2_compiler_store.py b/test/io_v2_compiler_store.py
@@ -50,7 +50,6 @@ def main():
parser.add_argument('tricu',type=Path)
parser.add_argument('--directory',type=Path,required=True)
parser.add_argument('--compiler-smoke',action='store_true',help='also build a candidate stable sourceBuildGraph and compile a real dependency graph; not a generation pair')
- parser.add_argument('--zig-probe',type=Path,help='execute source-object IO controls on the private Zig host; pure helpers stay on Haskell')
args=parser.parse_args()
root=args.directory.resolve(); root.mkdir(parents=True,exist_ok=False)
projection=root/'projection'; projection.mkdir()
@@ -71,22 +70,12 @@ def main():
pin=runner.imported(bundle,'import')
checks=[]
candidate=None
- zig_records=[]
def invoke(label,name,values,expected,io=True):
argv=['--io'] if io else []
- zig_args=[]
for i,value in enumerate(values):
path=root/f'{label}-{i}.tree'; path.write_bytes(value)
argv+=['--arg-tree-file',path]
- zig_args.append('tree-file:'+str(path))
- if args.zig_probe and io:
- command=[str(args.zig_probe.resolve()),str(bundle),name,*zig_args]
- out,peak=run_guarded(command,memory_mib=512,seconds=180)
- (root/(label+'-zig.log')).write_text(out)
- assert bytes(map(int,out.strip()))==expected,label
- zig_records.append(dict(label=label,peak_kib=peak,command=command))
- else:
- assert runner.invoke(label,pin,name,argv)==expected,label
+ assert runner.invoke(label,pin,name,argv)==expected,label
checks.append(label)
def clean(path):
assert not list(path.rglob('.tricu-*')),path
@@ -202,8 +191,6 @@ def main():
command=[str(runner.exe),'arboricx','run','--module',pin,'--name','replace','--store',str(runner.store),
'--io','--format','ternary','--arg-tree-file',str(alias_arg),'--arg-tree-file',str(argument),
'+RTS','-N1','-M512m','-RTS']
- if args.zig_probe:
- command=[str(args.zig_probe.resolve()),str(bundle),'replace','tree-file:'+str(alias_arg),'tree-file:'+str(argument)]
jobs.append(subprocess.Popen(command,stdout=subprocess.PIPE,stderr=subprocess.PIPE))
try:
for process in jobs:
@@ -254,37 +241,6 @@ def main():
expected=hashlib.sha256(b'arboricx.source.v1\0'+file.read_bytes()).hexdigest().encode()
assert any(expected in payload for payload in all_objects),file
clean(application)
- if args.zig_probe:
- from source_order import field, names
- zig_application=root/'zig-application'
- zig_application.mkdir()
- for label in ['cold','warm']:
- command=[str(args.zig_probe.resolve()),str(compiler_bundle),'main',
- 'text:'+str(app),'text:'+str(zig_application),'text:'+compiler,'text:main']
- output,peak=run_guarded(command,memory_mib=1024,seconds=600)
- (root/('zig-compiler-'+label+'.log')).write_text(output)
- answer=bytes(map(int,output.strip()))
- assert field(answer,0)==b'\1\0',('zig-compiler',label,answer)
- pair_value=field(answer,1,0)
- manifest,receipt=names(tree_list([field(pair_value,0),field(pair_value,1)]))
- receipt_path=zig_application/'objects'/receipt[:3]/receipt
- assert hashlib.sha256(b'arboricx.build-receipt.v1\0'+receipt_path.read_bytes()).hexdigest()==receipt
- # Load and authenticate the native-produced store through the
- # independent Haskell host; execute both actual module exports.
- old_store=runner.store
- runner.store=zig_application
- try:
- assert runner.invoke('zig-run-'+label,manifest,'main',[])==number(42)
- assert runner.invoke('zig-accent-'+label,manifest,'accent',[])==tree_list([number(101),number(769)])
- finally:
- runner.store=old_store
- zig_records.append(dict(label='compiler-'+label,peak_kib=peak,manifest=manifest,receipt=receipt,command=command))
- checks.append('private-zig-compiler-'+label)
- zig_bundle=root/'zig-application.arboricx'
- runner.run('zig-export',['export','--module',manifest,'--store',zig_application,'--output',zig_bundle])
- assert zig_bundle.read_bytes()==compiled.read_bytes()
- checks.append('private-zig-complete-bundle-identity')
- clean(zig_application)
original=(app/'main.tri').read_bytes()
accepted_output=compiled.read_bytes()
for label,bad,diagnostic in [('bom',b'\xef\xbb\xbf'+original,'initial source BOM is not permitted'),
@@ -308,32 +264,12 @@ def main():
legacy=subprocess.run(reference,capture_output=True,text=True,timeout=90)
assert legacy.returncode!=0
assert legacy.stdout+legacy.stderr==rejected.stdout+rejected.stderr
- if args.zig_probe:
- # Compare ordinary failure payloads against the same compiled
- # worker on Haskell, not host-formatted stderr or a fallback.
- raw_args=[]
- for i,value in enumerate([str(app),str(zig_application),compiler,'main']):
- arg=root/f'negative-{label}-{i}.tree'
- arg.write_bytes(text(value))
- raw_args+=['--arg-tree-file',arg]
- expected_failure=runner.invoke('worker-reject-'+label,compiler,'main',['--io',*raw_args])
- assert field(expected_failure,0)==b'\0'
- command=[str(args.zig_probe.resolve()),str(compiler_bundle),'main',
- 'text:'+str(app),'text:'+str(zig_application),'text:'+compiler,'text:main']
- output,peak=run_guarded(command,memory_mib=1024,seconds=180)
- (root/('zig-rejected-'+label+'.log')).write_text(output)
- assert bytes(map(int,output.strip()))==expected_failure,label
- assert zig_bundle.read_bytes()==accepted_output
- clean(zig_application)
- zig_records.append(dict(label='compiler-rejected-'+label,peak_kib=peak,command=command))
- checks.append('private-zig-compiler-rejects-'+label)
checks.append('actual-v2-compiler-rejects-'+label)
(app/'main.tri').write_bytes(original)
clean(application)
files=['lib/source/object.tri','lib/source/io-v2.tri','lib/io-v2/storage.tri','lib/io-v2.tri','lib/io-v2/text.tri','test/io_v2_compiler_store.py','bootstrap/build-stable.arboricx']
- if args.zig_probe: files += ['ext/zig/src/io_v2_file_host.zig','ext/zig/src/indexed.zig','ext/zig/src/io_v2_snapshot.zig']
- (root/'report.json').write_text(json.dumps(dict(checks=checks,records=runner.records,candidate=candidate,zig_records=zig_records,
- zig_probe_sha256=hashlib.sha256(args.zig_probe.read_bytes()).hexdigest() if args.zig_probe else None,
+ (root/'report.json').write_text(json.dumps(dict(checks=checks,records=runner.records,candidate=candidate,
+
projected_inputs={p.relative_to(projection).as_posix():hashlib.sha256(p.read_bytes()).hexdigest() for p in projection.rglob('*.tri')},
source_hashes={p:hashlib.sha256((ROOT/p).read_bytes()).hexdigest() for p in files}),indent=2)+'\n')
print(f'{len(checks)} actual compiler-store and storage controls passed')
diff --git a/test/io_v2_generation.py b/test/io_v2_generation.py
@@ -24,7 +24,7 @@ def main():
p.add_argument('tricu', type=Path)
p.add_argument('--kit', type=Path, help='validate an explicitly prepared v2 kit instead of preparing a current candidate')
p.add_argument('--directory', type=Path, required=True)
- p.add_argument('--zig-library',type=Path,help='also run the explicit v2 executor and compare complete inventories')
+ p.add_argument('--haskell-library',type=Path,help='also run the explicit v2 executor and compare complete inventories')
args = p.parse_args()
root = args.directory.resolve(); root.mkdir(parents=True, exist_ok=False)
mapping = ROOT/'bootstrap/compiler-source-map.io-v2.v1'
@@ -86,8 +86,8 @@ def main():
generation('wrong-version',('--execution-io-version','1'),False)
generation('missing-produced-adapter',('--produced-io-version','1'),False)
generation('noncanonical-version',('--execution-io-version','02'),False)
- fake_library=root/'not-a-zig-library'; fake_library.write_bytes(b'not loaded')
- generation('invalid-zig-runtime',('--execution-host','zig','--zig-library',str(fake_library)),False)
+ fake_library=root/'not-an-embedding-library'; fake_library.write_bytes(b'not loaded')
+ generation('invalid-embedding-runtime',('--execution-host','haskell-ffi','--haskell-library',str(fake_library)),False)
generation('invalid-produced-adapter',('--produced-io-version','1','--produced-adapter',str(fake_library)),False,preparation_failure=True)
output=generation('scoped')
# Independent existing store/receipt audit runs inside the generation runner.
@@ -123,8 +123,8 @@ def main():
'native\tnative\tnative\tnative.tri\t-\n')
mixed_manifests=[]
selections=[('candidate',kit,[]),('packaged',ROOT/'bootstrap/rebuild',[])]
- if args.zig_library:
- selections.append(('zig',kit,['--execution-host','zig','--zig-library',str(args.zig_library.resolve())]))
+ if args.haskell_library:
+ selections.append(('haskell-ffi',kit,['--execution-host','haskell-ffi','--haskell-library',str(args.haskell_library.resolve())]))
for label,selected,extra in selections:
destination=root/('mixed-'+label)
command=[sys.executable,str(ROOT/'test/bootstrap_generations.py'),str(destination),
@@ -143,27 +143,29 @@ def main():
mixed_manifests.append(manifest(destination))
checks.append('mixed-staging-digests-and-overlays-'+label)
assert len(set(mixed_manifests))==1,mixed_manifests
- zig_output = None
- if args.zig_library:
- zig_output = generation('zig-scoped',('--execution-host','zig','--zig-library',str(args.zig_library.resolve())))
+ ffi_output = None
+ if args.haskell_library:
+ ffi_output = generation('ffi-scoped',('--execution-host','haskell-ffi','--haskell-library',str(args.haskell_library.resolve())))
def inventory(directory):
return {p.name:hashlib.sha256(p.read_bytes()).hexdigest() for p in (directory/'cas/objects').glob('*/*')}
- assert inventory(zig_output)==inventory(output)
- assert manifest(zig_output)==manifest(output)
- checks.append('haskell-zig-complete-inventory-equality')
- from zig_generation_executor import validate_library
+ assert inventory(ffi_output)==inventory(output)
+ assert manifest(ffi_output)==manifest(output)
+ checks.append('cli-ffi-complete-inventory-equality')
+ from haskell_generation_executor import validate_library
try:
- validate_library(args.zig_library.resolve(), 1)
+ validate_library(args.haskell_library.resolve(), 1)
except (ValueError, AttributeError):
- checks.append('current-zig-rejects-v1-before-preparation')
+ checks.append('current-embedding-rejects-v1-before-preparation')
else:
raise AssertionError('current runtime still exposes v1 execution')
# Captured request tampering must fail before any IO execution, not select
# another interpreter or silently reinterpret absent/Boolean metadata.
- request_path=zig_output/'execution-request.json'
+ request_path=ffi_output/'execution-request.json'
original=request_path.read_bytes()
- before=inventory(zig_output)
- command=json.loads((zig_output/'invocation.json').read_text())
+ map_path=ffi_output/'source-map.v1'
+ original_map=map_path.read_bytes()
+ before=inventory(ffi_output)
+ command=json.loads((ffi_output/'invocation.json').read_text())
try:
for label,value in [('wrong',1),('boolean',True),('future',3),('missing',None)]:
request=json.loads(original)
@@ -173,12 +175,34 @@ def main():
reply=subprocess.run(command,capture_output=True,text=True,timeout=30)
assert reply.returncode!=0,(label,reply.stdout)
assert 'IO version' in reply.stderr,(label,reply.stderr)
- assert inventory(zig_output)==before
- checks.append('zig-request-'+label)
+ assert inventory(ffi_output)==before
+ checks.append('ffi-request-'+label)
+ for key in ['worker_export', 'compiler_bundle_sha256', 'adapter_sha256', 'arguments', 'extra']:
+ request=json.loads(original)
+ if key=='arguments':
+ request[key][2]=str(root/'undeclared-store')
+ elif key=='extra':
+ request[key]=True
+ else:
+ request[key]='0'*64
+ request_path.write_text(json.dumps(request))
+ reply=subprocess.run(command,capture_output=True,text=True,timeout=30)
+ assert reply.returncode!=0 and 'identity/arguments mismatch' in reply.stderr,(key,reply.stderr)
+ assert inventory(ffi_output)==before and not (root/'undeclared-store').exists()
+ checks.append('ffi-request-'+key)
+ request=json.loads(original)
+ request['arguments'][4]+='\n'
+ map_path.write_bytes(original_map+b'\n')
+ request_path.write_text(json.dumps(request))
+ reply=subprocess.run(command,capture_output=True,text=True,timeout=30)
+ assert reply.returncode!=0 and 'execution map identity mismatch' in reply.stderr
+ assert inventory(ffi_output)==before
+ checks.append('ffi-captured-map-tampering')
finally:
+ map_path.write_bytes(original_map)
request_path.write_bytes(original)
(root/'report.json').write_text(json.dumps(dict(checks=checks,records=runner.records,
- kit=metadata,scoped_generation=str(output),zig_generation=str(zig_output) if zig_output else None,generation_pair=False),indent=2)+'\n')
+ kit=metadata,scoped_generation=str(output),ffi_generation=str(ffi_output) if ffi_output else None,generation_pair=False),indent=2)+'\n')
print(f'{len(checks)} generation boundary controls passed; no full compiler pair built')
diff --git a/test/io_v2_indexed.py b/test/io_v2_indexed.py
@@ -36,7 +36,6 @@ def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('tricu', type=Path)
parser.add_argument('--directory', required=True, type=Path)
- parser.add_argument('--zig-probe', type=Path)
args = parser.parse_args()
directory = args.directory.resolve()
directory.mkdir(parents=True, exist_ok=False)
@@ -48,27 +47,17 @@ def main():
runner.run('compile', ['compile', '--file', source, '--store', runner.store, '--output', bundle])
pin = runner.imported(bundle, 'import')
checks = []
- zig_observations = []
def check(label, payload, size, limit, expected_digest, expected_reply, position):
path = directory/(label+'.bin')
path.write_bytes(payload)
values = [text(str(path)), text(str(scratch)), number(limit), number(size), text(expected_digest)]
argv = ['--io']
- zig_arguments = []
for i, value in enumerate(values):
argument = directory/f'{label}-{i}.tree'
argument.write_bytes(value)
argv += ['--arg-tree-file', argument]
- zig_arguments.append('tree-file:'+str(argument))
expected = tree_list([expected_reply, number(position)])
- if args.zig_probe:
- command = [str(args.zig_probe.resolve()),str(bundle),'probe',*zig_arguments]
- output,peak = run_guarded(command,memory_mib=48,seconds=180)
- (directory/(label+'-zig.log')).write_text(output)
- assert bytes(map(int,output.strip())) == expected,label
- zig_observations.append(dict(label=label,peak_kib=peak,command=command))
- else:
- assert runner.invoke(label, pin, 'probe', argv) == expected, label
+ assert runner.invoke(label, pin, 'probe', argv) == expected, label
assert path.read_bytes() == payload, label
assert list(scratch.iterdir()) == [], label
checks.append(dict(label=label, input_bytes=len(payload), frame_bytes=size))
@@ -88,7 +77,7 @@ def main():
problem('invalid-argument', '', 'none'), 0)
malformed = b'not a bundle'
check('malformed-authenticated', malformed, len(malformed), len(malformed), digest(INDEXED, malformed),
- problem('invalid-data', 'invalid indexed frame' if args.zig_probe else 'section extends beyond bundle end', 'unknown'), len(malformed))
+ problem('invalid-data', 'section extends beyond bundle end', 'unknown'), len(malformed))
check('malformed-unauthenticated', malformed, len(malformed), len(malformed), '0'*64,
problem('integrity-failed', 'CAS digest mismatch', 'unknown'), len(malformed))
for size in [4096, 1048576, 16777216, 67108864]:
@@ -104,9 +93,8 @@ def main():
'src/ContentStore/Indexed.hs', 'src/Wire.hs', 'src/IOV2/Codec.hs',
'src/IOV2/Resources.hs', 'src/IOV2/Driver.hs', 'src/IOV2/Protocol.hs',
'bootstrap/build-stable.arboricx']
- if args.zig_probe: files += ['ext/zig/src/indexed.zig','ext/zig/src/io_v2_snapshot.zig','ext/zig/src/io_v2_file_host.zig']
- (directory/'report.json').write_text(json.dumps(dict(checks=checks, observations=observations, zig_observations=zig_observations,
- zig_probe_sha256=hashlib.sha256(args.zig_probe.read_bytes()).hexdigest() if args.zig_probe else None,
+ (directory/'report.json').write_text(json.dumps(dict(checks=checks, observations=observations,
+
measurement_notes='Opaque extension grows transport, not DAG. RSS is sampled: tiny runs can exit before representative samples; use the 64-MiB control for the transfer RSS guard. No concurrency or large-DAG capacity claim.',
source_hashes={p:hashlib.sha256((ROOT/p).read_bytes()).hexdigest() for p in files}), indent=2)+'\n')
print(f'{len(checks)} indexed-read controls passed')
diff --git a/test/io_v2_indexed_write.py b/test/io_v2_indexed_write.py
@@ -34,11 +34,7 @@ def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('tricu', type=Path)
parser.add_argument('--directory', type=Path, required=True)
- parser.add_argument('--zig-writer-probe', type=Path, help='private writer-only subset: no authenticated read/round-trip acceptance')
- parser.add_argument('--zig-probe', type=Path, help='private complete indexed transport controls including authenticated reads')
args = parser.parse_args()
- if args.zig_probe and args.zig_writer_probe: parser.error('select only one private probe mode')
- private_probe = args.zig_probe or args.zig_writer_probe
directory = args.directory.resolve()
directory.mkdir(parents=True, exist_ok=False)
runner = Runner(args.tricu, directory)
@@ -47,23 +43,13 @@ def main():
runner.run('compile', ['compile', '--file', source, '--store', runner.store, '--output', bundle])
pin = runner.imported(bundle, 'import')
checks = []
- zig_observations = []
def invoke(label, name, values, expected):
argv = ['--io']
- zig_arguments = []
for i, value in enumerate(values):
path = directory/f'{label}-{i}.tree'
path.write_bytes(value)
argv += ['--arg-tree-file', path]
- zig_arguments.append('tree-file:'+str(path))
- if private_probe:
- command = [str(private_probe.resolve()),str(bundle),name,*zig_arguments]
- output,peak = run_guarded(command,memory_mib=256,seconds=180)
- (directory/(label+'-zig.log')).write_text(output)
- assert bytes(map(int,output.strip())) == expected,label
- zig_observations.append(dict(label=label,peak_kib=peak,command=command))
- else:
- assert runner.invoke(label, pin, name, argv) == expected, label
+ assert runner.invoke(label, pin, name, argv) == expected, label
checks.append(label)
for i, tree in enumerate([b'\0', b'\1\0', b'\2\1\0\0', number(42)]):
payload = indexed_bytes(tree)
@@ -99,7 +85,7 @@ def main():
invoke(label, 'write', [text(str(path)),number(limit),text(expected),tree], answer)
assert path.read_bytes() == b'untouched'
# Raw input uses linear-depth stems, not an expanded exponentially shared
- # tree. Expectations come from the independent wire assembler, not Zig.
+ # tree. Expectations come from the independent wire assembler.
for depth in [1000,10000]:
payload = dag(depth,False)
tree = b'\1'*depth+b'\0'
@@ -122,7 +108,7 @@ def main():
bundle,pin = original_bundle,original_pin
scratch = directory/'spool'
scratch.mkdir()
- for depth, fork in ([] if args.zig_writer_probe else [(200,True),(1000,False),(10000,False),(100000,False)]):
+ for depth, fork in ([(200,True),(1000,False),(10000,False),(100000,False)]):
payload = dag(depth,fork)
input_path = directory/f'dag-{depth}-{fork}.bin'
output = directory/f'dag-{depth}-{fork}-output.bin'
@@ -141,12 +127,11 @@ def main():
'test/stable_bundle.py','test/source_order.py','test/rts_metrics.py','lib/io-v2.tri',
'demos/io-v2/indexed-write.tri','src/IOV2/Codec.hs','src/IOV2/Resources.hs','src/Wire.hs',
'bootstrap/build-stable.arboricx']
- if private_probe: paths += ['ext/zig/src/indexed.zig','ext/zig/src/io_v2_file_host.zig']
- (directory/'report.json').write_text(json.dumps(dict(checks=checks, observations=observations, zig_observations=zig_observations,
- scope="private-zig-writer-only" if args.zig_writer_probe else "private-zig-complete" if args.zig_probe else "haskell-complete",
- zig_probe_sha256=hashlib.sha256(private_probe.read_bytes()).hexdigest() if private_probe else None,
+ (directory/'report.json').write_text(json.dumps(dict(checks=checks, observations=observations,
+ scope="haskell-complete",
+
source_hashes={p:hashlib.sha256((ROOT/p).read_bytes()).hexdigest() for p in paths}),indent=2)+'\n')
- print(f'{len(checks)} independent indexed-writer checks passed'+(' (private Zig writer-only; authenticated read NOT tested)' if args.zig_writer_probe else ''))
+ print(f'{len(checks)} independent indexed-writer checks passed'+(''))
if __name__ == '__main__':
main()
diff --git a/test/io_v2_native_cli.py b/test/io_v2_native_cli.py
@@ -1,66 +0,0 @@
-#!/usr/bin/env python3
-"""Actual production Zig boundary; private probes are not execution substitutes."""
-import argparse,json,hashlib,subprocess
-from pathlib import Path
-from source_order import Runner
-from bundle_dag import merge_bundles
-ROOT=Path(__file__).resolve().parent.parent
-
-def main():
- p=argparse.ArgumentParser(description=__doc__)
- p.add_argument('tricu',type=Path);p.add_argument('zig',type=Path);p.add_argument('--directory',type=Path,required=True)
- p.add_argument('--compiler-bundle',type=Path,help='also run an explicit v2 sourceBuildGraph worker through the production CLI')
- args=p.parse_args();d=args.directory.resolve();d.mkdir(parents=True,exist_ok=False)
- runner=Runner(args.tricu,d);bundle=d/'controls.arboricx'
- runner.run('compile',['compile','--file',ROOT/'test/io-v2/native-cli.tri','--store',runner.store,'--output',bundle])
- checks=[]
- def run(name,values=(),flags=(),expected=None,error=None):
- selected=d/(name+'.arboricx')
- merge_bundles(selected,[(bundle,[(name,'main')])],prune_unreachable=True)
- command=[str(args.zig.resolve()),'--io','--type','number',*flags,str(selected),*values]
- out=subprocess.run(command,capture_output=True,text=True,timeout=30)
- (d/(str(len(checks))+'-'+name+'.log')).write_text(out.stdout+out.stderr)
- if error:
- assert out.returncode!=0 and error in out.stderr,(command,out.stdout,out.stderr)
- else:
- assert out.returncode==0 and out.stdout==str(expected)+'\n',(command,out.stdout,out.stderr)
- checks.append(dict(name=name,command=command,returncode=out.returncode))
- for name,expected in [('pure',42),('unsupported',1),('inspect',1),('capabilities',1),('groups',1)]:run(name,['0'] if name in ['unsupported','inspect'] else [],expected=expected)
- run('legacy',error='UnsupportedIOVersion')
- run('legacy',flags=['--unsafe-io'],error='RetiredIOGrants')
- path=d/'output-λ😀.bin';run('write',['text:'+str(path)],expected=3)
- assert path.read_bytes()==bytes([0,255,65])
- denied=d/'must-not-exist'
- run('write',['text:'+str(denied)],['--unsafe-io'],error='RetiredIOGrants')
- assert not denied.exists()
- run('divergent',['0'],['--unsafe-io'],error='RetiredIOGrants')
- run('future',['0'],error='UnsupportedIOVersion')
- run('failed',['0'],error='IOV2Failed');run('cancelled',['0'],error='IOV2Cancelled')
- if args.compiler_bundle:
- worker=d/'worker.arboricx'
- merge_bundles(worker,[(args.compiler_bundle,[('sourceBuildGraph','main')])],prune_unreachable=True)
- selected=d/'compile.arboricx'
- merge_bundles(selected,[(bundle,[('compile','main')])],prune_unreachable=True)
- workspace=d/'workspace-λ';workspace.mkdir()
- (workspace/'dependency.tri').write_text('!export pick\npick [x | _] = x\npick _ = 0\n')
- (workspace/'main.tri').write_text('!import dependency as D\n!export main\n!export accent\nmain = D.pick [42 7]\naccent = "é"\n')
- store=d/'compiled';store.mkdir()
- identity=hashlib.sha256(args.compiler_bundle.read_bytes()).hexdigest()
- for label in ['cold','warm']:
- command=[str(args.zig.resolve()),'--io','--type','string',str(selected),str(worker),
- 'text:'+str(workspace),'text:'+str(store),'text:'+identity,'text:main']
- out=subprocess.run(command,capture_output=True,text=True,timeout=600)
- (d/('compiler-'+label+'.log')).write_text(out.stdout+out.stderr)
- manifest=out.stdout.strip()
- assert out.returncode==0 and len(manifest)==64 and all(c in '0123456789abcdef' for c in manifest),(out.stdout,out.stderr)
- from http_headers import number,tree_list
- old_store=runner.store;runner.store=store
- try:
- assert runner.invoke('compiler-main-'+label,manifest,'main',[])==number(42)
- assert runner.invoke('compiler-accent-'+label,manifest,'accent',[])==tree_list([number(101),number(769)])
- finally: runner.store=old_store
- checks.append(dict(name='compiler-'+label,command=command,manifest=manifest,compiler_sha256=identity))
- assert not list(store.rglob('.tricu-*'))
- (d/'report.json').write_text(json.dumps(dict(checks=checks,zig_sha256=hashlib.sha256(args.zig.read_bytes()).hexdigest()),indent=2)+'\n')
- print(f'PASS {len(checks)} production Zig v2 and retired-v1 CLI controls')
-if __name__=='__main__':main()
diff --git a/test/io_v2_text.py b/test/io_v2_text.py
@@ -21,7 +21,6 @@ def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('tricu', type=Path)
parser.add_argument('--directory', type=Path, required=True)
- parser.add_argument('--zig-probe', type=Path, help='also check the private Zig host against the same independent oracles')
args = parser.parse_args()
directory = args.directory.resolve()
directory.mkdir(parents=True, exist_ok=False)
@@ -31,22 +30,13 @@ def main():
runner.run('compile',['compile','--file',source,'--store',runner.store,'--output',bundle])
pin = runner.imported(bundle,'import')
checks = []
- zig_observations = []
def invoke(label,name,values,expected):
argv = ['--io']
- zig_arguments = []
for i,value in enumerate(values):
path = directory/f'{label}-{i}.tree'
path.write_bytes(value)
argv += ['--arg-tree-file',path]
- zig_arguments.append('tree-file:'+str(path))
assert runner.invoke(label,pin,name,argv) == expected,label
- if args.zig_probe:
- command = [str(args.zig_probe.resolve()),str(bundle),name,*zig_arguments]
- output,peak = run_guarded(command,memory_mib=256,seconds=180)
- (directory/(label+'-zig.log')).write_text(output)
- assert bytes(map(int,output.strip())) == expected,('zig',label)
- zig_observations.append(dict(label=label,peak_kib=peak,command=command))
checks.append(label)
for i,value in enumerate(['','ascii\r\n\0','λ😀e\u0301é','\ufeff','\uffff\U0010ffff','😀'*1025,'a'*1024+'😀'+'b'*1024]):
payload = value.encode('utf-8')
@@ -109,19 +99,18 @@ def main():
input_path.write_bytes(payload)
invoke(f'pipeline-{size}','roundTrip',[text(str(input_path)),text(str(output)),number(len(payload))],result(True,b'\0'))
assert output.read_bytes() == payload
- if args.zig_probe:
- # Independent raw-protocol boundaries; excess heads AND tails diverge.
- # Both backends must reject without demanding them or writing a prefix.
- boundary = directory/'boundaries.arboricx'
- runner.run('compile-boundaries',['compile','--file',ROOT/'test/io-v2/zig-utf8.tri','--store',runner.store,'--output',boundary])
- original_bundle, original_pin = bundle, pin
- bundle, pin = boundary, runner.imported(boundary,'import-boundaries')
- for name,operation in [('excessEncode','utf8/encode'),('excessDecode','utf8/decode'),('excessDigest','digest/update')]:
- invoke(name,name,[number(0)],result(False,problem('invalid-argument',operation,'')))
- path = directory/'excess-write'
- invoke('excessWrite','excessWrite',[text(str(path)),number(0)],result(False,problem('invalid-argument','stream/write','')))
- assert path.read_bytes() == b''
- bundle, pin = original_bundle, original_pin
+ # Independent raw-protocol boundaries; excess heads AND tails diverge.
+ # Reject without demanding excess cells or writing a prefix.
+ boundary = directory/'boundaries.arboricx'
+ runner.run('compile-boundaries',['compile','--file',ROOT/'test/io-v2/utf8-boundaries.tri','--store',runner.store,'--output',boundary])
+ original_bundle, original_pin = bundle, pin
+ bundle, pin = boundary, runner.imported(boundary,'import-boundaries')
+ for name,operation in [('excessEncode','utf8/encode'),('excessDecode','utf8/decode'),('excessDigest','digest/update')]:
+ invoke(name,name,[number(0)],result(False,problem('invalid-argument',operation,'')))
+ path = directory/'excess-write'
+ invoke('excessWrite','excessWrite',[text(str(path)),number(0)],result(False,problem('invalid-argument','stream/write','')))
+ assert path.read_bytes() == b''
+ bundle, pin = original_bundle, original_pin
observations = [r for r in runner.records if r['label'].startswith('pipeline-')]
for row in observations:
assert row['peak_kib'] <= 256*1024,row
@@ -129,10 +118,9 @@ def main():
files = ['lib/io-v2.tri','lib/io-v2/text.tri','demos/io-v2/text.tri','test/io_v2_text.py',
'test/source_order.py','test/rts_metrics.py','src/IOV2/UTF8.hs','src/IOV2/Driver.hs',
'src/IOV2/Protocol.hs','bootstrap/build-stable.arboricx']
- if args.zig_probe: files += ['test/io-v2/zig-utf8.tri','ext/zig/src/io_v2_utf8.zig','ext/zig/src/io_v2_file_host.zig','ext/zig/src/io_v2_core_probe.zig']
- (directory/'report.json').write_text(json.dumps(dict(checks=checks,observations=observations,zig_observations=zig_observations,
- zig_probe_sha256=hashlib.sha256(args.zig_probe.read_bytes()).hexdigest() if args.zig_probe else None,
+ (directory/'report.json').write_text(json.dumps(dict(checks=checks,observations=observations,
+
source_hashes={p:hashlib.sha256((ROOT/p).read_bytes()).hexdigest() for p in files}),indent=2)+'\n')
- print(f'{len(checks)} independent text/UTF-8 checks passed'+(' on Haskell and private Zig' if args.zig_probe else ''))
+ print(f'{len(checks)} independent text/UTF-8 checks passed'+(''))
if __name__ == '__main__': main()
diff --git a/test/io_v2_zig_core.py b/test/io_v2_zig_core.py
@@ -1,101 +0,0 @@
-#!/usr/bin/env python3
-"""Actual source core/handler parity using the PRIVATE Zig core probe, not native IO."""
-import argparse
-import hashlib
-import json
-import subprocess
-from pathlib import Path
-from source_order import Runner
-from io_v2_tasks import core_cases
-from process_rss import run_guarded
-from http_headers import number, result, tree_list
-from stable_bundle import byte_tree
-import time
-
-ROOT = Path(__file__).resolve().parent.parent
-
-def main():
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('tricu', type=Path)
- parser.add_argument('probe', type=Path)
- parser.add_argument('--directory', type=Path, required=True)
- args = parser.parse_args()
- d = args.directory.resolve(); d.mkdir(parents=True, exist_ok=False)
- r = Runner(args.tricu, d)
- probe = args.probe.resolve()
- source = ROOT/'test/io-v2/tasks.tri'
- bundle = d/'tasks.arboricx'
- r.run('compile', ['compile', '--file', source, '--store', r.store, '--output', bundle])
- pin = r.imported(bundle, 'import')
- checks = []
- for name, expected in core_cases().items():
- flags = ['--io']; argv = []
- if name == 'lazyLabels':
- (d/'zero.tree').write_bytes(b'\0')
- flags += ['--arg-tree-file', d/'zero.tree']; argv = ['0']
- assert r.invoke('haskell-'+name, pin, name, flags) == expected, ('haskell', name)
- process = subprocess.run([str(probe), str(bundle), name, *argv], capture_output=True, timeout=60)
- (d/(name+'.stdout')).write_bytes(process.stdout)
- (d/(name+'.stderr')).write_bytes(process.stderr)
- assert process.returncode == 0, (name, process.returncode, process.stderr)
- assert process.stdout == ''.join(map(str, expected)).encode()+b'\n', name
- checks.append(name)
- controls_source = ROOT/'test/io-v2/core-controls.tri'
- controls_bundle = d/'controls.arboricx'
- r.run('compile-controls', ['compile', '--file', controls_source, '--store', r.store, '--output', controls_bundle])
- controls_pin = r.imported(controls_bundle, 'import-controls')
- def text(s): return byte_tree(s.encode())
- def problem(category, operation, detail=''):
- return result(False, tree_list([text(category), text(operation), text(detail), text('none')]))
- controls = {
- 'cancelChild': (tree_list([text('cancelled'), text('stop'), b'\0']), False),
- 'currentScopes': (number(1), False),
- 'badFlags': (problem('invalid-argument', 'core/spawn'), True),
- 'badTransfer': (problem('invalid-argument', 'core/spawn'), True),
- 'discardedTicket': (problem('stale-task', 'core/join'), False),
- 'selfCycle': (number(42), False),
- 'pureDomainError': (result(False, text('ordinary')), False),
- 'unknownWithPoison': (problem('unsupported', 'future/call', 'group revision 3'), True),
- }
- for name, (expected, seeded) in controls.items():
- flags = ['--io'] + (['--arg-tree-file', d/'zero.tree'] if seeded else [])
- assert r.invoke('haskell-'+name, controls_pin, name, flags) == expected, ('haskell', name)
- p = subprocess.run([str(probe), str(controls_bundle), name] + (['0'] if seeded else []), capture_output=True, timeout=60)
- (d/(name+'.stderr')).write_bytes(p.stderr)
- assert p.returncode == 0 and p.stdout == ''.join(map(str, expected)).encode()+b'\n', (name, p.returncode, p.stderr, p.stdout)
- checks.append(name)
- for name, seeded in [('selfCycleWrong', False), ('rootFailure', True), ('rootCancelled', True), ('malformedBind', False)]:
- flags = ['--io'] + (['--arg-tree-file', d/'zero.tree'] if seeded else [])
- try:
- r.invoke('haskell-'+name, controls_pin, name, flags)
- except subprocess.CalledProcessError as error:
- assert 'IO v2 task '+('cancelled' if name == 'rootCancelled' else 'failed') in error.output
- else:
- raise AssertionError(('Haskell negative succeeded', name))
- p = subprocess.run([str(probe), str(controls_bundle), name] + (['0'] if seeded else []), capture_output=True, timeout=60)
- assert p.returncode == 1 and p.stdout == b'' and b'not forced' in p.stderr, (name, p.returncode, p.stderr)
- checks.append(name+'-negative')
- p = subprocess.run([str(probe), str(controls_bundle), 'legacyEnvelope', '0'], capture_output=True, timeout=10)
- assert p.returncode != 0 and p.stdout == b'' and b'UnsupportedVersion' in p.stderr, p.stderr
- checks.append('v1-not-retried')
- retention_source = ROOT/'test/io-v2/core-retention.tri'
- retention_bundle = d/'retention.arboricx'
- r.run('compile-retention', ['compile', '--file', retention_source, '--store', r.store, '--output', retention_bundle])
- retention_pin = r.imported(retention_bundle, 'import-retention')
- retention = []
- for name in ('joined', 'discarded'):
- for count in (16, 128, 512):
- label = f'{name}-{count}'
- argument = d/(label+'.tree'); argument.write_bytes(number(count))
- assert r.invoke('haskell-retention-'+label, retention_pin, name, ['--io', '--arg-tree-file', argument]) == b'\0'
- command = [str(probe), str(retention_bundle), name, str(count)]
- start = time.monotonic()
- output, peak = run_guarded(command, memory_mib=128, seconds=180)
- (d/('zig-retention-'+label+'.log')).write_text(output)
- assert output == '0\n', (label, output)
- retention.append(dict(name=name, count=count, peak_kib=peak, seconds=time.monotonic()-start, command=command))
- paths = [controls_source, retention_source, source, probe, ROOT/'ext/zig/src/io_v2_core.zig', ROOT/'ext/zig/src/io_v2_tasks.zig', ROOT/'ext/zig/src/io_v2_resources.zig', ROOT/'ext/zig/src/io_v2_protocol.zig', ROOT/'test/io_v2_tasks.py']
- (d/'report.json').write_text(json.dumps({'checks': checks, 'scope': 'private-core-only-no-OS-facilities', 'retention': retention, 'sources': {str(p): hashlib.sha256(p.read_bytes()).hexdigest() for p in paths}, 'haskell_records': r.records}, indent=2)+'\n')
- print(f'PASS {len(checks)} actual source core/handler controls on Haskell and private Zig core')
-
-if __name__ == '__main__': main()
diff --git a/test/io_v2_zig_digest.py b/test/io_v2_zig_digest.py
@@ -1,39 +0,0 @@
-#!/usr/bin/env python3
-"""Actual incremental digest source on private Zig and Haskell; external SHA oracle."""
-import argparse
-import hashlib
-import json
-from pathlib import Path
-from source_order import Runner
-from http_headers import tree_list, result, number
-from process_rss import run_guarded
-ROOT = Path(__file__).resolve().parent.parent
-
-def text(s): return tree_list([number(ord(c)) for c in s])
-def main():
- p=argparse.ArgumentParser(description=__doc__)
- p.add_argument('tricu',type=Path); p.add_argument('probe',type=Path); p.add_argument('--directory',type=Path,required=True)
- args=p.parse_args(); d=args.directory.resolve(); d.mkdir(parents=True,exist_ok=False)
- r=Runner(args.tricu,d); bundle=d/'digest.arboricx'
- r.run('compile',['compile','--file',ROOT/'test/io-v2/zig-digest.tri','--store',r.store,'--output',bundle]); pin=r.imported(bundle,'import')
- def problem(op): return result(False,tree_list([text('invalid-state'),text(op),text('digest already finished'),text('none')]))
- empty=hashlib.sha256(b'').hexdigest(); abc=hashlib.sha256(b'abc').hexdigest()
- cases=[('hash','abc',text(abc)),('hash','ÿ',text(hashlib.sha256(bytes([255])).hexdigest())),('hash','a'*4096,text(hashlib.sha256(b'a'*4096).hexdigest())),
- ('lifecycle',None,tree_list([text(empty),problem('digest/finish'),problem('digest/update')])),
- ('transferred',None,tree_list([text('completed'),text(abc)]))]
- for algorithm in ('sha512','é😀'*100):
- cases.append(('unsupported',algorithm,result(False,tree_list([text('unsupported'),text('digest/open'),text(algorithm[:128]),text('none')]))))
- checks=[]
- for i,(name,argument,expected) in enumerate(cases):
- label=f'{i}-{name}'; command=[str(args.probe.resolve()),str(bundle),name]+(['text:'+argument] if argument is not None else [])
- output,peak=run_guarded(command,memory_mib=128,seconds=60)
- (d/(label+'.log')).write_text(output)
- assert bytes(map(int,output.strip()))==expected,('zig',label)
- flags=['--io']
- if argument is not None:
- arg=d/(label+'.tree');arg.write_bytes(text(argument));flags+=['--arg-tree-file',arg]
- assert r.invoke(label,pin,name,flags)==expected,('haskell',label)
- checks.append(dict(label=label,peak_kib=peak))
- (d/'report.json').write_text(json.dumps(dict(checks=checks,haskell=r.records,probe_sha256=hashlib.sha256(args.probe.read_bytes()).hexdigest()),indent=2)+'\n')
- print(f'PASS {len(checks)} digest source controls on Haskell and private Zig')
-if __name__=='__main__':main()
diff --git a/test/io_v2_zig_files.py b/test/io_v2_zig_files.py
@@ -1,129 +0,0 @@
-#!/usr/bin/env python3
-"""Private Zig regular-file host: actual source, independent bytes and trees."""
-import argparse
-import hashlib
-import json
-import subprocess
-from pathlib import Path
-from source_order import Runner, field
-from http_headers import result, tree_list, number
-from stable_bundle import byte_tree
-from process_rss import run_guarded
-
-ROOT = Path(__file__).resolve().parent.parent
-
-def main():
- p = argparse.ArgumentParser(description=__doc__)
- p.add_argument('tricu', type=Path)
- p.add_argument('probe', type=Path)
- p.add_argument('--directory', type=Path, required=True)
- args = p.parse_args()
- d = args.directory.resolve(); d.mkdir(parents=True, exist_ok=False)
- r = Runner(args.tricu, d)
- bundle = d/'copy.arboricx'
- r.run('compile', ['compile', '--file', ROOT/'demos/io-v2/copy.tri', '--store', r.store, '--output', bundle])
- pin = r.imported(bundle, 'import')
- checks = []
- def invoke(name, source, target, label):
- command = [str(args.probe.resolve()), str(bundle), name, 'text:'+str(source), 'text:'+str(target)]
- output, peak = run_guarded(command, memory_mib=128, seconds=180)
- (d/(label+'.log')).write_text(output)
- assert output.endswith('\n') and set(output.strip()) <= set('012'), (label, output)
- return bytes(map(int, output.strip())), peak
- for name in ('copy', 'handledCopy', 'borrowedCopy', 'transferredCopy', 'joinedOnExitCopy', 'handledChildCopy', 'cancelledCopy'):
- for size in (0, 1, 4095, 4096, 4097, 65536, 1048576):
- label = f'{name}-{size}'
- source, target = d/(label+'.in'), d/(label+'.out')
- body = (bytes(range(256))*((size+255)//256))[:size]
- source.write_bytes(body)
- expected = result(True, b'\0')
- if name == 'handledCopy': expected = result(True, expected)
- if name == 'cancelledCopy': expected = tree_list([byte_tree(b'cancelled'), byte_tree(b'requested'), b'\0'])
- actual, peak = invoke(name, source, target, label)
- assert actual == expected, (label, actual)
- assert target.read_bytes() == (body[:4096] if name == 'cancelledCopy' else body), label
- # Actual Haskell comparison on each composition at the boundary; the
- # complete corpus is independently checked by io_v2_files.py too.
- if size == 4097:
- argv = ['--io']
- for i, path in enumerate((source, d/(label+'.haskell'))):
- argument = d/f'{label}-{i}.tree'; argument.write_bytes(tree_list([number(ord(c)) for c in str(path)]))
- argv += ['--arg-tree-file', argument]
- assert r.invoke('haskell-'+label, pin, name, argv) == expected
- assert (d/(label+'.haskell')).read_bytes() == target.read_bytes()
- checks.append(dict(label=label, peak_kib=peak, sha256=hashlib.sha256(target.read_bytes()).hexdigest()))
- source, target = d/'source-é😀', d/'target-é😀'
- source.write_bytes(b'\x00\xffunicode-path')
- actual, peak = invoke('copy', source, target, 'unicode-path')
- assert actual == result(True, b'\0') and target.read_bytes() == source.read_bytes()
- flags = ['--io']
- target.unlink()
- for i, path in enumerate((source, target)):
- arg = d/f'unicode-{i}.tree'; arg.write_bytes(tree_list([number(ord(c)) for c in str(path)]))
- flags += ['--arg-tree-file', arg]
- assert r.invoke('unicode-path-haskell', pin, 'copy', flags) == result(True, b'\0')
- assert target.read_bytes() == source.read_bytes()
- checks.append(dict(label='unicode-path', peak_kib=peak))
- for case in ('missing-source', 'missing-parent', 'fifo', 'directory', 'device'):
- source, target = d/(case+'.in'), d/(case+'.out')
- source.write_bytes(b'keep')
- category = b'not-found'; operation = b'file/open-reader'
- if case == 'missing-source': source = d/'does-not-exist'
- elif case == 'missing-parent': target = d/'missing-parent'/'out'; operation = b'file/open-writer'
- elif case == 'fifo':
- import os
- source.unlink(); os.mkfifo(source); category = b'unsupported'
- elif case == 'directory': source = d; category = b'unsupported'
- elif case == 'device': source = Path('/dev/null'); category = b'unsupported'
- actual, peak = invoke('copy', source, target, case)
- assert field(actual, 0) == b'\0', case
- problem = field(actual, 1, 0)
- assert field(problem, 0) == byte_tree(category), case
- assert field(problem, 1, 0) == byte_tree(operation), case
- assert not target.exists(), case
- checks.append(dict(label=case, peak_kib=peak))
- controls = d/'controls.arboricx'
- r.run('compile-controls', ['compile', '--file', ROOT/'test/io-v2/zig-files.tri', '--store', r.store, '--output', controls])
- control_pin = r.imported(controls, 'import-controls')
- def problem(category, operation, detail=b''):
- return result(False, tree_list([byte_tree(category), byte_tree(operation), byte_tree(detail), byte_tree(b'none')]))
- data = bytes([2])+byte_tree(b'data')+byte_tree(b'bc')
- eof = bytes([2])+byte_tree(b'eof')+b'\0'
- control_cases = {
- 'positioned': (b'', tree_list([data, number(3), number(10), eof]), b'a'),
- 'appended': (b'k', tree_list([number(3), problem(b'unsupported', b'file/seek', b'operation unavailable in this file mode')]), b'kxy'),
- 'emptyWrite': (b'previous', problem(b'invalid-bound', b'stream/write'), b''),
- 'closed': (b'keep', problem(b'stale-handle', b'resource/close'), b'keep'),
- 'readerWrite': (b'keep', problem(b'wrong-kind', b'stream/write'), b'keep'),
- }
- for name, (before, expected, after) in control_cases.items():
- for backend in ('zig', 'haskell'):
- target = d/(name+'-'+backend+'.file'); target.write_bytes(before)
- if backend == 'zig':
- output, peak = run_guarded([str(args.probe.resolve()), str(controls), name, 'text:'+str(target)], memory_mib=128, seconds=60)
- actual = bytes(map(int, output.strip()))
- else:
- argument = d/(name+'.tree'); argument.write_bytes(tree_list([number(ord(c)) for c in str(target)]))
- actual = r.invoke(name, control_pin, name, ['--io', '--arg-tree-file', argument])
- assert actual == expected, (name, backend, actual, expected)
- assert target.read_bytes() == after, (name, backend)
- checks.append(dict(label=name))
- for backend in ('zig', 'haskell'):
- target = d/('failed-'+backend)
- if backend == 'zig':
- process = subprocess.run([str(args.probe.resolve()), str(controls), 'failedAfterWrite', 'text:'+str(target)], capture_output=True, timeout=60)
- assert process.returncode == 1 and b'not forced' in process.stderr
- else:
- argument = d/'failed.tree'; argument.write_bytes(tree_list([number(ord(c)) for c in str(target)]))
- try: r.invoke('failedAfterWrite', control_pin, 'failedAfterWrite', ['--io', '--arg-tree-file', argument])
- except subprocess.CalledProcessError as error: assert 'IO v2 task failed' in error.output
- else: raise AssertionError('failedAfterWrite succeeded')
- assert target.read_bytes() == b'*'
- checks.append(dict(label='failed-after-write-no-rollback'))
- output, _ = run_guarded([str(args.probe.resolve()), str(controls), 'unknownWithPoison', '0'], memory_mib=128, seconds=10)
- assert bytes(map(int, output.strip())) == problem(b'unsupported', b'file/open-reader', b'group revision 99')
- checks.append(dict(label='unknown-revision-no-demand'))
- paths = [ROOT/'test/io-v2/zig-files.tri', ROOT/'demos/io-v2/copy.tri', ROOT/'ext/zig/src/io_v2_files.zig', ROOT/'ext/zig/src/io_v2_file_host.zig', ROOT/'ext/zig/src/io_v2_core.zig', args.probe.resolve()]
- (d/'report.json').write_text(json.dumps(dict(checks=checks, sources={str(p):hashlib.sha256(p.read_bytes()).hexdigest() for p in paths}, haskell=r.records, scope='private-regular-file-host-no-public-v2'), indent=2)+'\n')
- print(f'PASS {len(checks)} private file-host source/byte controls; 14 Haskell boundary/mode/failure comparisons')
-if __name__ == '__main__': main()
diff --git a/test/io_v2_zig_paths.py b/test/io_v2_zig_paths.py
@@ -1,55 +0,0 @@
-#!/usr/bin/env python3
-"""Private Zig path/directory operations: independent results, Haskell comparison."""
-import argparse
-import hashlib
-import json
-import os
-from pathlib import Path
-from source_order import Runner
-from http_headers import tree_list, number
-from stable_bundle import byte_tree
-from process_rss import run_guarded
-ROOT = Path(__file__).resolve().parent.parent
-
-def main():
- p = argparse.ArgumentParser(description=__doc__)
- p.add_argument('tricu', type=Path); p.add_argument('probe', type=Path)
- p.add_argument('--directory', type=Path, required=True)
- args = p.parse_args(); d = args.directory.resolve(); d.mkdir(parents=True, exist_ok=False)
- r = Runner(args.tricu, d); bundle = d/'paths.arboricx'
- r.run('compile', ['compile', '--file', ROOT/'test/io-v2/zig-paths.tri', '--store', r.store, '--output', bundle])
- pin = r.imported(bundle, 'import'); checks = []
- def run(name, paths, expected, label, haskell=True):
- output, peak = run_guarded([str(args.probe.resolve()), str(bundle), name, *['text:'+str(path) for path in paths]], memory_mib=128, seconds=60)
- (d/(label+'.log')).write_text(output)
- assert bytes(map(int, output.strip())) == expected, label
- if haskell:
- flags = ['--io']
- for i, path in enumerate(paths):
- arg = d/f'{label}-{i}.tree'; arg.write_bytes(tree_list([number(ord(c)) for c in str(path)]))
- flags += ['--arg-tree-file', arg]
- assert r.invoke(label+'-haskell', pin, name, flags) == expected, label
- checks.append(dict(label=label, peak_kib=peak))
- regular = tree_list([byte_tree(b'file'), number(3)])
- for case in ('ascii', 'é😀'):
- directory, source = d/(case+'-dir'), d/(case+'-source')
- target = directory/'target'
- run('paths', [directory, source, target], tree_list([regular, tree_list([byte_tree(b'directory')]), regular, byte_tree(b'not-found')]), case)
- assert not directory.exists() and not source.exists() and not target.exists()
- source = d/'source'; source.write_bytes(b'abc')
- link = d/'link'; link.symlink_to(source)
- run('observe', [link], tree_list([tree_list([byte_tree(b'symlink')]), regular]), 'symlink')
- run('observe', [source], tree_list([regular, regular]), 'regular')
- directory_status = tree_list([byte_tree(b'directory')])
- run('observe', [d], tree_list([directory_status, directory_status]), 'directory')
- fifo = d/'fifo'; os.mkfifo(fifo)
- other = tree_list([byte_tree(b'other')])
- run('observe', [fifo], tree_list([other, other]), 'fifo-observation')
- run('badDirectory', [source], byte_tree(b'not-directory'), 'sync-regular')
- run('badDirectory', [fifo], byte_tree(b'not-directory'), 'sync-fifo')
- target = d/'missing-parent'/'out'
- run('failedRename', [source, target], byte_tree(b'not-found'), 'rename-missing-parent')
- assert source.read_bytes() == b'abc' and not target.exists()
- (d/'report.json').write_text(json.dumps(dict(checks=checks, haskell=r.records, source_sha256=hashlib.sha256((ROOT/'test/io-v2/zig-paths.tri').read_bytes()).hexdigest(), probe_sha256=hashlib.sha256(args.probe.read_bytes()).hexdigest()), indent=2)+'\n')
- print(f'PASS {len(checks)} path/directory controls on private Zig and Haskell')
-if __name__ == '__main__': main()
diff --git a/test/io_v2_zig_publication.py b/test/io_v2_zig_publication.py
@@ -1,70 +0,0 @@
-#!/usr/bin/env python3
-"""Real source-owned publication policy on private Zig, with Haskell comparisons."""
-import argparse
-import hashlib
-import json
-import subprocess
-from pathlib import Path
-from source_order import Runner, field
-from http_headers import result, tree_list, number
-from process_rss import run_guarded
-ROOT = Path(__file__).resolve().parent.parent
-
-def text(s): return tree_list([number(ord(c)) for c in str(s)])
-def main():
- p=argparse.ArgumentParser(description=__doc__)
- p.add_argument('tricu',type=Path);p.add_argument('probe',type=Path);p.add_argument('--directory',type=Path,required=True)
- args=p.parse_args();d=args.directory.resolve();d.mkdir(parents=True,exist_ok=False)
- r=Runner(args.tricu,d);bundle=d/'publication.arboricx'
- r.run('compile',['compile','--file',ROOT/'demos/io-v2/publish.tri','--store',r.store,'--output',bundle]);pin=r.imported(bundle,'import')
- checks=[]
- def invoke(label,name,values,expected,compare=False):
- command=[str(args.probe.resolve()),str(bundle),name,*[str(v) if isinstance(v,int) else 'text:'+str(v) for v in values]]
- output,peak=run_guarded(command,memory_mib=128,seconds=180)
- (d/(label+'.log')).write_text(output)
- actual=bytes(map(int,output.strip()))
- if expected is not None: assert actual==expected,(label,actual,expected)
- if compare:
- flags=['--io']
- for i,v in enumerate(values):
- arg=d/f'{label}-{i}.tree';arg.write_bytes(number(v) if isinstance(v,int) else text(v));flags+=['--arg-tree-file',arg]
- assert r.invoke(label+'-haskell',pin,name,flags)==actual,label
- checks.append(dict(label=label,peak_kib=peak))
- return actual
- for size in (0,1,4095,4096,4097,65536,1048576):
- source=d/f'input-{size}';body=(bytes(range(256))*((size+255)//256))[:size];source.write_bytes(body)
- target=d/f'objects-{size}';target.mkdir();digest=hashlib.sha256(body).hexdigest()
- for state in ('new','existing'):
- invoke(f'{state}-{size}','publish',[source,target,size],result(True,text(digest)),size==4097)
- assert (target/digest).read_bytes()==body
- assert (target/digest).stat().st_mode&0o077==0
- assert sorted(p.name for p in target.iterdir())==[digest]
- source=d/'source-λ-树';source.write_bytes(b'\0\xffbinary');target=d/'objects-λ-树';target.mkdir()
- digest=hashlib.sha256(source.read_bytes()).hexdigest()
- invoke('unicode','publish',[source,target,8],result(True,text(digest)),True)
- assert (target/digest).read_bytes()==source.read_bytes()
- source=d/'negative-input';source.write_bytes(b'abc');digest=hashlib.sha256(b'abc').hexdigest()
- for label,count,winner,message in [('short',4,None,'Incomplete artifact stream'),('excess',2,None,'Artifact exceeds declared length'),('bad-winner',3,b'xyz','Existing artifact digest mismatch'),('short-winner',3,b'ab','Existing artifact length mismatch'),('long-winner',3,b'abcd','Existing artifact length mismatch')]:
- target=d/label;target.mkdir()
- if winner is not None:(target/digest).write_bytes(winner)
- invoke(label,'publish',[source,target,count],result(False,text(message)),True)
- assert sorted(p.name for p in target.iterdir())==([] if winner is None else [digest])
- if winner is not None:assert (target/digest).read_bytes()==winner
- for name,message in [('earlySuccess','Incomplete artifact stream'),('ignoresWriteFailure','Artifact exceeds declared length'),('emptyChunk','Empty artifact chunk'),('overrideWrite','Incomplete artifact stream'),('failedAfterFull','Producer failed'),('rejectedDestination','Destination rejected')]:
- target=d/name;target.mkdir();invoke(name,name,[target],result(False,text(message)),True);assert list(target.iterdir())==[]
- for name,problem in [
- ('malformedDestination',tree_list([text('invalid-response'),text('artifact-stream'),text('destination'),text('Expected destination Result'),b'\0'])),
- ('badDigest',tree_list([text('invalid-response'),text('artifact-stream'),text('digest'),text('Invalid digest reply'),b'\0'])),
- *[(n,tree_list([text('producer-boundary'),text('artifact-stream'),text('publish'),text('Producer must be sequential'),tree_list([text('outer'),text('inner')])])) for n in ('nestedProducer','forkedProducer')]]:
- target=d/name;target.mkdir();invoke(name,name,[target],tree_list([text('failed'),problem,b'\0']),True);assert list(target.iterdir())==[]
- target=d/'invalidChunk';target.mkdir();actual=invoke('invalidChunk','invalidChunk',[target],None,True)
- assert field(actual,0)==b'\0' and field(actual,1,0,0)==text('write-all') and field(actual,1,0,1,0)==number(0)
- assert field(actual,1,0,1,1,0,0)==text('invalid-argument');assert list(target.iterdir())==[]
- target=d/'cancelled';target.mkdir()
- process=subprocess.run([str(args.probe.resolve()),str(bundle),'cancelled','text:'+str(target)],capture_output=True,timeout=60)
- assert process.returncode==1 and b'cancelled (reason not forced)' in process.stderr
- assert list(target.iterdir())==[] and not (d/'escape').exists()
- checks.append(dict(label='cancelled-no-private-names'))
- (d/'report.json').write_text(json.dumps(dict(checks=checks,haskell=r.records,probe_sha256=hashlib.sha256(args.probe.read_bytes()).hexdigest()),indent=2)+'\n')
- print(f'PASS {len(checks)} private Zig source publication/policy controls')
-if __name__=='__main__':main()
diff --git a/test/io_v2_zig_stages.py b/test/io_v2_zig_stages.py
@@ -1,52 +0,0 @@
-#!/usr/bin/env python3
-"""Cross-directory publication, EXDEV, invalid destinations and no rollback."""
-import argparse
-import hashlib
-import json
-import subprocess
-import tempfile
-from pathlib import Path
-from source_order import Runner
-from http_headers import result,tree_list,number
-from process_rss import run_guarded
-ROOT=Path(__file__).resolve().parent.parent
-
-def text(s):return tree_list([number(ord(c)) for c in str(s)])
-def main():
- p=argparse.ArgumentParser(description=__doc__);p.add_argument('tricu',type=Path);p.add_argument('probe',type=Path);p.add_argument('--directory',type=Path,required=True)
- args=p.parse_args();d=args.directory.resolve();d.mkdir(parents=True,exist_ok=False);r=Runner(args.tricu,d);bundle=d/'stages.arboricx'
- r.run('compile',['compile','--file',ROOT/'test/io-v2/zig-stages.tri','--store',r.store,'--output',bundle]);pin=r.imported(bundle,'import');checks=[]
- a,b=d/'a',d/'b';a.mkdir();b.mkdir();target=b/'published'
- def invoke(name,values,label,expected):
- command=[str(args.probe.resolve()),str(bundle),name,*[str(v) if isinstance(v,int) else 'text:'+str(v) for v in values]]
- output,peak=run_guarded(command,memory_mib=128,seconds=60);(d/(label+'.log')).write_text(output)
- assert bytes(map(int,output.strip()))==expected,label
- checks.append(dict(label=label,peak_kib=peak))
- for mode,state in [(0,'published'),(0,'exists'),(1,'published')]:
- invoke('publish',[a,target,mode],f'publish-{mode}-{state}',result(True,text(state)))
- assert target.read_bytes()==b'abc' and list(a.iterdir())==[]
- target.unlink() # prove visibility was created before the terminal failure
- process=subprocess.run([str(args.probe.resolve()),str(bundle),'failureAfterPublication','text:'+str(a),'text:'+str(target)],capture_output=True,timeout=60)
- assert process.returncode==1 and b'failed (payload not forced)' in process.stderr
- assert target.read_bytes()==b'abc' and list(a.iterdir())==[];checks.append(dict(label='no-rollback-after-failure'))
- for name in ('.tricu-stage-protected','x.tricu-stage-protected'):
- bad=b/name
- expected=tree_list([result(False,tree_list([text('invalid-destination'),text('stage/publish'),text('destination must name a file outside the private staging namespace'),text('not-published')])),result(False,tree_list([text('invalid-state'),text('stage/publish'),text('stage is not in the required phase'),text('none')]))])
- invoke('invalidDestination',[a,bad],name,expected)
- assert not bad.exists() and list(a.iterdir())==[]
- # Do not manufacture EXDEV through a mock here: require a distinct device.
- with tempfile.TemporaryDirectory(prefix='tricu-zig-exdev-',dir='/dev/shm') as foreign:
- foreign=Path(foreign)
- assert foreign.stat().st_dev != a.stat().st_dev,'EXDEV control needs distinct filesystems'
- output=foreign/'out'
- expected=tree_list([tree_list([text('io-error'),text('unknown')]),tree_list([text('invalid-state'),text('none')])])
- invoke('crossDevice',[a,output],'actual-exdev',expected)
- assert not output.exists() and list(a.iterdir())==[]
- flags=['--io']
- for i,v in enumerate((a,output)):
- arg=d/f'exdev-{i}.tree';arg.write_bytes(text(v));flags+=['--arg-tree-file',arg]
- assert r.invoke('exdev-haskell',pin,'crossDevice',flags)==expected
- assert not output.exists() and list(a.iterdir())==[]
- (d/'report.json').write_text(json.dumps(dict(checks=checks,haskell=r.records,probe_sha256=hashlib.sha256(args.probe.read_bytes()).hexdigest()),indent=2)+'\n')
- print(f'PASS {len(checks)} private staged-publication controls including real EXDEV')
-if __name__=='__main__':main()
diff --git a/test/reducer_comparison.py b/test/reducer_comparison.py
@@ -1,72 +0,0 @@
-#!/usr/bin/env python3
-"""Paired Haskell/Zig runtime measurements with complete-result checks."""
-import hashlib
-import json
-import os
-from pathlib import Path
-import platform
-import statistics
-import sys
-import time
-from process_rss import run_guarded
-
-haskell, zig, fixtures, output = sys.argv[1:]
-fixtures = Path(fixtures)
-affinity = sorted(os.sched_getaffinity(0))
-os.sched_setaffinity(0, {affinity[0]})
-cases = [(kind, n, 20) for kind in ('id', 'append') for n in (256, 4096, 16384)]
-cases += [(kind, n, 5) for kind in ('deep', 'distribution') for n in (1000, 10000, 100000)]
-cases += [('append', 4096, 256)]
-report = dict(
- complete=False, platform=platform.platform(), machine=platform.machine(),
- cpu_model=next(line.split(':', 1)[1].strip() for line in Path('/proc/cpuinfo').read_text().splitlines() if line.startswith('model name')),
- pinned_cpu=affinity[0], original_affinity=affinity, samples_per_host_case=5,
- haskell_compiler=(Path(haskell).resolve().parents[1]/'compiler-version').read_text().strip(),
- fixture_sha256={name: hashlib.sha256((fixtures/name).read_bytes()).hexdigest() for name in ('id.arboricx', 'append.arboricx')},
- runners={host: dict(path=path, sha256=hashlib.sha256(Path(path).read_bytes()).hexdigest()) for host,path in [('haskell',haskell),('zig',zig)]},
- limits=dict(rss_mib=512, wall_seconds=65, cpu_seconds=60, native_stack_mib=8,
- haskell_rts=['-N1','-M512m','-K8m'], zig_address_space_mib=512),
- notes=[
- 'Identical normal-form fixture functions and canonical input values; fresh application graphs each iteration.',
- 'Primary metric includes WHNF reduction, complete output decoding/checking and automatic GC.',
- 'Input construction is separate; Haskell forces input normal forms and uses opaque suspension boxes to prepare unevaluated applications.',
- 'Native allocations, FFI, codec implementations and automatic GC differ. All automatic GC remains charged to its triggering phase.',
- 'Explicit GC follows identical round cadence; total sums build, reduction, observation and explicit GC phases.',
- 'Memory is complete-process VmHWM, including runtime initialization.',
- 'Five fresh-process samples per case, sequential execution, alternating host order, single CPU affinity.'
- ], cases=[])
-def save():
- Path(output).write_text(json.dumps(report, indent=2)+'\n')
-save()
-for kind,n,rounds in cases:
- samples = {host: [] for host in ('haskell','zig')}
- fixture = fixtures / ('append.arboricx' if kind == 'append' else 'id.arboricx')
- for repeat in range(5):
- for host in (('haskell','zig') if repeat % 2 == 0 else ('zig','haskell')):
- command = [haskell if host == 'haskell' else zig, kind, str(n), str(rounds), str(fixture)]
- if host == 'haskell': command += ['+RTS','-N1','-M512m','-K8m','-RTS']
- start = time.monotonic()
- text, sampled_peak = run_guarded(command, memory_mib=512, seconds=65)
- sample = json.loads(text)
- assert (sample['kind'],sample['n'],sample['rounds']) == (kind,n,rounds)
- assert 0 < sample['peak_rss_kib'] <= 512*1024
- sample['process_wall_s'] = time.monotonic()-start
- sample['sampled_peak_rss_kib'] = sampled_peak
- samples[host].append(sample)
- row = dict(kind=kind,n=n,rounds=rounds,hosts={})
- for host,values in samples.items():
- metrics = {}
- for label,fields in dict(evaluate_observe=['reduce_s','observe_s'],
- total=['build_s','reduce_s','observe_s','gc_s'],
- build=['build_s'], whnf=['reduce_s'], observe=['observe_s'], gc=['gc_s']).items():
- series = [sum(s[f] for f in fields)*1000/rounds for s in values]
- metrics[label] = dict(median=statistics.median(series),min=min(series),max=max(series))
- row['hosts'][host] = dict(samples=values, ms_per_iteration=metrics,
- max_peak_rss_mib=max(s['peak_rss_kib'] for s in values)/1024)
- h,z = (row['hosts'][host]['ms_per_iteration']['evaluate_observe']['median'] for host in ('haskell','zig'))
- row['zig_over_haskell_evaluate_observe'] = z/h
- report['cases'].append(row)
- save()
- print(f'{kind}/{n} x{rounds}: Haskell {h:.4f} ms, Zig {z:.4f} ms; Zig/Haskell {z/h:.2f}x', flush=True)
-report['complete'] = True
-save()
diff --git a/test/shared_library_generation.py b/test/shared_library_generation.py
@@ -0,0 +1,203 @@
+#!/usr/bin/env python3
+"""Bounded compiler compatibility checks through the shared generation runner.
+Haskell supplies transport and result execution; each selected Haskell execution interface
+compiles and publishes the test modules. Independent audits verify the stores.
+"""
+import argparse
+import hashlib
+import json
+from pathlib import Path
+import sys
+import subprocess
+from process_rss import run_guarded
+
+ROOT = Path(__file__).resolve().parent.parent
+
+
+def published_objects(directory):
+ """Committed names only; SIGKILL can leave private IO-v2 stages behind."""
+ objects = {}
+ for path in (directory / 'objects').glob('*/*'):
+ if '.tricu-stage-' in path.name:
+ continue # Reserved by IOV2.Staging; never a publishable object name.
+ assert len(path.name) == 64 and all(c in '0123456789abcdef' for c in path.name), path
+ assert path.parent.name == path.name[:3], path
+ objects[path.name] = path.read_bytes()
+ return objects
+
+
+def main():
+ p = argparse.ArgumentParser(description=__doc__)
+ p.add_argument('--tricu', type=Path, required=True)
+ p.add_argument('--library', type=Path, required=True)
+ p.add_argument('--directory', type=Path, required=True)
+ p.add_argument('--compatibility', action='store_true', help='also check broader syntax, invalidation and negative controls')
+ args = p.parse_args()
+ directory = args.directory.resolve()
+ directory.mkdir(parents=True, exist_ok=False)
+ source = directory / 'source'
+ source.mkdir()
+ (source / 'dep.tri').write_text('!export answer\nanswer = 41\n')
+ (source / 'app.tri').write_text('!import dep as D\n!export main\nmain = D.answer\n')
+ source_map = directory / 'source-map.v1'
+ source_map.write_text('arboricx.source-map.v1\nnative\tdep\tdep\tdep.tri\t-\nnative\tapp\tapp\tapp.tri\tdep\n')
+ report = dict(complete=False, stages=[])
+ def save():
+ (directory / 'report.json').write_text(json.dumps(report, indent=2)+'\n')
+ def run(label, command, success=True):
+ try:
+ output, peak = run_guarded(list(map(str,command)), memory_mib=1024, seconds=150)
+ if not success:
+ raise AssertionError('unexpected success: '+label)
+ except subprocess.CalledProcessError as error:
+ (directory / (label+'.log')).write_text(error.output)
+ if success:
+ report['failure'] = label
+ save()
+ raise
+ report['stages'].append(dict(label=label, expected_failure=True))
+ save()
+ return error.output
+ except Exception as error:
+ (directory / (label+'.log')).write_text(str(getattr(error,'output',''))+'\n'+str(error))
+ report['failure'] = label
+ save()
+ raise
+ (directory / (label+'.log')).write_text(output)
+ report['stages'].append(dict(label=label,peak_kib=peak))
+ save()
+ return output
+ def checkpoints(host):
+ return {name:(directory/host/'cas/cache/source-build'/name).read_bytes() for name in ('dep','app')}
+ save()
+ commands = {}
+ for host in ('haskell','haskell-ffi'):
+ output = directory / host
+ command = [sys.executable, ROOT/'test/bootstrap_generations.py', output,
+ '--bootstrap-kit', ROOT/'bootstrap/rebuild', '--tricu',args.tricu.resolve(),
+ '--execution-host',host,'--source-root',source,'--map',source_map,'--root','app',
+ '--backend','stable','--memory-mib','1024','--wall-seconds','120',
+ '--initial-seconds','60','--extended-seconds','100']
+ if host == 'haskell-ffi':
+ command += ['--haskell-library',args.library.resolve()]
+ commands[host] = command
+ run(host+'-cold',command+['--fresh'])
+ status = json.loads((output/'status.json').read_text())
+ assert status['complete'] and status['execution_host']==host and status['fresh_store']
+ assert status['transport_host']=='haskell'
+ runtime = args.tricu if host == 'haskell' else args.library
+ assert status['runtime_sha256']==hashlib.sha256(runtime.read_bytes()).hexdigest()
+ invocation = json.loads((output/'invocation.json').read_text())
+ assert ('+RTS' in invocation) == (host == 'haskell')
+ cold = checkpoints(host)
+ run(host+'-warm',command)
+ assert cold == checkpoints(host)
+ pin = cold['app'].decode().split('\t')[2]
+ answer = run(host+'-result',[args.tricu,'arboricx','run','--store',output/'cas',
+ '--module',pin,'--name','main','--format','number',
+ '+RTS','-N1','-M1024m','-RTS'])
+ assert answer.strip()=='41', answer
+ assert checkpoints('haskell') == checkpoints('haskell-ffi'), 'complete module receipts differ'
+ # Exact independently audited object sets, not just root result equality.
+ objects = lambda host: published_objects(directory/host/'cas')
+ assert objects('haskell') == objects('haskell-ffi'), 'published object sets differ'
+ if args.compatibility:
+ def same_objects():
+ assert objects('haskell') == objects('haskell-ffi'), 'published object sets differ'
+ def stage(label, success=True, diagnostic=None):
+ for host, command in commands.items():
+ reply = run(host+'-'+label, command, success=success)
+ status = json.loads((directory/host/'status.json').read_text())
+ assert status['complete'] == success
+ if diagnostic:
+ assert diagnostic in reply, (host,label,reply[-3000:])
+ same_objects()
+ def result(host, name, expected):
+ pin = (directory/host/'cas/cache/source-build/app').read_text().split('\t')[2]
+ reply = run(host+'-result-'+name,[args.tricu,'arboricx','run','--store',directory/host/'cas',
+ '--module',pin,'--name',name,'--format','number',
+ '+RTS','-N1','-M1024m','-RTS'])
+ assert reply.strip()==str(expected), reply
+ dep = '!export answer\n!export extra\nanswer = 42\nextra = 99\n-- preserved UTF-8: λ 😀\n'
+ (source/'dep.tri').write_text(dep)
+ (source/'bridge.tri').write_text('!export * from dep\n')
+ source_map.write_text('arboricx.source-map.v1\nnative\tdep\tdep\tdep.tri\t-\n'
+ 'native\tbridge\tbridge\tbridge.tri\tdep\n'
+ 'native\tapp\tapp\tapp.tri\tbridge\n')
+ app = '!import bridge as D\n!export main\n!export local\n!export pinned\n!export sequenced\n!export lazy\n!export partial\nwalk t = D.answer\nwalk (t x) = walk x\nwalk _ = 0\nmain = walk (t (t t))\nlocal = (let rec\n copy [] = []\n copy [x | xs] = t x (copy xs)\n copy _ = []\n in (x : case x of\n [head | _] = head\n _ = 0) (copy [42 8]))\nchoose ^D.extra = 42\nchoose _ = 0\npinned = choose 99\npass value next = next value\nsequenced = (do pass\n value <- 42\n (x : x) value)\nlazy = (x : 42) ((x : x x) (x : x x))\nfirst x y = x\npartial = first 42 0\n'
+ (source/'app.tri').write_text(app)
+ stage('features-and-change')
+ for host in commands:
+ for name in ('main','local','pinned','sequenced','lazy','partial'):
+ result(host,name,42)
+ before = {host:checkpoints(host) for host in commands}
+ stage('features-warm')
+ assert before == {host:checkpoints(host) for host in commands}
+ # Invalid source, not merely a supervisor preflight error.
+ (source/'app.tri').write_text('!import bridge as D\n!export main\nmain = missingName\n')
+ stage('undefined-name',success=False)
+ assert json.loads((directory/'haskell-ffi/execution.json').read_text())['diagnostic']=={'first_number':20}
+ assert 'error: [20, "missingName"' in (directory/'haskell-undefined-name.log').read_text()
+ (source/'app.tri').write_text(app)
+ stage('restore-source')
+ assert before == {host:checkpoints(host) for host in commands}
+ # A matching corrupt local checkpoint must fail closed, not recompile.
+ saved = {}
+ for host in commands:
+ path = directory/host/'cas/cache/source-build/app'
+ saved[host] = path.read_bytes()
+ path.write_text('corrupt')
+ stage('corrupt-checkpoint',success=False)
+ for host in commands:
+ (directory/host/'cas/cache/source-build/app').write_bytes(saved[host])
+ # Corrupt a referenced executable while keeping its digest/path.
+ for host in commands:
+ pin = saved[host].decode().split('\t')[2]
+ manifest = (directory/host/'cas/objects'/pin[:3]/pin).read_text()
+ digest = next(row.split('\t')[3] for row in manifest.splitlines()[1:] if row.split('\t')[1]=='main')
+ path = directory/host/'cas/objects'/digest[:3]/digest
+ saved[host] = (path,path.read_bytes())
+ path.write_bytes(b'corrupt')
+ stage('corrupt-executable',success=False)
+ for path,payload in saved.values(): path.write_bytes(payload)
+ stage('restore-object')
+ # An interrupted preparation must invalidate success; completed dependency
+ # publications survive and the next invocation reconstructs the root.
+ for host,command in commands.items():
+ (directory/host/'cas/cache/source-build/app').unlink()
+ run(host+'-resource-stop',command+['--memory-mib','1'],success=False)
+ assert not json.loads((directory/host/'status.json').read_text())['complete']
+ # Also stop a running shared-library invocation, not only Haskell transport setup.
+ run('ffi-execution-deadline',commands['haskell-ffi']+['--wall-seconds','0.5'],success=False)
+ stopped = json.loads((directory/'haskell-ffi/status.json').read_text())
+ assert not stopped['complete'] and stopped['reason']=='whole-job wall-time guard', stopped
+ assert 'code' in stopped, 'guard must reach the execution subprocess'
+ report['interrupted_private_stages'] = {
+ host: [dict(path=str(path.relative_to(directory/host)), bytes=path.stat().st_size)
+ for path in (directory/host/'cas/objects').glob('*/*')
+ if '.tricu-stage-' in path.name]
+ for host in commands}
+ stage('restart')
+ assert before == {host:checkpoints(host) for host in commands}
+ # Header preflight rejects initial BOM before an invocation starts;
+ # existing status remains historical, not evidence of this failed attempt.
+ (source/'app.tri').write_bytes(b'\xef\xbb\xbf'+app.encode())
+ for host, command in commands.items():
+ reply = run(host+'-source-bom',command,success=False)
+ assert 'header mismatch' in reply
+ (source/'app.tri').write_text(app)
+ stage('final-restored')
+ for host,command in commands.items():
+ reply = run(host+'-missing-dependency',command+['--root','absent'],success=False)
+ assert 'undeclared map dependency' in reply
+ reply = run('ffi-missing-library',commands['haskell-ffi']+['--haskell-library',str(directory/'missing.so')],success=False)
+ assert 'no fallback' in reply
+ report['compatibility'] = True
+ report['complete'] = True
+ save()
+ print('Shared-runner cold/warm parity and exact receipts/objects passed; compatibility='
+ +str(args.compatibility)+': '+str(directory))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/shared_library_smoke.py b/test/shared_library_smoke.py
@@ -0,0 +1,81 @@
+#!/usr/bin/env python3
+"""Compile and audit one application, then compare CLI/C-ABI complete results.
+
+The independent imports/recursive-list/invoice oracles also serve compiler
+post-acceptance smokes. This uses the packaged compiler and creates no compiler
+generation or acceptance checkpoint.
+"""
+import argparse
+import json
+import os
+from pathlib import Path
+import re
+import sys
+from bootstrap_smoke import LIBRARY, PROGRAM, smoke_cases, sha
+from collections_batch import ProbeRunner, audit_workspace
+from native_library_migration import digest
+from process_rss import run_guarded
+
+ROOT = Path(__file__).resolve().parent.parent
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--tricu', type=Path, required=True)
+ parser.add_argument('--library', type=Path, required=True)
+ parser.add_argument('--directory', type=Path, required=True)
+ args = parser.parse_args()
+ root = args.directory.resolve()
+ root.mkdir(parents=True, exist_ok=False)
+ workspace = root / 'workspace'
+ workspace.mkdir()
+ (workspace / 'tricu.workspace').write_text('tricu.workspace.v1\n')
+ for alias, relative in {'base':'lib/base.tri', 'bytes':'lib/bytes.tri',
+ 'conversions':'lib/conversions.tri', 'prelude':'lib/prelude.tri',
+ 'native_math':'lib/math/native.tri',
+ 'shapes':'demos/source-patterns/patterns.tri'}.items():
+ (workspace / (alias + '.tri')).write_bytes((ROOT / relative).read_bytes())
+ (workspace / 'recursive.tri').write_text(LIBRARY)
+ (workspace / 'program.tri').write_text(PROGRAM)
+ # Per-process test allocator policy; no library initialization changes.
+ os.environ['MALLOC_ARENA_MAX'] = '2'
+ runner = ProbeRunner(args.tricu, root, 300)
+ report = dict(complete=False, compiler_sha256=sha(ROOT / 'bootstrap/build-stable.arboricx'),
+ runtime_sha256=sha(args.library), observer_sha256=sha(ROOT / 'test/haskell_observe.py'),
+ binding_sha256=sha(ROOT / 'ext/haskell/python/tricu.py'), cases=[],
+ scope='packaged compiler application; no compiler generation')
+ try:
+ tool = runner.imported(ROOT / 'bootstrap/build-stable.arboricx', 'compiler')
+ bundle = root / 'program.arboricx'
+ runner.run('compile', ['compile', '--file', workspace/'program.tri',
+ '--toolchain', tool, '--toolchain-io-version', '2', '--tool-store', runner.store,
+ '--store', runner.store, '--output', bundle, '--compile-timeout', '180'], 210)
+ manifest_bytes = (runner.store / 'objects' / tool[:3] / tool).read_bytes()
+ assert digest('arboricx.module-manifest.v1', manifest_bytes) == tool
+ compiler = next(row.split('\t')[3] for row in manifest_bytes.decode().splitlines()[1:]
+ if row.split('\t')[1] == 'sourceBuildGraph')
+ report['modules'] = audit_workspace(workspace, runner.store, compiler)
+ imported = runner.run('import-program', ['import', '--file', bundle, '--store', runner.store])
+ manifest = re.search(r'Created module alias .* -> ([0-9a-f]{64})', imported)[1]
+ assert manifest == report['modules']['program']['manifest']
+ for i, (export, argument, expected) in enumerate(smoke_cases()):
+ path, output = root / f'{i}.input.tree', root / f'{i}.output.tree'
+ path.write_bytes(argument)
+ actual = runner.invoke(str(i), manifest, export, ['--arg-tree-file', path])
+ assert actual == expected, (export, i, 'CLI')
+ reply, peak = run_guarded([sys.executable, str(ROOT / 'test/haskell_observe.py'),
+ '--library', str(args.library), '--bundle', str(bundle), '--export', export,
+ '--argument', str(path), '--output', str(output)], memory_mib=512, seconds=65)
+ (root / f'{i}.log').write_text(reply)
+ assert output.read_bytes() == expected, (export, i, 'C ABI')
+ report['cases'].append(dict(export=export, input_sha256=sha(path),
+ output_sha256=sha(output), peak_kib=peak))
+ assert output.read_bytes() != b'\0', 'wrong-output control'
+ report.update(complete=True, bundle_sha256=sha(bundle), wrong_output_rejected=True)
+ print('PASS: 12 independent complete results through CLI and C ABI')
+ finally:
+ (root / 'report.json').write_text(json.dumps(report, indent=2) + '\n')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/sourceContractMini.tri b/test/sourceContractMini.tri
@@ -1,5 +1,5 @@
-- An independent tiny Result protocol: checks use only withContract and
--- matchResult, rather than importing the huge base closure into the Zig gate.
+-- matchResult with a small local result protocol.
err msg rest = (onFail onOk : onFail msg rest)
ok value rest = (onFail onOk : onOk value rest)
matchResult onFail onOk result = result onFail onOk
diff --git a/test/sourceGateCompiler.tri b/test/sourceGateCompiler.tri
@@ -8,7 +8,7 @@ main = (source : compileGateSource [] source)
-- Expected is an independently constructed Result Tree Diagnostic, supplied
-- as a separate Arboricx artifact by each host. The comparison forces all
--- returned fields despite Zig's weak-head-normal-form public reducer.
+-- returned fields through complete observations.
matchesExpected = (source expected :
equal? (compileGateSource [] source) expected)
diff --git a/test/source_map_end_to_end.sh b/test/source_map_end_to_end.sh
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
-# Focused Haskell-only nested native-map gate. The old cold-source/Zig matrix
+# Focused Haskell-only nested native-map gate. The historical cold-source matrix
# is not a current validation prerequisite; compiler closure has its own audits.
set -euo pipefail
if (( $# < 2 || $# > 3 )); then
diff --git a/test/unit.py b/test/unit.py
@@ -1613,6 +1613,43 @@ class BackendDefaultTests(unittest.TestCase):
self.assertEqual(captured[0].backend, override or 'stable')
+class PublishedInventoryTests(unittest.TestCase):
+ def test_only_reserved_unpublished_stage_names_are_excluded(self):
+ from shared_library_generation import published_objects
+ with tempfile.TemporaryDirectory() as temporary:
+ root = Path(temporary)
+ shard = root / 'objects' / 'aaa'
+ shard.mkdir(parents=True)
+ (shard / ('a' * 64)).write_bytes(b'committed')
+ (shard / '.tricu-stage-interrupted').write_bytes(b'partial')
+ (shard / '473-17.tricu-stage-').write_bytes(b'GHC temporary naming')
+ self.assertEqual(published_objects(root), {'a' * 64: b'committed'})
+ (shard / ('a' * 63 + 'b')).write_bytes(b'extra committed object')
+ self.assertEqual(len(published_objects(root)), 2)
+ (shard / 'unexpected').write_bytes(b'not a reserved stage')
+ with self.assertRaises(AssertionError):
+ published_objects(root)
+
+
+class RuntimeRetirementTests(unittest.TestCase):
+ def test_generation_parser_rejects_retired_runtime_controls_before_output(self):
+ import runpy
+ import sys
+ from contextlib import redirect_stderr
+ from io import StringIO
+ from unittest.mock import patch
+ with tempfile.TemporaryDirectory() as temporary:
+ output = Path(temporary) / 'not-created'
+ for options in [('--execution-host', 'zig'), ('--zig-library', '/missing'),
+ ('--zig-gc-policy', '1', '1')]:
+ with self.subTest(options=options), patch.object(sys, 'argv',
+ ['bootstrap_generations.py', 'previous', str(output), *options]):
+ with redirect_stderr(StringIO()), self.assertRaises(SystemExit) as error:
+ runpy.run_path(str(Path(__file__).parent / 'bootstrap_generations.py'), run_name='__main__')
+ self.assertEqual(error.exception.code, 2)
+ self.assertFalse(output.exists())
+
+
class BootstrapKitTests(unittest.TestCase):
def setUp(self):
from bootstrap_inputs import SCHEMA, ROLES
@@ -1696,48 +1733,6 @@ class BootstrapKitTests(unittest.TestCase):
with self.assertRaises(FileNotFoundError): self.load()
-class CrossHostIOSelectionTests(unittest.TestCase):
- def test_maps_and_sandbox_selection(self):
- from bootstrap_cross_host import map_name, sandbox_command
- for io,expected in [(1,'bootstrap/compiler-source-map.v1'),(2,'bootstrap/compiler-source-map.io-v2.v1')]:
- self.assertEqual(map_name(io),expected)
- command=sandbox_command(Path('/source'),Path('/kit'),Path('/stage'),Path('/tricu'),Path('/library'),'zig','source_map',32768,3600,io)
- self.assertEqual(command[command.index('--map')+1],'/source/'+expected)
- self.assertNotIn('--unsafe-io',command)
- for bad in [True,'2',3]:
- with self.assertRaises(ValueError): map_name(bad)
-
- def test_output_adapter_and_seed_are_bound(self):
- from bootstrap_cross_host import kit_from_generation, freeze_kit
- from bootstrap_inputs import load_kit, kit_io_version
- with tempfile.TemporaryDirectory() as tmp:
- root=Path(tmp); run=root/'generation'; run.mkdir()
- (run/'rebuilt-compiler.arboricx').write_bytes(b'compiler')
- (run/'io-adapter.arboricx').write_bytes(b'old adapter')
- (run/'produced-io-adapter.arboricx').write_bytes(b'new adapter')
- record=dict(execution_io_version=1,produced_io_version=2,produced_adapter_sha256=hashlib.sha256(b'new adapter').hexdigest())
- (run/'inputs.json').write_text(json.dumps(record))
- kit=kit_from_generation(run,root/'kit')
- raw,payloads,_=load_kit(kit,'stable')
- self.assertEqual(kit_io_version(json.loads(raw)),2)
- self.assertEqual(payloads['adapter'],b'new adapter')
- self.assertEqual(kit_from_generation(run,kit),kit)
- with self.assertRaises(ValueError): freeze_kit(kit,root/'wrong',1)
- self.assertFalse((root/'wrong').exists())
- freeze_kit(kit,root/'seed',2)
- freeze_kit(kit,root/'seed',2)
- (run/'produced-io-adapter.arboricx').write_bytes(b'corrupt')
- with self.assertRaises(ValueError): kit_from_generation(run,root/'corrupt')
- self.assertFalse((root/'corrupt').exists())
- (root/'seed/io-adapter.arboricx').write_bytes(b'corrupt')
- with self.assertRaises(ValueError): freeze_kit(kit,root/'seed',2)
- # Historical absent generation framing still converts to v1 only.
- (run/'inputs.json').write_text(json.dumps(dict(adapter_sha256=hashlib.sha256(b'old adapter').hexdigest())))
- raw,payloads,_=load_kit(kit_from_generation(run,root/'legacy'),'stable')
- self.assertEqual(kit_io_version(json.loads(raw)),1)
- self.assertEqual(payloads['adapter'],b'old adapter')
-
-
class GenerationIOTests(unittest.TestCase):
def test_strict_versions_and_historical_default(self):
from generation_io import versions, version
@@ -1754,10 +1749,10 @@ class GenerationIOTests(unittest.TestCase):
self.assertEqual(compiler_flags(1), [])
self.assertEqual(compiler_flags(2), ['--toolchain-io-version', '2'])
self.assertEqual(execution_flags(1), ['--unsafe-io'])
- self.assertEqual(execution_flags(1, 'zig'), ['--unsafe-io'])
+ with self.assertRaises(ValueError): execution_flags(1, 'haskell-ffi')
self.assertEqual(execution_flags(2), [])
- self.assertEqual(execution_flags(2, 'zig'), [])
- for host in ['browser', 'unknown']:
+ self.assertEqual(execution_flags(2, 'haskell-ffi'), [])
+ for host in ['browser', 'zig', 'unknown']:
with self.assertRaises(ValueError): execution_flags(2, host)
def test_invocation_grants_and_canonical_cli_selection(self):
diff --git a/test/zig_generation_executor.py b/test/zig_generation_executor.py
@@ -1,176 +0,0 @@
-#!/usr/bin/env python3
-"""Zig execution child for bootstrap_generations.
-
-The supervisor authenticates/imports captured bundles, supplies six arguments
-and independently audits all publications. This child rechecks captured bundle
-digests and runs the selected entrypoint through the owned ABI. V1 grants broad file IO to trusted compiler jobs. Explicit v2 requests use the
-same native v2 interpreter as the CLI, with OS process authority and no grants.
-"""
-import argparse
-import ctypes as C
-import hashlib
-import json
-from pathlib import Path
-import resource
-import time
-from generation_io import version, versions
-
-
-def save(path, value):
- path.write_text(json.dumps(value, indent=2) + '\n')
-
-
-def validate_library(path, io_version):
- """Trusted selected runtime preflight, no IO execution or fallback."""
- selected = version(io_version)
- library = C.CDLL(str(path))
- abi = library.arb_abi_version
- abi.argtypes, abi.restype = [], C.c_uint32
- if abi() not in (2, 3):
- raise ValueError('owned ABI version 2 or 3 required')
- if abi() == 3 and selected != 2:
- raise ValueError('IO v1 requires a retained ABI 2 runtime')
- for name in ('arboricx_init','arboricx_free','arb_load_bundle','arb_app',
- 'arb_of_number','arb_of_list','arb_of_string','arb_release',
- 'arb_reduce','arb_unwrap_result','arb_run_io' if selected == 1 else 'arb_run_io_v2'):
- getattr(library,name)
-
-
-def child(args):
- # No asynchronous subprocesses in the Zig IO driver; the parent additionally
- # guards this whole process group with a wall deadline and RSS sampling.
- resource.setrlimit(resource.RLIMIT_AS, (args.memory_mib * 1024**2,) * 2)
- resource.setrlimit(resource.RLIMIT_STACK, (8 * 1024**2,) * 2)
- resource.setrlimit(resource.RLIMIT_CPU, (args.seconds, args.seconds))
- resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
- lib = C.CDLL(str(args.library))
- handle, ctx_t = C.c_uint64, C.c_void_p
- def bind(name, types, result=C.c_int):
- f = getattr(lib, name)
- f.argtypes, f.restype = types, result
- return f
- init = bind('arboricx_init', [], ctx_t)
- free = bind('arboricx_free', [ctx_t], None)
- abi_version = bind('arb_abi_version', [], C.c_uint32)
- assert abi_version() in (2, 3)
- load = bind('arb_load_bundle', [ctx_t, C.c_void_p, C.c_size_t, C.c_char_p, C.POINTER(handle)])
- string = bind('arb_of_string', [ctx_t, C.c_char_p, C.POINTER(handle)])
- app = bind('arb_app', [ctx_t, handle, handle, C.POINTER(handle)])
- release = bind('arb_release', [ctx_t, handle])
- reduce = bind('arb_reduce', [ctx_t, handle, C.c_uint64, C.POINTER(handle)])
- unwrap = bind('arb_unwrap_result', [ctx_t, handle, C.POINTER(C.c_int), C.POINTER(handle), C.POINTER(handle)])
- to_string = bind('arb_to_string', [ctx_t, handle, C.POINTER(C.c_void_p), C.POINTER(C.c_size_t)])
- number = bind('arb_to_number', [ctx_t, handle, C.POINTER(C.c_uint64)])
- pair = bind('arb_get_fork_children', [ctx_t, handle, C.POINTER(handle), C.POINTER(handle)])
- free_buf = bind('arboricx_free_buf', [ctx_t, C.c_void_p, C.c_size_t], None)
- class Perms(C.Structure):
- _fields_ = [('read', C.c_int), ('write', C.c_int)]
- of_number = bind('arb_of_number', [ctx_t, C.c_uint64, C.POINTER(handle)])
- of_list = bind('arb_of_list', [ctx_t, C.POINTER(handle), C.c_size_t, C.POINTER(handle)])
- def check(status):
- if status != 0:
- raise RuntimeError(f'ABI status {status}')
- ctx = init()
- if not ctx:
- raise MemoryError('context initialization')
- def owned(f, *arguments):
- out = handle()
- check(f(ctx, *arguments, C.byref(out)))
- return out.value
- def apply(f, x):
- result = owned(app, f, x)
- check(release(ctx, f)); check(release(ctx, x))
- return result
- run = args.output
- request = json.loads((run / 'execution-request.json').read_text())
- started = time.monotonic()
- try:
- execution_io = version(request.get('execution_io_version', 1))
- recorded = json.loads((run / 'inputs.json').read_text())
- if execution_io != versions(recorded)[0]:
- raise ValueError('execution request IO version mismatch')
- for key in ('worker_export','compiler_bundle_sha256','adapter_sha256'):
- if request[key] != recorded[key]:
- raise ValueError('execution request identity mismatch: '+key)
- # Symbol selection is declared metadata, never a trial execution. Old
- # libraries remain usable for v1; missing v2 symbols are terminal.
- selected_io = bind('arb_run_io_v2', [ctx_t, handle, C.POINTER(handle)]) if execution_io == 2 else bind('arb_run_io', [ctx_t, handle, C.POINTER(Perms), C.POINTER(handle)])
- if args.gc_policy:
- configure = bind('arb_set_gc_policy', [ctx_t, C.c_uint32, C.c_uint32])
- check(configure(ctx, *args.gc_policy))
- compiler_bytes = (run / 'compiler.arboricx').read_bytes()
- adapter_bytes = (run / 'io-adapter.arboricx').read_bytes()
- if hashlib.sha256(compiler_bytes).hexdigest() != request['compiler_bundle_sha256'] or hashlib.sha256(adapter_bytes).hexdigest() != request['adapter_sha256']:
- raise ValueError('captured executable identity mismatch')
- compiler = owned(load, compiler_bytes, len(compiler_bytes), request['worker_export'].encode())
- adapter = owned(load, adapter_bytes, len(adapter_bytes), b'main')
- current = apply(adapter, compiler)
- for argument in request['arguments']:
- if '\0' in argument:
- raise ValueError('NUL in compiler argument')
- if execution_io == 1:
- value = owned(string, argument.encode())
- else:
- # ABI byte strings and language scalar text are different.
- scalars = [owned(of_number, ord(ch)) for ch in argument]
- items = (handle * max(1,len(scalars)))(*scalars)
- value = owned(of_list, items, len(scalars))
- for scalar in scalars: check(release(ctx,scalar))
- current = apply(current,value)
- prepared = time.monotonic()
- program = owned(reduce, current, 1_000_000_000)
- check(release(ctx, current))
- forced = time.monotonic()
- result = owned(selected_io, program) if execution_io == 2 else owned(selected_io, program, C.byref(Perms(1, 1)))
- check(release(ctx, program))
- ran = time.monotonic()
- ok, value, rest = C.c_int(), handle(), handle()
- check(unwrap(ctx, result, C.byref(ok), C.byref(value), C.byref(rest)))
- diagnostic = None
- if not ok.value:
- ptr, length = C.c_void_p(), C.c_size_t()
- status = to_string(ctx, value, C.byref(ptr), C.byref(length))
- if status == 0:
- diagnostic = C.string_at(ptr, length.value).decode('utf-8', 'replace')
- free_buf(ctx, ptr, length.value)
- else:
- # Preserve a useful bounded prefix of structured diagnostics.
- # This recognizes only a generic list prefix, not compiler tags.
- prefix, tail = handle(), handle()
- tag = C.c_uint64()
- normalized = owned(reduce, value.value, 1_000_000)
- if pair(ctx, normalized, C.byref(prefix), C.byref(tail)) == 0:
- if number(ctx, prefix, C.byref(tag)) == 0:
- diagnostic = dict(first_number=tag.value)
- check(release(ctx, prefix))
- check(release(ctx, tail))
- check(release(ctx, normalized))
- if diagnostic is None:
- diagnostic = f'non-string diagnostic (decode status {status})'
- record = dict(ok=bool(ok.value), diagnostic=diagnostic, execution_io_version=execution_io,
- prepare_seconds=prepared-started, initial_reduce_seconds=forced-prepared,
- io_seconds=ran-forced, total_seconds=time.monotonic()-started)
- if args.gc_policy:
- record['gc_policy'] = args.gc_policy
- save(run / 'execution.json', record)
- if not ok.value:
- raise RuntimeError(diagnostic)
- print('ok: Zig map-builder completed', flush=True)
- print(json.dumps(record), flush=True)
- finally:
- free(ctx)
-
-
-if __name__ == '__main__':
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('--library', type=Path, required=True)
- parser.add_argument('--output', type=Path, required=True)
- parser.add_argument('--memory-mib', type=int, required=True)
- parser.add_argument('--gc-policy', type=int, nargs=2, metavar=('MIN_ALLOCATIONS', 'LIVE_MULTIPLIER'))
- parser.add_argument('--seconds', type=int, required=True)
- args = parser.parse_args()
- if not 0 < args.memory_mib <= 65536 or not 0 < args.seconds <= 7200:
- parser.error('invalid resource boundaries')
- if args.gc_policy and any(not 0 < n <= 0xffffffff for n in args.gc_policy):
- parser.error('GC policy requires positive u32 values')
- child(args)
diff --git a/test/zig_observe.py b/test/zig_observe.py
@@ -1,75 +0,0 @@
-#!/usr/bin/env python3
-"""Bounded test observer: invoke a named bundle export and emit its entire tree.
-Reads a flat argument tree and writes the complete result in the same format.
-"""
-import argparse
-import ctypes as C
-from pathlib import Path
-import resource
-from executable_object import FLAT, verify_payload
-
-
-def main():
- p=argparse.ArgumentParser(description=__doc__)
- for key in ('library','bundle','argument','output'): p.add_argument('--'+key,type=Path,required=True)
- p.add_argument('--export',required=True)
- a=p.parse_args()
- for kind,limit in [(resource.RLIMIT_AS,512*1024**2),(resource.RLIMIT_STACK,8*1024**2),
- (resource.RLIMIT_CPU,60),(resource.RLIMIT_CORE,0)]:
- resource.setrlimit(kind,(limit,limit))
- lib=C.CDLL(str(a.library)); H=C.c_uint64; CTX=C.c_void_p
- def bind(name,types,result=C.c_int):
- f=getattr(lib,name); f.argtypes=types; f.restype=result; return f
- init=bind('arboricx_init',[],CTX); free=bind('arboricx_free',[CTX],None)
- load=bind('arb_load_bundle',[CTX,C.c_void_p,C.c_size_t,C.c_char_p,C.POINTER(H)])
- leaf=bind('arb_leaf',[CTX,C.POINTER(H)])
- stem=bind('arb_stem',[CTX,H,C.POINTER(H)])
- fork=bind('arb_fork',[CTX,H,H,C.POINTER(H)])
- app=bind('arb_app',[CTX,H,H,C.POINTER(H)])
- reduce=bind('arb_reduce',[CTX,H,C.c_uint64,C.POINTER(H)])
- release=bind('arb_release',[CTX,H])
- is_leaf=bind('arb_is_leaf',[CTX,H,C.POINTER(C.c_int)])
- is_stem=bind('arb_is_stem',[CTX,H,C.POINTER(C.c_int)])
- child=bind('arb_get_stem_child',[CTX,H,C.POINTER(H)])
- children=bind('arb_get_fork_children',[CTX,H,C.POINTER(H),C.POINTER(H)])
- ctx=init()
- if not ctx: raise MemoryError('context')
- def check(status):
- if status: raise RuntimeError('ABI status '+str(status))
- def owned(f,*args):
- out=H(); check(f(ctx,*args,C.byref(out))); return out.value
- try:
- data=a.argument.read_bytes(); verify_payload(FLAT,data)
- cursor=0
- def construct():
- nonlocal cursor
- tag=data[cursor]; cursor+=1
- args=[construct() for _ in range(tag)]
- value=owned((leaf,stem,fork)[tag],*args)
- for h in args: check(release(ctx,h))
- return value
- argument=construct(); bundle=a.bundle.read_bytes()
- worker=owned(load,bundle,len(bundle),a.export.encode())
- term=owned(app,worker,argument)
- check(release(ctx,worker)); check(release(ctx,argument))
- todo=[term]; output=bytearray()
- while todo:
- h=todo.pop(); normalized=owned(reduce,h,10_000_000); check(release(ctx,h)); h=normalized
- flag=C.c_int(); check(is_leaf(ctx,h,C.byref(flag)))
- if flag.value: output.append(0)
- else:
- check(is_stem(ctx,h,C.byref(flag)))
- if flag.value:
- output.append(1); todo.append(owned(child,h))
- else:
- output.append(2); left,right=H(),H()
- check(children(ctx,h,C.byref(left),C.byref(right)))
- todo.extend((right.value,left.value))
- check(release(ctx,h))
- if len(output)>1024*1024: raise ValueError('test output boundary')
- verify_payload(FLAT,output)
- a.output.write_bytes(output)
- finally: free(ctx)
-
-
-if __name__=='__main__': main()