Skip to content

Commit 3acf8a1

Browse files
test(transpile): cover fatal cleanup and async result errors
Add documented WAT fixtures for direct and composed cleanup traps, async-lowered calls, and result completion with wrapped or raw errors. Check that healthy sibling instances remain callable. Shorten the existing Rust fixture and regression comments.
1 parent 1410b71 commit 3acf8a1

13 files changed

Lines changed: 313 additions & 48 deletions

File tree

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
mod bindings {
22
wit_bindgen::generate!({
33
world: "async-post-return-trap",
4-
// The shared fixture module provides this core export and its post-return hook.
5-
// Skip wit-bindgen's adapter for this function; keep its WIT declaration.
6-
// jco generates the JavaScript returnWithCleanup() wrapper from that declaration.
4+
// The fixture supplies the core export and its post-return hook.
75
skip: ["return-with-cleanup"],
86
});
97

@@ -14,5 +12,4 @@ mod bindings {
1412
#[path = "support/post_return_trap.rs"]
1513
mod fixture;
1614

17-
// Required because this fixture is built as a binary.
1815
fn main() {}
Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
mod bindings {
22
wit_bindgen::generate!({
33
world: "post-return-trap",
4-
// The shared fixture module provides this core export and its post-return hook.
5-
// Skip wit-bindgen's adapter for this function; keep its WIT declaration.
6-
// jco generates the JavaScript returnWithCleanup() wrapper from that declaration.
4+
// The fixture supplies the core export and its post-return hook.
75
skip: ["return-with-cleanup"],
86
});
97

@@ -14,5 +12,4 @@ mod bindings {
1412
#[path = "support/post_return_trap.rs"]
1513
mod fixture;
1614

17-
// Required because this fixture is built as a binary.
1815
fn main() {}

‎crates/test-components/src/bin/support/post_return_trap.rs‎

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,19 @@
1-
/*!
2-
Test traps during canonical post-return cleanup.
3-
4-
1. Main call: the exported function returns its argument successfully.
5-
Trapping inside that function would exercise jco's handling around the
6-
main Wasm call, rather than the post-return path tested here.
7-
8-
2. Cleanup: the generated bindings invoke the post-return hook with the
9-
returned value. This scalar result needs no actual cleanup; we supply
10-
the hook specifically to test a failure after the export succeeds.
11-
12-
3. Test controls: zero lets cleanup finish normally. Any nonzero value
13-
deliberately executes Wasm `unreachable` inside the cleanup hook.
14-
These values have no special meaning to jco.
15-
16-
4. Current call: a cleanup trap must reach the caller as an exception or
17-
Promise rejection. An internally recorded result must not become a
18-
successful completion after cleanup has failed.
19-
20-
5. Subsequent calls: the cleanup trap must prevent further component
21-
entry. The independent `ok` export returns 42 before the trap and
22-
must be prevented from executing afterward.
23-
*/
1+
//! The core export returns its argument; only cleanup traps for nonzero values.
242
253
pub struct Component;
264

275
impl crate::bindings::Guest for Component {
28-
// An independent export used to check whether the component still accepts calls.
296
fn ok() -> u32 {
307
42
318
}
329
}
3310

34-
// 1. Return the argument successfully; the core export itself never traps.
3511
#[unsafe(export_name = "return-with-cleanup")]
3612
extern "C" fn return_with_cleanup(value: u32) -> u32 {
3713
value
3814
}
3915

40-
// 2. Run canonical post-return cleanup after the core export has returned.
41-
// Zero completes normally; any nonzero value traps.
42-
// The test-component build also checks native binaries, so gate this Wasm hook.
16+
// Native fixture builds have no Wasm trap instruction.
4317
#[cfg(target_arch = "wasm32")]
4418
#[unsafe(export_name = "cabi_post_return-with-cleanup")]
4519
extern "C" fn post_return(value: u32) {

‎packages/jco-transpile/test/codegen.ts‎

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -336,9 +336,6 @@ suite('Trap detection', () => {
336336
}
337337
});
338338

339-
// 1. Return 0 and complete post-return normally; verify the result and another export.
340-
// 2. Return 1 successfully from Rust, then trap in post-return; verify the caller gets the trap.
341-
// 3. Call the other export again; verify the trapped component refuses further entry.
342339
test.concurrent('post-return trap prevents subsequent synchronous calls', testSynchronousPostReturnTrap);
343340

344341
describe.skipIf(typeof WebAssembly.Suspending !== 'function')('post-return traps with JSPI', () => {
@@ -366,18 +363,15 @@ suite('Trap detection', () => {
366363
const subsequentResult = instance.ok();
367364
assert.strictEqual(subsequentResult, 42);
368365

369-
// The core export succeeds; its post-return failure must reach the caller.
370366
assert.throws(() => instance.returnWithCleanup(1), WebAssembly.RuntimeError, /unreachable/);
371367

372-
// The trapped instance must reject entry into an independent export.
373368
assert.throws(() => instance.ok(), WebAssembly.RuntimeError, 'wasm trap: cannot enter component instance');
374369
} finally {
375370
await cleanupFixture();
376371
}
377372
}
378373

379374
async function testJspiPostReturnTrap() {
380-
// Expose a synchronous WIT operation through a JSPI Promise wrapper.
381375
const { instance, cleanup: cleanupFixture } = await setupAsyncTest({
382376
asyncMode: 'jspi',
383377
component: {
@@ -398,20 +392,17 @@ suite('Trap detection', () => {
398392
const subsequentResult = instance.ok();
399393
assert.strictEqual(subsequentResult, 42);
400394

401-
// The core export succeeds; its post-return failure must reach the caller.
402395
const failingCall = instance.returnWithCleanup(1);
403396
await expect(failingCall).rejects.toBeInstanceOf(WebAssembly.RuntimeError);
404397
await expect(failingCall).rejects.toThrow(/unreachable/);
405398

406-
// The trapped instance must reject entry into an independent export.
407399
assert.throws(() => instance.ok(), WebAssembly.RuntimeError, 'wasm trap: cannot enter component instance');
408400
} finally {
409401
await cleanupFixture();
410402
}
411403
}
412404

413405
async function testWitAsyncPostReturnTrap() {
414-
// Exercise an operation declared asynchronous in WIT, using JSPI.
415406
const { instance, cleanup: cleanupFixture } = await setupAsyncTest({
416407
asyncMode: 'jspi',
417408
component: {
@@ -427,13 +418,10 @@ suite('Trap detection', () => {
427418
const subsequentResult = instance.ok();
428419
assert.strictEqual(subsequentResult, 42);
429420

430-
// Post-return traps after the task has recorded the successful core result.
431-
// The caller must receive the trap instead of that recorded result.
432421
const failingCall = instance.returnWithCleanup(1);
433422
await expect(failingCall).rejects.toBeInstanceOf(WebAssembly.RuntimeError);
434423
await expect(failingCall).rejects.toThrow(/unreachable/);
435424

436-
// The trapped instance must reject entry into an independent export.
437425
assert.throws(() => instance.ok(), WebAssembly.RuntimeError, 'wasm trap: cannot enter component instance');
438426
} finally {
439427
await cleanupFixture();
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
;; Exercise task-managed cleanup through an async-lowered import of a sync core ABI callee.
2+
;; run(0) completes eagerly; run(1) traps in cleanup and must reject the root call and disable the callee.
3+
(component
4+
(component $callee
5+
(core module $m
6+
(func (export "run") (param i32) (result i32) local.get 0)
7+
(func (export "post") (param i32)
8+
(if (local.get 0) (then unreachable)))
9+
(func (export "ok") (result i32) i32.const 42))
10+
(core instance $i (instantiate $m))
11+
(func (export "run") async (param "value" u32) (result u32)
12+
(canon lift (core func $i "run") (post-return (core func $i "post"))))
13+
(func (export "ok") (result u32) (canon lift (core func $i "ok"))))
14+
(component $caller
15+
(import "callee" (func $callee async (param "value" u32) (result u32)))
16+
(core module $memory (memory (export "memory") 1))
17+
(core instance $memory (instantiate $memory))
18+
(core func $lower (canon lower (func $callee) async (memory (core memory $memory "memory"))))
19+
(core func $return (canon task.return (result u32)))
20+
(core module $m
21+
(import "" "memory" (memory 1))
22+
(import "" "callee" (func $callee (param i32 i32) (result i32)))
23+
(import "" "return" (func $return (param i32)))
24+
(func (export "run") (param i32) (result i32)
25+
(if (i32.ne (call $callee (local.get 0) (i32.const 0)) (i32.const 2)) (then unreachable))
26+
(call $return (i32.load (i32.const 0)))
27+
i32.const 0)
28+
(func (export "callback") (param i32 i32 i32) (result i32) unreachable))
29+
(core instance $i (instantiate $m
30+
(with "" (instance
31+
(export "memory" (memory $memory "memory"))
32+
(export "callee" (func $lower))
33+
(export "return" (func $return))))))
34+
(func (export "run") async (param "value" u32) (result u32)
35+
(canon lift (core func $i "run") async (callback (core func $i "callback")))))
36+
(instance $callee (instantiate $callee))
37+
(instance $caller (instantiate $caller (with "callee" (func $callee "run"))))
38+
(export "run" (func $caller "run"))
39+
(export "ok" (func $callee "ok")))
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
;; Verify fused post-return traps disable both caller and callee, preserving a healthy sibling.
2+
;; run(0) succeeds; cleanup for run(1) executes unreachable, and run(2) exhausts the stack.
3+
(component
4+
(component $callee
5+
(core module $m
6+
(func $recurse (call $recurse))
7+
(func (export "run") (param i32) (result i32) local.get 0)
8+
(func (export "post") (param i32)
9+
(if (local.get 0) (then
10+
(if (i32.eq (local.get 0) (i32.const 1))
11+
(then unreachable)
12+
(else (call $recurse))))))
13+
(func (export "ok") (result i32) i32.const 42))
14+
(core instance $i (instantiate $m))
15+
(func (export "run") (param "value" u32) (result u32)
16+
(canon lift (core func $i "run") (post-return (core func $i "post"))))
17+
(func (export "ok") (result u32) (canon lift (core func $i "ok"))))
18+
(component $caller
19+
(import "callee" (func $callee (param "value" u32) (result u32)))
20+
(core func $lower (canon lower (func $callee)))
21+
(core module $m
22+
(import "" "callee" (func $callee (param i32) (result i32)))
23+
(func (export "run") (param i32) (result i32) (call $callee (local.get 0)))
24+
(func (export "ok") (result i32) i32.const 43))
25+
(core instance $i (instantiate $m (with "" (instance (export "callee" (func $lower))))))
26+
(func (export "run") async (param "value" u32) (result u32)
27+
(canon lift (core func $i "run")))
28+
(func (export "ok") (result u32) (canon lift (core func $i "ok"))))
29+
(instance $callee (instantiate $callee))
30+
(instance $healthy (instantiate $callee))
31+
(instance $caller (instantiate $caller (with "callee" (func $callee "run"))))
32+
(export "run" (func $caller "run"))
33+
(export "callee-ok" (func $callee "ok"))
34+
(export "caller-ok" (func $caller "ok"))
35+
(export "healthy-ok" (func $healthy "ok")))
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
;; Verify fused post-return traps disable both caller and callee, preserving a healthy sibling.
2+
;; run(0) succeeds; cleanup for run(1) executes unreachable, and run(2) exhausts the stack.
3+
(component
4+
(component $callee
5+
(core module $m
6+
(func $recurse (call $recurse))
7+
(func (export "run") (param i32) (result i32) local.get 0)
8+
(func (export "post") (param i32)
9+
(if (local.get 0) (then
10+
(if (i32.eq (local.get 0) (i32.const 1))
11+
(then unreachable)
12+
(else (call $recurse))))))
13+
(func (export "ok") (result i32) i32.const 42))
14+
(core instance $i (instantiate $m))
15+
(func (export "run") (param "value" u32) (result u32)
16+
(canon lift (core func $i "run") (post-return (core func $i "post"))))
17+
(func (export "ok") (result u32) (canon lift (core func $i "ok"))))
18+
(component $caller
19+
(import "callee" (func $callee (param "value" u32) (result u32)))
20+
(core func $lower (canon lower (func $callee)))
21+
(core module $m
22+
(import "" "callee" (func $callee (param i32) (result i32)))
23+
(func (export "run") (param i32) (result i32) (call $callee (local.get 0)))
24+
(func (export "ok") (result i32) i32.const 43))
25+
(core instance $i (instantiate $m (with "" (instance (export "callee" (func $lower))))))
26+
(func (export "run") (param "value" u32) (result u32)
27+
(canon lift (core func $i "run")))
28+
(func (export "ok") (result u32) (canon lift (core func $i "ok"))))
29+
(instance $callee (instantiate $callee))
30+
(instance $healthy (instantiate $callee))
31+
(instance $caller (instantiate $caller (with "callee" (func $callee "run"))))
32+
(export "run" (func $caller "run"))
33+
(export "callee-ok" (func $callee "ok"))
34+
(export "caller-ok" (func $caller "ok"))
35+
(export "healthy-ok" (func $healthy "ok")))
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
;; Verify async exports reject failed cleanup and refuse subsequent entry.
2+
;; run(0) succeeds; run(1) returns first, then throws a core Wasm exception in post-return.
3+
;; ok() must remain callable only until cleanup fails.
4+
(component
5+
(core module $m
6+
(tag $error (param i32))
7+
(func (export "run") (param i32) (result i32) local.get 0)
8+
(func (export "post") (param i32)
9+
(if (local.get 0) (then (throw $error (i32.const 42)))))
10+
(func (export "ok") (result i32) i32.const 42))
11+
(core instance $i (instantiate $m))
12+
(func (export "run") async (param "value" u32) (result u32)
13+
(canon lift (core func $i "run") (post-return (core func $i "post"))))
14+
(func (export "ok") (result u32) (canon lift (core func $i "ok"))))
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
;; Verify sync exports reject failed cleanup and refuse subsequent entry.
2+
;; run(0) succeeds; run(1) returns first, then throws a core Wasm exception in post-return.
3+
;; ok() must remain callable only until cleanup fails.
4+
(component
5+
(core module $m
6+
(tag $error (param i32))
7+
(func (export "run") (param i32) (result i32) local.get 0)
8+
(func (export "post") (param i32)
9+
(if (local.get 0) (then (throw $error (i32.const 42)))))
10+
(func (export "ok") (result i32) i32.const 42))
11+
(core instance $i (instantiate $m))
12+
(func (export "run") (param "value" u32) (result u32)
13+
(canon lift (core func $i "run") (post-return (core func $i "post"))))
14+
(func (export "ok") (result u32) (canon lift (core func $i "ok"))))
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
;; Verify async result tags survive cleanup and completion, including raw error payloads.
2+
;; tag=0 returns ok(42), tag=1 returns err(42); fail=1 traps in cleanup and takes precedence over either result.
3+
(component
4+
(core module $m
5+
(memory (export "memory") 1)
6+
(global $fail (mut i32) (i32.const 0))
7+
(func (export "run") (param $tag i32) (param $fail i32) (result i32)
8+
(global.set $fail (local.get $fail))
9+
(i32.store (i32.const 0) (local.get $tag))
10+
(i32.store (i32.const 4) (i32.const 42))
11+
i32.const 0)
12+
(func (export "post") (param i32)
13+
(if (global.get $fail) (then unreachable)))
14+
(func (export "ok") (result i32) i32.const 42))
15+
(core instance $i (instantiate $m))
16+
(func (export "run") async (param "tag" u32) (param "fail" u32) (result (result u32 (error u32)))
17+
(canon lift (core func $i "run") (memory (core memory $i "memory"))
18+
(post-return (core func $i "post"))))
19+
(func (export "ok") (result u32) (canon lift (core func $i "ok"))))

0 commit comments

Comments
 (0)