|
| 1 | +// SPDX-License-Identifier: PMPL-1.0-or-later |
| 2 | +// INT-02 / #179 — host-agnostic loader bridge, end-to-end acceptance. |
| 3 | +// |
| 4 | +// Proves the *actual* INT-02 loader API (packages/affine-js/loader.js) |
| 5 | +// drives genuine compiler-emitted, cross-module wasm — not synthetic |
| 6 | +// bytes (loader_test.js does the unit level) and not a hand-rolled |
| 7 | +// `Deno.readFile` + manual import object (that is the SAT-02 anti-pattern |
| 8 | +// the loader exists to replace; the INT-01 xmod-link harness still does it |
| 9 | +// by hand — this closes INT-01 ↔ INT-02). |
| 10 | +// |
| 11 | +// callee.wasm ← module CrossCallee; pub fn consume(own x: Int) -> Int { x } |
| 12 | +// caller.wasm ← use CrossCallee::{consume}; pub fn main() -> Int { consume(42) } |
| 13 | +// |
| 14 | +// Usage: deno run --allow-read=<dir> bridge.mjs <callee.wasm> <caller.wasm> |
| 15 | +// Exit 0 + PASS iff: readBytes loads both; buildImportObject wires the |
| 16 | +// cross-module import; caller.main() === 42; parseOwnershipSection returns |
| 17 | +// real entries from the compiler-emitted module. |
| 18 | + |
| 19 | +import { |
| 20 | + buildImportObject, |
| 21 | + parseOwnershipSection, |
| 22 | + readBytes, |
| 23 | +} from "../../../packages/affine-js/loader.js"; |
| 24 | + |
| 25 | +const [calleePath, callerPath] = Deno.args; |
| 26 | +if (!calleePath || !callerPath) { |
| 27 | + console.error("usage: bridge.mjs <callee.wasm> <caller.wasm>"); |
| 28 | + Deno.exit(64); |
| 29 | +} |
| 30 | + |
| 31 | +// WASI is host-supplied, a catch-all namespace (println-style codegen |
| 32 | +// imports fd_write). `buildImportObject` *spreads* module members (to merge |
| 33 | +// rather than clobber), so a catch-all Proxy must be attached as a whole |
| 34 | +// namespace, not via the spread — mirroring real usage where the host owns |
| 35 | +// wasi while the loader owns the affine runtime + cross-module namespaces. |
| 36 | +const wasiStub = new Proxy({}, { get: () => () => 0 }); |
| 37 | +const withWasi = (io) => { |
| 38 | + io.wasi_snapshot_preview1 = wasiStub; |
| 39 | + return io; |
| 40 | +}; |
| 41 | + |
| 42 | +// 1. readBytes — the host-agnostic reader (the SAT-02 fix). |
| 43 | +const calleeBytes = await readBytes(calleePath); |
| 44 | +const callerBytes = await readBytes(callerPath); |
| 45 | + |
| 46 | +// 2. buildImportObject — the loader builds the affine import object; the |
| 47 | +// caller's is multi-namespace (NOT env-only): it carries the genuine |
| 48 | +// `CrossCallee` cross-module namespace INT-01/#178 emits. |
| 49 | +const callee = await WebAssembly.instantiate( |
| 50 | + calleeBytes, |
| 51 | + withWasi(buildImportObject({})), |
| 52 | +); |
| 53 | +const consume = callee.instance.exports.consume; |
| 54 | +if (typeof consume !== "function") { |
| 55 | + console.error("FAIL: callee does not export a callable `consume`"); |
| 56 | + Deno.exit(2); |
| 57 | +} |
| 58 | + |
| 59 | +let caller; |
| 60 | +try { |
| 61 | + caller = await WebAssembly.instantiate( |
| 62 | + callerBytes, |
| 63 | + withWasi(buildImportObject({}, { modules: { CrossCallee: { consume } } })), |
| 64 | + ); |
| 65 | +} catch (e) { |
| 66 | + console.error("FAIL: caller did not link via the loader import object:", e.message); |
| 67 | + Deno.exit(3); |
| 68 | +} |
| 69 | + |
| 70 | +const got = caller.instance.exports.main(); |
| 71 | +if (got !== 42) { |
| 72 | + console.error(`FAIL: cross-module call returned ${got}, expected 42`); |
| 73 | + Deno.exit(4); |
| 74 | +} |
| 75 | + |
| 76 | +// 3. parseOwnershipSection — the typed-wasm contract carrier, on REAL |
| 77 | +// compiler output (loader_test.js only round-trips a hand-built buffer). |
| 78 | +const calleeMod = await WebAssembly.compile(calleeBytes); |
| 79 | +const ownership = parseOwnershipSection(calleeMod); |
| 80 | +if (!Array.isArray(ownership) || ownership.length === 0) { |
| 81 | + console.error( |
| 82 | + "FAIL: parseOwnershipSection returned no entries for compiler-emitted " + |
| 83 | + "wasm (expected at least CrossCallee.consume's linear param)", |
| 84 | + ); |
| 85 | + Deno.exit(5); |
| 86 | +} |
| 87 | +const consumeOwn = ownership.find((e) => e.paramKinds.includes("linear")); |
| 88 | +if (!consumeOwn) { |
| 89 | + console.error( |
| 90 | + "FAIL: no linear param found; `consume(own x: Int)` should be Linear", |
| 91 | + ); |
| 92 | + Deno.exit(6); |
| 93 | +} |
| 94 | + |
| 95 | +console.log( |
| 96 | + `PASS: loader bridge — readBytes + buildImportObject linked ` + |
| 97 | + `CrossCallee.consume(42) === ${got}; parseOwnershipSection read ` + |
| 98 | + `${ownership.length} entr${ownership.length === 1 ? "y" : "ies"} ` + |
| 99 | + `(consume has a Linear param) from real compiler output`, |
| 100 | +); |
0 commit comments