Skip to content

Commit bb647f4

Browse files
test(xmod): INT-01 — prove + regression-lock cross-module wasm linking (Refs #178) (#244)
#178 said "use A.B inlines at AST level; no real multi-file libraries shippable". Empirically falsified for the canonical form: `gen_imports` (lib/codegen.ml) already emits real `(import "Mod" "fn" (func …))` for `use Mod::{fn}` / `use Mod::*`, and the .wasm caller path feeds the import-shaped program. What was missing was the *guarantee* — emission was correct but never execution-tested; the existing cross-module tests are structural (Tw_interface) only. This locks the substrate in: 1. Hermetic structural gate test (test/test_e2e.ml, "E2E Boundary Verify › INT-01 #178"): compiles CrossCallee.affine + cross_caller_ok.affine, asserts the caller emits import (CrossCallee . consume) and the callee exports `consume` — i.e. the two separately-compiled modules are link-compatible. Pure OCaml on the emitted Wasm.wasm_module; no external runtime; gate 270→271. 2. Reproducible execution proof (tests/modules/xmod-link/): run.sh compiles both fixtures and deno-links them; link.mjs instantiates callee.wasm, passes its `consume` export as the caller's `CrossCallee.consume` import, runs `main()`, asserts 42. Verified: "PASS: cross-module call CrossCallee.consume(42) === 42". Kept out of the hermetic gate by design (needs a wasm engine); documented in the directory README. Honest status (ledger truthed — docs/TECH-DEBT.adoc + ECOSYSTEM.adoc INT-01): `use Mod::{fn}` / `use Mod::*` cross-module libraries are *shippable, proven, regression-locked*. The `use Mod;` + qualified value call `Mod.fn(x)` form is a *distinct resolution gap* (post-#228 qualified-value resolution is unwired — `Resolution error`, before codegen) — explicitly NOT folded into this emission substrate; tracked as the remaining INT-01 follow-up. No compiler change — emission was already correct; this proves and guards it. Refs #178 (not Closes — qualified-value-call follow-up remains; owner-gated). Co-authored-by: hyperpolymath <hyperpolymath@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 81a59bf commit bb647f4

6 files changed

Lines changed: 163 additions & 2 deletions

File tree

docs/ECOSYSTEM.adoc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,9 @@ link:TECH-DEBT.adoc[TECH-DEBT.adoc].
175175
|===
176176
|ID |Item |Issue |Status
177177

178-
|INT-01 |Cross-module WASM import emission (the substrate) |#178 |open, S1
178+
|INT-01 |Cross-module WASM import emission (the substrate) |#178 |
179+
`use Mod::{fn}`/`::*` PROVEN+locked (271 gate + deno link harness);
180+
`use Mod;`+qualified-value-call resolver gap remains (distinct)
179181
|INT-02 |Host-agnostic loader bridge (`affinescript-dom-loader`) |#179 |open,
180182
S1 (blocks INT-05/08/11)
181183
|INT-03 |WASI preview2 / host I/O beyond stdout |#180 |open, S1

docs/TECH-DEBT.adoc

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,16 @@ Substrate summary:
129129
[cols="1,3,1,2"]
130130
|===
131131
|ID |Item |Sev |Status
132-
|INT-01 |Cross-module WASM import emission (substrate) |S1 |open #178
132+
|INT-01 |Cross-module WASM import emission (substrate). `use Mod::{fn}` /
133+
`use Mod::*` → real `(import "Mod" "fn" …)`, callee exports the symbol,
134+
indices line up: *PROVEN end-to-end* (two separately-compiled `.wasm`
135+
link + execute, cross-call = 42) and regression-locked — hermetic
136+
structural test in the gate (271/271) + reproducible deno harness
137+
`tests/modules/xmod-link/`. Multi-file libraries via `use Mod::{…}` are
138+
shippable. *Remaining (distinct, NOT emission):* `use Mod;` + qualified
139+
value call `Mod.fn(x)` hits a resolution error (post-#228 qualified-value
140+
resolution unwired) — own follow-up. |S1 |`use ::{}`/`::*` DONE (PR Refs
141+
#178); qualified-value-call resolver gap open #178
133142
|INT-02 |Host-agnostic loader bridge |S1 |open #179 (blocks INT-05/08/11)
134143
|INT-03 |WASI preview2 / host I/O |S1 |open #180
135144
|INT-04 |Publish to JSR/npm |S2 |open #181 (◄ INT-01)

test/test_e2e.ml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2693,6 +2693,33 @@ let test_xmod_drop_violation () =
26932693
true has_drop)
26942694
| Error e, _ | _, Error e -> Alcotest.fail e
26952695

2696+
(* ---- INT-01 / #178: cross-module WASM import-emission substrate ----
2697+
2698+
The structural half of the substrate guarantee, hermetic (pure OCaml,
2699+
inspects the emitted Wasm.wasm_module). Regression-locks that a real
2700+
multi-file `use Mod::{fn}` program emits an actual cross-module import
2701+
and the callee module exports the imported symbol — i.e. the two
2702+
separately-compiled `.wasm` modules are link-compatible. The
2703+
*execution* half (instantiate both, cross-call returns 42) is the
2704+
committed reproducible harness at tests/modules/xmod-link/ (deno;
2705+
kept out of the hermetic gate by design). *)
2706+
let test_int01_xmod_import_emission () =
2707+
match compile_fixture_to_wasm (fixture "CrossCallee.affine"),
2708+
compile_fixture_to_wasm (fixture "cross_caller_ok.affine") with
2709+
| Ok callee, Ok caller ->
2710+
let callee_exports_consume =
2711+
List.exists (fun (e : Wasm.export) -> e.Wasm.e_name = "consume")
2712+
callee.Wasm.exports in
2713+
let caller_imports_consume =
2714+
List.exists (fun (i : Wasm.import) ->
2715+
i.Wasm.i_module = "CrossCallee" && i.Wasm.i_name = "consume")
2716+
caller.Wasm.imports in
2717+
Alcotest.(check bool)
2718+
"callee module exports `consume`" true callee_exports_consume;
2719+
Alcotest.(check bool)
2720+
"caller emits import (CrossCallee . consume)" true caller_imports_consume
2721+
| Error e, _ | _, Error e -> Alcotest.fail e
2722+
26962723
(* ---- WasmGC backend: loud-failure regression markers ----
26972724
26982725
Lock in the BUG-005-class fixes that replaced silent miscompilation with
@@ -3591,6 +3618,7 @@ let tw_interface_tests = [
35913618
Alcotest.test_case "src-level xmod: ok caller verifies clean" `Quick test_xmod_clean;
35923619
Alcotest.test_case "src-level xmod: dup caller → LinearImportCalledMultiple" `Quick test_xmod_dup_violation;
35933620
Alcotest.test_case "src-level xmod: drop caller → LinearImportDroppedOnSomePath" `Quick test_xmod_drop_violation;
3621+
Alcotest.test_case "INT-01 #178: use Mod::{fn} emits real xmod import + callee exports it" `Quick test_int01_xmod_import_emission;
35943622
]
35953623

35963624
(* ============================================================================
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
= INT-01 / #178 — cross-module WASM link+execute acceptance
3+
4+
Reproducible proof that *real multi-file AffineScript libraries are
5+
shippable*: two separately-compiled `.wasm` modules link and execute
6+
across the wasm module boundary.
7+
8+
[cols="1,3"]
9+
|===
10+
|Module |Source
11+
12+
|callee |`test/e2e/fixtures/CrossCallee.affine` — `module CrossCallee;
13+
pub fn consume(own x: Int) -> Int { x }`
14+
|caller |`test/e2e/fixtures/cross_caller_ok.affine` — `use
15+
CrossCallee::{consume}; pub fn main() -> Int { consume(42) }`
16+
|===
17+
18+
== Run
19+
20+
[source,sh]
21+
----
22+
dune build bin/main.exe
23+
tests/modules/xmod-link/run.sh # deno on PATH, or $AFFINESCRIPT_DENO
24+
----
25+
26+
Exit 0 + `PASS` ⇒ `caller.main()` returned `42` through the
27+
cross-module call `CrossCallee.consume(42)`, with the caller emitting a
28+
real `(import "CrossCallee" "consume" (func …))` and the callee
29+
exporting `consume`.
30+
31+
== Why this lives here, not in `dune runtest`
32+
33+
The OCaml gate is hermetic (no external runtime). The *structural* half
34+
of the substrate guarantee — caller emits the import, callee exports the
35+
symbol, indices line up — IS in the gate as
36+
`E2E Boundary Verify › INT-01 #178 …` (`test/test_e2e.ml`). This
37+
directory holds the *execution* half, which needs a wasm engine (deno);
38+
keeping it out of the hermetic gate is deliberate.
39+
40+
== Status (INT-01)
41+
42+
* `use Mod::{fn}` / `use Mod::*` → real cross-module imports: *works,
43+
proven here, regression-locked structurally in the gate*.
44+
* `use Mod;` + qualified value call `Mod.fn(x)` → *resolution error*
45+
(post-#228 qualified-value resolution is unwired). This is a distinct
46+
follow-up, NOT part of the INT-01 emission substrate. See
47+
`docs/TECH-DEBT.adoc` INT-01.

tests/modules/xmod-link/link.mjs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// INT-01 / #178 — cross-module WASM link+execute acceptance harness.
3+
//
4+
// Proves that two SEPARATELY-compiled AffineScript modules link and run
5+
// across the wasm module boundary:
6+
// callee.wasm ← module CrossCallee; pub fn consume(own x: Int) -> Int { x }
7+
// caller.wasm ← use CrossCallee::{consume}; pub fn main() -> Int { consume(42) }
8+
//
9+
// Usage: deno run --allow-read=<dir> link.mjs <callee.wasm> <caller.wasm>
10+
// Exits 0 and prints PASS iff caller.main() === 42 via the cross-module call.
11+
12+
const [calleePath, callerPath] = Deno.args;
13+
if (!calleePath || !callerPath) {
14+
console.error("usage: link.mjs <callee.wasm> <caller.wasm>");
15+
Deno.exit(64);
16+
}
17+
18+
// Minimal WASI shim — the AffineScript wasm imports fd_write for println-style
19+
// codegen; cross-module linking is what is under test, not I/O.
20+
const wasiStub = new Proxy({}, { get: () => () => 0 });
21+
22+
const callee = await WebAssembly.instantiate(
23+
await Deno.readFile(calleePath),
24+
{ wasi_snapshot_preview1: wasiStub },
25+
);
26+
const consume = callee.instance.exports.consume;
27+
if (typeof consume !== "function") {
28+
console.error("FAIL: callee does not export a callable `consume`");
29+
Deno.exit(2);
30+
}
31+
32+
let caller;
33+
try {
34+
caller = await WebAssembly.instantiate(
35+
await Deno.readFile(callerPath),
36+
{ wasi_snapshot_preview1: wasiStub, CrossCallee: { consume } },
37+
);
38+
} catch (e) {
39+
console.error("FAIL: caller did not link against CrossCallee.consume:", e.message);
40+
Deno.exit(3);
41+
}
42+
43+
const r = caller.instance.exports.main();
44+
if (r === 42) {
45+
console.log("PASS: cross-module call CrossCallee.consume(42) === 42");
46+
Deno.exit(0);
47+
}
48+
console.error(`FAIL: expected 42, got ${r}`);
49+
Deno.exit(5);

tests/modules/xmod-link/run.sh

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/usr/bin/env bash
2+
# SPDX-License-Identifier: PMPL-1.0-or-later
3+
# INT-01 / #178 — compile the two cross-module fixtures and prove they
4+
# link + execute across the wasm boundary. Reproducible acceptance run.
5+
#
6+
# ./run.sh # uses `deno` on PATH (or $AFFINESCRIPT_DENO)
7+
# Exit 0 = substrate proven; non-zero = regression.
8+
set -euo pipefail
9+
10+
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11+
root="$(cd "$here/../../.." && pwd)"
12+
bin="$root/_build/default/bin/main.exe"
13+
fix="$root/test/e2e/fixtures"
14+
deno="${AFFINESCRIPT_DENO:-deno}"
15+
16+
export AFFINESCRIPT_STDLIB="$root/stdlib"
17+
tmp="$(mktemp -d)"
18+
trap 'rm -rf "$tmp"' EXIT
19+
20+
[ -x "$bin" ] || { echo "build the compiler first: dune build bin/main.exe" >&2; exit 9; }
21+
command -v "$deno" >/dev/null 2>&1 || { echo "deno not found (set \$AFFINESCRIPT_DENO)" >&2; exit 9; }
22+
23+
( cd "$fix" && "$bin" compile CrossCallee.affine -o "$tmp/callee.wasm" >/dev/null )
24+
( cd "$fix" && "$bin" compile cross_caller_ok.affine -o "$tmp/caller.wasm" >/dev/null )
25+
26+
"$deno" run --allow-read="$tmp" "$here/link.mjs" "$tmp/callee.wasm" "$tmp/caller.wasm"

0 commit comments

Comments
 (0)