From 38b07481675953ac7566bc617d4b9881841a64e5 Mon Sep 17 00:00:00 2001 From: hyperpolymath Date: Tue, 19 May 2026 19:02:16 +0100 Subject: [PATCH] =?UTF-8?q?test(loader):=20INT-02=20=E2=80=94=20prove=20+?= =?UTF-8?q?=20regression-lock=20the=20host-agnostic=20loader=20bridge=20(R?= =?UTF-8?q?efs=20#179)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The INT-02 loader (`packages/affine-js/loader.js`) was already implemented + merged (host-agnostic readBytes, multi-namespace buildImportObject, parseOwnershipSection, Deno/Node/browser detectHost; mod.js consumes it — SAT-02 gone). What was missing was the *guarantee*, exactly the INT-01 #244 pattern: 1. loader_test.js (14 unit tests) had NO run task — green only with the right --allow-read/--allow-write flags; a fresh run looked "1 failed" (a permission error, not a loader bug). Pinned: deno.json `tasks.test` -> `deno task test` = 14/14, reproducible. 2. Nothing proved the loader on REAL compiler-emitted wasm (unit tests use synthetic bytes; the INT-01 xmod-link harness still hand-rolls Deno.readFile + a manual import object — the SAT-02 anti-pattern the loader replaces). Added tests/modules/loader-bridge/ (bridge.mjs + run.sh + README): compiles the INT-01 fixtures and drives them through the *actual* loader API — readBytes loads both; buildImportObject wires the genuine `CrossCallee` cross-module namespace; linked caller.main() === 42; parseOwnershipSection reads a real Linear-param ownership entry from compiler output. Closes INT-01 <-> INT-02. Finding (documented in the harness README): buildImportObject *spreads* module members (merge-not-clobber), so a catch-all Proxy wasi stub must be attached as a whole namespace, not via the spread — mirroring real usage (host owns wasi; loader owns the affine runtime + cross-module namespaces). Ledger truthed: ECOSYSTEM/TECH-DEBT INT-02 -> PROVEN+locked; SAT-02 -> FIXED. No compiler change (loader is JS); hermetic dune gate unaffected by construction (docs + deno.json + harness only; dune build clean). Refs #179 (not Closes — owner-gated; satellite shell downstream). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/ECOSYSTEM.adoc | 12 ++- docs/TECH-DEBT.adoc | 13 ++- packages/affine-js/deno.json | 3 + tests/modules/loader-bridge/README.adoc | 42 ++++++++++ tests/modules/loader-bridge/bridge.mjs | 100 ++++++++++++++++++++++++ tests/modules/loader-bridge/run.sh | 28 +++++++ 6 files changed, 192 insertions(+), 6 deletions(-) create mode 100644 tests/modules/loader-bridge/README.adoc create mode 100644 tests/modules/loader-bridge/bridge.mjs create mode 100755 tests/modules/loader-bridge/run.sh diff --git a/docs/ECOSYSTEM.adoc b/docs/ECOSYSTEM.adoc index 634fe631..567d7aa6 100644 --- a/docs/ECOSYSTEM.adoc +++ b/docs/ECOSYSTEM.adoc @@ -188,9 +188,15 @@ link:TECH-DEBT.adoc[TECH-DEBT.adoc]. + `Mod.fn(x)` qualified-value path WIRED+locked (parse-boundary lowering; 4 hermetic tests). Distinct parser follow-up: `Mod::fn(x)` in expr position |INT-02 |Host-agnostic loader bridge (`affinescript-dom-loader`) |#179 |loader -landed in `packages/affine-js` (SAT-02 fixed; Deno/Node/browser parity, -multi-namespace import object, ownership-section accessor). S1; unblocks -INT-05/08/11. The `affinescript-dom-loader` satellite shell is downstream. +in `packages/affine-js` (SAT-02 fixed; Deno/Node/browser parity, +multi-namespace import object, ownership-section accessor). *PROVEN + +regression-locked:* 14 unit tests via pinned `deno task test` (was +flag-fragile — no run task) + `tests/modules/loader-bridge/` drives the +*real* loader API over genuine compiler-emitted cross-module wasm +(`readBytes`+`buildImportObject` link `CrossCallee.consume(42)`=42; +`parseOwnershipSection` reads a real Linear-param entry) — closes +INT-01 ↔ INT-02. S1; unblocks INT-05/08/11. The `affinescript-dom-loader` +satellite shell is downstream. |INT-03 |WASI preview2 / host I/O beyond stdout |#180 |S1, ADR-015 ACCEPTED (owner-chosen full WASM Component-Model re-target). Staged S1..S6; legacy preview1 stdout path is the default until S6 diff --git a/docs/TECH-DEBT.adoc b/docs/TECH-DEBT.adoc index 408f0de0..2ae8f133 100644 --- a/docs/TECH-DEBT.adoc +++ b/docs/TECH-DEBT.adoc @@ -158,7 +158,11 @@ a parse error (`::`-in-value-expr unwired; `::` reserved for `Type::Variant`). |S1 |`use ::{}`/`::*` + `use Mod;`/`as`-qualified `Mod.fn(x)` DONE (PR Refs #178); `::`-in-expression a separate parser follow-up -|INT-02 |Host-agnostic loader bridge |S1 |open #179 (blocks INT-05/08/11) +|INT-02 |Host-agnostic loader bridge |S1 |*PROVEN + locked* (Refs #179): +`packages/affine-js` loader (SAT-02 fixed; Deno/Node/browser parity, +multi-ns import object, ownership accessor); 14 unit tests via pinned +`deno task test` + `tests/modules/loader-bridge/` e2e on real +compiler-emitted xmod wasm (closes INT-01↔INT-02). Unblocks INT-05/08/11 |INT-03 |WASI preview2 / host I/O |S1 |#180 ADR-015 accepted (full Component-Model re-target, staged S1..S6); S3+ hard-gated on S2 toolchain (`wasm-tools`/`wasm-component-ld`) @@ -177,8 +181,11 @@ fixed; runtime blocked by #255 (wasm loop-codegen defect) |=== |ID |Item |Sev |Status |SAT-01 |`affinescript-dom` reconciler (skeleton → real) |S2 |open (=INT-08) -|SAT-02 |`packages/affine-js` hardcoded path / env-only imports |S1 |open -(addressed by INT-02) +|SAT-02 |`packages/affine-js` hardcoded path / env-only imports |S1 +|*FIXED* by INT-02 (Refs #179): `Deno.readFile(url.pathname)` replaced by +host-agnostic `readBytes`; `env`-only import shape replaced by +multi-namespace `buildImportObject`; `mod.js` consumes the loader. +Proven + locked (see INT-02) |SAT-03 |`affinescript-tea` runtime build-out |S2 |open (=INT-07) |SAT-04 |`affinescript-cadre` router runtime |S2 |planned (=INT-09) |SAT-05 |`affinescript-pixijs` migration prerequisite |S3 |open #56 diff --git a/packages/affine-js/deno.json b/packages/affine-js/deno.json index 3cd2a5db..bf960629 100644 --- a/packages/affine-js/deno.json +++ b/packages/affine-js/deno.json @@ -8,6 +8,9 @@ "./runtime": "./runtime.js" }, "license": "PMPL-1.0-or-later", + "tasks": { + "test": "deno test --allow-read --allow-write loader_test.js" + }, "publish": { "exclude": ["loader_test.js"] } diff --git a/tests/modules/loader-bridge/README.adoc b/tests/modules/loader-bridge/README.adoc new file mode 100644 index 00000000..c4ddeabd --- /dev/null +++ b/tests/modules/loader-bridge/README.adoc @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: PMPL-1.0-or-later += INT-02 loader-bridge acceptance harness (#179) + +Proves the host-agnostic loader (`packages/affine-js/loader.js`) on +*real* compiler-emitted, cross-module wasm — the level above +`packages/affine-js/loader_test.js` (14 unit tests on synthetic bytes, +run via `deno task test` in that package). + +== What it asserts + +`bridge.mjs` uses the **actual loader API** (not a hand-rolled +`Deno.readFile` + manual import object — that is the SAT-02 anti-pattern +the loader replaces, still present by hand in the INT-01 +`tests/modules/xmod-link` harness; this closes INT-01 ↔ INT-02): + +* `readBytes` loads both compiled fixtures host-agnostically; +* `buildImportObject` wires the genuine `CrossCallee` cross-module + namespace INT-01/#178 emits (multi-namespace, not `env`-only); +* the linked `caller.main()` returns `42` across the wasm boundary; +* `parseOwnershipSection` reads a real ownership entry (a Linear param + for `consume(own x: Int)`) from compiler-emitted wasm — the typed-wasm + contract carrier, exercised on genuine output. + +== Run + +[source,sh] +---- +dune build bin/main.exe +./run.sh # deno on PATH, or $AFFINESCRIPT_DENO +---- + +Exit 0 + `PASS:` line = INT-02 substrate proven. Reproducible; kept out +of the hermetic OCaml gate by design (needs a wasm engine + deno), +exactly like `tests/modules/xmod-link`. + +== Note + +WASI is host-supplied as a whole catch-all namespace. `buildImportObject` +*spreads* module members (to merge, not clobber), so a catch-all `Proxy` +is attached directly as `wasi_snapshot_preview1` rather than via the +spread — mirroring real usage: the host owns wasi, the loader owns the +affine runtime + cross-module namespaces. diff --git a/tests/modules/loader-bridge/bridge.mjs b/tests/modules/loader-bridge/bridge.mjs new file mode 100644 index 00000000..ac0c1341 --- /dev/null +++ b/tests/modules/loader-bridge/bridge.mjs @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: PMPL-1.0-or-later +// INT-02 / #179 — host-agnostic loader bridge, end-to-end acceptance. +// +// Proves the *actual* INT-02 loader API (packages/affine-js/loader.js) +// drives genuine compiler-emitted, cross-module wasm — not synthetic +// bytes (loader_test.js does the unit level) and not a hand-rolled +// `Deno.readFile` + manual import object (that is the SAT-02 anti-pattern +// the loader exists to replace; the INT-01 xmod-link harness still does it +// by hand — this closes INT-01 ↔ INT-02). +// +// callee.wasm ← module CrossCallee; pub fn consume(own x: Int) -> Int { x } +// caller.wasm ← use CrossCallee::{consume}; pub fn main() -> Int { consume(42) } +// +// Usage: deno run --allow-read= bridge.mjs +// Exit 0 + PASS iff: readBytes loads both; buildImportObject wires the +// cross-module import; caller.main() === 42; parseOwnershipSection returns +// real entries from the compiler-emitted module. + +import { + buildImportObject, + parseOwnershipSection, + readBytes, +} from "../../../packages/affine-js/loader.js"; + +const [calleePath, callerPath] = Deno.args; +if (!calleePath || !callerPath) { + console.error("usage: bridge.mjs "); + Deno.exit(64); +} + +// WASI is host-supplied, a catch-all namespace (println-style codegen +// imports fd_write). `buildImportObject` *spreads* module members (to merge +// rather than clobber), so a catch-all Proxy must be attached as a whole +// namespace, not via the spread — mirroring real usage where the host owns +// wasi while the loader owns the affine runtime + cross-module namespaces. +const wasiStub = new Proxy({}, { get: () => () => 0 }); +const withWasi = (io) => { + io.wasi_snapshot_preview1 = wasiStub; + return io; +}; + +// 1. readBytes — the host-agnostic reader (the SAT-02 fix). +const calleeBytes = await readBytes(calleePath); +const callerBytes = await readBytes(callerPath); + +// 2. buildImportObject — the loader builds the affine import object; the +// caller's is multi-namespace (NOT env-only): it carries the genuine +// `CrossCallee` cross-module namespace INT-01/#178 emits. +const callee = await WebAssembly.instantiate( + calleeBytes, + withWasi(buildImportObject({})), +); +const consume = callee.instance.exports.consume; +if (typeof consume !== "function") { + console.error("FAIL: callee does not export a callable `consume`"); + Deno.exit(2); +} + +let caller; +try { + caller = await WebAssembly.instantiate( + callerBytes, + withWasi(buildImportObject({}, { modules: { CrossCallee: { consume } } })), + ); +} catch (e) { + console.error("FAIL: caller did not link via the loader import object:", e.message); + Deno.exit(3); +} + +const got = caller.instance.exports.main(); +if (got !== 42) { + console.error(`FAIL: cross-module call returned ${got}, expected 42`); + Deno.exit(4); +} + +// 3. parseOwnershipSection — the typed-wasm contract carrier, on REAL +// compiler output (loader_test.js only round-trips a hand-built buffer). +const calleeMod = await WebAssembly.compile(calleeBytes); +const ownership = parseOwnershipSection(calleeMod); +if (!Array.isArray(ownership) || ownership.length === 0) { + console.error( + "FAIL: parseOwnershipSection returned no entries for compiler-emitted " + + "wasm (expected at least CrossCallee.consume's linear param)", + ); + Deno.exit(5); +} +const consumeOwn = ownership.find((e) => e.paramKinds.includes("linear")); +if (!consumeOwn) { + console.error( + "FAIL: no linear param found; `consume(own x: Int)` should be Linear", + ); + Deno.exit(6); +} + +console.log( + `PASS: loader bridge — readBytes + buildImportObject linked ` + + `CrossCallee.consume(42) === ${got}; parseOwnershipSection read ` + + `${ownership.length} entr${ownership.length === 1 ? "y" : "ies"} ` + + `(consume has a Linear param) from real compiler output`, +); diff --git a/tests/modules/loader-bridge/run.sh b/tests/modules/loader-bridge/run.sh new file mode 100755 index 00000000..bd9fb875 --- /dev/null +++ b/tests/modules/loader-bridge/run.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: PMPL-1.0-or-later +# INT-02 / #179 — compile the INT-01 cross-module fixtures and drive them +# through the real packages/affine-js loader API. Reproducible acceptance. +# +# ./run.sh # uses `deno` on PATH (or $AFFINESCRIPT_DENO) +# Exit 0 = loader bridge proven on real compiler output; non-zero = regression. +set -euo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +root="$(cd "$here/../../.." && pwd)" +bin="$root/_build/default/bin/main.exe" +fix="$root/test/e2e/fixtures" +deno="${AFFINESCRIPT_DENO:-deno}" + +export AFFINESCRIPT_STDLIB="$root/stdlib" +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT + +[ -x "$bin" ] || { echo "build the compiler first: dune build bin/main.exe" >&2; exit 9; } +command -v "$deno" >/dev/null 2>&1 || { echo "deno not found (set \$AFFINESCRIPT_DENO)" >&2; exit 9; } + +( cd "$fix" && "$bin" compile CrossCallee.affine -o "$tmp/callee.wasm" >/dev/null ) +( cd "$fix" && "$bin" compile cross_caller_ok.affine -o "$tmp/caller.wasm" >/dev/null ) + +# bridge.mjs imports the loader from packages/affine-js, so allow-read spans +# the repo root (loader source) + the temp dir (the .wasm). +"$deno" run --allow-read="$root","$tmp" "$here/bridge.mjs" "$tmp/callee.wasm" "$tmp/caller.wasm"