tricu

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

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:
MAGENTS.md | 2+-
Mdocs/build-and-run.md | 6++----
Mdocs/compiler-rebuilding.md | 154++++++++++++++++++++++++-------------------------------------------------------
Mdocs/host-abi.md | 3+--
Mext/demo-site/README.md | 184++++++++++++++++++++++++++++++-------------------------------------------------
Mext/demo-site/bundle-assets.py | 1-
Mext/demo-site/dist/runtime-backends.js | 1-
Mext/demo-site/dist/runtime-session.js | 4----
Dext/demo-site/dist/zig-helpers.js | 15---------------
Mext/demo-site/index.html | 2+-
Mext/demo-site/reference-input/getting-started.md | 2+-
Mext/demo-site/release.nix | 6+-----
Mext/demo-site/src/runtime-backends.ts | 3+--
Mext/demo-site/src/runtime-session.ts | 4----
Dext/demo-site/src/zig-helpers.ts | 16----------------
Mext/demo-site/test/asset-release.py | 3+--
Mext/demo-site/test/backend-benchmark.mjs | 2+-
Mext/demo-site/test/backend-probe.mjs | 7+++----
Mext/demo-site/test/backend-server.mjs | 10++++------
Mext/demo-site/test/benchmark-backends.mjs | 4++--
Mext/demo-site/test/cold-load-probe.mjs | 4++--
Mext/demo-site/test/ide-probe.mjs | 2+-
Mext/demo-site/test/io-v2-core-probe.mjs | 29++---------------------------
Mext/demo-site/test/io-v2-lifecycle-probe.mjs | 2+-
Mext/demo-site/test/io-v2-protocol-probe.mjs | 10+---------
Mext/demo-site/test/io-v2-session-probe.mjs | 12+-----------
Mext/demo-site/test/io-v2-source-probe.mjs | 12------------
Mext/demo-site/test/io-version-probe.mjs | 2+-
Mext/demo-site/test/live-ux-probe.mjs | 4++--
Mext/demo-site/test/local-module-probe.mjs | 6+++---
Mext/demo-site/test/module-probe.mjs | 2+-
Mext/demo-site/test/release.py | 10+++++-----
Mext/demo-site/test/runtime-backends.test.mjs | 13++++++-------
Rext/demo-site/test/zig-negative-fixtures.mjs -> ext/demo-site/test/runtime-fixtures.mjs | 0
Mext/demo-site/test/summarize-backends.mjs | 2+-
Dext/demo-site/test/zig-session-probe.mjs | 112-------------------------------------------------------------------------------
Mext/haskell/README.md | 27+++++++++++++++++++++++++--
Mext/js/README.md | 2+-
Mext/php/README.md | 2+-
Dext/wasm/README.md | 279-------------------------------------------------------------------------------
Dext/wasm/backend-check.mjs | 37-------------------------------------
Dext/wasm/backends.mjs | 29-----------------------------
Dext/wasm/backends/js.mjs | 14--------------
Dext/wasm/backends/zig-wasm.mjs | 15---------------
Dext/wasm/browser-page.mjs | 129-------------------------------------------------------------------------------
Dext/wasm/browser.mjs | 82-------------------------------------------------------------------------------
Dext/wasm/check.mjs | 173-------------------------------------------------------------------------------
Dext/wasm/compare.mjs | 70----------------------------------------------------------------------
Dext/wasm/preview-compare.mjs | 48------------------------------------------------
Dext/wasm/runtime.mjs | 223-------------------------------------------------------------------------------
Dext/wasm/site-session.mjs | 191-------------------------------------------------------------------------------
Dext/wasm/tree-session.mjs | 34----------------------------------
Dext/wasm/worker.mjs | 38--------------------------------------
Dext/zig/.gitignore | 13-------------
Dext/zig/README.md | 583-------------------------------------------------------------------------------
Dext/zig/build.zig | 217-------------------------------------------------------------------------------
Dext/zig/build.zig.zon | 13-------------
Dext/zig/include/arboricx.h | 100-------------------------------------------------------------------------------
Dext/zig/kernel_run_arboricx_typed.dag | 2578-------------------------------------------------------------------------------
Dext/zig/result | 2--
Dext/zig/src/arena.zig | 151------------------------------------------------------------------------------
Dext/zig/src/bundle.zig | 12------------
Dext/zig/src/c_abi.zig | 334-------------------------------------------------------------------------------
Dext/zig/src/codecs.zig | 208-------------------------------------------------------------------------------
Dext/zig/src/handles.zig | 58----------------------------------------------------------
Dext/zig/src/indexed.zig | 666-------------------------------------------------------------------------------
Dext/zig/src/io_v2_core.zig | 445-------------------------------------------------------------------------------
Dext/zig/src/io_v2_core_probe.zig | 114-------------------------------------------------------------------------------
Dext/zig/src/io_v2_digest.zig | 41-----------------------------------------
Dext/zig/src/io_v2_file_host.zig | 864-------------------------------------------------------------------------------
Dext/zig/src/io_v2_file_probe.zig | 4----
Dext/zig/src/io_v2_files.zig | 397-------------------------------------------------------------------------------
Dext/zig/src/io_v2_native.zig | 81-------------------------------------------------------------------------------
Dext/zig/src/io_v2_paths.zig | 120-------------------------------------------------------------------------------
Dext/zig/src/io_v2_protocol.zig | 253-------------------------------------------------------------------------------
Dext/zig/src/io_v2_resources.zig | 375-------------------------------------------------------------------------------
Dext/zig/src/io_v2_snapshot.zig | 68--------------------------------------------------------------------
Dext/zig/src/io_v2_stage.zig | 208-------------------------------------------------------------------------------
Dext/zig/src/io_v2_stage_state.zig | 229-------------------------------------------------------------------------------
Dext/zig/src/io_v2_tasks.zig | 582------------------------------------------------------------------------------
Dext/zig/src/io_v2_utf8.zig | 63---------------------------------------------------------------
Dext/zig/src/kernel.zig | 22----------------------
Dext/zig/src/main.zig | 273-------------------------------------------------------------------------------
Dext/zig/src/preview.zig | 173-------------------------------------------------------------------------------
Dext/zig/src/reduce.zig | 235-------------------------------------------------------------------------------
Dext/zig/src/reduce_test.zig | 644-------------------------------------------------------------------------------
Dext/zig/src/source_text.zig | 176-------------------------------------------------------------------------------
Dext/zig/src/ternary.zig | 27---------------------------
Dext/zig/src/tree.zig | 181-------------------------------------------------------------------------------
Dext/zig/tests/c_abi_append_test.c | 86-------------------------------------------------------------------------------
Dext/zig/tests/c_abi_test.c | 120-------------------------------------------------------------------------------
Dext/zig/tests/handles_test.c | 84-------------------------------------------------------------------------------
Dext/zig/tests/helpers.h | 111-------------------------------------------------------------------------------
Dext/zig/tests/io_protocol_test.c | 223-------------------------------------------------------------------------------
Dext/zig/tests/io_run_test.c | 63---------------------------------------------------------------
Dext/zig/tests/native_bundle_append_test.c | 84-------------------------------------------------------------------------------
Dext/zig/tests/native_bundle_bools_test.c | 60------------------------------------------------------------
Dext/zig/tests/native_bundle_id_test.c | 60------------------------------------------------------------
Dext/zig/tests/performance_smoke.c | 155-------------------------------------------------------------------------------
Dext/zig/tests/performance_smoke.py | 63---------------------------------------------------------------
Dext/zig/tests/python_ffi_test.py | 383-------------------------------------------------------------------------------
Dext/zig/tools/gen_kernel.zig | 92-------------------------------------------------------------------------------
Mflake.nix | 247+++++++++++++------------------------------------------------------------------
Mtest/bootstrap_acceptance.py | 2+-
Dtest/bootstrap_cross_host.py | 281-------------------------------------------------------------------------------
Dtest/bootstrap_cross_host_controls.py | 80-------------------------------------------------------------------------------
Dtest/bootstrap_cross_host_smoke.py | 54------------------------------------------------------
Mtest/bootstrap_generations.py | 44+++++++++++++++++++-------------------------
Mtest/bootstrap_store_check.py | 3+++
Dtest/cross_host_generation.py | 194-------------------------------------------------------------------------------
Mtest/generation_io.py | 8+++++---
Atest/haskell_generation_executor.py | 146+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/haskell_observe.py | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/io-v2/README.md | 107++++++++++++++++++++++++++-----------------------------------------------------
Dtest/io-v2/native-cli.tri | 54------------------------------------------------------
Rtest/io-v2/zig-utf8.tri -> test/io-v2/utf8-boundaries.tri | 0
Dtest/io-v2/zig-digest.tri | 27---------------------------
Dtest/io-v2/zig-files.tri | 40----------------------------------------
Dtest/io-v2/zig-paths.tri | 28----------------------------
Dtest/io-v2/zig-stages.tri | 24------------------------
Mtest/io_v2_compiler_store.py | 70+++-------------------------------------------------------------------
Mtest/io_v2_generation.py | 64++++++++++++++++++++++++++++++++++++++++++++--------------------
Mtest/io_v2_indexed.py | 20++++----------------
Mtest/io_v2_indexed_write.py | 29+++++++----------------------
Dtest/io_v2_native_cli.py | 66------------------------------------------------------------------
Mtest/io_v2_text.py | 42+++++++++++++++---------------------------
Dtest/io_v2_zig_core.py | 101-------------------------------------------------------------------------------
Dtest/io_v2_zig_digest.py | 39---------------------------------------
Dtest/io_v2_zig_files.py | 129-------------------------------------------------------------------------------
Dtest/io_v2_zig_paths.py | 55-------------------------------------------------------
Dtest/io_v2_zig_publication.py | 70----------------------------------------------------------------------
Dtest/io_v2_zig_stages.py | 52----------------------------------------------------
Dtest/reducer_comparison.py | 72------------------------------------------------------------------------
Atest/shared_library_generation.py | 203+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/shared_library_smoke.py | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/sourceContractMini.tri | 2+-
Mtest/sourceGateCompiler.tri | 2+-
Mtest/source_map_end_to_end.sh | 2+-
Mtest/unit.py | 85+++++++++++++++++++++++++++++++++++++------------------------------------------
Dtest/zig_generation_executor.py | 176-------------------------------------------------------------------------------
Dtest/zig_observe.py | 75---------------------------------------------------------------------------
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, &registry, 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(&registry, 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(&registry, 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(&registry, 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(&registry, 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(&registry, 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, &registry, 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, &registry, 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, &registry, 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, &registry, 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, &registry, 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, &registry, 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, &registry, 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, &registry, 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, &registry, 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(), &registry, 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()