Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions tests/codegen/closure_indirect_dispatch.affine
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: PMPL-1.0-or-later
// issue #235 — the #199 closure-pointer ABI exercised in a real wasm
// engine INDEPENDENT of the async / CPS transform.
//
// Until #225 PR2 the #199 ABI ([fnId@0,envPtr@4] dispatched via the
// exported __indirect_function_table) had only ever been STATICALLY
// compiled across the estate — the two defects PR2 found had hidden in
// that blind spot. The async tests now exercise it, but only *through*
// the CPS transform. tests/codegen/test_closure_capture.affine (and
// the other test_closure_*.affine) have NO `.mjs` host — they are
// compile-only. This fixture closes that gap: a captured-closure
// callback handed to a plain `extern fn`, dispatched by the host via
// __indirect_function_table, with NO `thenableThen` / Async effect, so
// the async transform is provably not involved.

extern fn invokeCallback(cb: fn(Unit) -> Int) -> Int;

pub fn launch() -> Int {
let base = 7;
invokeCallback(fn(u: Unit) => base + 35) // host-dispatched -> 42
}
57 changes: 57 additions & 0 deletions tests/codegen/test_closure_indirect_dispatch.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// SPDX-License-Identifier: PMPL-1.0-or-later
// issue #235 — proves the #199 [fnId@0,envPtr@4] closure ABI through
// the exported __indirect_function_table in a real wasm engine, with
// NO async/CPS transform involved (no thenableThen import). This is
// the negative-control the http_cps_* tests cannot isolate: it
// validates the table export + captured-env marshalling for ANY
// closure user (rsr-certifier, my-lang, …), independent of #205.
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';

let inst = null;
let cbFired = 0;

// Identical dispatch to wrapHandler in packages/affine-vscode/mod.js:
// closure = heap [i32 fnId @+0][i32 envPtr @+4]; look fnId up in the
// exported table, call with envPtr first, zero-pad to arity.
function wrapHandler(closurePtr) {
return () => {
const tbl = inst.exports.__indirect_function_table;
const dv = new DataView(inst.exports.memory.buffer);
const fnId = dv.getInt32(closurePtr, true);
const envPtr = dv.getInt32(closurePtr + 4, true);
const fn = tbl.get(fnId);
const args = [envPtr];
while (args.length < fn.length) args.push(0);
return fn(...args);
};
}

const imports = {
wasi_snapshot_preview1: { fd_write: () => 0 },
env: {
// Plain synchronous extern: dispatch the closure immediately —
// no Promise/Thenable. Proves the captured local (7) survives
// table dispatch via envPtr.
invokeCallback: (closurePtr) => {
cbFired += 1;
return wrapHandler(closurePtr)();
},
},
};

const buf = await readFile('./tests/codegen/closure_indirect_dispatch.wasm');
inst = (await WebAssembly.instantiate(buf, imports)).instance;

assert.ok(
inst.exports.__indirect_function_table,
'__indirect_function_table is exported for a closure-bearing, non-async unit',
);
const r = inst.exports.launch();
assert.equal(cbFired, 1, 'host dispatched the closure exactly once');
assert.equal(
r,
42,
'captured local (7) reached the closure via envPtr; 7 + 35 = 42',
);
console.log('test_closure_indirect_dispatch.mjs OK');
Loading