Skip to content

Commit c6ee9e4

Browse files
committed
test(codegen): #235 — engine-validate the #199 closure ABI independent of CPS (Refs #235)
Until #225 PR2 the #199 closure ABI ([fnId@0,envPtr@4] via the exported __indirect_function_table) had only ever been *statically* compiled estate-wide — the blind spot the two PR2 defects hid in. The async tests now exercise it but only THROUGH the CPS transform; the existing tests/codegen/test_closure_*.affine have NO `.mjs` host (compile-only). This adds the missing negative-control. tests/codegen/closure_indirect_dispatch.{affine,mjs}: a captured closure (`fn(u:Unit) => base + 35`, base = local 7) passed to a plain `extern fn invokeCallback(cb: fn(Unit)->Int)->Int`; the host dispatches it via __indirect_function_table (wrapHandler, identical to packages/affine-vscode/mod.js). Verified the unit imports ONLY `env.invokeCallback` (no thenableThen ⇒ async transform provably not involved) yet still exports `__indirect_function_table`; asserts table exported + closure fired once + captured local reached via envPtr (7+35=42). Protects the shared #199 path for ALL closure users independent of #205. Gate: full tools/run_codegen_wasm_tests.sh green incl. the new test; dune test --force 290/290. Zero regression. #235 estate-consumer audit (recorded on the issue): the ONLY estate consumer that genuinely warrants its own runtime smoke is rsr-certifier (standards#123 — full #199 closures + #205 path, ships its own wrapHandler clone, zero wasm tests). my-lang (#199-only), idaptik/reposystem (.ts generic vscode, not this ABI), boj-server/ gitbot-fleet (no consumer) are defensibly static-only now this upstream guarantee exists. Refs #235 — deliverable (1) done; the rsr-certifier per-repo smoke (standards repo) is the remaining tracked item, so not Closes.
1 parent 46bb174 commit c6ee9e4

2 files changed

Lines changed: 78 additions & 0 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// issue #235 — the #199 closure-pointer ABI exercised in a real wasm
3+
// engine INDEPENDENT of the async / CPS transform.
4+
//
5+
// Until #225 PR2 the #199 ABI ([fnId@0,envPtr@4] dispatched via the
6+
// exported __indirect_function_table) had only ever been STATICALLY
7+
// compiled across the estate — the two defects PR2 found had hidden in
8+
// that blind spot. The async tests now exercise it, but only *through*
9+
// the CPS transform. tests/codegen/test_closure_capture.affine (and
10+
// the other test_closure_*.affine) have NO `.mjs` host — they are
11+
// compile-only. This fixture closes that gap: a captured-closure
12+
// callback handed to a plain `extern fn`, dispatched by the host via
13+
// __indirect_function_table, with NO `thenableThen` / Async effect, so
14+
// the async transform is provably not involved.
15+
16+
extern fn invokeCallback(cb: fn(Unit) -> Int) -> Int;
17+
18+
pub fn launch() -> Int {
19+
let base = 7;
20+
invokeCallback(fn(u: Unit) => base + 35) // host-dispatched -> 42
21+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// issue #235 — proves the #199 [fnId@0,envPtr@4] closure ABI through
3+
// the exported __indirect_function_table in a real wasm engine, with
4+
// NO async/CPS transform involved (no thenableThen import). This is
5+
// the negative-control the http_cps_* tests cannot isolate: it
6+
// validates the table export + captured-env marshalling for ANY
7+
// closure user (rsr-certifier, my-lang, …), independent of #205.
8+
import assert from 'node:assert/strict';
9+
import { readFile } from 'node:fs/promises';
10+
11+
let inst = null;
12+
let cbFired = 0;
13+
14+
// Identical dispatch to wrapHandler in packages/affine-vscode/mod.js:
15+
// closure = heap [i32 fnId @+0][i32 envPtr @+4]; look fnId up in the
16+
// exported table, call with envPtr first, zero-pad to arity.
17+
function wrapHandler(closurePtr) {
18+
return () => {
19+
const tbl = inst.exports.__indirect_function_table;
20+
const dv = new DataView(inst.exports.memory.buffer);
21+
const fnId = dv.getInt32(closurePtr, true);
22+
const envPtr = dv.getInt32(closurePtr + 4, true);
23+
const fn = tbl.get(fnId);
24+
const args = [envPtr];
25+
while (args.length < fn.length) args.push(0);
26+
return fn(...args);
27+
};
28+
}
29+
30+
const imports = {
31+
wasi_snapshot_preview1: { fd_write: () => 0 },
32+
env: {
33+
// Plain synchronous extern: dispatch the closure immediately —
34+
// no Promise/Thenable. Proves the captured local (7) survives
35+
// table dispatch via envPtr.
36+
invokeCallback: (closurePtr) => {
37+
cbFired += 1;
38+
return wrapHandler(closurePtr)();
39+
},
40+
},
41+
};
42+
43+
const buf = await readFile('./tests/codegen/closure_indirect_dispatch.wasm');
44+
inst = (await WebAssembly.instantiate(buf, imports)).instance;
45+
46+
assert.ok(
47+
inst.exports.__indirect_function_table,
48+
'__indirect_function_table is exported for a closure-bearing, non-async unit',
49+
);
50+
const r = inst.exports.launch();
51+
assert.equal(cbFired, 1, 'host dispatched the closure exactly once');
52+
assert.equal(
53+
r,
54+
42,
55+
'captured local (7) reached the closure via envPtr; 7 + 35 = 42',
56+
);
57+
console.log('test_closure_indirect_dispatch.mjs OK');

0 commit comments

Comments
 (0)