From 0fa2af6daeaf9f5cafa4e2a81c91ae099cc60528 Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Mon, 8 Jun 2026 19:53:39 +0900 Subject: [PATCH 1/8] chore(jco): add component-model repo wat tests --- .../async/async-calls-sync.wast | 251 +++++++++ .../component-model/async/cancel-stream.wast | 202 +++++++ .../component-model/async/cancel-subtask.wast | 201 +++++++ .../component-model/async/cancellable.wast | 325 +++++++++++ .../component-model/async/closed-stream.wast | 102 ++++ .../async/cross-abi-calls.wast | 519 ++++++++++++++++++ .../wat/component-model/async/deadlock.wast | 73 +++ .../async/dont-block-start.wast | 50 ++ .../async/drop-cross-task-borrow.wast | 309 +++++++++++ .../component-model/async/drop-stream.wast | 160 ++++++ .../component-model/async/drop-subtask.wast | 140 +++++ .../async/drop-waitable-set.wast | 84 +++ .../wat/component-model/async/empty-wait.wast | 199 +++++++ .../async/futures-must-write.wast | 118 ++++ .../async/partial-stream-copies.wast | 238 ++++++++ .../async/passing-resources.wast | 176 ++++++ .../async/same-component-stream-future.wast | 259 +++++++++ .../component-model/async/sync-barges-in.wast | 311 +++++++++++ .../component-model/async/sync-streams.wast | 178 ++++++ .../async/trap-if-block-and-sync.wast | 343 ++++++++++++ .../component-model/async/trap-if-done.wast | 468 ++++++++++++++++ .../async/trap-on-reenter.wast | 110 ++++ .../async/wait-during-callback.wast | 77 +++ .../component-model/async/zero-length.wast | 223 ++++++++ 24 files changed, 5116 insertions(+) create mode 100644 packages/jco/test/fixtures/wat/component-model/async/async-calls-sync.wast create mode 100644 packages/jco/test/fixtures/wat/component-model/async/cancel-stream.wast create mode 100644 packages/jco/test/fixtures/wat/component-model/async/cancel-subtask.wast create mode 100644 packages/jco/test/fixtures/wat/component-model/async/cancellable.wast create mode 100644 packages/jco/test/fixtures/wat/component-model/async/closed-stream.wast create mode 100644 packages/jco/test/fixtures/wat/component-model/async/cross-abi-calls.wast create mode 100644 packages/jco/test/fixtures/wat/component-model/async/deadlock.wast create mode 100644 packages/jco/test/fixtures/wat/component-model/async/dont-block-start.wast create mode 100644 packages/jco/test/fixtures/wat/component-model/async/drop-cross-task-borrow.wast create mode 100644 packages/jco/test/fixtures/wat/component-model/async/drop-stream.wast create mode 100644 packages/jco/test/fixtures/wat/component-model/async/drop-subtask.wast create mode 100644 packages/jco/test/fixtures/wat/component-model/async/drop-waitable-set.wast create mode 100644 packages/jco/test/fixtures/wat/component-model/async/empty-wait.wast create mode 100644 packages/jco/test/fixtures/wat/component-model/async/futures-must-write.wast create mode 100644 packages/jco/test/fixtures/wat/component-model/async/partial-stream-copies.wast create mode 100644 packages/jco/test/fixtures/wat/component-model/async/passing-resources.wast create mode 100644 packages/jco/test/fixtures/wat/component-model/async/same-component-stream-future.wast create mode 100644 packages/jco/test/fixtures/wat/component-model/async/sync-barges-in.wast create mode 100644 packages/jco/test/fixtures/wat/component-model/async/sync-streams.wast create mode 100644 packages/jco/test/fixtures/wat/component-model/async/trap-if-block-and-sync.wast create mode 100644 packages/jco/test/fixtures/wat/component-model/async/trap-if-done.wast create mode 100644 packages/jco/test/fixtures/wat/component-model/async/trap-on-reenter.wast create mode 100644 packages/jco/test/fixtures/wat/component-model/async/wait-during-callback.wast create mode 100644 packages/jco/test/fixtures/wat/component-model/async/zero-length.wast diff --git a/packages/jco/test/fixtures/wat/component-model/async/async-calls-sync.wast b/packages/jco/test/fixtures/wat/component-model/async/async-calls-sync.wast new file mode 100644 index 000000000..5172bb401 --- /dev/null +++ b/packages/jco/test/fixtures/wat/component-model/async/async-calls-sync.wast @@ -0,0 +1,251 @@ +;; This test contains 3 components, $AsyncInner, $SyncMiddle and $AsyncOuter, +;; where there are two instances of $SyncMiddle that import a single instance +;; of $AsyncInner, and $AsyncOuter imports all 3 preceding instances. +;; +;; $AsyncOuter.run asynchronously calls $SyncMiddle.sync-func twice concurrently +;; in each instance (4 total calls), hitting the synchronous backpressure case +;; in 2 of the 4 calls. +;; +;; $SyncMiddle.sync-func makes a blocking call to $AsyncInner.blocking-call +;; which is used to emulate a host call that blocks until $AsyncOuter.run +;; calls $AsyncInner.unblock to unblock all the 'blocking-call' calls. +(component + (component $AsyncInner + (core module $CoreAsyncInner + (import "" "context.set" (func $context.set (param i32))) + (import "" "context.get" (func $context.get (result i32))) + (import "" "task.return0" (func $task.return0)) + (import "" "task.return1" (func $task.return1 (param i32))) + + (memory 1) + (global $blocked (mut i32) (i32.const 1)) + (global $counter (mut i32) (i32.const 2)) + + ;; 'blocking-call' cooperatively "spin-waits" until $blocked is 0. + (func $blocking-call (export "blocking-call") (result i32) + (call $context.set (global.get $counter)) + (global.set $counter (i32.add (i32.const 1) (global.get $counter))) + (i32.const 1 (; YIELD ;)) + ) + (func $blocking-call-cb (export "blocking-call-cb") (param i32 i32 i32) (result i32) + (if (i32.eqz (global.get $blocked)) (then + (call $task.return1 (call $context.get)) + (return (i32.const 0 (; EXIT ;))) + )) + (i32.const 1 (; YIELD ;)) + ) + (func $unblock (export "unblock") (result i32) + (global.set $blocked (i32.const 0)) + (call $task.return0) + (i32.const 0 (; EXIT ;)) + ) + (func $unblock-cb (export "unblock-cb") (param i32 i32 i32) (result i32) + unreachable + ) + ) + (canon task.return (core func $task.return0)) + (canon task.return (result u32) (core func $task.return1)) + (canon context.set i32 0 (core func $context.set)) + (canon context.get i32 0 (core func $context.get)) + (core instance $core_async_inner (instantiate $CoreAsyncInner (with "" (instance + (export "task.return0" (func $task.return0)) + (export "task.return1" (func $task.return1)) + (export "context.set" (func $context.set)) + (export "context.get" (func $context.get)) + )))) + (func (export "blocking-call") async (result u32) (canon lift + (core func $core_async_inner "blocking-call") + async (callback (func $core_async_inner "blocking-call-cb")) + )) + (func (export "unblock") async (canon lift + (core func $core_async_inner "unblock") + async (callback (func $core_async_inner "unblock-cb")) + )) + ) + + (component $SyncMiddle + (import "blocking-call" (func $blocking-call async (result u32))) + (core module $CoreSyncMiddle + (import "" "blocking-call" (func $blocking-call (result i32))) + (func $sync-func (export "sync-func") (result i32) + (call $blocking-call) + ) + ) + (canon lower (func $blocking-call) (core func $blocking-call')) + (core instance $core_sync_middle (instantiate $CoreSyncMiddle (with "" (instance + (export "blocking-call" (func $blocking-call')) + )))) + (func (export "sync-func") async (result u32) (canon lift + (core func $core_sync_middle "sync-func") + )) + ) + + (component $AsyncMiddle + (import "blocking-call" (func $blocking-call async (result u32))) + (core module $CoreSyncMiddle + (import "" "task.return" (func $task.return (param i32))) + (import "" "blocking-call" (func $blocking-call (result i32))) + (func $sync-func (export "sync-func") (result i32) + (call $task.return (call $blocking-call)) + (i32.const 0 (; EXIT ;)) + ) + (func $sync-func-cb (export "sync-func-cb") (param i32 i32 i32) (result i32) + unreachable + ) + ) + (canon task.return (result u32) (core func $task.return)) + (canon lower (func $blocking-call) (core func $blocking-call')) + (core instance $core_sync_middle (instantiate $CoreSyncMiddle (with "" (instance + (export "task.return" (func $task.return)) + (export "blocking-call" (func $blocking-call')) + )))) + (func (export "sync-func") async (result u32) (canon lift + (core func $core_sync_middle "sync-func") + async (callback (func $core_sync_middle "sync-func-cb")) + )) + ) + + (component $AsyncOuter + (import "unblock" (func $unblock async)) + (import "sync-func1" (func $sync-func1 async (result u32))) + (import "sync-func2" (func $sync-func2 async (result u32))) + + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $CoreAsyncOuter + (import "" "mem" (memory 1)) + (import "" "task.return" (func $task.return (param i32))) + (import "" "subtask.drop" (func $subtask.drop (param i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "unblock" (func $unblock)) + (import "" "sync-func1" (func $sync-func1 (param i32) (result i32))) + (import "" "sync-func2" (func $sync-func2 (param i32) (result i32))) + + (global $ws (mut i32) (i32.const 0)) + (func $start (global.set $ws (call $waitable-set.new))) + (start $start) + + ;; set $remain to the number of tasks to wait to complete + (global $remain (mut i32) (i32.const 4)) + + (func $run (export "run") (result i32) + (local $ret i32) + + ;; call 'sync-func1' and 'sync-func2' asynchronously, both of which will block + ;; (on $AsyncInner.blocking-call). because 'sync-func1/2' are in different instances, + ;; both calls will reach the STARTED state. + (local.set $ret (call $sync-func1 (i32.const 8))) + (if (i32.ne (i32.const 0x21 (; STARTED=1 | (subtask=2 << 4) ;)) (local.get $ret)) + (then unreachable)) + (call $waitable.join (i32.const 2) (global.get $ws)) + (local.set $ret (call $sync-func2 (i32.const 12))) + (if (i32.ne (i32.const 0x31 (; STARTED=1 | (subtask=3 << 4) ;)) (local.get $ret)) + (then unreachable)) + (call $waitable.join (i32.const 3) (global.get $ws)) + + ;; now start another pair of 'sync-func1/2' calls, both of which should see auto + ;; backpressure and get stuck in the STARTING state. + (local.set $ret (call $sync-func1 (i32.const 16))) + (if (i32.ne (i32.const 0x40 (; STARTING=0 | (subtask=4 << 4) ;)) (local.get $ret)) + (then unreachable)) + (call $waitable.join (i32.const 4) (global.get $ws)) + (local.set $ret (call $sync-func2 (i32.const 20))) + (if (i32.ne (i32.const 0x50 (; STARTING=0 | (subtask=5 << 4) ;)) (local.get $ret)) + (then unreachable)) + (call $waitable.join (i32.const 5) (global.get $ws)) + + ;; unblock all the tasks and start waiting to complete + (call $unblock) + (i32.or (i32.const 2 (; WAIT ;)) (i32.shl (global.get $ws) (i32.const 4))) + ) + (func $run-cb (export "run-cb") (param $event_code i32) (param $index i32) (param $payload i32) (result i32) + (local $ret i32) + + ;; confirm we only receive SUBTASK events. + (if (i32.ne (local.get $event_code) (i32.const 1 (; SUBTASK ;))) + (then unreachable)) + + ;; if we receive a SUBTASK STARTED event, it should only be for the 3rd or + ;; 4th subtask (at indices 4/5, resp), so keep waiting for completion + (if (i32.eq (local.get $payload) (i32.const 1 (; STARTED ;))) (then + (if (i32.and + (i32.ne (local.get $index) (i32.const 4)) + (i32.ne (local.get $index) (i32.const 5))) + (then unreachable)) + (return (i32.or (i32.const 2 (; WAIT ;)) (i32.shl (global.get $ws) (i32.const 4)))) + )) + + ;; when we receive a SUBTASK RETURNED event, check the return value is equal to the + ;; subtask index (which we've ensured by having $AsyncInner.$counter start at 2, the + ;; first subtask index. The address of the return buffer is the index*4. + (if (i32.ne (local.get $payload) (i32.const 2 (; RETURNED ;))) + (then unreachable)) + (if (i32.ne (local.get $index) (i32.load (i32.mul (local.get $index) (i32.const 4)))) + (then unreachable)) + + ;; decrement $remain and exit if 0 + (call $subtask.drop (local.get $index)) + (global.set $remain (i32.sub (global.get $remain) (i32.const 1))) + (if (i32.gt_u (global.get $remain) (i32.const 0)) (then + (return (i32.or (i32.const 2 (; WAIT ;)) (i32.shl (global.get $ws) (i32.const 4)))) + )) + (call $task.return (i32.const 42)) + (i32.const 0 (; EXIT ;)) + ) + ) + (canon task.return (result u32) (core func $task.return)) + (canon subtask.drop (core func $subtask.drop)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon lower (func $unblock) (core func $unblock)) + (canon lower (func $sync-func1) async (memory $memory "mem") (core func $sync-func1')) + (canon lower (func $sync-func2) async (memory $memory "mem") (core func $sync-func2')) + (core instance $em (instantiate $CoreAsyncOuter (with "" (instance + (export "mem" (memory $memory "mem")) + (export "task.return" (func $task.return)) + (export "subtask.drop" (func $subtask.drop)) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "unblock" (func $unblock)) + (export "sync-func1" (func $sync-func1')) + (export "sync-func2" (func $sync-func2')) + )))) + (func (export "run") async (result u32) (canon lift + (core func $em "run") + async (callback (func $em "run-cb")) + )) + ) + + ;; run1 uses $SyncMiddle + (instance $async_inner1 (instantiate $AsyncInner)) + (instance $sync_middle11 (instantiate $SyncMiddle + (with "blocking-call" (func $async_inner1 "blocking-call")) + )) + (instance $sync_middle12 (instantiate $SyncMiddle + (with "blocking-call" (func $async_inner1 "blocking-call")) + )) + (instance $async_outer1 (instantiate $AsyncOuter + (with "unblock" (func $async_inner1 "unblock")) + (with "sync-func1" (func $sync_middle11 "sync-func")) + (with "sync-func2" (func $sync_middle12 "sync-func")) + )) + (func (export "run1") (alias export $async_outer1 "run")) + + ;; run2 uses $AsyncMiddle + (instance $async_inner2 (instantiate $AsyncInner)) + (instance $sync_middle21 (instantiate $SyncMiddle + (with "blocking-call" (func $async_inner2 "blocking-call")) + )) + (instance $sync_middle22 (instantiate $AsyncMiddle + (with "blocking-call" (func $async_inner2 "blocking-call")) + )) + (instance $async_outer2 (instantiate $AsyncOuter + (with "unblock" (func $async_inner2 "unblock")) + (with "sync-func1" (func $sync_middle21 "sync-func")) + (with "sync-func2" (func $sync_middle22 "sync-func")) + )) + (func (export "run2") (alias export $async_outer2 "run")) +) +(assert_return (invoke "run1") (u32.const 42)) +(assert_return (invoke "run2") (u32.const 42)) diff --git a/packages/jco/test/fixtures/wat/component-model/async/cancel-stream.wast b/packages/jco/test/fixtures/wat/component-model/async/cancel-stream.wast new file mode 100644 index 000000000..336b5acb8 --- /dev/null +++ b/packages/jco/test/fixtures/wat/component-model/async/cancel-stream.wast @@ -0,0 +1,202 @@ +;; This test contains two components $C and $D that test cancelling reads +;; and writes in the presence and absence of partial reads/writes. +;; +;; $C exports a function 'start-stream' that creates and holds onto a writable +;; stream in the global $sw as well as various operations that operate on $sw. +;; $D calls $C.start-stream to get the readable end and then drives the test. +(component + (component $C + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $CM + (import "" "mem" (memory 1)) + (import "" "task.return" (func $task.return (param i32))) + (import "" "stream.new" (func $stream.new (result i64))) + (import "" "stream.write" (func $stream.write (param i32 i32 i32) (result i32))) + (import "" "stream.cancel-write" (func $stream.cancel-write (param i32) (result i32))) + (import "" "stream.drop-writable" (func $stream.drop-writable (param i32))) + + (global $sw (mut i32) (i32.const 0)) + + (func $start-stream (export "start-stream") (result i32) + ;; create a new stream, return the readable end to the caller + (local $ret64 i64) + (local.set $ret64 (call $stream.new)) + (global.set $sw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + (i32.wrap_i64 (local.get $ret64)) + ) + + (func $write4 (export "write4") + ;; write 6 bytes into the stream, expecting to rendezvous with a stream.read + (local $ret i32) + (i32.store (i32.const 8) (i32.const 0xabcd)) + (local.set $ret (call $stream.write (global.get $sw) (i32.const 8) (i32.const 4))) + (if (i32.ne (i32.const 0x40 (; COMPLETED=0 | (4<<4) ;)) (local.get $ret)) + (then unreachable)) + ) + + (func $write4-and-drop (export "write4-and-drop") + (call $write4) + (call $stream.drop-writable (global.get $sw)) + ) + + (func $start-blocking-write (export "start-blocking-write") + (local $ret i32) + + ;; prepare the write buffer + (i64.store (i32.const 8) (i64.const 0x123456789abcdef)) + + ;; start one blocking write and immediately cancel it + (local.set $ret (call $stream.write (global.get $sw) (i32.const 8) (i32.const 8))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (local.set $ret (call $stream.cancel-write (global.get $sw))) + (if (i32.ne (i32.const 0x2 (; CANCELLED ;)) (local.get $ret)) + (then unreachable)) + + ;; start a second blockign write and leave it pending + (local.set $ret (call $stream.write (global.get $sw) (i32.const 8) (i32.const 8))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + ) + + (func $cancel-after-read4 (export "cancel-after-read4") + (local $ret i32) + (local.set $ret (call $stream.cancel-write (global.get $sw))) + (if (i32.ne (i32.const 0x42 (; CANCELLED=2 | (4<<4) ;)) (local.get $ret)) + (then unreachable)) + ) + ) + (type $ST (stream u8)) + (canon task.return (result u32) (core func $task.return)) + (canon stream.new $ST (core func $stream.new)) + (canon stream.write $ST async (memory $memory "mem") (core func $stream.write)) + (canon stream.cancel-write $ST (core func $stream.cancel-write)) + (canon stream.drop-writable $ST (core func $stream.drop-writable)) + (core instance $cm (instantiate $CM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "task.return" (func $task.return)) + (export "stream.new" (func $stream.new)) + (export "stream.write" (func $stream.write)) + (export "stream.cancel-write" (func $stream.cancel-write)) + (export "stream.drop-writable" (func $stream.drop-writable)) + )))) + (func (export "start-stream") async (result (stream u8)) (canon lift (core func $cm "start-stream"))) + (func (export "write4") async (canon lift (core func $cm "write4"))) + (func (export "write4-and-drop") async (canon lift (core func $cm "write4-and-drop"))) + (func (export "start-blocking-write") async (canon lift (core func $cm "start-blocking-write"))) + (func (export "cancel-after-read4") async (canon lift (core func $cm "cancel-after-read4"))) + ) + + (component $D + (import "c" (instance $c + (export "start-stream" (func async (result (stream u8)))) + (export "write4" (func async)) + (export "write4-and-drop" (func async)) + (export "start-blocking-write" (func async)) + (export "cancel-after-read4" (func async)) + )) + + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $DM + (import "" "mem" (memory 1)) + (import "" "stream.read" (func $stream.read (param i32 i32 i32) (result i32))) + (import "" "stream.cancel-read" (func $stream.cancel-read (param i32) (result i32))) + (import "" "stream.drop-readable" (func $stream.drop-readable (param i32))) + (import "" "start-stream" (func $start-stream (result i32))) + (import "" "write4" (func $write4)) + (import "" "write4-and-drop" (func $write4-and-drop)) + (import "" "start-blocking-write" (func $start-blocking-write)) + (import "" "cancel-after-read4" (func $cancel-after-read4)) + + (func $run (export "run") (result i32) + (local $ret i32) + (local $sr i32) + + ;; call 'start-stream' to get the stream we'll be working with + (local.set $sr (call $start-stream)) + (if (i32.ne (i32.const 1) (local.get $sr)) + (then unreachable)) + + ;; start read that will block + (local.set $ret (call $stream.read (local.get $sr) (i32.const 8) (i32.const 100))) + (if (i32.ne (i32.const -1 (; BLOCKED;)) (local.get $ret)) + (then unreachable)) + + ;; cancelling it will finish without anything having been written + (local.set $ret (call $stream.cancel-read (local.get $sr))) + (if (i32.ne (i32.const 0x2 (; CANCELLED ;)) (local.get $ret)) + (then unreachable)) + + ;; read, block, call $C to write 4 bytes into the buffer, + ;; then cancel, which should show "4+cancelled" + (local.set $ret (call $stream.read (local.get $sr) (i32.const 8) (i32.const 100))) + (if (i32.ne (i32.const -1 (; BLOCKED;)) (local.get $ret)) + (then unreachable)) + (call $write4) + (local.set $ret (call $stream.cancel-read (local.get $sr))) + (if (i32.ne (i32.const 0x42 (; CANCELLED=2 | (4<<4) ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 0xabcd) (i32.load (i32.const 8))) + (then unreachable)) + + ;; read, block, call $C to write 4 bytes into the buffer and drop, then cancel + (local.set $ret (call $stream.read (local.get $sr) (i32.const 8) (i32.const 100))) + (if (i32.ne (i32.const -1 (; BLOCKED;)) (local.get $ret)) + (then unreachable)) + (call $write4-and-drop) + (local.set $ret (call $stream.cancel-read (local.get $sr))) + (if (i32.ne (i32.const 0x41 (; DROPPED=1 | (4<<4) ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 0xabcd) (i32.load (i32.const 8))) + (then unreachable)) + (call $stream.drop-readable (local.get $sr)) + + ;; get a new $sr + (local.set $sr (call $start-stream)) + (if (i32.ne (i32.const 1) (local.get $sr)) + (then unreachable)) + + ;; start outstanding write in $C, read 4 of it, then call back into $C + ;; which will cancel and see 4 written. + (call $start-blocking-write) + (local.set $ret (call $stream.read (local.get $sr) (i32.const 8) (i32.const 4))) + (if (i32.ne (i32.const 0x40 (; COMPLETED=0 | (4<<4) ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 0x89abcdef) (i32.load (i32.const 8))) + (then unreachable)) + (call $cancel-after-read4) + + ;; return 42 to the top-level assert_return + (i32.const 42) + ) + ) + (type $ST (stream u8)) + (canon stream.read $ST async (memory $memory "mem") (core func $stream.read)) + (canon stream.cancel-read $ST (core func $stream.cancel-read)) + (canon stream.drop-readable $ST (core func $stream.drop-readable)) + (canon lower (func $c "start-stream") (core func $start-stream')) + (canon lower (func $c "write4") (core func $write4')) + (canon lower (func $c "write4-and-drop") (core func $write4-and-drop')) + (canon lower (func $c "start-blocking-write") (core func $start-blocking-write')) + (canon lower (func $c "cancel-after-read4") (core func $cancel-after-read4')) + (core instance $dm (instantiate $DM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "stream.read" (func $stream.read)) + (export "stream.cancel-read" (func $stream.cancel-read)) + (export "stream.drop-readable" (func $stream.drop-readable)) + (export "start-stream" (func $start-stream')) + (export "write4" (func $write4')) + (export "write4-and-drop" (func $write4-and-drop')) + (export "start-blocking-write" (func $start-blocking-write')) + (export "cancel-after-read4" (func $cancel-after-read4')) + )))) + (func (export "run") async (result u32) (canon lift (core func $dm "run"))) + ) + + (instance $c (instantiate $C)) + (instance $d (instantiate $D (with "c" (instance $c)))) + (func (export "run") (alias export $d "run")) +) +(assert_return (invoke "run") (u32.const 42)) diff --git a/packages/jco/test/fixtures/wat/component-model/async/cancel-subtask.wast b/packages/jco/test/fixtures/wat/component-model/async/cancel-subtask.wast new file mode 100644 index 000000000..57a0f33e2 --- /dev/null +++ b/packages/jco/test/fixtures/wat/component-model/async/cancel-subtask.wast @@ -0,0 +1,201 @@ +;; This test contains two components $C and $D where $D imports and calls $C. +;; $D.run calls $C.f, which blocks on an empty waitable set +;; $D.run then subtask.cancels $C.f, which resumes $C.f which promptly resolves +;; without returning a value. +(component + (component $C + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $CM + (import "" "mem" (memory 1)) + (import "" "task.cancel" (func $task.cancel)) + (import "" "future.read" (func $future.read (param i32 i32) (result i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + + ;; $ws is waited on by 'f' + (global $ws (mut i32) (i32.const 0)) + (func $start (global.set $ws (call $waitable-set.new))) + (start $start) + + (func $f (export "f") (result i32) + ;; wait on $ws which is currently empty, expected to get cancelled + (i32.or (i32.const 2 (; WAIT ;)) (i32.shl (global.get $ws) (i32.const 4))) + ) + (func $f_cb (export "f_cb") (param $event_code i32) (param $index i32) (param $payload i32) (result i32) + ;; confirm that we've received a cancellation request + (if (i32.ne (local.get $event_code) (i32.const 6 (; TASK_CANCELLED ;))) + (then unreachable)) + (if (i32.ne (local.get $index) (i32.const 0)) + (then unreachable)) + (if (i32.ne (local.get $payload) (i32.const 0)) + (then unreachable)) + + ;; finish without returning a value + (call $task.cancel) + (i32.const 0 (; EXIT ;)) + ) + + (func $g (export "g") (param $futr i32) (result i32) + (local $ret i32) + (local $event_code i32) + + ;; perform a future.read which will block, waiting for the caller to write + (local.set $ret (call $future.read (local.get $futr) (i32.const 0xdeadbeef))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (call $waitable.join (local.get $futr) (global.get $ws)) + + ;; wait on $ws synchronously, don't expect cancellation + (local.set $event_code (call $waitable-set.wait (global.get $ws) (i32.const 0))) + (if (i32.ne (i32.const 4 (; FUTURE_READ ;)) (local.get $event_code)) + (then unreachable)) + + ;; finish returning a value + (i32.const 42) + ) + ) + (type $FT (future)) + (canon task.cancel (core func $task.cancel)) + (canon future.read $FT async (memory $memory "mem") (core func $future.read)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.wait (memory $memory "mem") (core func $waitable-set.wait)) + (core instance $cm (instantiate $CM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "task.cancel" (func $task.cancel)) + (export "future.read" (func $future.read)) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.wait" (func $waitable-set.wait)) + )))) + (func (export "f") async (result u32) (canon lift + (core func $cm "f") + async (callback (func $cm "f_cb")) + )) + (func (export "g") async (param "fut" $FT) (result u32) (canon lift + (core func $cm "g") + )) + ) + + (component $D + (type $FT (future)) + (import "f" (func $f async (result u32))) + (import "g" (func $g async (param "fut" $FT) (result u32))) + + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $DM + (import "" "mem" (memory 1)) + (import "" "subtask.cancel" (func $subtask.cancel (param i32) (result i32))) + (import "" "subtask.drop" (func $subtask.drop (param i32))) + (import "" "future.new" (func $future.new (result i64))) + (import "" "future.write" (func $future.write (param i32 i32) (result i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + (import "" "f" (func $f (param i32) (result i32))) + (import "" "g" (func $g (param i32 i32) (result i32))) + + (func $run (export "run") (result i32) + (local $ret i32) (local $ret64 i64) + (local $retp i32) (local $retp1 i32) (local $retp2 i32) + (local $subtask i32) + (local $event_code i32) + (local $futr i32) (local $futw i32) + (local $ws i32) + + ;; call 'f'; it should block + (local.set $retp (i32.const 4)) + (i32.store (local.get $retp) (i32.const 0xbad0bad0)) + (local.set $ret (call $f (local.get $retp))) + (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) + (then unreachable)) + (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) + + ;; cancel 'f'; it should complete without blocking + (local.set $ret (call $subtask.cancel (local.get $subtask))) + (if (i32.ne (i32.const 4 (; CANCELLED_BEFORE_RETURNED ;)) (local.get $ret)) + (then unreachable)) + + ;; The $retp memory shouldn't have changed + (if (i32.ne (i32.load (local.get $retp)) (i32.const 0xbad0bad0)) + (then unreachable)) + + (call $subtask.drop (local.get $subtask)) + + ;; create future that g will wait on + (local.set $ret64 (call $future.new)) + (local.set $futr (i32.wrap_i64 (local.get $ret64))) + (local.set $futw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + + ;; call 'g'; it should block + (local.set $retp1 (i32.const 4)) + (local.set $retp2 (i32.const 8)) + (i32.store (local.get $retp1) (i32.const 0xbad0bad0)) + (i32.store (local.get $retp2) (i32.const 0xbad0bad0)) + (local.set $ret (call $g (local.get $futr) (local.get $retp1))) + (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) + (then unreachable)) + (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) + + ;; cancel 'g'; it should block + (local.set $ret (call $subtask.cancel (local.get $subtask))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + + ;; future.write, unblocking 'g' + (local.set $ret (call $future.write (local.get $futw) (i32.const 0xdeadbeef))) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (local.get $ret)) + (then unreachable)) + + ;; wait to see 'g' finish and check its return value + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (local.get $subtask) (local.get $ws)) + (local.set $event_code (call $waitable-set.wait (local.get $ws) (local.get $retp2))) + (if (i32.ne (i32.const 1 (; SUBTASK ;)) (local.get $event_code)) + (then unreachable)) + (if (i32.ne (local.get $subtask) (i32.load (local.get $retp2))) + (then unreachable)) + (if (i32.ne (i32.const 2 (; RETURNED=2 | (0<<4) ;)) (i32.load offset=4 (local.get $retp2))) + (then unreachable)) + (if (i32.ne (i32.const 42) (i32.load (local.get $retp1))) + (then unreachable)) + + ;; return to the top-level assert_return + (i32.const 42) + ) + ) + (canon subtask.cancel async (core func $subtask.cancel)) + (canon subtask.drop (core func $subtask.drop)) + (canon future.new $FT (core func $future.new)) + (canon future.write $FT async (memory $memory "mem") (core func $future.write)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.wait (memory $memory "mem") (core func $waitable-set.wait)) + (canon lower (func $f) async (memory $memory "mem") (core func $f')) + (canon lower (func $g) async (memory $memory "mem") (core func $g')) + (core instance $dm (instantiate $DM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "subtask.cancel" (func $subtask.cancel)) + (export "subtask.drop" (func $subtask.drop)) + (export "future.new" (func $future.new)) + (export "future.write" (func $future.write)) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.wait" (func $waitable-set.wait)) + (export "f" (func $f')) + (export "g" (func $g')) + )))) + (func (export "run") async (result u32) (canon lift (core func $dm "run"))) + ) + + (instance $c (instantiate $C)) + (instance $d (instantiate $D + (with "f" (func $c "f")) + (with "g" (func $c "g")) + )) + (func (export "run") (alias export $d "run")) +) +(assert_return (invoke "run") (u32.const 42)) diff --git a/packages/jco/test/fixtures/wat/component-model/async/cancellable.wast b/packages/jco/test/fixtures/wat/component-model/async/cancellable.wast new file mode 100644 index 000000000..2c5614cb7 --- /dev/null +++ b/packages/jco/test/fixtures/wat/component-model/async/cancellable.wast @@ -0,0 +1,325 @@ +;; This test exercises the 'cancellable' immediate on waitable-set.wait, +;; waitable-set.poll, and thread.yield. +;; +;; Component $C exports five async callback-lifted functions that block in +;; their initial core function (the callbacks are never invoked): +;; wait-cancel: blocks on cancellable waitable-set.wait, expects TASK_CANCELLED +;; yield-cancel: yields with cancellable, caller cancels during yield +;; poll-cancel-pending: blocks on non-cancellable wait, then polls with cancellable +;; yield-cancel-pending: blocks on non-cancellable wait, then yields with cancellable +;; +;; Component $D calls each function and cancels it, verifying the cancel is +;; delivered correctly through the cancellable built-in in each case. +(component + (component $C + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $CM + (import "" "mem" (memory 1)) + (import "" "task.cancel" (func $task.cancel)) + (import "" "future.read" (func $future.read (param i32 i32) (result i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.wait-cancellable" (func $waitable-set.wait-cancellable (param i32 i32) (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + (import "" "waitable-set.poll-cancellable" (func $waitable-set.poll-cancellable (param i32 i32) (result i32))) + (import "" "thread.yield-cancellable" (func $thread.yield-cancellable (result i32))) + + ;; Test 1: direct cancel delivery through cancellable waitable-set.wait + (func $wait-cancel (export "wait-cancel") (result i32) + (local $event_code i32) + (local $ws i32) + (local.set $ws (call $waitable-set.new)) + ;; wait on empty waitable set with cancellable; blocks until cancelled + (local.set $event_code (call $waitable-set.wait-cancellable (local.get $ws) (i32.const 0))) + (if (i32.ne (local.get $event_code) (i32.const 6 (; TASK_CANCELLED ;))) + (then unreachable)) + (call $task.cancel) + (i32.const 0 (; EXIT ;)) + ) + + ;; Test 2: direct cancel delivery through cancellable thread.yield + (func $yield-cancel (export "yield-cancel") (result i32) + (local $ret i32) + ;; yield with cancellable; suspends with cancellable=true, caller cancels + (local.set $ret (call $thread.yield-cancellable)) + (if (i32.ne (i32.const 1 (; CANCELLED ;)) (local.get $ret)) + (then unreachable)) + (call $task.cancel) + (i32.const 0 (; EXIT ;)) + ) + + ;; Test 3: deferred cancel delivered through cancellable waitable-set.poll + (func $poll-cancel-pending (export "poll-cancel-pending") (param $futr i32) (result i32) + (local $ws i32) + (local $ret i32) + (local $event_code i32) + (local.set $ws (call $waitable-set.new)) + ;; read future - blocks (caller hasn't written yet) + (local.set $ret (call $future.read (local.get $futr) (i32.const 0))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (call $waitable.join (local.get $futr) (local.get $ws)) + ;; wait WITHOUT cancellable - cancel will be deferred as PENDING_CANCEL + (local.set $event_code (call $waitable-set.wait (local.get $ws) (i32.const 0))) + (if (i32.ne (i32.const 4 (; FUTURE_READ ;)) (local.get $event_code)) + (then unreachable)) + ;; poll WITH cancellable - delivers the pending cancel + (local.set $event_code (call $waitable-set.poll-cancellable (local.get $ws) (i32.const 0))) + (if (i32.ne (i32.const 6 (; TASK_CANCELLED ;)) (local.get $event_code)) + (then unreachable)) + (call $task.cancel) + (i32.const 0 (; EXIT ;)) + ) + + ;; Test 4: deferred cancel delivered through cancellable thread.yield + (func $yield-cancel-pending (export "yield-cancel-pending") (param $futr i32) (result i32) + (local $ws i32) + (local $ret i32) + (local $event_code i32) + (local.set $ws (call $waitable-set.new)) + ;; read future - blocks (caller hasn't written yet) + (local.set $ret (call $future.read (local.get $futr) (i32.const 0))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (call $waitable.join (local.get $futr) (local.get $ws)) + ;; wait WITHOUT cancellable - cancel will be deferred as PENDING_CANCEL + (local.set $event_code (call $waitable-set.wait (local.get $ws) (i32.const 0))) + (if (i32.ne (i32.const 4 (; FUTURE_READ ;)) (local.get $event_code)) + (then unreachable)) + ;; yield WITH cancellable - delivers the pending cancel + (local.set $ret (call $thread.yield-cancellable)) + (if (i32.ne (i32.const 1 (; CANCELLED ;)) (local.get $ret)) + (then unreachable)) + (call $task.cancel) + (i32.const 0 (; EXIT ;)) + ) + + ;; callback that should never be called + (func (export "unreachable-cb") (param i32 i32 i32) (result i32) + unreachable + ) + ) + (type $FT (future)) + (canon task.cancel (core func $task.cancel)) + (canon future.read $FT async (memory $memory "mem") (core func $future.read)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.wait cancellable (memory $memory "mem") (core func $waitable-set.wait-cancellable)) + (canon waitable-set.wait (memory $memory "mem") (core func $waitable-set.wait)) + (canon waitable-set.poll cancellable (memory $memory "mem") (core func $waitable-set.poll-cancellable)) + (canon thread.yield cancellable (core func $thread.yield-cancellable)) + (core instance $cm (instantiate $CM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "task.cancel" (func $task.cancel)) + (export "future.read" (func $future.read)) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.wait-cancellable" (func $waitable-set.wait-cancellable)) + (export "waitable-set.wait" (func $waitable-set.wait)) + (export "waitable-set.poll-cancellable" (func $waitable-set.poll-cancellable)) + (export "thread.yield-cancellable" (func $thread.yield-cancellable)) + )))) + (func (export "wait-cancel") async (result u32) (canon lift + (core func $cm "wait-cancel") + async (callback (func $cm "unreachable-cb")) + )) + (func (export "yield-cancel") async (result u32) (canon lift + (core func $cm "yield-cancel") + async (callback (func $cm "unreachable-cb")) + )) + (func (export "poll-cancel-pending") async (param "fut" $FT) (result u32) (canon lift + (core func $cm "poll-cancel-pending") + async (callback (func $cm "unreachable-cb")) + )) + (func (export "yield-cancel-pending") async (param "fut" $FT) (result u32) (canon lift + (core func $cm "yield-cancel-pending") + async (callback (func $cm "unreachable-cb")) + )) + ) + + (component $D + (type $FT (future)) + (import "wait-cancel" (func $wait-cancel async (result u32))) + (import "yield-cancel" (func $yield-cancel async (result u32))) + (import "poll-cancel-pending" (func $poll-cancel-pending async (param "fut" $FT) (result u32))) + (import "yield-cancel-pending" (func $yield-cancel-pending async (param "fut" $FT) (result u32))) + + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $DM + (import "" "mem" (memory 1)) + (import "" "subtask.cancel" (func $subtask.cancel (param i32) (result i32))) + (import "" "subtask.drop" (func $subtask.drop (param i32))) + (import "" "future.new" (func $future.new (result i64))) + (import "" "future.write" (func $future.write (param i32 i32) (result i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + (import "" "wait-cancel" (func $wait-cancel (param i32) (result i32))) + (import "" "yield-cancel" (func $yield-cancel (param i32) (result i32))) + (import "" "poll-cancel-pending" (func $poll-cancel-pending (param i32 i32) (result i32))) + (import "" "yield-cancel-pending" (func $yield-cancel-pending (param i32 i32) (result i32))) + + (func $run (export "run") (result i32) + (local $ret i32) (local $ret64 i64) + (local $retp i32) (local $retp2 i32) + (local $subtask i32) + (local $event_code i32) + (local $futr i32) (local $futw i32) + (local $ws i32) + + ;; ========================================== + ;; Test 1: waitable-set.wait cancellable + ;; ========================================== + + ;; call wait-cancel; it should block in cancellable wait + (local.set $retp (i32.const 4)) + (local.set $ret (call $wait-cancel (local.get $retp))) + (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) + (then unreachable)) + (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) + + ;; cancel; completes immediately (C is in cancellable wait) + (local.set $ret (call $subtask.cancel (local.get $subtask))) + (if (i32.ne (i32.const 4 (; CANCELLED_BEFORE_RETURNED ;)) (local.get $ret)) + (then unreachable)) + (call $subtask.drop (local.get $subtask)) + + ;; ========================================== + ;; Test 2: thread.yield cancellable + ;; ========================================== + + ;; call yield-cancel; it should suspend in cancellable yield + (local.set $ret (call $yield-cancel (local.get $retp))) + (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) + (then unreachable)) + (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) + + ;; cancel; completes immediately (C is in cancellable yield) + (local.set $ret (call $subtask.cancel (local.get $subtask))) + (if (i32.ne (i32.const 4 (; CANCELLED_BEFORE_RETURNED ;)) (local.get $ret)) + ;; TODO: this currently fails in Wasmtime due to cancellable + ;; thread.yield not being directly resumed by subtask.cancel, but it + ;; seems like it should pass: + (then unreachable)) + (call $subtask.drop (local.get $subtask)) + + ;; ========================================== + ;; Test 3: waitable-set.poll cancellable (pending) + ;; ========================================== + + ;; create future for poll-cancel-pending to read + (local.set $ret64 (call $future.new)) + (local.set $futr (i32.wrap_i64 (local.get $ret64))) + (local.set $futw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + + ;; call poll-cancel-pending; it should block in non-cancellable wait + (local.set $retp (i32.const 4)) + (local.set $retp2 (i32.const 8)) + (local.set $ret (call $poll-cancel-pending (local.get $futr) (local.get $retp))) + (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) + (then unreachable)) + (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) + + ;; cancel; blocks because C's wait is not cancellable + (local.set $ret (call $subtask.cancel (local.get $subtask))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + + ;; write to future; unblocks C's non-cancellable wait + (local.set $ret (call $future.write (local.get $futw) (i32.const 0))) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (local.get $ret)) + (then unreachable)) + + ;; wait for subtask to complete + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (local.get $subtask) (local.get $ws)) + (local.set $event_code (call $waitable-set.wait (local.get $ws) (local.get $retp2))) + (if (i32.ne (i32.const 1 (; SUBTASK ;)) (local.get $event_code)) + (then unreachable)) + (if (i32.ne (local.get $subtask) (i32.load (local.get $retp2))) + (then unreachable)) + (if (i32.ne (i32.const 4 (; CANCELLED_BEFORE_RETURNED ;)) (i32.load offset=4 (local.get $retp2))) + (then unreachable)) + (call $subtask.drop (local.get $subtask)) + + ;; ========================================== + ;; Test 4: thread.yield cancellable (pending) + ;; ========================================== + + ;; create future for yield-cancel-pending to read + (local.set $ret64 (call $future.new)) + (local.set $futr (i32.wrap_i64 (local.get $ret64))) + (local.set $futw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + + ;; call yield-cancel-pending; it should block in non-cancellable wait + (local.set $ret (call $yield-cancel-pending (local.get $futr) (local.get $retp))) + (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) + (then unreachable)) + (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) + + ;; cancel; blocks because C's wait is not cancellable + (local.set $ret (call $subtask.cancel (local.get $subtask))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + + ;; write to future; unblocks C's non-cancellable wait + (local.set $ret (call $future.write (local.get $futw) (i32.const 0))) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (local.get $ret)) + (then unreachable)) + + ;; wait for subtask to complete + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (local.get $subtask) (local.get $ws)) + (local.set $event_code (call $waitable-set.wait (local.get $ws) (local.get $retp2))) + (if (i32.ne (i32.const 1 (; SUBTASK ;)) (local.get $event_code)) + (then unreachable)) + (if (i32.ne (local.get $subtask) (i32.load (local.get $retp2))) + (then unreachable)) + (if (i32.ne (i32.const 4 (; CANCELLED_BEFORE_RETURNED ;)) (i32.load offset=4 (local.get $retp2))) + (then unreachable)) + (call $subtask.drop (local.get $subtask)) + + ;; all tests passed + (i32.const 42) + ) + ) + (canon subtask.cancel async (core func $subtask.cancel)) + (canon subtask.drop (core func $subtask.drop)) + (canon future.new $FT (core func $future.new)) + (canon future.write $FT async (memory $memory "mem") (core func $future.write)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.wait (memory $memory "mem") (core func $waitable-set.wait)) + (canon lower (func $wait-cancel) async (memory $memory "mem") (core func $wait-cancel')) + (canon lower (func $yield-cancel) async (memory $memory "mem") (core func $yield-cancel')) + (canon lower (func $poll-cancel-pending) async (memory $memory "mem") (core func $poll-cancel-pending')) + (canon lower (func $yield-cancel-pending) async (memory $memory "mem") (core func $yield-cancel-pending')) + (core instance $dm (instantiate $DM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "subtask.cancel" (func $subtask.cancel)) + (export "subtask.drop" (func $subtask.drop)) + (export "future.new" (func $future.new)) + (export "future.write" (func $future.write)) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.wait" (func $waitable-set.wait)) + (export "wait-cancel" (func $wait-cancel')) + (export "yield-cancel" (func $yield-cancel')) + (export "poll-cancel-pending" (func $poll-cancel-pending')) + (export "yield-cancel-pending" (func $yield-cancel-pending')) + )))) + (func (export "run") async (result u32) (canon lift (core func $dm "run"))) + ) + + (instance $c (instantiate $C)) + (instance $d (instantiate $D + (with "wait-cancel" (func $c "wait-cancel")) + (with "yield-cancel" (func $c "yield-cancel")) + (with "poll-cancel-pending" (func $c "poll-cancel-pending")) + (with "yield-cancel-pending" (func $c "yield-cancel-pending")) + )) + (func (export "run") (alias export $d "run")) +) +(assert_return (invoke "run") (u32.const 42)) diff --git a/packages/jco/test/fixtures/wat/component-model/async/closed-stream.wast b/packages/jco/test/fixtures/wat/component-model/async/closed-stream.wast new file mode 100644 index 000000000..eb462c85f --- /dev/null +++ b/packages/jco/test/fixtures/wat/component-model/async/closed-stream.wast @@ -0,0 +1,102 @@ +;; This test contains two components $C and $D that test that if the writable side +;; of a stream is dropped, the other side registers a STREAM DROPPED status +;; when attempting to read from the stream. +(component definition $Tester + ;; Creates a stream and keeps a handle to the writable end of it. + (component $C + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $CM + (import "" "mem" (memory 1)) + (import "" "stream.new" (func $stream.new (result i64))) + (import "" "stream.write" (func $stream.write (param i32 i32 i32) (result i32))) + (import "" "stream.drop-writable" (func $stream.drop-writable (param i32))) + + ;; Store the writable end of a stream + (global $sw (mut i32) (i32.const 0)) + + ;; Create a new stream, return the readable end to the caller + (func $start-stream (export "start-stream") (result i32) + (local $ret64 i64) + (local.set $ret64 (call $stream.new)) + (global.set $sw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + (i32.wrap_i64 (local.get $ret64)) + ) + + ;; Drop the writable end of a stream + (func $drop-writable (export "drop-writable") + (call $stream.drop-writable (global.get $sw)) + ) + ) + (type $ST (stream u8)) + (canon stream.new $ST (core func $stream.new)) + (canon stream.write $ST async (memory $memory "mem") (core func $stream.write)) + (canon stream.drop-writable $ST (core func $stream.drop-writable)) + (core instance $cm (instantiate $CM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "stream.new" (func $stream.new)) + (export "stream.write" (func $stream.write)) + (export "stream.drop-writable" (func $stream.drop-writable)) + )))) + (func (export "start-stream") (result (stream u8)) (canon lift (core func $cm "start-stream"))) + (func (export "drop-writable") (canon lift (core func $cm "drop-writable"))) + ) + + ;; Gets a readable stream from component $C and calls operations on it. + (component $D + (import "c" (instance $c + (export "start-stream" (func (result (stream u8)))) + (export "drop-writable" (func)) + )) + + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $Core + (import "" "mem" (memory 1)) + (import "" "stream.new" (func $stream.new (result i64))) + (import "" "stream.read" (func $stream.read (param i32 i32 i32) (result i32))) + (import "" "stream.write" (func $stream.write (param i32 i32 i32) (result i32))) + (import "" "stream.drop-writable" (func $stream.drop-writable (param i32))) + (import "" "start-stream" (func $start-stream (result i32))) + (import "" "drop-writable" (func $drop-writable)) + + (func (export "read-from-closed-stream") + (local $ret i32) (local $sr i32) + + ;; call 'start-stream' to get the stream we'll be working with + (local.set $sr (call $start-stream)) + (if (i32.ne (i32.const 1) (local.get $sr)) + (then unreachable)) + + ;; drop the writable end and then attempt to read from it + (call $drop-writable) + (local.set $ret (call $stream.read (local.get $sr) (i32.const 8) (i32.const 4))) + (if (i32.ne (i32.const 1 (; DROPPED ;)) (local.get $ret)) + (then unreachable)) + ) + ) + (type $ST (stream u8)) + (canon stream.new $ST (core func $stream.new)) + (canon stream.read $ST async (memory $memory "mem") (core func $stream.read)) + (canon stream.write $ST async (memory $memory "mem") (core func $stream.write)) + (canon stream.drop-writable $ST (core func $stream.drop-writable)) + (canon lower (func $c "start-stream") (core func $start-stream')) + (canon lower (func $c "drop-writable") (core func $drop-writable')) + (core instance $core (instantiate $Core (with "" (instance + (export "mem" (memory $memory "mem")) + (export "stream.new" (func $stream.new)) + (export "stream.read" (func $stream.read)) + (export "stream.write" (func $stream.write)) + (export "stream.drop-writable" (func $stream.drop-writable)) + (export "start-stream" (func $start-stream')) + (export "drop-writable" (func $drop-writable')) + )))) + (func (export "read-from-closed-stream") (canon lift (core func $core "read-from-closed-stream"))) + ) + (instance $c (instantiate $C)) + (instance $d (instantiate $D (with "c" (instance $c)))) + (func (export "read-from-closed-stream") (alias export $d "read-from-closed-stream")) +) + +(component instance $new-tester-instance $Tester) +(invoke "read-from-closed-stream") diff --git a/packages/jco/test/fixtures/wat/component-model/async/cross-abi-calls.wast b/packages/jco/test/fixtures/wat/component-model/async/cross-abi-calls.wast new file mode 100644 index 000000000..8f1370b22 --- /dev/null +++ b/packages/jco/test/fixtures/wat/component-model/async/cross-abi-calls.wast @@ -0,0 +1,519 @@ +;; This test pairs sync and async-callback calls from $Bottom into $Top, +;; testing different numbers of parameters and results that hit the various +;; flat-vs-heap cases in the spec. +(component definition $C + (component $Top + (core module $Memory + (memory (export "mem") 1) + (func (export "realloc") (param i32 i32 i32 i32) (result i32) + i32.const 0 ;; cheat -- there's never more than 1 allocation alive + ) + ) + (core instance $memory (instantiate $Memory)) + (core module $Core + (import "" "mem" (memory 0)) + (import "" "task.return0" (func $task.return0)) + (import "" "task.return1" (func $task.return1 (param f64))) + (import "" "task.return16" (func $task.return16 (param i32 i64 f32 f64 i32 i64 f32 f64 i32 i64 f32 f64 i32 i64 f32 f64))) + (import "" "task.return17" (func $task.return17 (param i32))) + (func (export "sync-4-param") (param i32 i64 f32 f64) + (if (i32.ne (i32.const 42) (local.get 0)) + (then unreachable)) + (if (i64.ne (i64.const 43) (local.get 1)) + (then unreachable)) + (if (f32.ne (f32.const 44.4) (local.get 2)) + (then unreachable)) + (if (f64.ne (f64.const 45.5) (local.get 3)) + (then unreachable)) + ) + (func (export "sync-5-param") (param i32 i64 f32 f64 i32) + (if (i32.ne (i32.const -1) (local.get 0)) + (then unreachable)) + (if (i64.ne (i64.const -2) (local.get 1)) + (then unreachable)) + (if (f32.ne (f32.const -3.3) (local.get 2)) + (then unreachable)) + (if (f64.ne (f64.const -4.4) (local.get 3)) + (then unreachable)) + (if (i32.ne (i32.const -5) (local.get 4)) + (then unreachable)) + ) + (func $check-17 (param $ptr i32) + (if (i32.ne (i32.const -1) (i32.load (local.get $ptr))) + (then unreachable)) + (if (i64.ne (i64.const -2) (i64.load offset=8 (local.get $ptr))) + (then unreachable)) + (if (f32.ne (f32.const -3.3) (f32.load offset=16 (local.get $ptr))) + (then unreachable)) + (if (f64.ne (f64.const -4.4) (f64.load offset=24 (local.get $ptr))) + (then unreachable)) + (if (i32.ne (i32.const -5) (i32.load offset=32 (local.get $ptr))) + (then unreachable)) + (if (i64.ne (i64.const -6) (i64.load offset=40 (local.get $ptr))) + (then unreachable)) + (if (f32.ne (f32.const -7.7) (f32.load offset=48 (local.get $ptr))) + (then unreachable)) + (if (f64.ne (f64.const -8.8) (f64.load offset=56 (local.get $ptr))) + (then unreachable)) + (if (i32.ne (i32.const -9) (i32.load offset=64 (local.get $ptr))) + (then unreachable)) + (if (i64.ne (i64.const -10) (i64.load offset=72 (local.get $ptr))) + (then unreachable)) + (if (f32.ne (f32.const -11.11) (f32.load offset=80 (local.get $ptr))) + (then unreachable)) + (if (f64.ne (f64.const -12.12) (f64.load offset=88 (local.get $ptr))) + (then unreachable)) + (if (i32.ne (i32.const -13) (i32.load offset=96 (local.get $ptr))) + (then unreachable)) + (if (i64.ne (i64.const -14) (i64.load offset=104 (local.get $ptr))) + (then unreachable)) + (if (f32.ne (f32.const -15.15) (f32.load offset=112 (local.get $ptr))) + (then unreachable)) + (if (f64.ne (f64.const -16.16) (f64.load offset=120 (local.get $ptr))) + (then unreachable)) + (if (i32.ne (i32.const -17) (i32.load offset=128 (local.get $ptr))) + (then unreachable)) + ) + (func (export "sync-17-param") (param $ptr i32) + (call $check-17 (local.get $ptr)) + ) + (func (export "sync-1-result") (result f64) + (f64.const -1.1) + ) + (func $setup-16 (param $ptr i32) + (i32.store (local.get $ptr) (i32.const -1)) + (i64.store offset=8 (local.get $ptr) (i64.const -2)) + (f32.store offset=16 (local.get $ptr) (f32.const -3.3)) + (f64.store offset=24 (local.get $ptr) (f64.const -4.4)) + (i32.store offset=32 (local.get $ptr) (i32.const -5)) + (i64.store offset=40 (local.get $ptr) (i64.const -6)) + (f32.store offset=48 (local.get $ptr) (f32.const -7.7)) + (f64.store offset=56 (local.get $ptr) (f64.const -8.8)) + (i32.store offset=64 (local.get $ptr) (i32.const -9)) + (i64.store offset=72 (local.get $ptr) (i64.const -10)) + (f32.store offset=80 (local.get $ptr) (f32.const -11.11)) + (f64.store offset=88 (local.get $ptr) (f64.const -12.12)) + (i32.store offset=96 (local.get $ptr) (i32.const -13)) + (i64.store offset=104 (local.get $ptr) (i64.const -14)) + (f32.store offset=112 (local.get $ptr) (f32.const -15.15)) + (f64.store offset=120 (local.get $ptr) (f64.const -16.16)) + ) + (func $setup-17 (param $ptr i32) + (call $setup-16 (local.get $ptr)) + (i32.store offset=128 (local.get $ptr) (i32.const -17)) + ) + (func (export "sync-16-result") (result i32) + (local $ptr i32) + (call $setup-16 (local.get $ptr)) + (local.get $ptr) + ) + (func (export "sync-17-result") (result i32) + (local $ptr i32) + (call $setup-17 (local.get $ptr)) + (local.get $ptr) + ) + (func (export "async-4-param") (param i32 i64 f32 f64) (result i32) + (if (i32.ne (i32.const 42) (local.get 0)) + (then unreachable)) + (if (i64.ne (i64.const 43) (local.get 1)) + (then unreachable)) + (if (f32.ne (f32.const 44.4) (local.get 2)) + (then unreachable)) + (if (f64.ne (f64.const 45.5) (local.get 3)) + (then unreachable)) + (call $task.return0) + (i32.const 0 (; EXIT ;)) + ) + (func (export "async-5-param") (param i32 i64 f32 f64 i32) (result i32) + (if (i32.ne (i32.const -1) (local.get 0)) + (then unreachable)) + (if (i64.ne (i64.const -2) (local.get 1)) + (then unreachable)) + (if (f32.ne (f32.const -3.3) (local.get 2)) + (then unreachable)) + (if (f64.ne (f64.const -4.4) (local.get 3)) + (then unreachable)) + (if (i32.ne (i32.const -5) (local.get 4)) + (then unreachable)) + (call $task.return0) + (i32.const 0 (; EXIT ;)) + ) + (func (export "async-17-param") (param $ptr i32) (result i32) + (call $check-17 (local.get $ptr)) + (call $task.return0) + (i32.const 0 (; EXIT ;)) + ) + (func (export "async-1-result") (result i32) + (call $task.return1 (f64.const -1.1)) + (i32.const 0 (; EXIT ;)) + ) + (func (export "async-16-result") (result i32) + (call $task.return16 (i32.const -1) (i64.const -2) (f32.const -3.3) (f64.const -4.4) + (i32.const -5) (i64.const -6) (f32.const -7.7) (f64.const -8.8) + (i32.const -9) (i64.const -10) (f32.const -11.11) (f64.const -12.12) + (i32.const -13) (i64.const -14) (f32.const -15.15) (f64.const -16.16)) + (i32.const 0 (; EXIT ;)) + ) + (func (export "async-17-result") (result i32) + (local $ptr i32) + (call $setup-17 (local.get $ptr)) + (call $task.return17 (local.get $ptr)) + (i32.const 0 (; EXIT ;)) + ) + (func (export "unreachable-cb") (param i32 i32 i32) (result i32) + unreachable + ) + ) + (canon task.return (core func $task.return0)) + (canon task.return (result f64) (core func $task.return1)) + (canon task.return (result (tuple u32 u64 f32 f64 u32 u64 f32 f64 u32 u64 f32 f64 u32 u64 f32 f64)) (core func $task.return16)) + (canon task.return (result (tuple u32 u64 f32 f64 u32 u64 f32 f64 u32 u64 f32 f64 u32 u64 f32 f64 u32)) (memory $memory "mem") (core func $task.return17)) + (core instance $core (instantiate $Core (with "" (instance + (export "mem" (memory $memory "mem")) + (export "task.return0" (func $task.return0)) + (export "task.return1" (func $task.return1)) + (export "task.return16" (func $task.return16)) + (export "task.return17" (func $task.return17)) + )))) + (func (export "sync-4-param") async (param "a" u32) (param "b" u64) (param "c" f32) (param "d" f64) + (canon lift (core func $core "sync-4-param")) + ) + (func (export "sync-5-param") async (param "a" u32) (param "b" u64) (param "c" f32) (param "d" f64) (param "e" u32) + (canon lift (core func $core "sync-5-param")) + ) + (func (export "sync-17-param") async + (param "a" u32) (param "b" u64) (param "c" f32) (param "d" f64) + (param "e" u32) (param "f" u64) (param "g" f32) (param "h" f64) + (param "i" u32) (param "j" u64) (param "k" f32) (param "l" f64) + (param "m" u32) (param "n" u64) (param "o" f32) (param "p" f64) + (param "q" u32) + (canon lift (core func $core "sync-17-param") (memory $memory "mem") (realloc (func $memory "realloc"))) + ) + (func (export "sync-1-result") async (result f64) + (canon lift (core func $core "sync-1-result")) + ) + (func (export "sync-16-result") async (result (tuple u32 u64 f32 f64 u32 u64 f32 f64 u32 u64 f32 f64 u32 u64 f32 f64)) + (canon lift (core func $core "sync-16-result") (memory $memory "mem")) + ) + (func (export "sync-17-result") async (result (tuple u32 u64 f32 f64 u32 u64 f32 f64 u32 u64 f32 f64 u32 u64 f32 f64 u32)) + (canon lift (core func $core "sync-17-result") (memory $memory "mem")) + ) + (func (export "async-4-param") async + (param "a" u32) (param "b" u64) (param "c" f32) (param "d" f64) + (canon lift (core func $core "async-4-param") async (callback (func $core "unreachable-cb"))) + ) + (func (export "async-5-param") async + (param "a" u32) (param "b" u64) (param "c" f32) (param "d" f64) (param "e" u32) + (canon lift (core func $core "async-5-param") async (callback (func $core "unreachable-cb"))) + ) + (func (export "async-17-param") async + (param "a" u32) (param "b" u64) (param "c" f32) (param "d" f64) + (param "e" u32) (param "f" u64) (param "g" f32) (param "h" f64) + (param "i" u32) (param "j" u64) (param "k" f32) (param "l" f64) + (param "m" u32) (param "n" u64) (param "o" f32) (param "p" f64) + (param "q" u32) + (canon lift (core func $core "async-17-param") async (callback (func $core "unreachable-cb")) (memory $memory "mem") (realloc (func $memory "realloc"))) + ) + (func (export "async-1-result") async (result f64) + (canon lift (core func $core "async-1-result") async (callback (func $core "unreachable-cb"))) + ) + (func (export "async-16-result") async (result (tuple u32 u64 f32 f64 u32 u64 f32 f64 u32 u64 f32 f64 u32 u64 f32 f64)) + (canon lift (core func $core "async-16-result") async (callback (func $core "unreachable-cb"))) + ) + (func (export "async-17-result") async (result (tuple u32 u64 f32 f64 u32 u64 f32 f64 u32 u64 f32 f64 u32 u64 f32 f64 u32)) + (canon lift (core func $core "async-17-result") async (callback (func $core "unreachable-cb")) (memory $memory "mem") (realloc (func $memory "realloc"))) + ) + ) + (component $Bottom + (import "func-4-param" (func $func-4-param async (param "a" u32) (param "b" u64) (param "c" f32) (param "d" f64))) + (import "func-5-param" (func $func-5-param async (param "a" u32) (param "b" u64) (param "c" f32) (param "d" f64) (param "e" u32))) + (import "func-17-param" (func $func-17-param async + (param "a" u32) (param "b" u64) (param "c" f32) (param "d" f64) + (param "e" u32) (param "f" u64) (param "g" f32) (param "h" f64) + (param "i" u32) (param "j" u64) (param "k" f32) (param "l" f64) + (param "m" u32) (param "n" u64) (param "o" f32) (param "p" f64) + (param "q" u32))) + (import "func-1-result" (func $func-1-result async (result f64))) + (import "func-16-result" (func $func-16-result async (result (tuple u32 u64 f32 f64 u32 u64 f32 f64 u32 u64 f32 f64 u32 u64 f32 f64)))) + (import "func-17-result" (func $func-17-result async (result (tuple u32 u64 f32 f64 u32 u64 f32 f64 u32 u64 f32 f64 u32 u64 f32 f64 u32)))) + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $Core + (import "" "mem" (memory 1)) + (import "" "sync-4-param" (func $sync-4-param (param i32 i64 f32 f64))) + (import "" "async-4-param" (func $async-4-param (param i32 i64 f32 f64) (result i32))) + (import "" "sync-5-param" (func $sync-5-param (param i32 i64 f32 f64 i32))) + (import "" "async-5-param" (func $async-5-param (param i32) (result i32))) + (import "" "sync-17-param" (func $sync-17-param (param i32))) + (import "" "async-17-param" (func $async-17-param (param i32) (result i32))) + (import "" "sync-1-result" (func $sync-1-result (result f64))) + (import "" "async-1-result" (func $async-1-result (param i32) (result i32))) + (import "" "sync-16-result" (func $sync-16-result (param i32))) + (import "" "async-16-result" (func $async-16-result (param i32) (result i32))) + (import "" "sync-17-result" (func $sync-17-result (param i32))) + (import "" "async-17-result" (func $async-17-result (param i32) (result i32))) + (func (export "call-sync-4-param") (result i32) + (call $sync-4-param (i32.const 42) (i64.const 43) (f32.const 44.4) (f64.const 45.5)) + (i32.const 83) + ) + (func (export "call-async-4-param") (result i32) + (if (i32.ne (call $async-4-param (i32.const 42) (i64.const 43) (f32.const 44.4) (f64.const 45.5)) + (i32.const 2 (; RETURNED ;))) + (then unreachable)) + (i32.const 84) + ) + (func (export "call-sync-5-param") (result i32) + (call $sync-5-param (i32.const -1) (i64.const -2) (f32.const -3.3) (f64.const -4.4) (i32.const -5)) + (i32.const 85) + ) + (func (export "call-async-5-param") (result i32) + (local $ptr i32) + (i32.store (local.get $ptr) (i32.const -1)) + (i64.store offset=8 (local.get $ptr) (i64.const -2)) + (f32.store offset=16 (local.get $ptr) (f32.const -3.3)) + (f64.store offset=24 (local.get $ptr) (f64.const -4.4)) + (i32.store offset=32 (local.get $ptr) (i32.const -5)) + (if (i32.ne (call $async-5-param (local.get $ptr)) (i32.const 2 (; RETURNED ;))) + (then unreachable)) + (i32.const 86) + ) + (func $setup-17 (param $ptr i32) + (i32.store (local.get $ptr) (i32.const -1)) + (i64.store offset=8 (local.get $ptr) (i64.const -2)) + (f32.store offset=16 (local.get $ptr) (f32.const -3.3)) + (f64.store offset=24 (local.get $ptr) (f64.const -4.4)) + (i32.store offset=32 (local.get $ptr) (i32.const -5)) + (i64.store offset=40 (local.get $ptr) (i64.const -6)) + (f32.store offset=48 (local.get $ptr) (f32.const -7.7)) + (f64.store offset=56 (local.get $ptr) (f64.const -8.8)) + (i32.store offset=64 (local.get $ptr) (i32.const -9)) + (i64.store offset=72(local.get $ptr) (i64.const -10)) + (f32.store offset=80 (local.get $ptr) (f32.const -11.11)) + (f64.store offset=88 (local.get $ptr) (f64.const -12.12)) + (i32.store offset=96 (local.get $ptr) (i32.const -13)) + (i64.store offset=104 (local.get $ptr) (i64.const -14)) + (f32.store offset=112 (local.get $ptr) (f32.const -15.15)) + (f64.store offset=120 (local.get $ptr) (f64.const -16.16)) + (i32.store offset=128 (local.get $ptr) (i32.const -17)) + ) + (func (export "call-sync-17-param") (result i32) + (call $setup-17 (i32.const 0)) + (call $sync-17-param (i32.const 0)) + (i32.const 87) + ) + (func (export "call-async-17-param") (result i32) + (call $setup-17 (i32.const 0)) + (if (i32.ne (call $async-17-param (i32.const 0)) (i32.const 2 (; RETURNED ;))) + (then unreachable)) + (i32.const 88) + ) + (func (export "call-sync-1-result") (result i32) + (if (f64.ne (call $sync-1-result) (f64.const -1.1)) + (then unreachable)) + (i32.const 89) + ) + (func (export "call-async-1-result") (result i32) + (local $ptr i32) + (if (i32.ne (call $async-1-result (local.get $ptr)) (i32.const 2 (; RETURNED ;))) + (then unreachable)) + (if (f64.ne (f64.load (local.get $ptr)) (f64.const -1.1)) + (then unreachable)) + (i32.const 90) + ) + (func $check-16 (param $ptr i32) + (if (i32.ne (i32.const -1) (i32.load (local.get $ptr))) + (then unreachable)) + (if (i64.ne (i64.const -2) (i64.load offset=8 (local.get $ptr))) + (then unreachable)) + (if (f32.ne (f32.const -3.3) (f32.load offset=16 (local.get $ptr))) + (then unreachable)) + (if (f64.ne (f64.const -4.4) (f64.load offset=24 (local.get $ptr))) + (then unreachable)) + (if (i32.ne (i32.const -5) (i32.load offset=32 (local.get $ptr))) + (then unreachable)) + (if (i64.ne (i64.const -6) (i64.load offset=40 (local.get $ptr))) + (then unreachable)) + (if (f32.ne (f32.const -7.7) (f32.load offset=48 (local.get $ptr))) + (then unreachable)) + (if (f64.ne (f64.const -8.8) (f64.load offset=56 (local.get $ptr))) + (then unreachable)) + (if (i32.ne (i32.const -9) (i32.load offset=64 (local.get $ptr))) + (then unreachable)) + (if (i64.ne (i64.const -10) (i64.load offset=72 (local.get $ptr))) + (then unreachable)) + (if (f32.ne (f32.const -11.11) (f32.load offset=80 (local.get $ptr))) + (then unreachable)) + (if (f64.ne (f64.const -12.12) (f64.load offset=88 (local.get $ptr))) + (then unreachable)) + (if (i32.ne (i32.const -13) (i32.load offset=96 (local.get $ptr))) + (then unreachable)) + (if (i64.ne (i64.const -14) (i64.load offset=104 (local.get $ptr))) + (then unreachable)) + (if (f32.ne (f32.const -15.15) (f32.load offset=112 (local.get $ptr))) + (then unreachable)) + (if (f64.ne (f64.const -16.16) (f64.load offset=120 (local.get $ptr))) + (then unreachable)) + ) + (func $check-17 (param $ptr i32) + (call $check-16 (local.get $ptr)) + (if (i32.ne (i32.const -17) (i32.load offset=128 (local.get $ptr))) + (then unreachable)) + ) + (func (export "call-sync-16-result") (result i32) + (local $ptr i32) + (call $sync-16-result (local.get $ptr)) + (call $check-16 (local.get $ptr)) + (i32.const 91) + ) + (func (export "call-async-16-result") (result i32) + (local $ptr i32) + (if (i32.ne (call $async-16-result (local.get $ptr)) (i32.const 2 (; RETURNED ;))) + (then unreachable)) + (call $check-16 (local.get $ptr)) + (i32.const 92) + ) + (func (export "call-sync-17-result") (result i32) + (local $ptr i32) + (call $sync-17-result (local.get $ptr)) + (call $check-17 (local.get $ptr)) + (i32.const 93) + ) + (func (export "call-async-17-result") (result i32) + (local $ptr i32) + (if (i32.ne (call $async-17-result (local.get $ptr)) (i32.const 2 (; RETURNED ;))) + (then unreachable)) + (call $check-17 (local.get $ptr)) + (i32.const 94) + ) + ) + (canon lower (func $func-4-param) (core func $sync-4-param)) + (canon lower (func $func-4-param) async (memory $memory "mem") (core func $async-4-param)) + (canon lower (func $func-5-param) (core func $sync-5-param)) + (canon lower (func $func-5-param) async (memory $memory "mem") (core func $async-5-param)) + (canon lower (func $func-17-param) (memory $memory "mem") (core func $sync-17-param)) + (canon lower (func $func-17-param) async (memory $memory "mem") (core func $async-17-param)) + (canon lower (func $func-1-result) (core func $sync-1-result)) + (canon lower (func $func-1-result) async (memory $memory "mem") (core func $async-1-result)) + (canon lower (func $func-16-result) (memory $memory "mem") (core func $sync-16-result)) + (canon lower (func $func-16-result) async (memory $memory "mem") (core func $async-16-result)) + (canon lower (func $func-17-result) (memory $memory "mem") (core func $sync-17-result)) + (canon lower (func $func-17-result) async (memory $memory "mem") (core func $async-17-result)) + (core instance $core (instantiate $Core (with "" (instance + (export "mem" (memory $memory "mem")) + (export "sync-4-param" (func $sync-4-param)) + (export "async-4-param" (func $async-4-param)) + (export "sync-5-param" (func $sync-5-param)) + (export "async-5-param" (func $async-5-param)) + (export "sync-17-param" (func $sync-17-param)) + (export "async-17-param" (func $async-17-param)) + (export "sync-1-result" (func $sync-1-result)) + (export "async-1-result" (func $async-1-result)) + (export "sync-16-result" (func $sync-16-result)) + (export "async-16-result" (func $async-16-result)) + (export "sync-17-result" (func $sync-17-result)) + (export "async-17-result" (func $async-17-result)) + )))) + (func (export "call-sync-4-param") async (result u32) (canon lift (core func $core "call-sync-4-param"))) + (func (export "call-async-4-param") async (result u32) (canon lift (core func $core "call-async-4-param"))) + (func (export "call-sync-5-param") async (result u32) (canon lift (core func $core "call-sync-5-param"))) + (func (export "call-async-5-param") async (result u32) (canon lift (core func $core "call-async-5-param"))) + (func (export "call-sync-17-param") async (result u32) (canon lift (core func $core "call-sync-17-param"))) + (func (export "call-async-17-param") async (result u32) (canon lift (core func $core "call-async-17-param"))) + (func (export "call-sync-1-result") async (result u32) (canon lift (core func $core "call-sync-1-result"))) + (func (export "call-async-1-result") async (result u32) (canon lift (core func $core "call-async-1-result"))) + (func (export "call-sync-16-result") async (result u32) (canon lift (core func $core "call-sync-16-result"))) + (func (export "call-async-16-result") async (result u32) (canon lift (core func $core "call-async-16-result"))) + (func (export "call-sync-17-result") async (result u32) (canon lift (core func $core "call-sync-17-result"))) + (func (export "call-async-17-result") async (result u32) (canon lift (core func $core "call-async-17-result"))) + ) + (instance $top (instantiate $Top)) + (instance $bottom-to-sync (instantiate $Bottom + (with "func-4-param" (func $top "sync-4-param")) + (with "func-5-param" (func $top "sync-5-param")) + (with "func-17-param" (func $top "sync-17-param")) + (with "func-1-result" (func $top "sync-1-result")) + (with "func-16-result" (func $top "sync-16-result")) + (with "func-17-result" (func $top "sync-17-result")) + )) + (instance $bottom-to-async (instantiate $Bottom + (with "func-4-param" (func $top "async-4-param")) + (with "func-5-param" (func $top "async-5-param")) + (with "func-17-param" (func $top "async-17-param")) + (with "func-1-result" (func $top "async-1-result")) + (with "func-16-result" (func $top "async-16-result")) + (with "func-17-result" (func $top "async-17-result")) + )) + (func (export "sync-calls-sync-4-param") (alias export $bottom-to-sync "call-sync-4-param")) + (func (export "sync-calls-async-4-param") (alias export $bottom-to-async "call-sync-4-param")) + (func (export "async-calls-sync-4-param") (alias export $bottom-to-sync "call-async-4-param")) + (func (export "async-calls-async-4-param") (alias export $bottom-to-async "call-async-4-param")) + (func (export "sync-calls-sync-5-param") (alias export $bottom-to-sync "call-sync-5-param")) + (func (export "sync-calls-async-5-param") (alias export $bottom-to-async "call-sync-5-param")) + (func (export "async-calls-sync-5-param") (alias export $bottom-to-sync "call-async-5-param")) + (func (export "async-calls-async-5-param") (alias export $bottom-to-async "call-async-5-param")) + (func (export "sync-calls-sync-17-param") (alias export $bottom-to-sync "call-sync-17-param")) + (func (export "sync-calls-async-17-param") (alias export $bottom-to-async "call-sync-17-param")) + (func (export "async-calls-sync-17-param") (alias export $bottom-to-sync "call-async-17-param")) + (func (export "async-calls-async-17-param") (alias export $bottom-to-async "call-async-17-param")) + (func (export "sync-calls-sync-1-result") (alias export $bottom-to-sync "call-sync-1-result")) + (func (export "sync-calls-async-1-result") (alias export $bottom-to-async "call-sync-1-result")) + (func (export "async-calls-sync-1-result") (alias export $bottom-to-sync "call-async-1-result")) + (func (export "async-calls-async-1-result") (alias export $bottom-to-async "call-async-1-result")) + (func (export "sync-calls-sync-16-result") (alias export $bottom-to-sync "call-sync-16-result")) + (func (export "sync-calls-async-16-result") (alias export $bottom-to-async "call-sync-16-result")) + (func (export "async-calls-sync-16-result") (alias export $bottom-to-sync "call-async-16-result")) + (func (export "async-calls-async-16-result") (alias export $bottom-to-async "call-async-16-result")) + (func (export "sync-calls-sync-17-result") (alias export $bottom-to-sync "call-sync-17-result")) + (func (export "sync-calls-async-17-result") (alias export $bottom-to-async "call-sync-17-result")) + (func (export "async-calls-sync-17-result") (alias export $bottom-to-sync "call-async-17-result")) + (func (export "async-calls-async-17-result") (alias export $bottom-to-async "call-async-17-result")) +) + +(component instance $i $C) +(assert_return (invoke "sync-calls-sync-4-param") (u32.const 83)) +(component instance $i $C) +(assert_return (invoke "sync-calls-async-4-param") (u32.const 83)) +(component instance $i $C) +(assert_return (invoke "async-calls-sync-4-param") (u32.const 84)) +(component instance $i $C) +(assert_return (invoke "async-calls-async-4-param") (u32.const 84)) +(component instance $i $C) +(assert_return (invoke "sync-calls-sync-5-param") (u32.const 85)) +(component instance $i $C) +(assert_return (invoke "sync-calls-async-5-param") (u32.const 85)) +(component instance $i $C) +(assert_return (invoke "async-calls-sync-5-param") (u32.const 86)) +(component instance $i $C) +(assert_return (invoke "async-calls-async-5-param") (u32.const 86)) +(component instance $i $C) +(assert_return (invoke "sync-calls-sync-17-param") (u32.const 87)) +(component instance $i $C) +(assert_return (invoke "sync-calls-async-17-param") (u32.const 87)) +(component instance $i $C) +(assert_return (invoke "async-calls-sync-17-param") (u32.const 88)) +(component instance $i $C) +(assert_return (invoke "async-calls-async-17-param") (u32.const 88)) +(component instance $i $C) +(assert_return (invoke "sync-calls-sync-1-result") (u32.const 89)) +(component instance $i $C) +(assert_return (invoke "sync-calls-async-1-result") (u32.const 89)) +(component instance $i $C) +(assert_return (invoke "async-calls-sync-1-result") (u32.const 90)) +(component instance $i $C) +(assert_return (invoke "async-calls-async-1-result") (u32.const 90)) +(component instance $i $C) +(assert_return (invoke "sync-calls-sync-16-result") (u32.const 91)) +(component instance $i $C) +(assert_return (invoke "sync-calls-async-16-result") (u32.const 91)) +(component instance $i $C) +(assert_return (invoke "async-calls-sync-16-result") (u32.const 92)) +(component instance $i $C) +(assert_return (invoke "async-calls-async-16-result") (u32.const 92)) +(component instance $i $C) +(assert_return (invoke "sync-calls-sync-17-result") (u32.const 93)) +(component instance $i $C) +(assert_return (invoke "sync-calls-async-17-result") (u32.const 93)) +(component instance $i $C) +(assert_return (invoke "async-calls-sync-17-result") (u32.const 94)) +(component instance $i $C) +(assert_return (invoke "async-calls-async-17-result") (u32.const 94)) diff --git a/packages/jco/test/fixtures/wat/component-model/async/deadlock.wast b/packages/jco/test/fixtures/wat/component-model/async/deadlock.wast new file mode 100644 index 000000000..6d50c9f22 --- /dev/null +++ b/packages/jco/test/fixtures/wat/component-model/async/deadlock.wast @@ -0,0 +1,73 @@ +;; This test defines components $C and $D where $D imports and calls $C +;; $C.f waits on an empty waitable set +;; $D.g calls $C.f and then waits for it to finish, which fails due to deadlock +(component + (component $C + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $CM + (import "" "mem" (memory 1)) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + + (func (export "f") (result i32) + ;; wait on a new empty waitable set + (local $ws i32) + (local.set $ws (call $waitable-set.new)) + (i32.or (i32.const 2 (; WAIT ;)) (i32.shl (local.get $ws) (i32.const 4))) + ) + (func (export "cb") (param $event_code i32) (param $index i32) (param $payload i32) (result i32) + unreachable + ) + ) + (canon waitable-set.new (core func $waitable-set.new)) + (core instance $cm (instantiate $CM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "waitable-set.new" (func $waitable-set.new)) + )))) + (func (export "f") async (result u32) (canon lift + (core func $cm "f") + async (memory $memory "mem") (callback (func $cm "cb")) + )) + ) + + (component $D + (import "f" (func $f async (result u32))) + + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $DM + (import "" "mem" (memory 1)) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + (import "" "f" (func $f (param i32) (result i32))) + + (func (export "g") (result i32) + (local $ws i32) (local $ret i32) (local $subtaski i32) + (local.set $ret (call $f (i32.const 0))) + (local.set $subtaski (i32.shr_u (local.get $ret) (i32.const 4))) + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (local.get $subtaski) (local.get $ws)) + (call $waitable-set.wait (local.get $ws) (i32.const 0)) + unreachable + ) + ) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.wait (memory $memory "mem") (core func $waitable-set.wait)) + (canon lower (func $f) async (memory $memory "mem") (core func $f')) + (core instance $dm (instantiate $DM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.wait" (func $waitable-set.wait)) + (export "f" (func $f')) + )))) + (func (export "f") async (result u32) (canon lift (core func $dm "g"))) + ) + + (instance $c (instantiate $C)) + (instance $d (instantiate $D (with "f" (func $c "f")))) + (func (export "f") (alias export $d "f")) +) +(assert_trap (invoke "f") "wasm trap: deadlock detected: event loop cannot make further progress") diff --git a/packages/jco/test/fixtures/wat/component-model/async/dont-block-start.wast b/packages/jco/test/fixtures/wat/component-model/async/dont-block-start.wast new file mode 100644 index 000000000..5fab77f3d --- /dev/null +++ b/packages/jco/test/fixtures/wat/component-model/async/dont-block-start.wast @@ -0,0 +1,50 @@ +;; test a few cases where components trap during core module instantiation +;; due to blocking during the (implicitly sync) start function +(assert_trap + (component + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $M + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + (func $start + (drop (call $waitable-set.wait (call $waitable-set.new) (i32.const 0))) + ) + (start $start) + ) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.wait (memory $memory "mem") (core func $waitable-set.wait)) + (core instance $m (instantiate $M (with "" (instance + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.wait" (func $waitable-set.wait)) + )))) + ) + "cannot block a synchronous task before returning" +) +(assert_trap + (component + (component $C + (core module $M + (func (export "f") (result i32) unreachable) + (func (export "f_cb") (param i32 i32 i32) (result i32) unreachable) + ) + (core instance $i (instantiate $M)) + (func (export "f") async (canon lift (core func $i "f") async (callback (func $i "f_cb")))) + ) + (component $D + (import "f" (func $f async)) + (core module $M + (import "" "f" (func $f)) + (func $start (call $f)) + (start $start) + ) + (canon lower (func $f) (core func $f')) + (core instance $m (instantiate $M (with "" (instance + (export "f" (func $f')) + )))) + ) + (instance $c (instantiate $C)) + (instance $d (instantiate $D (with "f" (func $c "f")))) + ) + "cannot block a synchronous task before returning" +) diff --git a/packages/jco/test/fixtures/wat/component-model/async/drop-cross-task-borrow.wast b/packages/jco/test/fixtures/wat/component-model/async/drop-cross-task-borrow.wast new file mode 100644 index 000000000..139981cd6 --- /dev/null +++ b/packages/jco/test/fixtures/wat/component-model/async/drop-cross-task-borrow.wast @@ -0,0 +1,309 @@ +;; This test has 3 components $C, $D and $E +;; $C just implements a resource type that's used by $D and $E +;; $E calls async function $D.dont-drop, lending it a handle. +;; $D.dont-drop blocks, waiting on an empty waitable-set +;; $E then calls $D.drop-handle which drops the handle that $D.dont-drop +;; was lent, albeit from the "wrong" task ($D.drop-handle). +;; Then $E calls $D.resume-dont-drop to unblock $D.dont-drop, which +;; will call task.return which should not trap. +(component definition $Test + (component $C + (type $R' (resource (rep i32))) + (canon resource.new $R' (core func $resource.new)) + (core module $CM (func (export "id") (param i32) (result i32) (local.get 0))) + (core instance $cm (instantiate $CM)) + (alias core export $cm "id" (core func $resource.rep)) + (export $R "R" (type $R')) + (func (export "R-new") (param "rep" u32) (result (own $R)) (canon lift (core func $resource.new))) + (func (export "R-rep") (param "self" (borrow $R)) (result u32) (canon lift (core func $resource.rep))) + ) + + (component $D + (import "c" (instance $d + (export "R" (type $R (sub resource))) + (export "R-new" (func (param "rep" u32) (result (own $R)))) + (export "R-rep" (func (param "self" (borrow $R)) (result u32))) + )) + (core module $DM + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "future.new" (func $future.new (result i64))) + (import "" "future.read" (func $future.read (param i32 i32) (result i32))) + (import "" "future.write" (func $future.write (param i32 i32) (result i32))) + (import "" "task.return0" (func $task.return0)) + (import "" "task.return1" (func $task.return1 (param i32))) + (import "" "R-rep" (func $R-rep (param i32) (result i32))) + (import "" "R-drop" (func $R-drop (param i32))) + + (global $handle (mut i32) (i32.const 0)) + (global $dont-drop-result (mut i32) (i32.const 0)) + (global $dont-drop-ws (mut i32) (i32.const 0)) + + (func (export "dont-drop") (param $h i32) (result i32) + ;; Stash the given (borrow $R) handle in a global. + (global.set $handle (local.get $h)) + ;; Stash the result of $R-rep in a global for later task.return + (global.set $dont-drop-result (call $R-rep (local.get $h))) + ;; Stash the waitable-set we're waiting on in a global for resume-dont-drop to use + (global.set $dont-drop-ws (call $waitable-set.new)) + (i32.or (i32.const 2 (; WAIT ;)) (i32.shl (global.get $dont-drop-ws) (i32.const 4))) + ) + (func (export "dont-drop-cb") (param i32 i32 i32) (result i32) + ;; We were resumed by resume-dont-drop + (call $task.return1 (global.get $dont-drop-result)) + (i32.const 0 (; EXIT ;)) + ) + (func (export "drop-handle") (result i32) + ;; Drops the borrowed handle passed to dont-drop + (local $result i32) + (local.set $result (call $R-rep (global.get $handle))) + (call $R-drop (global.get $handle)) + (local.get $result) + ) + (func (export "resume-dont-drop") + ;; Add a waitable with a pending event to dont-drop's waitable-set to + ;; wake it up. + (local $ret i32) (local $ret64 i64) + (local $futw i32) (local $futr i32) + (local.set $ret64 (call $future.new)) + (local.set $futr (i32.wrap_i64 (local.get $ret64))) + (local.set $futw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + (local.set $ret (call $future.read (local.get $futr) (i32.const 0xdeadbeef))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (local.set $ret (call $future.write (local.get $futw) (i32.const 0xdeadbeef))) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (local.get $ret)) + (then unreachable)) + (call $waitable.join (local.get $futr) (global.get $dont-drop-ws)) + ) + (func (export "drop-other-and-self") (param $h i32) (result i32) + (local $result i32) + (local.set $result (call $R-rep (global.get $handle))) + (call $R-drop (global.get $handle)) + (call $R-drop (local.get $h)) + (call $task.return1 (local.get $result)) + (i32.const 0 (; EXIT ;)) + ) + (func (export "drop-wrong-one") (param $h i32) (result i32) + (call $R-drop (global.get $handle)) + ;; trap b/c $h wasn't dropped + (call $task.return0) + (i32.const 0 (; EXIT ;)) + ) + (func (export "unreachable-cb") (param i32 i32 i32) (result i32) + unreachable + ) + ) + (type $FT (future)) + (alias export $d "R" (type $R)) + (canon task.return (core func $task.return0)) + (canon task.return (result u32) (core func $task.return1)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon future.new $FT (core func $future.new)) + (canon future.read $FT async (core func $future.read)) + (canon future.write $FT async (core func $future.write)) + (canon lower (func $d "R-rep") (core func $R-rep)) + (canon resource.drop $R (core func $R-drop)) + (core instance $dm (instantiate $DM (with "" (instance + (export "task.return0" (func $task.return0)) + (export "task.return1" (func $task.return1)) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "future.new" (func $future.new)) + (export "future.read" (func $future.read)) + (export "future.write" (func $future.write)) + (export "R-rep" (func $R-rep)) + (export "R-drop" (func $R-drop)) + )))) + (func (export "dont-drop") async (param "self" (borrow $R)) (result u32) + (canon lift (core func $dm "dont-drop") async (callback (func $dm "dont-drop-cb"))) + ) + (func (export "drop-handle") (result u32) + (canon lift (core func $dm "drop-handle")) + ) + (func (export "resume-dont-drop") + (canon lift (core func $dm "resume-dont-drop")) + ) + (func (export "drop-other-and-self") async (param "self" (borrow $R)) (result u32) + (canon lift (core func $dm "drop-other-and-self") async (callback (func $dm "unreachable-cb"))) + ) + (func (export "drop-wrong-one") async (param "self" (borrow $R)) + (canon lift (core func $dm "drop-wrong-one") async (callback (func $dm "unreachable-cb"))) + ) + ) + + (component $E + (import "c" (instance $c + (export "R" (type $R (sub resource))) + (export "R-new" (func (param "rep" u32) (result (own $R)))) + )) + (alias export $c "R" (type $R)) + (import "d" (instance $d + (export "dont-drop" (func async (param "self" (borrow $R)) (result u32))) + (export "drop-handle" (func (result u32))) + (export "resume-dont-drop" (func)) + (export "drop-other-and-self" (func async (param "self" (borrow $R)) (result u32))) + (export "drop-wrong-one" (func async (param "self" (borrow $R)))) + )) + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $EM + (import "" "mem" (memory 1)) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + (import "" "R-new" (func $R-new (param i32) (result i32))) + (import "" "dont-drop" (func $dont-drop (param i32 i32) (result i32))) + (import "" "drop-handle" (func $drop-handle (result i32))) + (import "" "resume-dont-drop" (func $resume-dont-drop)) + (import "" "drop-other-and-self" (func $drop-other-and-self (param i32) (result i32))) + (import "" "drop-wrong-one" (func $drop-wrong-one (param i32))) + (func (export "drop-other-no-self") (result i32) + (local $ret i32) + (local $retp i32) (local $retp2 i32) + (local $handle i32) + (local $subtask i32) + (local $magic i32) + (local $ws i32) (local $event_code i32) + + ;; Create a resource storing $magic as it's rep + (local.set $magic (i32.const 10)) + (local.set $handle (call $R-new (local.get $magic))) + + ;; Kick off a call to dont-drop that will block + (local.set $retp (i32.const 16)) + (local.set $ret (call $dont-drop (local.get $handle) (local.get $retp))) + (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) + (then unreachable)) + (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) + + ;; drop-handle should return the rep of the handle passed to dont-drop + (local.set $ret (call $drop-handle)) + (if (i32.ne (local.get $magic) (local.get $ret)) + (then unreachable)) + + ;; this unblocks $subtask + (call $resume-dont-drop) + + ;; now wait for $subtask to return, so that it can run before the test is over + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (local.get $subtask) (local.get $ws)) + (local.set $retp2 (i32.const 32)) + (local.set $event_code (call $waitable-set.wait (local.get $ws) (local.get $retp2))) + (if (i32.ne (i32.const 1 (; SUBTASK ;)) (local.get $event_code)) + (then unreachable)) + (if (i32.ne (local.get $subtask) (i32.load (local.get $retp2))) + (then unreachable)) + (if (i32.ne (i32.const 2 (; RETURNED=2 | (0<<4) ;)) (i32.load offset=4 (local.get $retp2))) + (then unreachable)) + + ;; $subtask should return the rep passed to $R-new. + (if (i32.ne (local.get $magic) (i32.load (local.get $retp))) + (then unreachable)) + + i32.const 42 + ) + (func (export "drop-other-and-self") (result i32) + (local $ret i32) + (local $retp i32) (local $retp2 i32) + (local $handle i32) + (local $subtask i32) + (local $magic i32) + (local $ws i32) (local $event_code i32) + + ;; Create a resource storing $magic as it's rep + (local.set $magic (i32.const 11)) + (local.set $handle (call $R-new (local.get $magic))) + + ;; Kick off a call to dont-drop that will block + (local.set $retp (i32.const 16)) + (local.set $ret (call $dont-drop (local.get $handle) (local.get $retp))) + (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) + (then unreachable)) + (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) + + ;; This will drop dont-drop's *and* its own borrowed handle + (local.set $ret (call $drop-other-and-self (local.get $handle))) + (if (i32.ne (local.get $magic) (local.get $ret)) + (then unreachable)) + + ;; this unblocks $subtask + (call $resume-dont-drop) + + ;; now wait for $subtask to return, so that it can run before the test is over + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (local.get $subtask) (local.get $ws)) + (local.set $retp2 (i32.const 32)) + (local.set $event_code (call $waitable-set.wait (local.get $ws) (local.get $retp2))) + (if (i32.ne (i32.const 1 (; SUBTASK ;)) (local.get $event_code)) + (then unreachable)) + (if (i32.ne (local.get $subtask) (i32.load (local.get $retp2))) + (then unreachable)) + (if (i32.ne (i32.const 2 (; RETURNED=2 | (0<<4) ;)) (i32.load offset=4 (local.get $retp2))) + (then unreachable)) + + ;; $subtask should return the rep passed to $R-new. + (if (i32.ne (local.get $magic) (i32.load (local.get $retp))) + (then unreachable)) + + i32.const 43 + ) + (func (export "drop-other-miss-self") + (local $ret i32) + (local $retp i32) + (local $handle i32) + (local $subtask i32) + + (local.set $handle (call $R-new (i32.const 42))) + + ;; Kick off a call to dont-drop that will block + (local.set $retp (i32.const 16)) + (local.set $ret (call $dont-drop (local.get $handle) (local.get $retp))) + (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) + (then unreachable)) + (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) + + ;; Call drop-wrong-one which will drop the above call's borrow, but not its own and trap + (call $drop-wrong-one (local.get $handle)) + ) + ) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.wait (memory $memory "mem") (core func $waitable-set.wait)) + (canon lower (func $c "R-new") (core func $R-new)) + (canon lower (func $d "dont-drop") async (memory $memory "mem") (core func $dont-drop)) + (canon lower (func $d "drop-handle") (core func $drop-handle)) + (canon lower (func $d "resume-dont-drop") (core func $resume-dont-drop)) + (canon lower (func $d "drop-other-and-self") (core func $drop-other-and-self)) + (canon lower (func $d "drop-wrong-one") (core func $drop-wrong-one)) + (core instance $em (instantiate $EM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.wait" (func $waitable-set.wait)) + (export "R-new" (func $R-new)) + (export "dont-drop" (func $dont-drop)) + (export "drop-handle" (func $drop-handle)) + (export "resume-dont-drop" (func $resume-dont-drop)) + (export "drop-other-and-self" (func $drop-other-and-self)) + (export "drop-wrong-one" (func $drop-wrong-one)) + )))) + (func (export "drop-other-no-self") async (result u32) (canon lift (core func $em "drop-other-no-self"))) + (func (export "drop-other-and-self") async (result u32) (canon lift (core func $em "drop-other-and-self"))) + (func (export "drop-other-miss-self") async (canon lift (core func $em "drop-other-miss-self"))) + ) + (instance $c (instantiate $C)) + (instance $d (instantiate $D (with "c" (instance $c)))) + (instance $e (instantiate $E (with "c" (instance $c)) (with "d" (instance $d)))) + (func (export "drop-other-no-self") (alias export $e "drop-other-no-self")) + (func (export "drop-other-and-self") (alias export $e "drop-other-and-self")) + (func (export "drop-other-miss-self") (alias export $e "drop-other-miss-self")) +) + +(component instance $i $Test) +(assert_return (invoke "drop-other-no-self") (u32.const 42)) +(component instance $i $Test) +(assert_return (invoke "drop-other-and-self") (u32.const 43)) +(component instance $i $Test) +(assert_trap (invoke "drop-other-miss-self") "borrow handles still remain at the end of the call") diff --git a/packages/jco/test/fixtures/wat/component-model/async/drop-stream.wast b/packages/jco/test/fixtures/wat/component-model/async/drop-stream.wast new file mode 100644 index 000000000..bf7212f2c --- /dev/null +++ b/packages/jco/test/fixtures/wat/component-model/async/drop-stream.wast @@ -0,0 +1,160 @@ +;; This test contains two components $C and $D that test that traps occur +;; when closing the readable or writable end of stream while a read or write +;; is pending. In particular, even if a partial copy has happened into the +;; buffer such that waiting/polling for an event *would* produce a STREAM +;; READ/WRITE event, if the event has not been delivered, the operation is +;; still considered pending. +(component definition $Tester + (component $C + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $CM + (import "" "mem" (memory 1)) + (import "" "stream.new" (func $stream.new (result i64))) + (import "" "stream.write" (func $stream.write (param i32 i32 i32) (result i32))) + (import "" "stream.drop-writable" (func $stream.drop-writable (param i32))) + + (global $sw (mut i32) (i32.const 0)) + + (func $start-stream (export "start-stream") (result i32) + ;; create a new stream, return the readable end to the caller + (local $ret64 i64) + (local.set $ret64 (call $stream.new)) + (global.set $sw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + (i32.wrap_i64 (local.get $ret64)) + ) + (func $write4 (export "write4") + ;; write 6 bytes into the stream, expecting to rendezvous with a stream.read + (local $ret i32) + (i32.store (i32.const 8) (i32.const 0x12345678)) + (local.set $ret (call $stream.write (global.get $sw) (i32.const 8) (i32.const 4))) + (if (i32.ne (i32.const 0x40 (; COMPLETED=0 | (4<<4) ;)) (local.get $ret)) + (then unreachable)) + ) + (func $start-blocking-write (export "start-blocking-write") + (local $ret i32) + + ;; prepare the write buffer + (i64.store (i32.const 8) (i64.const 0x123456789abcdef)) + + ;; start a blocking write + (local.set $ret (call $stream.write (global.get $sw) (i32.const 8) (i32.const 8))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + ) + (func $drop-writable (export "drop-writable") + ;; boom + (call $stream.drop-writable (global.get $sw)) + ) + ) + (type $ST (stream u8)) + (canon stream.new $ST (core func $stream.new)) + (canon stream.write $ST async (memory $memory "mem") (core func $stream.write)) + (canon stream.drop-writable $ST (core func $stream.drop-writable)) + (core instance $cm (instantiate $CM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "stream.new" (func $stream.new)) + (export "stream.write" (func $stream.write)) + (export "stream.drop-writable" (func $stream.drop-writable)) + )))) + (func (export "start-stream") (result (stream u8)) (canon lift (core func $cm "start-stream"))) + (func (export "write4") (canon lift (core func $cm "write4"))) + (func (export "start-blocking-write") (canon lift (core func $cm "start-blocking-write"))) + (func (export "drop-writable") (canon lift (core func $cm "drop-writable"))) + ) + (component $D + (import "c" (instance $c + (export "start-stream" (func (result (stream u8)))) + (export "write4" (func)) + (export "start-blocking-write" (func)) + (export "drop-writable" (func)) + )) + + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $Core + (import "" "mem" (memory 1)) + (import "" "stream.new" (func $stream.new (result i64))) + (import "" "stream.read" (func $stream.read (param i32 i32 i32) (result i32))) + (import "" "stream.write" (func $stream.write (param i32 i32 i32) (result i32))) + (import "" "stream.drop-readable" (func $stream.drop-readable (param i32))) + (import "" "stream.drop-writable" (func $stream.drop-writable (param i32))) + (import "" "start-stream" (func $start-stream (result i32))) + (import "" "write4" (func $write4)) + (import "" "start-blocking-write" (func $start-blocking-write)) + (import "" "drop-writable" (func $drop-writable)) + + (func (export "drop-while-reading") + (local $ret i32) (local $sr i32) + + ;; call 'start-stream' to get the stream we'll be working with + (local.set $sr (call $start-stream)) + (if (i32.ne (i32.const 1) (local.get $sr)) + (then unreachable)) + + ;; start a blocking read + (local.set $ret (call $stream.read (local.get $sr) (i32.const 8) (i32.const 100))) + (if (i32.ne (i32.const -1 (; BLOCKED;)) (local.get $ret)) + (then unreachable)) + + ;; write into the buffer, but the read is still in progress since we + ;; haven't received notification yet. + (call $write4) + (if (i32.ne (i32.const 0x12345678) (i32.load (i32.const 8))) + (then unreachable)) + + ;; boom + (call $stream.drop-readable (local.get $sr)) + ) + (func (export "drop-while-writing") + (local $ret i32) (local $sr i32) + + ;; call 'start-stream' to get the stream we'll be working with + (local.set $sr (call $start-stream)) + (if (i32.ne (i32.const 1) (local.get $sr)) + (then unreachable)) + + ;; start a blocking write and partially read from it + (call $start-blocking-write) + (local.set $ret (call $stream.read (local.get $sr) (i32.const 8) (i32.const 4))) + (if (i32.ne (i32.const 0x40 (; COMPLETED=0 | (4<<4) ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 0x89abcdef) (i32.load (i32.const 8))) + (then unreachable)) + (call $drop-writable) + ) + ) + (type $ST (stream u8)) + (canon stream.new $ST (core func $stream.new)) + (canon stream.read $ST async (memory $memory "mem") (core func $stream.read)) + (canon stream.write $ST async (memory $memory "mem") (core func $stream.write)) + (canon stream.drop-readable $ST (core func $stream.drop-readable)) + (canon stream.drop-writable $ST (core func $stream.drop-writable)) + (canon lower (func $c "start-stream") (core func $start-stream')) + (canon lower (func $c "write4") (core func $write4')) + (canon lower (func $c "start-blocking-write") (core func $start-blocking-write')) + (canon lower (func $c "drop-writable") (core func $drop-writable')) + (core instance $core (instantiate $Core (with "" (instance + (export "mem" (memory $memory "mem")) + (export "stream.new" (func $stream.new)) + (export "stream.read" (func $stream.read)) + (export "stream.write" (func $stream.write)) + (export "stream.drop-readable" (func $stream.drop-readable)) + (export "stream.drop-writable" (func $stream.drop-writable)) + (export "start-stream" (func $start-stream')) + (export "write4" (func $write4')) + (export "start-blocking-write" (func $start-blocking-write')) + (export "drop-writable" (func $drop-writable')) + )))) + (func (export "drop-while-reading") (canon lift (core func $core "drop-while-reading"))) + (func (export "drop-while-writing") (canon lift (core func $core "drop-while-writing"))) + ) + (instance $c (instantiate $C)) + (instance $d (instantiate $D (with "c" (instance $c)))) + (func (export "drop-while-reading") (alias export $d "drop-while-reading")) + (func (export "drop-while-writing") (alias export $d "drop-while-writing")) +) +(component instance $new-tester-instance $Tester) +(assert_trap (invoke "drop-while-reading") "cannot remove busy stream") +(component instance $new-tester-instance $Tester) +(assert_trap (invoke "drop-while-writing") "cannot drop busy stream") diff --git a/packages/jco/test/fixtures/wat/component-model/async/drop-subtask.wast b/packages/jco/test/fixtures/wat/component-model/async/drop-subtask.wast new file mode 100644 index 000000000..21eb9fcd9 --- /dev/null +++ b/packages/jco/test/fixtures/wat/component-model/async/drop-subtask.wast @@ -0,0 +1,140 @@ +;; This test contains two components: $Looper and $Caller. +;; $Caller starts an async subtask for $Looper.loop and then drops these +;; subtasks in both allowed and disallowed cases, testing for success and +;; traps. +(component + (component $Looper + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $CoreLooper + (import "" "mem" (memory 1)) + (import "" "task.return" (func $task.return)) + + (global $done (mut i32) (i32.const 0)) + + (func $loop (export "loop") (result i32) + (i32.const 1 (; YIELD ;)) + ) + (func $loop_cb (export "loop_cb") (param $event_code i32) (param $index i32) (param $payload i32) (result i32) + ;; confirm that we've received a cancellation request + (if (i32.ne (local.get $event_code) (i32.const 0 (; NONE ;))) + (then unreachable)) + (if (i32.ne (local.get $index) (i32.const 0)) + (then unreachable)) + (if (i32.ne (local.get $payload) (i32.const 0)) + (then unreachable)) + + (if (i32.eqz (global.get $done)) + (then (return (i32.const 1 (; YIELD ;))))) + (call $task.return) + (i32.const 0 (; EXIT ;)) + ) + + (func $return (export "return") + (global.set $done (i32.const 1)) + ) + ) + (canon task.return (core func $task.return)) + (core instance $core_looper (instantiate $CoreLooper (with "" (instance + (export "mem" (memory $memory "mem")) + (export "task.return" (func $task.return)) + )))) + (func (export "loop") async (canon lift + (core func $core_looper "loop") + async (callback (func $core_looper "loop_cb")) + )) + (func (export "return") async (canon lift + (core func $core_looper "return") + )) + ) + + (component $Caller + (import "looper" (instance $looper + (export "loop" (func async)) + (export "return" (func async)) + )) + + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $CoreCaller + (import "" "mem" (memory 1)) + (import "" "subtask.drop" (func $subtask.drop (param i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + (import "" "loop" (func $loop (result i32))) + (import "" "return" (func $return)) + + (func $drop-after-return (export "drop-after-return") (result i32) + (local $ret i32) (local $ws i32) (local $subtask i32) + + ;; start 'loop' + (local.set $ret (call $loop)) + (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) + (then unreachable)) + (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) + + ;; tell 'loop' to stop + (call $return) + + ;; wait for 'loop' to run and return + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (local.get $subtask) (local.get $ws)) + (local.set $ret (call $waitable-set.wait (local.get $ws) (i32.const 0))) + (if (i32.ne (i32.const 1 (; SUBTASK ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (local.get $subtask) (i32.load (i32.const 0))) + (then unreachable)) + (if (i32.ne (i32.const 2 (; RETURNED ;)) (i32.load (i32.const 4))) + (then unreachable)) + + ;; ok to drop + (call $subtask.drop (local.get $subtask)) + (i32.const 42) + ) + + (func $drop-before-return (export "drop-before-return") (result i32) + (local $ret i32) (local $subtask i32) + + ;; start 'loop' + (local.set $ret (call $loop (i32.const 0xdead) (i32.const 0xbeef))) + (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) + (then unreachable)) + (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) + + ;; this should trap + (call $subtask.drop (local.get $subtask)) + unreachable + ) + ) + (canon subtask.drop (core func $subtask.drop)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.wait (memory $memory "mem") (core func $waitable-set.wait)) + (canon lower (func $looper "loop") async (memory $memory "mem") (core func $loop')) + (canon lower (func $looper "return") (memory $memory "mem") (core func $return')) + (core instance $core_caller (instantiate $CoreCaller (with "" (instance + (export "mem" (memory $memory "mem")) + (export "subtask.drop" (func $subtask.drop)) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.wait" (func $waitable-set.wait)) + (export "loop" (func $loop')) + (export "return" (func $return')) + )))) + (func (export "drop-after-return") async (result u32) (canon lift + (core func $core_caller "drop-after-return") + )) + (func (export "drop-before-return") async (result u32) (canon lift + (core func $core_caller "drop-before-return") + )) + ) + + (instance $looper (instantiate $Looper)) + (instance $caller1 (instantiate $Caller (with "looper" (instance $looper)))) + (instance $caller2 (instantiate $Caller (with "looper" (instance $looper)))) + (func (export "drop-after-return") (alias export $caller1 "drop-after-return")) + (func (export "drop-before-return") (alias export $caller2 "drop-before-return")) +) +(assert_return (invoke "drop-after-return") (u32.const 42)) +(assert_trap (invoke "drop-before-return") "cannot drop a subtask which has not yet resolved") diff --git a/packages/jco/test/fixtures/wat/component-model/async/drop-waitable-set.wast b/packages/jco/test/fixtures/wat/component-model/async/drop-waitable-set.wast new file mode 100644 index 000000000..23fff15b5 --- /dev/null +++ b/packages/jco/test/fixtures/wat/component-model/async/drop-waitable-set.wast @@ -0,0 +1,84 @@ +;; This test contains two components $C and $D +;; $D.run drives the test and first calls $C.wait-on-set, which waits on +;; a waitable-set. Then $D.run calls $C.drop-while-waiting which attempts +;; to drop the same waitable-set, which should trap. +(component + (component $C + (core module $Core + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.drop" (func $waitable-set.drop (param i32))) + + (global $ws (mut i32) (i32.const 0)) + (func $start (global.set $ws (call $waitable-set.new))) + (start $start) + + (func $wait-on-set (export "wait-on-set") (result i32) + ;; wait on $ws + (i32.or (i32.const 2 (; WAIT ;)) (i32.shl (global.get $ws) (i32.const 4))) + ) + (func $drop-while-waiting (export "drop-while-waiting") (result i32) + ;; boom + (call $waitable-set.drop (global.get $ws)) + unreachable + ) + (func $unreachable-cb (export "unreachable-cb") (param i32 i32 i32) (result i32) + unreachable + ) + ) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.drop (core func $waitable-set.drop)) + (core instance $core (instantiate $Core (with "" (instance + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.drop" (func $waitable-set.drop)) + )))) + (func (export "wait-on-set") async (canon lift + (core func $core "wait-on-set") + async (callback (func $core "unreachable-cb")) + )) + (func (export "drop-while-waiting") async (canon lift + (core func $core "drop-while-waiting") + async (callback (func $core "unreachable-cb")) + )) + ) + + (component $D + (import "c" (instance $c + (export "wait-on-set" (func async)) + (export "drop-while-waiting" (func async)) + )) + + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $Core + (import "" "mem" (memory 1)) + (import "" "wait-on-set" (func $wait-on-set (result i32))) + (import "" "drop-while-waiting" (func $drop-while-waiting)) + (func $run (export "run") (result i32) + (local $ret i32) + + ;; start an async call to 'wait-on-set' which blocks, waiting on a + ;; waitable-set. + (local.set $ret (call $wait-on-set)) + (if (i32.ne (i32.const 0x11) (local.get $ret)) + (then unreachable)) + + ;; this call will try to drop the same waitable-set, which should trap. + (call $drop-while-waiting) + unreachable + ) + ) + (canon lower (func $c "wait-on-set") async (memory $memory "mem") (core func $wait-on-set')) + (canon lower (func $c "drop-while-waiting") (core func $drop-while-waiting')) + (core instance $core (instantiate $Core (with "" (instance + (export "mem" (memory $memory "mem")) + (export "wait-on-set" (func $wait-on-set')) + (export "drop-while-waiting" (func $drop-while-waiting')) + )))) + (func (export "run") async (result u32) (canon lift (core func $core "run"))) + ) + + (instance $c (instantiate $C)) + (instance $d (instantiate $D (with "c" (instance $c)))) + (func (export "run") (alias export $d "run")) +) +(assert_trap (invoke "run") "cannot drop waitable set with waiters") diff --git a/packages/jco/test/fixtures/wat/component-model/async/empty-wait.wast b/packages/jco/test/fixtures/wat/component-model/async/empty-wait.wast new file mode 100644 index 000000000..8b4d789f9 --- /dev/null +++ b/packages/jco/test/fixtures/wat/component-model/async/empty-wait.wast @@ -0,0 +1,199 @@ +;; This test has two components $C and $D, where $D imports and calls $C +;; $C exports two functions: 'blocker' and 'unblocker' +;; 'blocker' blocks on an empty waitable set +;; 'unblocker' wakes blocker by adding a resolved future to blocker's waitable set +;; $D calls 'blocker' then 'unblocker', then waits for 'blocker' to finish +(component + (component $C + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $CM + (import "" "mem" (memory 1)) + (import "" "task.return" (func $task.return (param i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "future.new" (func $future.new (result i64))) + (import "" "future.read" (func $future.read (param i32 i32) (result i32))) + (import "" "future.write" (func $future.write (param i32 i32) (result i32))) + (import "" "future.drop-readable" (func $future.drop-readable (param i32))) + (import "" "future.drop-writable" (func $future.drop-writable (param i32))) + + ;; $ws is waited on by 'blocker' and added to by 'unblocker' + (global $ws (mut i32) (i32.const 0)) + (func $start (global.set $ws (call $waitable-set.new))) + (start $start) + + ;; 'unblocker' initializes $futr with the readable end of a resolved future + (global $futr (mut i32) (i32.const 0)) + + (func $blocker (export "blocker") (result i32) + ;; wait on $ws which is currently empty; 'unblocker' will wake us up + (i32.or (i32.const 2 (; WAIT ;)) (i32.shl (global.get $ws) (i32.const 4))) + ) + (func $blocker_cb (export "blocker_cb") (param $event_code i32) (param $index i32) (param $payload i32) (result i32) + ;; assert that we were in fact woken by 'unblocker' adding $futr to $ws + (if (i32.ne (i32.const 4 (; FUTURE_READ ;)) (local.get $event_code)) + (then unreachable)) + (if (i32.ne (global.get $futr) (local.get $index)) + (then unreachable)) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (local.get $payload)) + (then unreachable)) + + (call $future.drop-readable (global.get $futr)) + + ;; return 42 to $D.run + (call $task.return (i32.const 42)) + (i32.const 0) + ) + + (func $unblocker (export "unblocker") (result i32) + (local $ret i32) (local $ret64 i64) + (local $futw i32) + + ;; create a future that will be used to unblock 'blocker', storing r/w ends in $futr/$futw + (local.set $ret64 (call $future.new)) + (global.set $futr (i32.wrap_i64 (local.get $ret64))) + (if (i32.ne (i32.const 2) (global.get $futr)) + (then unreachable)) + (local.set $futw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + (if (i32.ne (i32.const 3) (local.get $futw)) + (then unreachable)) + + ;; perform a future.read which will block, and add this future to the waitable-set + ;; being waited on by 'blocker' + (local.set $ret (call $future.read (global.get $futr) (i32.const 0xdeadbeef))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (call $waitable.join (global.get $futr) (global.get $ws)) + + ;; perform a future.write which will rendezvous with the write and complete + (local.set $ret (call $future.write (local.get $futw) (i32.const 0xdeadbeef))) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (local.get $ret)) + (then unreachable)) + + (call $future.drop-writable (local.get $futw)) + + ;; return 43 to $D.run + (call $task.return (i32.const 43)) + (i32.const 0) + ) + (func $unblocker_cb (export "unblocker_cb") (param i32 i32 i32) (result i32) + ;; 'unblocker' doesn't block + unreachable + ) + ) + (type $FT (future)) + (canon task.return (result u32) (core func $task.return)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon future.new $FT (core func $future.new)) + (canon future.read $FT async (core func $future.read)) + (canon future.write $FT async (core func $future.write)) + (canon future.drop-readable $FT (core func $future.drop-readable)) + (canon future.drop-writable $FT (core func $future.drop-writable)) + (core instance $cm (instantiate $CM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "task.return" (func $task.return)) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "future.new" (func $future.new)) + (export "future.read" (func $future.read)) + (export "future.write" (func $future.write)) + (export "future.drop-readable" (func $future.drop-readable)) + (export "future.drop-writable" (func $future.drop-writable)) + )))) + (func (export "blocker") async (result u32) (canon lift + (core func $cm "blocker") + async (callback (func $cm "blocker_cb")) + )) + (func (export "unblocker") async (result u32) (canon lift + (core func $cm "unblocker") + async (callback (func $cm "unblocker_cb")) + )) + ) + + (component $D + (import "c" (instance $c + (export "blocker" (func async (result u32))) + (export "unblocker" (func async (result u32))) + )) + + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $DM + (import "" "mem" (memory 1)) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + (import "" "subtask.drop" (func $subtask.drop (param i32))) + (import "" "blocker" (func $blocker (param i32) (result i32))) + (import "" "unblocker" (func $unblocker (param i32) (result i32))) + + (global $ws (mut i32) (i32.const 0)) + (func $start (global.set $ws (call $waitable-set.new))) + (start $start) + + (func $run (export "run") (result i32) + (local $ret i32) (local $retp1 i32) (local $retp2 i32) + (local $subtask i32) + (local $event_code i32) + + ;; call 'blocker'; it should block + (local.set $retp1 (i32.const 4)) + (local.set $ret (call $blocker (local.get $retp1))) + (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) + (then unreachable)) + (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) + (if (i32.ne (i32.const 2) (local.get $subtask)) + (then unreachable)) + + ;; call 'unblocker' to unblock 'blocker'; it should complete eagerly + (local.set $retp2 (i32.const 8)) + (local.set $ret (call $unblocker (local.get $retp2))) + (if (i32.ne (i32.const 2 (; RETURNED ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 43) (i32.load (local.get $retp2))) + (then unreachable)) + + ;; wait for 'blocker' to be scheduled, run, and return + (call $waitable.join (local.get $subtask) (global.get $ws)) + (local.set $retp2 (i32.const 8)) + (local.set $event_code (call $waitable-set.wait (global.get $ws) (local.get $retp2))) + (if (i32.ne (i32.const 1 (; SUBTASK ;)) (local.get $event_code)) + (then unreachable)) + (if (i32.ne (local.get $subtask) (i32.load (local.get $retp2))) + (then unreachable)) + (if (i32.ne (i32.const 2 (; RETURNED=2 | (0<<4) ;)) (i32.load offset=4 (local.get $retp2))) + (then unreachable)) + (if (i32.ne (i32.const 42) (i32.load (local.get $retp1))) + (then unreachable)) + + (call $subtask.drop (local.get $subtask)) + + ;; return 44 to the top-level test harness + (i32.const 44) + ) + ) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.wait (memory $memory "mem") (core func $waitable-set.wait)) + (canon subtask.drop (core func $subtask.drop)) + (canon lower (func $c "blocker") async (memory $memory "mem") (core func $blocker')) + (canon lower (func $c "unblocker") async (memory $memory "mem") (core func $unblocker')) + (core instance $dm (instantiate $DM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.wait" (func $waitable-set.wait)) + (export "subtask.drop" (func $subtask.drop)) + (export "blocker" (func $blocker')) + (export "unblocker" (func $unblocker')) + )))) + (func (export "run") async (result u32) (canon lift (core func $dm "run"))) + ) + + (instance $c (instantiate $C)) + (instance $d (instantiate $D (with "c" (instance $c)))) + (func (export "run") (alias export $d "run")) +) +(assert_return (invoke "run") (u32.const 44)) diff --git a/packages/jco/test/fixtures/wat/component-model/async/futures-must-write.wast b/packages/jco/test/fixtures/wat/component-model/async/futures-must-write.wast new file mode 100644 index 000000000..ce2446b3f --- /dev/null +++ b/packages/jco/test/fixtures/wat/component-model/async/futures-must-write.wast @@ -0,0 +1,118 @@ +;; This test contains two components $C and $D that test that a trap occurs +;; when closing the writable end of a future (in $C) before having written +;; a value while closing the readable end of a future (in $D) before reading +;; a value is fine. +(component + (component $C + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $CM + (import "" "mem" (memory 1)) + (import "" "future.new" (func $future.new (result i64))) + (import "" "future.write" (func $future.write (param i32 i32) (result i32))) + (import "" "future.drop-writable" (func $future.drop-writable (param i32))) + + (global $fw (mut i32) (i32.const 0)) + + (func $start-future (export "start-future") (result i32) + ;; create a new future, return the readable end to the caller + (local $ret64 i64) + (local.set $ret64 (call $future.new)) + (global.set $fw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + (i32.wrap_i64 (local.get $ret64)) + ) + (func $attempt-write (export "attempt-write") (result i32) + ;; because the caller already dropped the readable end, this write will eagerly + ;; return DROPPED having written no values. + (local $ret i32) + (local.set $ret (call $future.write (global.get $fw) (i32.const 42))) + (if (i32.ne (i32.const 0x01 (; DROPPED ;)) (local.get $ret)) + (then unreachable)) + + ;; return without trapping + (i32.const 42) + ) + (func $drop-writable (export "drop-writable") + ;; maybe boom + (call $future.drop-writable (global.get $fw)) + ) + ) + (type $FT (future u8)) + (canon future.new $FT (core func $future.new)) + (canon future.write $FT async (memory $memory "mem") (core func $future.write)) + (canon future.drop-writable $FT (core func $future.drop-writable)) + (core instance $cm (instantiate $CM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "future.new" (func $future.new)) + (export "future.write" (func $future.write)) + (export "future.drop-writable" (func $future.drop-writable)) + )))) + (func (export "start-future") (result (future u8)) (canon lift (core func $cm "start-future"))) + (func (export "attempt-write") (result u32) (canon lift (core func $cm "attempt-write"))) + (func (export "drop-writable") (canon lift (core func $cm "drop-writable"))) + ) + (component $D + (import "c" (instance $c + (export "start-future" (func (result (future u8)))) + (export "attempt-write" (func (result u32))) + (export "drop-writable" (func)) + )) + + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $Core + (import "" "mem" (memory 1)) + (import "" "future.drop-readable" (func $future.drop-readable (param i32))) + (import "" "start-future" (func $start-future (result i32))) + (import "" "attempt-write" (func $attempt-write (result i32))) + (import "" "drop-writable" (func $drop-writable)) + + (func $drop-readable-future-before-read (export "drop-readable-future-before-read") (result i32) + ;; call 'start-future' to get the future we'll be working with + (local $fr i32) + (local.set $fr (call $start-future)) + (if (i32.ne (i32.const 1) (local.get $fr)) + (then unreachable)) + + ;; ok to immediately drop the readable end + (call $future.drop-readable (local.get $fr)) + + ;; the callee will see that we dropped the readable end when it tries to write + (call $attempt-write) + ) + (func $drop-writable-future-before-write (export "drop-writable-future-before-write") + ;; call 'start-future' to get the future we'll be working with + (local $fr i32) + (local.set $fr (call $start-future)) + (if (i32.ne (i32.const 1) (local.get $fr)) + (then unreachable)) + + ;; boom + (call $drop-writable) + ) + ) + (type $FT (future u8)) + (canon future.new $FT (core func $future.new)) + (canon future.drop-readable $FT (core func $future.drop-readable)) + (canon lower (func $c "start-future") (core func $start-future')) + (canon lower (func $c "attempt-write") (core func $attempt-write')) + (canon lower (func $c "drop-writable") (core func $drop-writable')) + (core instance $core (instantiate $Core (with "" (instance + (export "mem" (memory $memory "mem")) + (export "future.new" (func $future.new)) + (export "future.drop-readable" (func $future.drop-readable)) + (export "start-future" (func $start-future')) + (export "attempt-write" (func $attempt-write')) + (export "drop-writable" (func $drop-writable')) + )))) + (func (export "drop-readable-future-before-read") (result u32) (canon lift (core func $core "drop-readable-future-before-read"))) + (func (export "drop-writable-future-before-write") (canon lift (core func $core "drop-writable-future-before-write"))) + ) + (instance $c (instantiate $C)) + (instance $d (instantiate $D (with "c" (instance $c)))) + (func (export "drop-writable-future-before-write") (alias export $d "drop-writable-future-before-write")) + (func (export "drop-readable-future-before-read") (alias export $d "drop-readable-future-before-read")) +) + +(assert_return (invoke "drop-readable-future-before-read") (u32.const 42)) +(assert_trap (invoke "drop-writable-future-before-write") "cannot drop future write end without first writing a value") diff --git a/packages/jco/test/fixtures/wat/component-model/async/partial-stream-copies.wast b/packages/jco/test/fixtures/wat/component-model/async/partial-stream-copies.wast new file mode 100644 index 000000000..d70ece2ea --- /dev/null +++ b/packages/jco/test/fixtures/wat/component-model/async/partial-stream-copies.wast @@ -0,0 +1,238 @@ +;; This test has two components $C and $D, where $D imports and calls $C.transform +;; $C.transform takes and returns a stream +;; Before $C.transform blocks the first time, it supplies a 12-byte read buffer +;; When $D.run regains control after $C.transform blocks, it can perform multiple +;; successful writes until it fully uses up the 12-byte buffer. +;; ... and that's where I am so far ... +(component + (component $C + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $CM + (import "" "mem" (memory 1)) + (import "" "task.return" (func $task.return (param i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "stream.new" (func $stream.new (result i64))) + (import "" "stream.read" (func $stream.read (param i32 i32 i32) (result i32))) + (import "" "stream.write" (func $stream.write (param i32 i32 i32) (result i32))) + (import "" "stream.drop-readable" (func $stream.drop-readable (param i32))) + (import "" "stream.drop-writable" (func $stream.drop-writable (param i32))) + + ;; $ws is waited on by 'transform' + (global $ws (mut i32) (i32.const 0)) + (func $start (global.set $ws (call $waitable-set.new))) + (start $start) + + ;; $insr/$outsw are read/written by 'transform' + (global $insr (mut i32) (i32.const 0)) + (global $inbufp (mut i32) (i32.const 0x10)) + (global $outsw (mut i32) (i32.const 0)) + (global $outbufp (mut i32) (i32.const 0x20)) + + (func $transform (export "transform") (param i32) (result i32) + (local $ret i32) (local $ret64 i64) (local $outsr i32) + + ;; check the incoming readable stream end + (global.set $insr (local.get 0)) + (if (i32.ne (i32.const 2) (global.get $insr)) + (then unreachable)) + + ;; create a new stream r/w pair $outsr/$outsw + (local.set $ret64 (call $stream.new)) + (local.set $outsr (i32.wrap_i64 (local.get $ret64))) + (if (i32.ne (i32.const 3) (local.get $outsr)) + (then unreachable)) + (global.set $outsw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + (if (i32.ne (i32.const 4) (global.get $outsw)) + (then unreachable)) + + ;; start async read on $insr which will block + (local.set $ret (call $stream.read (global.get $insr) (global.get $inbufp) (i32.const 12))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + + ;; return the readable end of the outgoing stream to the caller + (call $task.return (local.get $outsr)) + + ;; wait for the stream.read/write to complete + (call $waitable.join (global.get $insr) (global.get $ws)) + (call $waitable.join (global.get $outsw) (global.get $ws)) + (i32.or (i32.const 2 (; WAIT ;)) (i32.shl (global.get $ws) (i32.const 4))) + ) + (func $transform_cb (export "transform_cb") (param $event_code i32) (param $index i32) (param $payload i32) (result i32) + (local $ret i32) (local $ret64 i64) + + ;; confirm the read succeeded fully + (if (i32.ne (local.get $event_code) (i32.const 2 (; STREAM_READ ;))) + (then unreachable)) + (if (i32.ne (local.get $index) (global.get $insr)) + (then unreachable)) + (if (i32.ne (local.get $payload) (i32.const 0xc0 (; COMPLETED=0 | (12 << 4) ;))) + (then unreachable)) + (if (i32.ne (i32.const 0x89abcdef) (i32.load offset=0 (global.get $inbufp))) + (then unreachable)) + (if (i32.ne (i32.const 0x01234567) (i32.load offset=4 (global.get $inbufp))) + (then unreachable)) + (if (i32.ne (i32.const 0x89abcdef) (i32.load offset=8 (global.get $inbufp))) + (then unreachable)) + + ;; multiple read calls succeed until 12-byte buffer is consumed + (local.set $ret (call $stream.read (global.get $insr) (global.get $inbufp) (i32.const 4))) + (if (i32.ne (i32.const 0x40) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 0x76543210) (i32.load (global.get $inbufp))) + (then unreachable)) + (local.set $ret (call $stream.read (global.get $insr) (global.get $inbufp) (i32.const 2))) + (if (i32.ne (i32.const 0x20) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 0xba98) (i32.load16_u (global.get $inbufp))) + (then unreachable)) + (local.set $ret (call $stream.read (global.get $insr) (global.get $inbufp) (i32.const 8))) + (if (i32.ne (i32.const 0x60) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 0x3210fedc) (i32.load (global.get $inbufp))) + (then unreachable)) + (if (i32.ne (i32.const 0x7654) (i32.load16_u offset=4 (global.get $inbufp))) + (then unreachable)) + + (call $stream.drop-readable (global.get $insr)) + (call $stream.drop-writable (global.get $outsw)) + (return (i32.const 0 (; EXIT ;))) + ) + ) + (type $ST (stream u8)) + (canon task.return (result $ST) (memory $memory "mem") (core func $task.return)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon stream.new $ST (core func $stream.new)) + (canon stream.read $ST async (memory $memory "mem") (core func $stream.read)) + (canon stream.write $ST async (memory $memory "mem") (core func $stream.write)) + (canon stream.drop-readable $ST (core func $stream.drop-readable)) + (canon stream.drop-writable $ST (core func $stream.drop-writable)) + (core instance $cm (instantiate $CM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "task.return" (func $task.return)) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "stream.new" (func $stream.new)) + (export "stream.read" (func $stream.read)) + (export "stream.write" (func $stream.write)) + (export "stream.drop-readable" (func $stream.drop-readable)) + (export "stream.drop-writable" (func $stream.drop-writable)) + )))) + (func (export "transform") async (param "in" (stream u8)) (result (stream u8)) (canon lift + (core func $cm "transform") + async (memory $memory "mem") (callback (func $cm "transform_cb")) + )) + ) + + (component $D + (import "transform" (func $transform async (param "in" (stream u8)) (result (stream u8)))) + + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $DM + (import "" "mem" (memory 1)) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + (import "" "stream.new" (func $stream.new (result i64))) + (import "" "stream.read" (func $stream.read (param i32 i32 i32) (result i32))) + (import "" "stream.write" (func $stream.write (param i32 i32 i32) (result i32))) + (import "" "stream.drop-readable" (func $stream.drop-readable (param i32))) + (import "" "stream.drop-writable" (func $stream.drop-writable (param i32))) + (import "" "transform" (func $transform (param i32 i32) (result i32))) + + (func $run (export "run") (result i32) + (local $ret i32) (local $ret64 i64) (local $retp i32) + (local $insr i32) (local $insw i32) (local $outsr i32) + (local $subtask i32) (local $event_code i32) (local $index i32) (local $payload i32) + (local $ws i32) + + ;; create a new stream r/w pair $insr/$insw + (local.set $ret64 (call $stream.new)) + (local.set $insr (i32.wrap_i64 (local.get $ret64))) + (if (i32.ne (i32.const 1) (local.get $insr)) + (then unreachable)) + (local.set $insw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + (if (i32.ne (i32.const 2) (local.get $insw)) + (then unreachable)) + + ;; call 'transform' which will return a readable stream $outsr eagerly + (local.set $retp (i32.const 8)) + (local.set $ret (call $transform (local.get $insr) (local.get $retp))) + (if (i32.ne (i32.const 2 (; RETURNED=2 | (0<<4) ;)) (local.get $ret)) + (then unreachable)) + (local.set $outsr (i32.load (local.get $retp))) + (if (i32.ne (i32.const 1) (local.get $outsr)) + (then unreachable)) + + ;; multiple write calls succeed until 12-byte buffer is filled + (i64.store (i32.const 16) (i64.const 0x0123456789abcdef)) + (local.set $ret (call $stream.write (local.get $insw) (i32.const 16) (i32.const 8))) + (if (i32.ne (i32.const 0x80) (local.get $ret)) + (then unreachable)) + (local.set $ret (call $stream.write (local.get $insw) (i32.const 16) (i32.const 8))) + (if (i32.ne (i32.const 0x40) (local.get $ret)) + (then unreachable)) + + ;; start a blocking write with a 12-byte buffer + (i64.store (i32.const 16) (i64.const 0xfedcba9876543210)) + (i32.store (i32.const 24) (i32.const 0x76543210)) + (local.set $ret (call $stream.write (local.get $insw) (i32.const 16) (i32.const 12))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + + ;; wait for transform to read our write and drop all the streams + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (local.get $insw) (local.get $ws)) + (local.set $event_code (call $waitable-set.wait (local.get $ws) (i32.const 0))) + (local.set $index (i32.load (i32.const 0))) + (local.set $payload (i32.load (i32.const 4))) + + ;; confirm the write and the dropped stream + (if (i32.ne (local.get $event_code) (i32.const 3 (; STREAM_WRITE ;))) + (then unreachable)) + (if (i32.ne (local.get $index) (local.get $insw)) + (then unreachable)) + (if (i32.ne (local.get $payload) (i32.const 0xc1 (; DROPPED=1 | (12 << 4) ;))) + (then unreachable)) + + (call $stream.drop-writable (local.get $insw)) + (call $stream.drop-readable (local.get $outsr)) + + ;; return 42 to the top-level test harness + (i32.const 42) + ) + ) + (type $ST (stream u8)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.wait (memory $memory "mem") (core func $waitable-set.wait)) + (canon stream.new $ST (core func $stream.new)) + (canon stream.read $ST async (memory $memory "mem") (core func $stream.read)) + (canon stream.write $ST async (memory $memory "mem") (core func $stream.write)) + (canon stream.drop-readable $ST (core func $stream.drop-readable)) + (canon stream.drop-writable $ST (core func $stream.drop-writable)) + (canon lower (func $transform) async (memory $memory "mem") (core func $transform')) + (core instance $dm (instantiate $DM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.wait" (func $waitable-set.wait)) + (export "stream.new" (func $stream.new)) + (export "stream.read" (func $stream.read)) + (export "stream.write" (func $stream.write)) + (export "stream.drop-readable" (func $stream.drop-readable)) + (export "stream.drop-writable" (func $stream.drop-writable)) + (export "transform" (func $transform')) + )))) + (func (export "run") async (result u32) (canon lift (core func $dm "run"))) + ) + + (instance $c (instantiate $C)) + (instance $d (instantiate $D (with "transform" (func $c "transform")))) + (func (export "run") (alias export $d "run")) +) +(assert_return (invoke "run") (u32.const 42)) diff --git a/packages/jco/test/fixtures/wat/component-model/async/passing-resources.wast b/packages/jco/test/fixtures/wat/component-model/async/passing-resources.wast new file mode 100644 index 000000000..d8a7c501f --- /dev/null +++ b/packages/jco/test/fixtures/wat/component-model/async/passing-resources.wast @@ -0,0 +1,176 @@ +;; This test contains two components, $Producer and $Consumer. +;; $Producer.run drives the test and calls $Producer.start-stream to create +;; a stream and attempt to write 2 owned handles. $Producer.run then reads +;; just 1 element. The test finishes by confirming that $Consumer owns the +;; first resource, $Producer (still) owns the second resource, and $Producer +;; traps if it attempts to access the index of the first resource. +(component + (component $Producer + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $Core + (import "" "mem" (memory 1)) + (import "" "resource.new" (func $resource.new (param i32) (result i32))) + (import "" "resource.rep" (func $resource.rep (param i32) (result i32))) + (import "" "stream.new" (func $stream.new (result i64))) + (import "" "stream.write" (func $stream.write (param i32 i32 i32) (result i32))) + (import "" "stream.cancel-write" (func $stream.cancel-write (param i32) (result i32))) + (import "" "stream.drop-writable" (func $stream.drop-writable (param i32))) + + (global $ws (mut i32) (i32.const 0)) + (global $res1 (mut i32) (i32.const 0)) + (global $res2 (mut i32) (i32.const 0)) + + (func $start-stream (export "start-stream") (result i32) + (local $ret i32) (local $ret64 i64) + (local $rs i32) + + ;; create a new stream, return the readable end to the caller + (local.set $ret64 (call $stream.new)) + (global.set $ws (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + (local.set $rs (i32.wrap_i64 (local.get $ret64))) + + ;; create two resources and write them into a buffer to pass to stream.write + (global.set $res1 (call $resource.new (i32.const 50))) + (global.set $res2 (call $resource.new (i32.const 51))) + (i32.store (i32.const 8) (global.get $res1)) + (i32.store (i32.const 12) (global.get $res2)) + + ;; start a write which will block + (local.set $ret (call $stream.write (global.get $ws) (i32.const 8) (i32.const 2))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + + ;; check that this instance still owns both resources (ownership has not + ;; yet been transferred). + (if (i32.ne (i32.const 50) (call $resource.rep (global.get $res1))) + (then unreachable)) + (if (i32.ne (i32.const 51) (call $resource.rep (global.get $res2))) + (then unreachable)) + + (local.get $rs) + ) + (func $cancel-write (export "cancel-write") + (local $ret i32) + + ;; cancel the write, confirming that the first element was transferred + (local.set $ret (call $stream.cancel-write (global.get $ws))) + (if (i32.ne (i32.const 0x11 (; DROPPED=1 | (1 << 4) ;)) (local.get $ret)) + (then unreachable)) + + ;; we still own $res2 + (if (i32.ne (i32.const 51) (call $resource.rep (global.get $res2))) + (then unreachable)) + + (call $stream.drop-writable (global.get $ws)) + ) + (func $R.foo (export "R.foo") (param $rep i32) (result i32) + (i32.add (local.get $rep) (i32.const 50)) + ) + (func $fail-accessing-res1 (export "fail-accessing-res1") + ;; boom + (call $resource.rep (global.get $res1)) + unreachable + ) + ) + (type $R (resource (rep i32))) + (type $ST (stream (own $R))) + (canon resource.new $R (core func $resource.new)) + (canon resource.rep $R (core func $resource.rep)) + (canon stream.new $ST (core func $stream.new)) + (canon stream.write $ST async (memory $memory "mem") (core func $stream.write)) + (canon stream.cancel-write $ST (core func $stream.cancel-write)) + (canon stream.drop-writable $ST (core func $stream.drop-writable)) + (core instance $core (instantiate $Core (with "" (instance + (export "mem" (memory $memory "mem")) + (export "resource.new" (func $resource.new)) + (export "resource.rep" (func $resource.rep)) + (export "stream.new" (func $stream.new)) + (export "stream.write" (func $stream.write)) + (export "stream.cancel-write" (func $stream.cancel-write)) + (export "stream.drop-writable" (func $stream.drop-writable)) + )))) + (export $R' "R" (type $R)) + (func (export "[method]R.foo") async (param "self" (borrow $R')) (result u32) (canon lift (core func $core "R.foo"))) + (func (export "start-stream") async (result (stream (own $R'))) (canon lift (core func $core "start-stream"))) + (func (export "cancel-write") async (canon lift (core func $core "cancel-write"))) + (func (export "fail-accessing-res1") async (canon lift (core func $core "fail-accessing-res1"))) + ) + + (component $Consumer + (import "producer" (instance $producer + (export "R" (type $R (sub resource))) + (export "[method]R.foo" (func async (param "self" (borrow $R)) (result u32))) + (export "start-stream" (func async (result (stream (own $R))))) + (export "cancel-write" (func async)) + )) + + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $Core + (import "" "mem" (memory 1)) + (import "" "stream.read" (func $stream.read (param i32 i32 i32) (result i32))) + (import "" "stream.drop-readable" (func $stream.drop-readable (param i32))) + (import "" "R.foo" (func $R.foo (param i32) (result i32))) + (import "" "start-stream" (func $start-stream (result i32))) + (import "" "cancel-write" (func $cancel-write)) + + (func $run (export "run") (result i32) + (local $ret i32) (local $rs i32) + (local $res1 i32) + + ;; get the readable end of a stream which has a pending write + (local.set $rs (call $start-stream)) + (if (i32.ne (local.get $rs) (i32.const 1)) + (then unreachable)) + + ;; read only 1 (of the 2 pending) elements, which won't block + (i64.store (i32.const 8) (i64.const 0xdeadbeefdeadbeef)) + (local.set $ret (call $stream.read (local.get $rs) (i32.const 8) (i32.const 1))) + (if (i32.ne (i32.const 0x10) (local.get $ret)) + (then unreachable)) + + ;; only 1 handle should have been transferred + (local.set $res1 (i32.load (i32.const 8))) + (if (i32.ne (i32.load (i32.const 12)) (i32.const 0xdeadbeef)) + (then unreachable)) + + ;; check that we got the first resource and it works + (local.set $ret (call $R.foo (local.get $res1))) + (if (i32.ne (i32.const 100) (local.get $ret)) + (then unreachable)) + + ;; drop the stream and then let $C run and assert stuff + (call $stream.drop-readable (local.get $rs)) + (call $cancel-write) + + (i32.const 42) + ) + ) + (alias export $producer "R" (type $R)) + (type $ST (stream (own $R))) + (canon stream.read $ST async (memory $memory "mem") (core func $stream.read)) + (canon stream.drop-readable $ST (core func $stream.drop-readable)) + (canon lower (func $producer "[method]R.foo") (core func $R.foo')) + (canon lower (func $producer "start-stream") (core func $start-stream')) + (canon lower (func $producer "cancel-write") (core func $cancel-write')) + (core instance $core (instantiate $Core (with "" (instance + (export "mem" (memory $memory "mem")) + (export "stream.read" (func $stream.read)) + (export "stream.drop-readable" (func $stream.drop-readable)) + (export "R.foo" (func $R.foo')) + (export "start-stream" (func $start-stream')) + (export "cancel-write" (func $cancel-write')) + )))) + (func (export "run") async (result u32) (canon lift + (core func $core "run") + )) + ) + + (instance $producer (instantiate $Producer)) + (instance $consumer (instantiate $Consumer (with "producer" (instance $producer)))) + (func (export "run") (alias export $consumer "run")) + (func (export "fail-accessing-res1") (alias export $producer "fail-accessing-res1")) +) +(assert_return (invoke "run") (u32.const 42)) +(assert_trap (invoke "fail-accessing-res1") "unknown handle index 3") diff --git a/packages/jco/test/fixtures/wat/component-model/async/same-component-stream-future.wast b/packages/jco/test/fixtures/wat/component-model/async/same-component-stream-future.wast new file mode 100644 index 000000000..18d4a144c --- /dev/null +++ b/packages/jco/test/fixtures/wat/component-model/async/same-component-stream-future.wast @@ -0,0 +1,259 @@ +;; This test tests same-component reading/writing of a stream and future +;; from the same component instance (which either traps or succeeds), +;; depending on the element type. + +(component definition $Tester + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $M + (import "" "mem" (memory 1)) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + (import "" "future.new" (func $future.new (result i64))) + (import "" "future.read" (func $future.read (param i32 i32) (result i32))) + (import "" "future.write" (func $future.write (param i32 i32) (result i32))) + (import "" "stream.new" (func $stream.new (result i64))) + (import "" "stream.read" (func $stream.read (param i32 i32 i32) (result i32))) + (import "" "stream.write" (func $stream.write (param i32 i32 i32) (result i32))) + (import "" "future.newb" (func $future.newb (result i64))) + (import "" "future.readb" (func $future.readb (param i32 i32) (result i32))) + (import "" "future.writeb" (func $future.writeb (param i32 i32) (result i32))) + (import "" "stream.newb" (func $stream.newb (result i64))) + (import "" "stream.readb" (func $stream.readb (param i32 i32 i32) (result i32))) + (import "" "stream.writeb" (func $stream.writeb (param i32 i32 i32) (result i32))) + (import "" "future.newc" (func $future.newc (result i64))) + (import "" "future.readc" (func $future.readc (param i32 i32) (result i32))) + (import "" "future.writec" (func $future.writec (param i32 i32) (result i32))) + + (func (export "test-empty") (result i32) + (local $ret i32) (local $ret64 i64) + (local $rx i32) (local $tx i32) + + ;; test future reader then writer + (local.set $ret64 (call $future.new)) + (local.set $rx (i32.wrap_i64 (local.get $ret64))) + (local.set $tx (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + (local.set $ret (call $future.read (local.get $rx) (i32.const 0xdeadbeef))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (local.set $ret (call $future.write (local.get $tx) (i32.const 0xdeadbeef))) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (local.get $ret)) + (then unreachable)) + + ;; test future writer than reader + (local.set $ret64 (call $future.new)) + (local.set $rx (i32.wrap_i64 (local.get $ret64))) + (local.set $tx (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + (local.set $ret (call $future.write (local.get $tx) (i32.const 0xdeadbeef))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (local.set $ret (call $future.read (local.get $rx) (i32.const 0xdeadbeef))) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (local.get $ret)) + (then unreachable)) + + ;; test stream reader then writer + (local.set $ret64 (call $stream.new)) + (local.set $rx (i32.wrap_i64 (local.get $ret64))) + (local.set $tx (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + (local.set $ret (call $stream.read (local.get $rx) (i32.const 0xdeadbeef) (i32.const 1))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (local.set $ret (call $stream.write (local.get $tx) (i32.const 0xdeadbeef) (i32.const 1))) + (if (i32.ne (i32.const 0x10 (; COMPLETED=0 | (1<<4) ;)) (local.get $ret)) + (then unreachable)) + + ;; test stream writer than reader + (local.set $ret64 (call $stream.new)) + (local.set $rx (i32.wrap_i64 (local.get $ret64))) + (local.set $tx (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + (local.set $ret (call $stream.write (local.get $tx) (i32.const 0xdeadbeef) (i32.const 1))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (local.set $ret (call $stream.read (local.get $rx) (i32.const 0xdeadbeef) (i32.const 1))) + (if (i32.ne (i32.const 0x10 (; COMPLETED=0 | (1<<4) ;)) (local.get $ret)) + (then unreachable)) + + (i32.const 42) + ) + + (func $test-stream (param $srcp i32) (param $dstp i32) + (local $ret i32) (local $ret64 i64) + (local $rx i32) (local $tx i32) + + ;; test stream reader then writer + (i64.store (local.get $dstp) (i64.const 0)) + (i64.store (local.get $srcp) (i64.const 0x0123456789abcdef)) + (local.set $ret64 (call $stream.newb)) + (local.set $rx (i32.wrap_i64 (local.get $ret64))) + (local.set $tx (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + (local.set $ret (call $stream.readb (local.get $rx) (local.get $dstp) (i32.const 8))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (local.set $ret (call $stream.writeb (local.get $tx) (local.get $srcp) (i32.const 8))) + (if (i32.ne (i32.const 0x80 (; COMPLETED=0 | (8<<4) ;)) (local.get $ret)) + (then unreachable)) + (if (i64.ne (i64.load (local.get $dstp)) (i64.const 0x0123456789abcdef)) + (then unreachable)) + + ;; test stream writer than reader + (i64.store (local.get $dstp) (i64.const 0)) + (i64.store (local.get $srcp) (i64.const 0x0123456789abcdef)) + (local.set $ret64 (call $stream.newb)) + (local.set $rx (i32.wrap_i64 (local.get $ret64))) + (local.set $tx (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + (local.set $ret (call $stream.writeb (local.get $tx) (local.get $srcp) (i32.const 8))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (local.set $ret (call $stream.readb (local.get $rx) (local.get $dstp) (i32.const 8))) + (if (i32.ne (i32.const 0x80 (; COMPLETED=0 | (8<<4) ;)) (local.get $ret)) + (then unreachable)) + (if (i64.ne (i64.load (local.get $dstp)) (i64.const 0x0123456789abcdef)) + (then unreachable)) + ) + + (func (export "test-bytes") (result i32) + (local $ret i32) (local $ret64 i64) + (local $rx i32) (local $tx i32) + (local $dstp i32) (local $srcp i32) + + ;; because pointers must be aligned and futures are single-element, + ;; it's not possible to test the interesting overlap case + (local.set $srcp (i32.const 16)) + (local.set $dstp (i32.const 17)) + (i32.store8 (local.get $dstp) (i32.const 0)) + (i32.store8 (local.get $srcp) (i32.const 42)) + + ;; test future reader then writer + (local.set $ret64 (call $future.newb)) + (local.set $rx (i32.wrap_i64 (local.get $ret64))) + (local.set $tx (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + (local.set $ret (call $future.readb (local.get $rx) (local.get $dstp))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (local.set $ret (call $future.writeb (local.get $tx) (local.get $srcp))) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.load8_u (local.get $dstp)) (i32.const 42)) + (then unreachable)) + + ;; reset memory and then test future writer than reader + (i32.store8 (local.get $dstp) (i32.const 0)) + (i32.store8 (local.get $srcp) (i32.const 42)) + (local.set $ret64 (call $future.newb)) + (local.set $rx (i32.wrap_i64 (local.get $ret64))) + (local.set $tx (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + (local.set $ret (call $future.writeb (local.get $tx) (local.get $srcp))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (local.set $ret (call $future.readb (local.get $rx) (local.get $dstp))) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.load8_u (local.get $dstp)) (i32.const 42)) + (then unreachable)) + + ;; test a bunch of different overlapping cases + (call $test-stream (i32.const 16) (i32.const 8)) + (call $test-stream (i32.const 16) (i32.const 9)) + (call $test-stream (i32.const 16) (i32.const 10)) + (call $test-stream (i32.const 16) (i32.const 11)) + (call $test-stream (i32.const 16) (i32.const 12)) + (call $test-stream (i32.const 16) (i32.const 13)) + (call $test-stream (i32.const 16) (i32.const 14)) + (call $test-stream (i32.const 16) (i32.const 15)) + (call $test-stream (i32.const 16) (i32.const 16)) + (call $test-stream (i32.const 16) (i32.const 17)) + (call $test-stream (i32.const 16) (i32.const 18)) + (call $test-stream (i32.const 16) (i32.const 19)) + (call $test-stream (i32.const 16) (i32.const 20)) + (call $test-stream (i32.const 16) (i32.const 21)) + (call $test-stream (i32.const 16) (i32.const 22)) + (call $test-stream (i32.const 16) (i32.const 23)) + (call $test-stream (i32.const 16) (i32.const 24)) + + (i32.const 43) + ) + + (func (export "test-no-read-char") + (local $ret i32) (local $ret64 i64) + (local $rx i32) (local $tx i32) + (local.set $ret64 (call $future.newc)) + (local.set $rx (i32.wrap_i64 (local.get $ret64))) + (local.set $tx (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + (local.set $ret (call $future.readc (local.get $rx) (i32.const 0))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (call $future.writec (local.get $tx) (i32.const 0)) + unreachable + ) + + (func (export "test-no-write-char") + (local $ret i32) (local $ret64 i64) + (local $rx i32) (local $tx i32) + (local.set $ret64 (call $future.newc)) + (local.set $rx (i32.wrap_i64 (local.get $ret64))) + (local.set $tx (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + (local.set $ret (call $future.writec (local.get $tx) (i32.const 0))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (call $future.readc (local.get $rx) (i32.const 0)) + unreachable + ) + ) + (type $FT (future)) + (type $ST (stream)) + (type $FTB (future u8)) + (type $STB (stream u8)) + (type $FTC (future char)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.wait (memory $memory "mem") (core func $waitable-set.wait)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon future.new $FT (core func $future.new)) + (canon future.read $FT async (core func $future.read)) + (canon future.write $FT async (core func $future.write)) + (canon stream.new $ST (core func $stream.new)) + (canon stream.read $ST async (core func $stream.read)) + (canon stream.write $ST async (core func $stream.write)) + (canon future.new $FTB (core func $future.newb)) + (canon future.read $FTB async (memory $memory "mem") (core func $future.readb)) + (canon future.write $FTB async (memory $memory "mem") (core func $future.writeb)) + (canon stream.new $STB (core func $stream.newb)) + (canon stream.read $STB async (memory $memory "mem") (core func $stream.readb)) + (canon stream.write $STB async (memory $memory "mem") (core func $stream.writeb)) + (canon future.new $FTC (core func $future.newc)) + (canon future.read $FTC async (memory $memory "mem") (core func $future.readc)) + (canon future.write $FTC async (memory $memory "mem") (core func $future.writec)) + (core instance $m (instantiate $M (with "" (instance + (export "mem" (memory $memory "mem")) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.wait" (func $waitable-set.wait)) + (export "future.new" (func $future.new)) + (export "future.read" (func $future.read)) + (export "future.write" (func $future.write)) + (export "stream.new" (func $stream.new)) + (export "stream.read" (func $stream.read)) + (export "stream.write" (func $stream.write)) + (export "future.newb" (func $future.newb)) + (export "future.readb" (func $future.readb)) + (export "future.writeb" (func $future.writeb)) + (export "stream.newb" (func $stream.newb)) + (export "stream.readb" (func $stream.readb)) + (export "stream.writeb" (func $stream.writeb)) + (export "future.newc" (func $future.newc)) + (export "future.readc" (func $future.readc)) + (export "future.writec" (func $future.writec)) + )))) + (func (export "test-empty") (result u32) (canon lift (core func $m "test-empty"))) + (func (export "test-bytes") (result u32) (canon lift (core func $m "test-bytes"))) + (func (export "test-no-read-char") (canon lift (core func $m "test-no-read-char"))) + (func (export "test-no-write-char") (canon lift (core func $m "test-no-write-char"))) +) +(component instance $i $Tester) +(assert_return (invoke "test-empty") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "test-bytes") (u32.const 43)) +(component instance $i $Tester) +(assert_trap (invoke "test-no-read-char") "cannot read from and write to intra-component future") +(component instance $i $Tester) +(assert_trap (invoke "test-no-write-char") "cannot read from and write to intra-component future") diff --git a/packages/jco/test/fixtures/wat/component-model/async/sync-barges-in.wast b/packages/jco/test/fixtures/wat/component-model/async/sync-barges-in.wast new file mode 100644 index 000000000..3161f93cb --- /dev/null +++ b/packages/jco/test/fixtures/wat/component-model/async/sync-barges-in.wast @@ -0,0 +1,311 @@ +;; This test tests that a blocked previous sync- or async-lifted callee +;; can be synchronously reentered by a sync-typed function without the +;; usual backpressure triggering. The $Tester component has two nested +;; components $C and $D, where $D imports and calls $C. $C contains utilities +;; used by $D to perform all the tests. +(component definition $Tester + (component $C + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $CM + (import "" "mem" (memory 1)) + (import "" "task.return" (func $task.return (param i32))) + (import "" "thread.yield" (func $thread.yield (result i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + (import "" "future.new" (func $future.new (result i64))) + (import "" "future.read" (func $future.read (param i32 i32) (result i32))) + (import "" "future.write" (func $future.write (param i32 i32) (result i32))) + + (global $unblock-value (mut i32) (i32.const 0)) + + ;; $ws is waited on by 'blocker' and added to by 'unblocker' + (global $ws (mut i32) (i32.const 0)) + (func $start (global.set $ws (call $waitable-set.new))) + (start $start) + + (func (export "blocker") + ;; wait on $ws, which is initially empty, but will be populated with + ;; a completed future when "unblocker" synchronously barges in. + (local $ret i32) + (local.set $ret (call $waitable-set.wait (global.get $ws) (i32.const 0))) + (if (i32.ne (i32.const 4 (; FUTURE_READ ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (i32.load (i32.const 4))) + (then unreachable)) + + (call $task.return (global.get $unblock-value)) + ) + + (func (export "blocker-cb") (result i32) + ;; wait on $ws, which is initially empty, but will be populated with + ;; a completed future when "unblocker" synchronously barges in. + (i32.or + (i32.const 2 (; WAIT ;)) + (i32.shl (global.get $ws) (i32.const 4))) + ) + (func (export "blocker-cb-cb") (param $event_code i32) (param $index i32) (param $payload i32) (result i32) + (if (i32.ne (i32.const 4 (; FUTURE_READ ;)) (local.get $event_code)) + (then unreachable)) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (local.get $payload)) + (then unreachable)) + + (call $task.return (global.get $unblock-value)) + (i32.const 0 (; EXIT ;)) + ) + + (func $unblocker (export "unblocker") (param $val i32) + (local $ret i32) (local $ret64 i64) + (local $futr i32) (local $futw i32) + + ;; create read/write futures that will be used to unblock 'blocker' + (local.set $ret64 (call $future.new)) + (local.set $futr (i32.wrap_i64 (local.get $ret64))) + (local.set $futw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + + ;; perform a future.read which will block, and add this future to the waitable-set + ;; being waited on by 'blocker' + (local.set $ret (call $future.read (local.get $futr) (i32.const 0xdeadbeef))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (call $waitable.join (local.get $futr) (global.get $ws)) + + ;; perform a future.write which will rendezvous with the write and complete + (local.set $ret (call $future.write (local.get $futw) (i32.const 0xdeadbeef))) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (local.get $ret)) + (then unreachable)) + + (global.set $unblock-value (local.get $val)) + ) + + (func (export "yielder") + (drop (call $thread.yield)) + (call $task.return (global.get $unblock-value)) + ) + (func (export "yielder-cb") (result i32) + (i32.const 1 (; YIELD ;)) + ) + (func (export "yielder-cb-cb") (param $event_code i32) (param $index i32) (param $payload i32) (result i32) + (if (i32.ne (i32.const 0 (; EVENT_NONE ;)) (local.get $event_code)) + (then unreachable)) + (if (i32.ne (i32.const 0) (local.get $index)) + (then unreachable)) + (if (i32.ne (i32.const 0) (local.get $payload)) + (then unreachable)) + (call $task.return (global.get $unblock-value)) + (i32.const 0 (; EXIT ;)) + ) + (func (export "poker") (param $val i32) + (global.set $unblock-value (local.get $val)) + ) + ) + (type $FT (future)) + (canon task.return (result u32) (core func $task.return)) + (canon thread.yield (core func $thread.yield)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.wait (memory $memory "mem") (core func $waitable-set.wait)) + (canon future.new $FT (core func $future.new)) + (canon future.read $FT async (core func $future.read)) + (canon future.write $FT async (core func $future.write)) + (core instance $cm (instantiate $CM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "thread.yield" (func $thread.yield)) + (export "task.return" (func $task.return)) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.wait" (func $waitable-set.wait)) + (export "future.new" (func $future.new)) + (export "future.read" (func $future.read)) + (export "future.write" (func $future.write)) + )))) + (type $R (resource (rep i32) (dtor (func $cm "unblocker")))) + (type $S (resource (rep i32) (dtor (func $cm "poker")))) + (canon resource.new $R (core func $new-R)) + (canon resource.new $S (core func $new-S)) + (export $R' "R" (type $R)) + (export $S' "S" (type $S)) + (func (export "new-R") (param "rep" u32) (result (own $R')) (canon lift (core func $new-R))) + (func (export "blocker") async (result u32) (canon lift (core func $cm "blocker") async)) + (func (export "blocker-cb") async (result u32) (canon lift (core func $cm "blocker-cb") async (callback (func $cm "blocker-cb-cb")))) + (func (export "unblocker") (param "val" u32) (canon lift (core func $cm "unblocker"))) + (func (export "new-S") (param "rep" u32) (result (own $S')) (canon lift (core func $new-S))) + (func (export "yielder") async (result u32) (canon lift (core func $cm "yielder") async)) + (func (export "yielder-cb") async (result u32) (canon lift (core func $cm "yielder-cb") async (callback (func $cm "yielder-cb-cb")))) + (func (export "poker") (param "val" u32) (canon lift (core func $cm "poker"))) + ) + (component $D + (import "c" (instance $c + (export "R" (type $R (sub resource))) + (export "new-R" (func (param "rep" u32) (result (own $R)))) + (export "blocker" (func async (result u32))) + (export "blocker-cb" (func async (result u32))) + (export "unblocker" (func (param "val" u32))) + (export "S" (type $S (sub resource))) + (export "new-S" (func (param "rep" u32) (result (own $S)))) + (export "yielder" (func async (result u32))) + (export "yielder-cb" (func async (result u32))) + (export "poker" (func (param "val" u32))) + )) + + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $Core + (import "" "mem" (memory 1)) + (import "" "subtask.drop" (func $subtask.drop (param i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.drop" (func $waitable-set.drop (param i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + (import "" "waitable-set.poll" (func $waitable-set.poll (param i32 i32) (result i32))) + (import "" "new-R" (func $new-R (param i32) (result i32))) + (import "" "drop-R" (func $drop-R (param i32))) + (import "" "blocker" (func $blocker (param i32) (result i32))) + (import "" "blocker-cb" (func $blocker-cb (param i32) (result i32))) + (import "" "unblocker" (func $unblocker (param i32))) + (import "" "new-S" (func $new-S (param i32) (result i32))) + (import "" "drop-S" (func $drop-S (param i32))) + (import "" "yielder" (func $yielder (param i32) (result i32))) + (import "" "yielder-cb" (func $yielder-cb (param i32) (result i32))) + (import "" "poker" (func $poker (param i32))) + + (func $wait-for-return (param $subtask i32) (param $retp i32) (param $expect i32) + (local $outp i32) + (local $ws i32) (local $event_code i32) + + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (local.get $subtask) (local.get $ws)) + (local.set $outp (i32.const 32)) + (local.set $event_code (call $waitable-set.wait (local.get $ws) (local.get $outp))) + (if (i32.ne (i32.const 1 (; SUBTASK ;)) (local.get $event_code)) + (then unreachable)) + (if (i32.ne (local.get $subtask) (i32.load (local.get $outp))) + (then unreachable)) + (if (i32.ne (i32.const 2 (; RETURNED=2 ;)) (i32.load offset=4 (local.get $outp))) + (then unreachable)) + (if (i32.ne (local.get $expect) (i32.load (local.get $retp))) + (then unreachable)) + (call $subtask.drop (local.get $subtask)) + (call $waitable-set.drop (local.get $ws)) + ) + + (func (export "run") (result i32) + (local $ret i32) (local $retp i32) + (local $subtask i32) (local $handle i32) + + (local.set $retp (i32.const 8)) + + ;; call $blocker which will block during a synchronous function. + ;; normally calling another function would hit backpressure until + ;; $blocker was done, but calling the sync-typed function $unblocker + ;; barges in synchronously. + (local.set $ret (call $blocker (local.get $retp))) + (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) + (then unreachable)) + (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) + (call $unblocker (i32.const 90)) + (call $wait-for-return (local.get $subtask) (local.get $retp) (i32.const 90)) + + ;; do it all again, but this time unblock via resource.drop: + (local.set $handle (call $new-R (i32.const 91))) + (local.set $ret (call $blocker (local.get $retp))) + (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) + (then unreachable)) + (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) + (call $drop-R (local.get $handle)) + (call $wait-for-return (local.get $subtask) (local.get $retp) (i32.const 91)) + + ;; do both of the above again, but for $blocker-cb instead of $blocker + (local.set $ret (call $blocker-cb (local.get $retp))) + (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) + (then unreachable)) + (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) + (call $unblocker (i32.const 92)) + (call $wait-for-return (local.get $subtask) (local.get $retp) (i32.const 92)) + (local.set $handle (call $new-R (i32.const 93))) + (local.set $ret (call $blocker-cb (local.get $retp))) + (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) + (then unreachable)) + (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) + (call $drop-R (local.get $handle)) + (call $wait-for-return (local.get $subtask) (local.get $retp) (i32.const 93)) + + ;; now do all the above again, but for 'yielder'/'yielder-cb', which + ;; yield, instead of waiting, using 'poker' to deliver a specific value + (local.set $ret (call $yielder (local.get $retp))) + (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) + (then unreachable)) + (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) + (call $poker (i32.const 94)) + (call $wait-for-return (local.get $subtask) (local.get $retp) (i32.const 94)) + (local.set $handle (call $new-S (i32.const 95))) + (local.set $ret (call $yielder (local.get $retp))) + (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) + (then unreachable)) + (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) + (call $drop-S (local.get $handle)) + (call $wait-for-return (local.get $subtask) (local.get $retp) (i32.const 95)) + (local.set $ret (call $yielder-cb (local.get $retp))) + (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) + (then unreachable)) + (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) + (call $poker (i32.const 96)) + (call $wait-for-return (local.get $subtask) (local.get $retp) (i32.const 96)) + (local.set $handle (call $new-S (i32.const 97))) + (local.set $ret (call $yielder-cb (local.get $retp))) + (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) + (then unreachable)) + (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) + (call $drop-S (local.get $handle)) + (call $wait-for-return (local.get $subtask) (local.get $retp) (i32.const 97)) + + (i32.const 100) + ) + ) + (alias export $c "R" (type $R)) + (alias export $c "S" (type $S)) + (canon resource.drop $R (core func $drop-R')) + (canon resource.drop $S (core func $drop-S')) + (canon subtask.drop (core func $subtask.drop)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.drop (core func $waitable-set.drop)) + (canon waitable-set.wait (memory $memory "mem") (core func $waitable-set.wait)) + (canon waitable-set.poll (memory $memory "mem") (core func $waitable-set.poll)) + (canon lower (func $c "new-R") (core func $new-R')) + (canon lower (func $c "blocker") (memory $memory "mem") async (core func $blocker')) + (canon lower (func $c "blocker-cb") (memory $memory "mem") async (core func $blocker-cb')) + (canon lower (func $c "unblocker") (core func $unblocker')) + (canon lower (func $c "new-S") (core func $new-S')) + (canon lower (func $c "yielder") (memory $memory "mem") async (core func $yielder')) + (canon lower (func $c "yielder-cb") (memory $memory "mem") async (core func $yielder-cb')) + (canon lower (func $c "poker") (core func $poker')) + (core instance $core (instantiate $Core (with "" (instance + (export "mem" (memory $memory "mem")) + (export "subtask.drop" (func $subtask.drop)) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.drop" (func $waitable-set.drop)) + (export "waitable-set.wait" (func $waitable-set.wait)) + (export "waitable-set.poll" (func $waitable-set.poll)) + (export "new-R" (func $new-R')) + (export "drop-R" (func $drop-R')) + (export "blocker" (func $blocker')) + (export "blocker-cb" (func $blocker-cb')) + (export "unblocker" (func $unblocker')) + (export "new-S" (func $new-S')) + (export "drop-S" (func $drop-S')) + (export "yielder" (func $yielder')) + (export "yielder-cb" (func $yielder-cb')) + (export "poker" (func $poker')) + )))) + (func (export "run") async (result u32) (canon lift (core func $core "run"))) + ) + (instance $c (instantiate $C)) + (instance $d (instantiate $D (with "c" (instance $c)))) + (func (export "run") (alias export $d "run")) +) + +(component instance $i $Tester) +(assert_return (invoke "run") (u32.const 100)) diff --git a/packages/jco/test/fixtures/wat/component-model/async/sync-streams.wast b/packages/jco/test/fixtures/wat/component-model/async/sync-streams.wast new file mode 100644 index 000000000..364ba9c34 --- /dev/null +++ b/packages/jco/test/fixtures/wat/component-model/async/sync-streams.wast @@ -0,0 +1,178 @@ +;; This test calls sync stream.write in $C.get and sync stream.read in $C.set. +;; Both of these calls block because $C is first to the rendezvous. But since +;; they are synchronous, control flow switches to $D.run which will do +;; a complementary read/write that rendezvous, and then control flow will +;; switch back to $C.get/set where the synchronous read/write will return +;; without blocking. +(component + (component $C + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $CM + (import "" "mem" (memory 1)) + (import "" "task.return0" (func $task.return0)) + (import "" "task.return1" (func $task.return1 (param i32))) + (import "" "stream.new" (func $stream.new (result i64))) + (import "" "stream.read" (func $stream.read (param i32 i32 i32) (result i32))) + (import "" "stream.write" (func $stream.write (param i32 i32 i32) (result i32))) + (import "" "stream.drop-readable" (func $stream.drop-readable (param i32))) + (import "" "stream.drop-writable" (func $stream.drop-writable (param i32))) + + (func (export "get") (result i32) + (local $ret i32) (local $ret64 i64) + (local $tx i32) (local $rx i32) + (local $bufp i32) + + ;; ($rx, $tx) = stream.new + (local.set $ret64 (call $stream.new)) + (local.set $rx (i32.wrap_i64 (local.get $ret64))) + (local.set $tx (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + + ;; return $rx + (call $task.return1 (local.get $rx)) + + ;; (stream.write $tx $bufp 4) will block and, because called + ;; synchronously, switch to the caller who will read and rendezvous + (local.set $bufp (i32.const 16)) + (i32.store (local.get $bufp) (i32.const 0x01234567)) + (local.set $ret (call $stream.write (local.get $tx) (local.get $bufp) (i32.const 4))) + (if (i32.ne (i32.const 0x41 (; DROPPED=1 | (4<<4) ;)) (local.get $ret)) + (then unreachable)) + + (call $stream.drop-writable (local.get $tx)) + (return (i32.const 0 (; EXIT ;))) + ) + (func (export "get_cb") (param i32 i32 i32) (result i32) + unreachable + ) + + (func (export "set") (param $rx i32) (result i32) + (local $ret i32) (local $ret64 i64) + (local $bufp i32) + + ;; return immediately so that the caller can just call synchronously + (call $task.return0) + + ;; (stream.read $rx $bufp 4) will block and, because called + ;; synchronously, switch to the caller who will write and rendezvous + (local.set $bufp (i32.const 16)) + (local.set $ret (call $stream.read (local.get $rx) (local.get $bufp) (i32.const 4))) + (if (i32.ne (i32.const 0x41 (; DROPPED=1 | (4<<4) ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 0x89abcdef) (i32.load (local.get $bufp))) + (then unreachable)) + + (call $stream.drop-readable (local.get $rx)) + (return (i32.const 0 (; EXIT ;))) + ) + (func (export "set_cb") (param i32 i32 i32) (result i32) + unreachable + ) + ) + (type $ST (stream u8)) + (canon task.return (memory $memory "mem") (core func $task.return0)) + (canon task.return (result $ST) (memory $memory "mem") (core func $task.return1)) + (canon stream.new $ST (core func $stream.new)) + (canon stream.read $ST (memory $memory "mem") (core func $stream.read)) + (canon stream.write $ST (memory $memory "mem") (core func $stream.write)) + (canon stream.drop-readable $ST (core func $stream.drop-readable)) + (canon stream.drop-writable $ST (core func $stream.drop-writable)) + (core instance $cm (instantiate $CM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "task.return0" (func $task.return0)) + (export "task.return1" (func $task.return1)) + (export "stream.new" (func $stream.new)) + (export "stream.read" (func $stream.read)) + (export "stream.write" (func $stream.write)) + (export "stream.drop-readable" (func $stream.drop-readable)) + (export "stream.drop-writable" (func $stream.drop-writable)) + )))) + (func (export "get") async (result (stream u8)) (canon lift + (core func $cm "get") + async (memory $memory "mem") (callback (func $cm "get_cb")) + )) + (func (export "set") async (param "in" (stream u8)) (canon lift + (core func $cm "set") + async (memory $memory "mem") (callback (func $cm "set_cb")) + )) + ) + (component $D + (import "get" (func $get async (result (stream u8)))) + (import "set" (func $set async (param "in" (stream u8)))) + + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $DM + (import "" "mem" (memory 1)) + (import "" "stream.new" (func $stream.new (result i64))) + (import "" "stream.read" (func $stream.read (param i32 i32 i32) (result i32))) + (import "" "stream.write" (func $stream.write (param i32 i32 i32) (result i32))) + (import "" "stream.drop-readable" (func $stream.drop-readable (param i32))) + (import "" "stream.drop-writable" (func $stream.drop-writable (param i32))) + (import "" "get" (func $get (result i32))) + (import "" "set" (func $set (param i32))) + + (func (export "run") (result i32) + (local $ret i32) (local $ret64 i64) + (local $rx i32) (local $tx i32) + (local $bufp i32) + + ;; $rx = $C.get() + (local.set $rx (call $get)) + + ;; (stream.read $rx $bufp 4) will succeed without blocking + (local.set $bufp (i32.const 20)) + (local.set $ret (call $stream.read (local.get $rx) (local.get $bufp) (i32.const 4))) + (if (i32.ne (i32.const 0x40 (; COMPLETED=0 | (4<<4) ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 0x01234567) (i32.load (local.get $bufp))) + (then unreachable)) + + (call $stream.drop-readable (local.get $rx)) + + ;; ($rx, $tx) = stream.new + ;; $C.set($rx) + (local.set $ret64 (call $stream.new)) + (local.set $rx (i32.wrap_i64 (local.get $ret64))) + (local.set $tx (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + (call $set (local.get $rx)) + + ;; (stream.write $tx $bufp 4) will succeed without blocking + (local.set $bufp (i32.const 16)) + (local.set $ret (call $stream.write (local.get $tx) (local.get $bufp) (i32.const 4))) + (if (i32.ne (i32.const 0x40 (; COMPLETED=0 | (4<<4) ;)) (local.get $ret)) + (then unreachable)) + + (call $stream.drop-writable (local.get $tx)) + (i32.const 42) + ) + ) + (type $ST (stream u8)) + (canon stream.new $ST (core func $stream.new)) + (canon stream.read $ST async (memory $memory "mem") (core func $stream.read)) + (canon stream.write $ST async (memory $memory "mem") (core func $stream.write)) + (canon stream.drop-readable $ST (core func $stream.drop-readable)) + (canon stream.drop-writable $ST (core func $stream.drop-writable)) + (canon lower (func $get) (core func $get')) + (canon lower (func $set) (core func $set')) + (core instance $dm (instantiate $DM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "stream.new" (func $stream.new)) + (export "stream.read" (func $stream.read)) + (export "stream.write" (func $stream.write)) + (export "stream.drop-readable" (func $stream.drop-readable)) + (export "stream.drop-writable" (func $stream.drop-writable)) + (export "get" (func $get')) + (export "set" (func $set')) + )))) + (func (export "run") async (result u32) (canon lift (core func $dm "run"))) + ) + + (instance $c (instantiate $C)) + (instance $d (instantiate $D + (with "get" (func $c "get")) + (with "set" (func $c "set")) + )) + (func (export "run") (alias export $d "run")) +) +(assert_return (invoke "run") (u32.const 42)) diff --git a/packages/jco/test/fixtures/wat/component-model/async/trap-if-block-and-sync.wast b/packages/jco/test/fixtures/wat/component-model/async/trap-if-block-and-sync.wast new file mode 100644 index 000000000..e2dc06bfe --- /dev/null +++ b/packages/jco/test/fixtures/wat/component-model/async/trap-if-block-and-sync.wast @@ -0,0 +1,343 @@ +;; The $Tester component has two nested components $C and $D, where $D imports +;; and calls $C. $C contains utilities used by $D to perform all the tests. +;; Most of the tests trap, $Tester exports 1 function per test and a fresh +;; $Tester is created to run each test. +(component definition $Tester + (component $C + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $CM + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + (func (export "sync-async-func") + unreachable + ) + (func (export "async-async-func") (result i32) + unreachable + ) + (func (export "async-async-func-cb") (param i32 i32 i32) (result i32) + unreachable + ) + (func (export "sync-blocks-and-traps") + (call $waitable-set.wait (call $waitable-set.new) (i32.const 0xdeadbeef)) + unreachable + ) + ) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.wait (memory $memory "mem") (core func $waitable-set.wait)) + (core instance $cm (instantiate $CM (with "" (instance + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.wait" (func $waitable-set.wait)) + )))) + (func (export "sync-async-func") async (canon lift (core func $cm "sync-async-func"))) + (func (export "async-async-func") async (canon lift (core func $cm "async-async-func") async (callback (func $cm "async-async-func-cb")))) + (func (export "sync-blocks-and-traps") (canon lift (core func $cm "sync-blocks-and-traps"))) + ) + (component $D + (import "c" (instance $c + (export "sync-async-func" (func async)) + (export "async-async-func" (func async)) + (export "sync-blocks-and-traps" (func)) + )) + + (core module $Memory (memory (export "mem") 1)) + (core module $Table + (table (export "__indirect_function_table") 2 funcref)) + (core instance $memory (instantiate $Memory)) + (core instance $table (instantiate $Table)) + (core module $Core + (import "" "mem" (memory 1)) + (import "" "task.return" (func $task.return (param i32))) + (import "" "subtask.cancel" (func $subtask.cancel (param i32) (result i32))) + (import "" "thread.yield" (func $thread.yield (result i32))) + ;;(import "" "thread.yield-to" (func $thread.yield-to (param i32) (result i32))) + ;;(import "" "thread.switch-to" (func $thread.switch-to (param i32) (result i32))) + ;;(import "" "thread.resume-later" (func $thread.resume-later (param i32))) + (import "" "thread.index" (func $thread-index (result i32))) + (import "" "thread.suspend" (func $thread.suspend (result i32))) + (import "" "thread.new-indirect" (func $thread.new-indirect (param i32 i32) (result i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + (import "" "waitable-set.poll" (func $waitable-set.poll (param i32 i32) (result i32))) + (import "" "stream.read" (func $stream.read (param i32 i32 i32) (result i32))) + (import "" "stream.write" (func $stream.write (param i32 i32 i32) (result i32))) + (import "" "future.read" (func $future.read (param i32 i32) (result i32))) + (import "" "future.write" (func $future.write (param i32 i32) (result i32))) + (import "" "stream.cancel-read" (func $stream.cancel-read (param i32) (result i32))) + (import "" "stream.cancel-write" (func $stream.cancel-write (param i32) (result i32))) + (import "" "future.cancel-read" (func $future.cancel-read (param i32) (result i32))) + (import "" "future.cancel-write" (func $future.cancel-write (param i32) (result i32))) + (import "" "await-sync-async-func" (func $await-sync-async-func)) + (import "" "await-async-async-func" (func $await-async-async-func)) + (import "" "sync-blocks-and-traps" (func $sync-blocks-and-traps)) + (import "" "__indirect_function_table" (table $indirect-function-table 2 funcref)) + + (func (export "unreachable-cb") (param i32 i32 i32) (result i32) + unreachable + ) + (func (export "return-42-cb") (param i32 i32 i32) (result i32) + (call $task.return (i32.const 42)) + (i32.const 0 (; EXIT ;)) + ) + + (func (export "trap-if-sync-call-async1") + (call $await-sync-async-func) + ) + (func (export "trap-if-sync-call-async2") + (call $await-async-async-func) + ) + (func (export "trap-if-async-calls-sync-and-blocks") (result i32) + (call $sync-blocks-and-traps) + unreachable + ) + (func (export "trap-if-suspend") + (call $thread.suspend) + unreachable + ) + (func (export "trap-if-wait") + (call $waitable-set.wait (call $waitable-set.new) (i32.const 0xdeadbeef)) + unreachable + ) + (func (export "trap-if-wait-cb") (result i32) + (i32.or + (i32.const 2 (; WAIT ;)) + (i32.shl (call $waitable-set.new) (i32.const 4))) + ) + (func (export "poll-is-fine") (result i32) + (local $ret i32) + (local.set $ret (call $waitable-set.poll (call $waitable-set.new) (i32.const 0))) + (if (i32.ne (i32.const 0 (; NONE ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 0) (i32.load (i32.const 0))) + (then unreachable)) + (if (i32.ne (i32.const 0) (i32.load (i32.const 4))) + (then unreachable)) + (i32.const 42) + ) + (func (export "trap-if-invalid-callback-code") (param $invalid-code i32) (result i32) + (i32.or + (local.get $invalid-code) + (i32.shl (call $waitable-set.new) (i32.const 4))) + ) + (func (export "yield-is-fine") (result i32) + (drop (call $thread.yield)) + (i32.const 42) + ) + (func (export "yield-is-fine-cb") (result i32) + (i32.or + (i32.const 1 (; YIELD ;)) + (i32.shl (i32.const 0xdead) (i32.const 4))) + ) + (func $thread-start-noop (param i32)) + (elem (table $indirect-function-table) (i32.const 0) func $thread-start-noop) + (func (export "yield-to-is-fine") (result i32) + ;; TODO: rename and reenable + ;;(drop (call $thread.yield-to (call $thread.new-indirect (i32.const 0) (i32.const 0)))) + (i32.const 42) + ) + (func $thread-start-switch-back (param i32) + ;;(drop (call $thread.switch-to (local.get 0))) + ) + (elem (table $indirect-function-table) (i32.const 1) func $thread-start-switch-back) + (func (export "switch-to-is-fine") (result i32) + ;; TODO: rename and reenable + ;;(drop (call $thread.switch-to (call $thread.new-indirect (i32.const 1) (call $thread-index)))) + (i32.const 42) + ) + (func (export "resume-later-is-fine") (result i32) + ;; TODO: rename and reenable + ;;(call $thread.resume-later (call $thread.new-indirect (i32.const 0) (i32.const 0))) + (i32.const 42) + ) + (func (export "trap-if-sync-cancel") + (call $subtask.cancel (i32.const 0xdeadbeef)) + unreachable + ) + (func (export "trap-if-sync-stream-read") + (call $stream.read (i32.const 0xdead) (i32.const 0xbeef) (i32.const 0xdead)) + unreachable + ) + (func (export "trap-if-sync-stream-write") + (call $stream.write (i32.const 0xdead) (i32.const 0xbeef) (i32.const 0xdead)) + unreachable + ) + (func (export "trap-if-sync-future-read") + (call $future.read (i32.const 0xdead) (i32.const 0xdeadbeef)) + unreachable + ) + (func (export "trap-if-sync-future-write") + (call $future.write (i32.const 0xdead) (i32.const 0xdeadbeef)) + unreachable + ) + (func (export "trap-if-sync-stream-cancel-read") + (call $stream.cancel-read (i32.const 0xdead)) + unreachable + ) + (func (export "trap-if-sync-stream-cancel-write") + (call $stream.cancel-write (i32.const 0xdead)) + unreachable + ) + (func (export "trap-if-sync-future-cancel-read") + (call $future.cancel-read (i32.const 0xdead) (i32.const 0xdeadbeef)) + unreachable + ) + (func (export "trap-if-sync-future-cancel-write") + (call $future.cancel-write (i32.const 0xdead) (i32.const 0xdeadbeef)) + unreachable + ) + ) + (type $FT (future u8)) + (type $ST (stream u8)) + (core type $start-func-ty (func (param i32))) + (alias core export $table "__indirect_function_table" (core table $indirect-function-table)) + (core func $thread.new-indirect + (canon thread.new-indirect $start-func-ty (table $indirect-function-table))) + (canon task.return (result u32) (core func $task.return)) + (canon subtask.cancel (core func $subtask.cancel)) + (canon thread.yield (core func $thread.yield)) + (canon thread.suspend (core func $thread.suspend)) + ;;(canon thread.yield-to (core func $thread.yield-to)) + ;;(canon thread.switch-to (core func $thread.switch-to)) + ;;(canon thread.resume-later (core func $thread.resume-later)) + (canon thread.index (core func $thread.index)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.wait (memory $memory "mem") (core func $waitable-set.wait)) + (canon waitable-set.poll (memory $memory "mem") (core func $waitable-set.poll)) + (canon stream.read $ST (memory $memory "mem") (core func $stream.read)) + (canon stream.write $ST (memory $memory "mem") (core func $stream.write)) + (canon future.read $FT (memory $memory "mem") (core func $future.read)) + (canon future.write $FT (memory $memory "mem") (core func $future.write)) + (canon stream.cancel-read $ST (core func $stream.cancel-read)) + (canon stream.cancel-write $ST (core func $stream.cancel-write)) + (canon future.cancel-read $FT (core func $future.cancel-read)) + (canon future.cancel-write $FT (core func $future.cancel-write)) + (canon lower (func $c "sync-async-func") (core func $await-sync-async-func')) + (canon lower (func $c "async-async-func") (core func $await-async-async-func')) + (canon lower (func $c "sync-blocks-and-traps") (core func $sync-blocks-and-traps')) + (core instance $core (instantiate $Core (with "" (instance + (export "mem" (memory $memory "mem")) + (export "task.return" (func $task.return)) + (export "subtask.cancel" (func $subtask.cancel)) + (export "thread.yield" (func $thread.yield)) + (export "thread.suspend" (func $thread.suspend)) + ;;(export "thread.yield-to" (func $thread.yield-to)) + ;;(export "thread.switch-to" (func $thread.switch-to)) + ;;(export "thread.resume-later" (func $thread.resume-later)) + (export "thread.index" (func $thread.index)) + (export "thread.new-indirect" (func $thread.new-indirect)) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.wait" (func $waitable-set.wait)) + (export "waitable-set.poll" (func $waitable-set.poll)) + (export "stream.read" (func $stream.read)) + (export "stream.write" (func $stream.write)) + (export "future.read" (func $future.read)) + (export "future.write" (func $future.write)) + (export "stream.cancel-read" (func $stream.cancel-read)) + (export "stream.cancel-write" (func $stream.cancel-write)) + (export "future.cancel-read" (func $future.cancel-read)) + (export "future.cancel-write" (func $future.cancel-write)) + (export "await-sync-async-func" (func $await-sync-async-func')) + (export "await-async-async-func" (func $await-async-async-func')) + (export "sync-blocks-and-traps" (func $sync-blocks-and-traps')) + (export "__indirect_function_table" (table $indirect-function-table)) + )))) + (func (export "trap-if-suspend") (canon lift (core func $core "trap-if-suspend"))) + (func (export "trap-if-wait") (canon lift (core func $core "trap-if-wait"))) + (func (export "trap-if-wait-cb") async (canon lift (core func $core "trap-if-wait-cb") async (callback (func $core "unreachable-cb")))) + (func (export "poll-is-fine") (result u32) (canon lift (core func $core "poll-is-fine"))) + (func (export "trap-if-invalid-callback-code") async (param "invalid-code" u32) (canon lift (core func $core "trap-if-invalid-callback-code") async (callback (func $core "unreachable-cb")))) + (func (export "yield-is-fine") (result u32) (canon lift (core func $core "yield-is-fine"))) + (func (export "yield-is-fine-cb") async (result u32) (canon lift (core func $core "yield-is-fine-cb") async (callback (func $core "return-42-cb")))) + (func (export "yield-to-is-fine") (result u32) (canon lift (core func $core "yield-to-is-fine"))) + (func (export "switch-to-is-fine") (result u32) (canon lift (core func $core "switch-to-is-fine"))) + (func (export "resume-later-is-fine") (result u32) (canon lift (core func $core "resume-later-is-fine"))) + (func (export "trap-if-sync-call-async1") (canon lift (core func $core "trap-if-sync-call-async1"))) + (func (export "trap-if-sync-call-async2") (canon lift (core func $core "trap-if-sync-call-async2"))) + (func (export "trap-if-async-calls-sync-and-blocks") async (canon lift (core func $core "trap-if-async-calls-sync-and-blocks") async (callback (func $core "unreachable-cb")))) + (func (export "trap-if-sync-cancel") (canon lift (core func $core "trap-if-sync-cancel"))) + (func (export "trap-if-sync-stream-read") (canon lift (core func $core "trap-if-sync-stream-read"))) + (func (export "trap-if-sync-stream-write") (canon lift (core func $core "trap-if-sync-stream-write"))) + (func (export "trap-if-sync-future-read") (canon lift (core func $core "trap-if-sync-future-read"))) + (func (export "trap-if-sync-future-write") (canon lift (core func $core "trap-if-sync-future-write"))) + (func (export "trap-if-sync-stream-cancel-read") (canon lift (core func $core "trap-if-sync-stream-cancel-read"))) + (func (export "trap-if-sync-stream-cancel-write") (canon lift (core func $core "trap-if-sync-stream-cancel-write"))) + (func (export "trap-if-sync-future-cancel-read") (canon lift (core func $core "trap-if-sync-future-cancel-read"))) + (func (export "trap-if-sync-future-cancel-write") (canon lift (core func $core "trap-if-sync-future-cancel-write"))) + ) + (instance $c (instantiate $C)) + (instance $d (instantiate $D (with "c" (instance $c)))) + (func (export "trap-if-sync-call-async1") (alias export $d "trap-if-sync-call-async1")) + (func (export "trap-if-sync-call-async2") (alias export $d "trap-if-sync-call-async2")) + (func (export "trap-if-async-calls-sync-and-blocks") (alias export $d "trap-if-async-calls-sync-and-blocks")) + (func (export "trap-if-suspend") (alias export $d "trap-if-suspend")) + (func (export "trap-if-wait") (alias export $d "trap-if-wait")) + (func (export "trap-if-wait-cb") (alias export $d "trap-if-wait-cb")) + (func (export "poll-is-fine") (alias export $d "poll-is-fine")) + (func (export "trap-if-invalid-callback-code") (alias export $d "trap-if-invalid-callback-code")) + (func (export "yield-is-fine") (alias export $d "yield-is-fine")) + (func (export "yield-is-fine-cb") (alias export $d "yield-is-fine-cb")) + (func (export "yield-to-is-fine") (alias export $d "yield-to-is-fine")) + (func (export "switch-to-is-fine") (alias export $d "switch-to-is-fine")) + (func (export "resume-later-is-fine") (alias export $d "resume-later-is-fine")) + (func (export "trap-if-sync-cancel") (alias export $d "trap-if-sync-cancel")) + (func (export "trap-if-sync-stream-read") (alias export $d "trap-if-sync-stream-read")) + (func (export "trap-if-sync-stream-write") (alias export $d "trap-if-sync-stream-write")) + (func (export "trap-if-sync-future-read") (alias export $d "trap-if-sync-future-read")) + (func (export "trap-if-sync-future-write") (alias export $d "trap-if-sync-future-write")) + (func (export "trap-if-sync-stream-cancel-read") (alias export $d "trap-if-sync-stream-cancel-read")) + (func (export "trap-if-sync-stream-cancel-write") (alias export $d "trap-if-sync-stream-cancel-write")) + (func (export "trap-if-sync-future-cancel-read") (alias export $d "trap-if-sync-future-cancel-read")) + (func (export "trap-if-sync-future-cancel-write") (alias export $d "trap-if-sync-future-cancel-write")) +) + +(component instance $i $Tester) +(assert_trap (invoke "trap-if-sync-call-async1") "cannot block a synchronous task before returning") +(component instance $i $Tester) +(assert_trap (invoke "trap-if-sync-call-async2") "cannot block a synchronous task before returning") +(component instance $i $Tester) +(assert_trap (invoke "trap-if-async-calls-sync-and-blocks") "cannot block a synchronous task before returning") +(component instance $i $Tester) +(assert_trap (invoke "trap-if-suspend") "cannot block a synchronous task before returning") +(component instance $i $Tester) +(assert_trap (invoke "trap-if-wait") "cannot block a synchronous task before returning") +(component instance $i $Tester) +(assert_trap (invoke "trap-if-wait-cb") "cannot block a synchronous task before returning") +(component instance $i $Tester) +(assert_return (invoke "poll-is-fine") (u32.const 42)) +(component instance $i $Tester) +(assert_trap (invoke "trap-if-invalid-callback-code" (u32.const 3)) "unsupported callback code") +(component instance $i $Tester) +(assert_trap (invoke "trap-if-invalid-callback-code" (u32.const 4)) "unsupported callback code") +(component instance $i $Tester) +(assert_trap (invoke "trap-if-invalid-callback-code" (u32.const 15)) "unsupported callback code") +(component instance $i $Tester) +(assert_return (invoke "yield-is-fine") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "yield-is-fine-cb") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "yield-to-is-fine") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "switch-to-is-fine") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "resume-later-is-fine") (u32.const 42)) +(component instance $i $Tester) +(assert_trap (invoke "trap-if-sync-cancel") "cannot block a synchronous task before returning") +(component instance $i $Tester) +(assert_trap (invoke "trap-if-sync-stream-read") "cannot block a synchronous task before returning") +(component instance $i $Tester) +(assert_trap (invoke "trap-if-sync-stream-write") "cannot block a synchronous task before returning") +(component instance $i $Tester) +(assert_trap (invoke "trap-if-sync-future-read") "cannot block a synchronous task before returning") +(component instance $i $Tester) +(assert_trap (invoke "trap-if-sync-future-write") "cannot block a synchronous task before returning") +(component instance $i $Tester) +(assert_trap (invoke "trap-if-sync-stream-cancel-read") "cannot block a synchronous task before returning") +(component instance $i $Tester) +(assert_trap (invoke "trap-if-sync-stream-cancel-write") "cannot block a synchronous task before returning") +(component instance $i $Tester) +(assert_trap (invoke "trap-if-sync-future-cancel-read") "cannot block a synchronous task before returning") +(component instance $i $Tester) +(assert_trap (invoke "trap-if-sync-future-cancel-write") "cannot block a synchronous task before returning") diff --git a/packages/jco/test/fixtures/wat/component-model/async/trap-if-done.wast b/packages/jco/test/fixtures/wat/component-model/async/trap-if-done.wast new file mode 100644 index 000000000..5d7d9aa05 --- /dev/null +++ b/packages/jco/test/fixtures/wat/component-model/async/trap-if-done.wast @@ -0,0 +1,468 @@ +;; This test has two components $C and $D, where $D imports and calls $C. +;; $C contains utility functions used by $D to create futures/streams, +;; write to them and close them. $D uses these utility functions to test for +;; all the cases where, once a future/stream is "done", further uses of the +;; future/stream trap. +;; +;; $D exports a list of functions, one for each case of trapping. Since traps +;; take out their containing instance, a fresh instance of $Tester is created +;; for each call to a $D export. +;; +;; When testing traps involving the readable end, the exports of $D take a +;; "bool" parameter that toggles whether the trap is triggered by +;; {stream,future}.{read,write} or by lifting, and the top-level commands +;; pass 'false' and 'true'. +(component definition $Tester + (component $C + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $CM + (import "" "mem" (memory 1)) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + (import "" "future.new" (func $future.new (result i64))) + (import "" "future.write" (func $future.write (param i32 i32) (result i32))) + (import "" "future.drop-writable" (func $future.drop-writable (param i32))) + (import "" "stream.new" (func $stream.new (result i64))) + (import "" "stream.write" (func $stream.write (param i32 i32 i32) (result i32))) + (import "" "stream.drop-writable" (func $stream.drop-writable (param i32))) + + (global $writable-end (mut i32) (i32.const 0)) + (global $ws (mut i32) (i32.const 0)) + + (func $start (global.set $ws (call $waitable-set.new))) + (start $start) + + (func $start-future (export "start-future") (result i32) + ;; create a new future, return the readable end to the caller + (local $ret64 i64) + (local.set $ret64 (call $future.new)) + (global.set $writable-end (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + (call $waitable.join (global.get $writable-end) (global.get $ws) ) + (i32.wrap_i64 (local.get $ret64)) + ) + (func $future-write (export "future-write") (result i32) + ;; the caller will assert what they expect the return value to be + (i32.store (i32.const 16) (i32.const 42)) + (call $future.write (global.get $writable-end) (i32.const 16)) + ) + (func $acknowledge-future-write (export "acknowledge-future-write") + ;; confirm we got a FUTURE_WRITE $writable-end COMPLETED event + (local $ret i32) + (local.set $ret (call $waitable-set.wait (global.get $ws) (i32.const 0))) + (if (i32.ne (i32.const 5 (; FUTURE_WRITE ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (global.get $writable-end) (i32.load (i32.const 0))) + (then unreachable)) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (i32.load (i32.const 4))) + (then unreachable)) + ) + (func $future-drop-writable (export "future-drop-writable") + ;; maybe boom + (call $future.drop-writable (global.get $writable-end)) + ) + + (func $start-stream (export "start-stream") (result i32) + ;; create a new stream, return the readable end to the caller + (local $ret64 i64) + (local.set $ret64 (call $stream.new)) + (global.set $writable-end (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + (call $waitable.join (global.get $writable-end) (global.get $ws) ) + (i32.wrap_i64 (local.get $ret64)) + ) + (func $stream-write (export "stream-write") (result i32) + ;; the caller will assert what they expect the return value to be + (i32.store (i32.const 16) (i32.const 42)) + (call $stream.write (global.get $writable-end) (i32.const 16) (i32.const 1)) + ) + (func $acknowledge-stream-write (export "acknowledge-stream-write") + ;; confirm we got a STREAM_WRITE $writable-end COMPLETED event + (local $ret i32) + (local.set $ret (call $waitable-set.wait (global.get $ws) (i32.const 0))) + (if (i32.ne (i32.const 3 (; STREAM_WRITE ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (global.get $writable-end) (i32.load (i32.const 0))) + (then unreachable)) + (if (i32.ne (i32.const 0x11 (; DROPPED=1 | (1<<4) ;)) (i32.load (i32.const 4))) + (then unreachable)) + ) + (func $stream-drop-writable (export "stream-drop-writable") + ;; maybe boom + (call $stream.drop-writable (global.get $writable-end)) + ) + ) + (type $FT (future u8)) + (type $ST (stream u8)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.wait (memory $memory "mem") (core func $waitable-set.wait)) + (canon future.new $FT (core func $future.new)) + (canon future.write $FT async (memory $memory "mem") (core func $future.write)) + (canon future.drop-writable $FT (core func $future.drop-writable)) + (canon stream.new $ST (core func $stream.new)) + (canon stream.write $ST async (memory $memory "mem") (core func $stream.write)) + (canon stream.drop-writable $ST (core func $stream.drop-writable)) + (core instance $cm (instantiate $CM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.wait" (func $waitable-set.wait)) + (export "future.new" (func $future.new)) + (export "future.write" (func $future.write)) + (export "future.drop-writable" (func $future.drop-writable)) + (export "stream.new" (func $stream.new)) + (export "stream.write" (func $stream.write)) + (export "stream.drop-writable" (func $stream.drop-writable)) + )))) + (func (export "start-future") async (result (future u8)) (canon lift (core func $cm "start-future"))) + (func (export "future-write") async (result u32) (canon lift (core func $cm "future-write"))) + (func (export "acknowledge-future-write") async (canon lift (core func $cm "acknowledge-future-write"))) + (func (export "future-drop-writable") async (canon lift (core func $cm "future-drop-writable"))) + (func (export "start-stream") async (result (stream u8)) (canon lift (core func $cm "start-stream"))) + (func (export "stream-write") async (result u32) (canon lift (core func $cm "stream-write"))) + (func (export "acknowledge-stream-write") async (canon lift (core func $cm "acknowledge-stream-write"))) + (func (export "stream-drop-writable") async (canon lift (core func $cm "stream-drop-writable"))) + ) + (component $D + (import "c" (instance $c + (export "start-future" (func async (result (future u8)))) + (export "future-write" (func async (result u32))) + (export "acknowledge-future-write" (func async)) + (export "future-drop-writable" (func async)) + (export "start-stream" (func async (result (stream u8)))) + (export "stream-write" (func async (result u32))) + (export "acknowledge-stream-write" (func async)) + (export "stream-drop-writable" (func async)) + )) + + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $Core + (import "" "mem" (memory 1)) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + (import "" "future.read" (func $future.read (param i32 i32) (result i32))) + (import "" "future.drop-readable" (func $future.drop-readable (param i32))) + (import "" "stream.read" (func $stream.read (param i32 i32 i32) (result i32))) + (import "" "stream.drop-readable" (func $stream.drop-readable (param i32))) + (import "" "start-future" (func $start-future (result i32))) + (import "" "future-write" (func $future-write (result i32))) + (import "" "acknowledge-future-write" (func $acknowledge-future-write)) + (import "" "future-drop-writable" (func $future-drop-writable)) + (import "" "start-stream" (func $start-stream (result i32))) + (import "" "stream-write" (func $stream-write (result i32))) + (import "" "acknowledge-stream-write" (func $acknowledge-stream-write)) + (import "" "stream-drop-writable" (func $stream-drop-writable)) + + (func $trap-after-future-eager-write (export "trap-after-future-eager-write") + (local $ret i32) + (local $fr i32) + (local.set $fr (call $start-future)) + + ;; start a read on our end so the next write will succeed + (local.set $ret (call $future.read (local.get $fr) (i32.const 16))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + + ;; calling future.write in $C should succeed eagerly + (local.set $ret (call $future-write)) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 42) (i32.load8_u (i32.const 16))) + (then unreachable)) + + ;; calling future.write in $C now should trap + (drop (call $future-write)) + ) + (func $trap-after-future-async-write (export "trap-after-future-async-write") + (local $ret i32) + (local $fr i32) + (local.set $fr (call $start-future)) + + ;; calling future.write in $C should block + (local.set $ret (call $future-write)) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + + ;; our future.read should then succeed eagerly + (local.set $ret (call $future.read (local.get $fr) (i32.const 16))) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 42) (i32.load8_u (i32.const 16))) + (then unreachable)) + + ;; let $C see the write completed so the future is 'done' + (call $acknowledge-future-write) + + ;; trying to call future.write again in $C should trap + (drop (call $future-write)) + ) + (func $trap-after-future-reader-dropped (export "trap-after-future-reader-dropped") + (local $ret i32) + (local $fr i32) + (local.set $fr (call $start-future)) + + ;; drop our readable end before writer can write + (call $future.drop-readable (local.get $fr)) + + ;; let $C try to future.write and find out we DROPPED + (local.set $ret (call $future-write)) + (if (i32.ne (i32.const 1 (; DROPPED ;)) (local.get $ret)) + (then unreachable)) + + ;; trying to call future.write again in $C should trap + (drop (call $future-write)) + ) + (func $trap-after-future-eager-read (export "trap-after-future-eager-read") (param $bool i32) (result i32) + (local $ret i32) + (local $fr i32) + (local.set $fr (call $start-future)) + + ;; calling future.write in $C should block + (local.set $ret (call $future-write)) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + + ;; our future.read should then succeed eagerly + (local.set $ret (call $future.read (local.get $fr) (i32.const 16))) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 42) (i32.load8_u (i32.const 16))) + (then unreachable)) + + (if (i32.eqz (local.get $bool)) (then + ;; calling future.read again should then trap + (drop (call $future.read (local.get $fr) (i32.const 16))) + ) (else + ;; lifting the future by returning it should also trap + (return (local.get $fr)) + )) + unreachable + ) + (func $trap-after-future-async-read (export "trap-after-future-async-read") (param $bool i32) (result i32) + (local $ret i32) (local $ws i32) + (local $fr i32) + (local.set $fr (call $start-future)) + + ;; read first, so it blocks + (local.set $ret (call $future.read (local.get $fr) (i32.const 16))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + + ;; calling future.write in $C should then succeed eagerly + (local.set $ret (call $future-write)) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 42) (i32.load8_u (i32.const 16))) + (then unreachable)) + + ;; wait to see that our blocked future.read COMPLETED, producing '42' + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (local.get $fr) (local.get $ws)) + (local.set $ret (call $waitable-set.wait (local.get $ws) (i32.const 0))) + (if (i32.ne (i32.const 4 (; FUTURE_READ ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (local.get $fr) (i32.load (i32.const 0))) + (then unreachable)) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (i32.load (i32.const 4))) + (then unreachable)) + (if (i32.ne (i32.const 42) (i32.load (i32.const 16))) + (then unreachable)) + + (if (i32.eqz (local.get $bool)) (then + ;; calling future.read again should then trap + (drop (call $future.read (local.get $fr) (i32.const 16))) + ) (else + ;; lifting the future by returning it should also trap + (return (local.get $fr)) + )) + unreachable + ) + (func $trap-after-stream-reader-eager-dropped (export "trap-after-stream-reader-eager-dropped") + (local $ret i32) + (local $sr i32) + (local.set $sr (call $start-stream)) + + ;; drop our readable end before writer can write + (call $stream.drop-readable (local.get $sr)) + + ;; let $C try to stream.write and find out we DROPPED + (local.set $ret (call $stream-write)) + (if (i32.ne (i32.const 1 (; DROPPED ;)) (local.get $ret)) + (then unreachable)) + + ;; trying to call stream.write again in $C should trap + (drop (call $stream-write)) + ) + (func $trap-after-stream-reader-async-dropped (export "trap-after-stream-reader-async-dropped") + (local $ret i32) + (local $sr i32) + (local.set $sr (call $start-stream)) + + ;; calling stream.write in $C should block + (local.set $ret (call $stream-write)) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + + ;; our stream.read should then succeed eagerly + (local.set $ret (call $stream.read (local.get $sr) (i32.const 16) (i32.const 100))) + (if (i32.ne (i32.const 0x10 (; COMPLETED=0 | (1<<4) ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 42) (i32.load8_u (i32.const 16))) + (then unreachable)) + + ;; then drop our readable end + (call $stream.drop-readable (local.get $sr)) + + ;; let $C see that it's stream.write COMPLETED and wrote 1 elem + (call $acknowledge-stream-write) + + ;; now calling stream.write again in $C will trap + (drop (call $stream-write)) + ) + (func $trap-after-stream-writer-eager-dropped (export "trap-after-stream-writer-eager-dropped") (param $bool i32) (result i32) + (local $ret i32) + (local $sr i32) + (local.set $sr (call $start-stream)) + + ;; immediately drop the writable end + (call $stream-drop-writable) + + ;; calling stream.read will see that the writer dropped + (local.set $ret (call $stream.read (local.get $sr) (i32.const 16) (i32.const 100))) + (if (i32.ne (i32.const 0x01 (; DROPPED=1 | (0<<4) ;)) (local.get $ret)) + (then unreachable)) + + (if (i32.eqz (local.get $bool)) (then + ;; calling stream.read again should then trap + (drop (call $stream.read (local.get $sr) (i32.const 16) (i32.const 100))) + ) (else + ;; lifting the stream by returning it should also trap + (return (local.get $sr)) + )) + unreachable + ) + (func $trap-after-stream-writer-async-dropped (export "trap-after-stream-writer-async-dropped") (param $bool i32) (result i32) + (local $ret i32) (local $ws i32) + (local $sr i32) + (local.set $sr (call $start-stream)) + + ;; start a read on our end first which will block + (local.set $ret (call $stream.read (local.get $sr) (i32.const 16) (i32.const 100))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + + ;; drop the writable end before writing anything + (call $stream-drop-writable) + + ;; wait to see that our blocked stream.read was DROPPED + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (local.get $sr) (local.get $ws)) + (local.set $ret (call $waitable-set.wait (local.get $ws) (i32.const 0))) + (if (i32.ne (i32.const 2 (; STREAM_READ ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (local.get $sr) (i32.load (i32.const 0))) + (then unreachable)) + (if (i32.ne (i32.const 0x01 (; DROPPED=1 | (0<<4) ;)) (i32.load (i32.const 4))) + (then unreachable)) + + (if (i32.eqz (local.get $bool)) (then + ;; calling stream.read again should then trap + (drop (call $stream.read (local.get $sr) (i32.const 16) (i32.const 100))) + ) (else + ;; lifting the stream by returning it should also trap + (return (local.get $sr)) + )) + unreachable + ) + ) + (type $FT (future u8)) + (type $ST (stream u8)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.wait (memory $memory "mem") (core func $waitable-set.wait)) + (canon future.new $FT (core func $future.new)) + (canon future.read $FT async (memory $memory "mem") (core func $future.read)) + (canon future.drop-readable $FT (core func $future.drop-readable)) + (canon stream.new $ST (core func $stream.new)) + (canon stream.read $ST async (memory $memory "mem") (core func $stream.read)) + (canon stream.drop-readable $ST (core func $stream.drop-readable)) + (canon lower (func $c "start-future") (core func $start-future')) + (canon lower (func $c "future-write") (core func $future-write')) + (canon lower (func $c "acknowledge-future-write") (core func $acknowledge-future-write')) + (canon lower (func $c "future-drop-writable") (core func $future-drop-writable')) + (canon lower (func $c "start-stream") (core func $start-stream')) + (canon lower (func $c "stream-write") (core func $stream-write')) + (canon lower (func $c "acknowledge-stream-write") (core func $acknowledge-stream-write')) + (canon lower (func $c "stream-drop-writable") (core func $stream-drop-writable')) + (core instance $core (instantiate $Core (with "" (instance + (export "mem" (memory $memory "mem")) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.wait" (func $waitable-set.wait)) + (export "future.new" (func $future.new)) + (export "future.read" (func $future.read)) + (export "future.drop-readable" (func $future.drop-readable)) + (export "stream.new" (func $stream.new)) + (export "stream.read" (func $stream.read)) + (export "stream.drop-readable" (func $stream.drop-readable)) + (export "start-future" (func $start-future')) + (export "future-write" (func $future-write')) + (export "acknowledge-future-write" (func $acknowledge-future-write')) + (export "future-drop-writable" (func $future-drop-writable')) + (export "start-stream" (func $start-stream')) + (export "stream-write" (func $stream-write')) + (export "acknowledge-stream-write" (func $acknowledge-stream-write')) + (export "stream-drop-writable" (func $stream-drop-writable')) + )))) + (func (export "trap-after-future-eager-write") async (canon lift (core func $core "trap-after-future-eager-write"))) + (func (export "trap-after-future-async-write") async (canon lift (core func $core "trap-after-future-async-write"))) + (func (export "trap-after-future-reader-dropped") async (canon lift (core func $core "trap-after-future-reader-dropped"))) + (func (export "trap-after-future-eager-read") async (param "bool" bool) (result $FT) (canon lift (core func $core "trap-after-future-eager-read"))) + (func (export "trap-after-future-async-read") async (param "bool" bool) (result $FT) (canon lift (core func $core "trap-after-future-async-read"))) + (func (export "trap-after-stream-reader-eager-dropped") async (canon lift (core func $core "trap-after-stream-reader-eager-dropped"))) + (func (export "trap-after-stream-reader-async-dropped") async (canon lift (core func $core "trap-after-stream-reader-async-dropped"))) + (func (export "trap-after-stream-writer-eager-dropped") async (param "bool" bool) (result $ST) (canon lift (core func $core "trap-after-stream-writer-eager-dropped"))) + (func (export "trap-after-stream-writer-async-dropped") async (param "bool" bool) (result $ST) (canon lift (core func $core "trap-after-stream-writer-async-dropped"))) + ) + (instance $c (instantiate $C)) + (instance $d (instantiate $D (with "c" (instance $c)))) + (func (export "trap-after-future-eager-write") (alias export $d "trap-after-future-eager-write")) + (func (export "trap-after-future-async-write") (alias export $d "trap-after-future-async-write")) + (func (export "trap-after-future-reader-dropped") (alias export $d "trap-after-future-reader-dropped")) + (func (export "trap-after-future-eager-read") (alias export $d "trap-after-future-eager-read")) + (func (export "trap-after-future-async-read") (alias export $d "trap-after-future-async-read")) + (func (export "trap-after-stream-reader-eager-dropped") (alias export $d "trap-after-stream-reader-eager-dropped")) + (func (export "trap-after-stream-reader-async-dropped") (alias export $d "trap-after-stream-reader-async-dropped")) + (func (export "trap-after-stream-writer-eager-dropped") (alias export $d "trap-after-stream-writer-eager-dropped")) + (func (export "trap-after-stream-writer-async-dropped") (alias export $d "trap-after-stream-writer-async-dropped")) +) + +(component instance $i1 $Tester) +(assert_trap (invoke "trap-after-future-eager-write") "cannot write to future after previous write succeeded") +(component instance $i2 $Tester) +(assert_trap (invoke "trap-after-future-async-write") "cannot write to future after previous write succeeded") +(component instance $i3 $Tester) +(assert_trap (invoke "trap-after-future-reader-dropped") "cannot write to future after previous write succeeded or readable end dropped") +(component instance $i4.1 $Tester) +(assert_trap (invoke "trap-after-future-eager-read" (bool.const false)) "cannot read from future after previous read succeeded") +(component instance $i4.2 $Tester) +(assert_trap (invoke "trap-after-future-eager-read" (bool.const true)) "cannot lift future after previous read succeeded") +(component instance $i5.1 $Tester) +(assert_trap (invoke "trap-after-future-async-read" (bool.const false)) "cannot read from future after previous read succeeded") +(component instance $i5.2 $Tester) +(assert_trap (invoke "trap-after-future-async-read" (bool.const true)) "cannot lift future after previous read succeeded") +(component instance $i6 $Tester) +(assert_trap (invoke "trap-after-stream-reader-eager-dropped") "cannot write to stream after being notified that the readable end dropped") +(component instance $i7 $Tester) +(assert_trap (invoke "trap-after-stream-reader-async-dropped") "cannot write to stream after being notified that the readable end dropped") +(component instance $i8.1 $Tester) +(assert_trap (invoke "trap-after-stream-writer-eager-dropped" (bool.const false)) "cannot read from stream after being notified that the writable end dropped") +(component instance $i8.2 $Tester) +(assert_trap (invoke "trap-after-stream-writer-eager-dropped" (bool.const true)) "cannot lift stream after being notified that the writable end dropped") +(component instance $i9.1 $Tester) +(assert_trap (invoke "trap-after-stream-writer-async-dropped" (bool.const false)) "cannot read from stream after being notified that the writable end dropped") +(component instance $i9.2 $Tester) +(assert_trap (invoke "trap-after-stream-writer-async-dropped" (bool.const true)) "cannot lift stream after being notified that the writable end dropped") diff --git a/packages/jco/test/fixtures/wat/component-model/async/trap-on-reenter.wast b/packages/jco/test/fixtures/wat/component-model/async/trap-on-reenter.wast new file mode 100644 index 000000000..f363f6d0a --- /dev/null +++ b/packages/jco/test/fixtures/wat/component-model/async/trap-on-reenter.wast @@ -0,0 +1,110 @@ +;; This test creates an asynchronous recursive call stack: +;; $Parent --> $Child --> $Parent +;; That should trap when $Child tries to call $Parent. +(component $Parent + (core module $CoreInner + (memory (export "mem") 1) + (func (export "a") (result i32) + unreachable + ) + (func (export "a-cb") (param i32 i32 i32) (result i32) + unreachable + ) + ) + (core instance $core_inner (instantiate $CoreInner)) + (func $a async (canon lift + (core func $core_inner "a") + async (callback (func $core_inner "a-cb")) + )) + + (component $Child + (import "a" (func $a async)) + + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + + (core module $CoreChild + (import "" "a" (func $a (result i32))) + (func (export "b") (result i32) + (i32.const 1 (; YIELD ;)) + ) + (func (export "b-cb") (param i32 i32 i32) (result i32) + (call $a) + unreachable + ) + ) + (canon lower (func $a) async (memory $memory "mem") (core func $a')) + (core instance $core_child (instantiate $CoreChild (with "" (instance + (export "a" (func $a')) + )))) + (func (export "b") async (canon lift + (core func $core_child "b") + async (callback (func $core_child "b-cb")) + )) + ) + (instance $child (instantiate $Child (with "a" (func $a)))) + + (core module $CoreOuter + (import "" "b" (func $b (result i32))) + (func $c (export "c") (result i32) + (i32.const 1 (; YIELD ;)) + ) + (func $c-cb (export "c-cb") (param i32 i32 i32) (result i32) + (call $b) + ) + ) + (canon lower (func $child "b") async (memory $core_inner "mem") (core func $b)) + (core instance $core_outer (instantiate $CoreOuter (with "" (instance + (export "b" (func $b)) + )))) + (func $c (export "c") async (canon lift + (core func $core_outer "c") + async (callback (func $core_outer "c-cb")) + )) +) +(assert_trap (invoke "c") "wasm trap: cannot enter component instance") + +;; also, for now, trap on parent-to-child +(component $Parent + (component $Child + (core module $CoreChild + (func (export "f")) + ) + (core instance $core_child (instantiate $CoreChild)) + (func (export "f") (canon lift (core func $core_child "f"))) + ) + (instance $child (instantiate $Child)) + (canon lower (func $child "f") (core func $f)) + + (core module $CoreOuter + (import "" "f" (func $f)) + (func (export "g") (call $f)) + ) + (core instance $core_outer (instantiate $CoreOuter (with "" (instance (export "f" (func $f)))))) + (func $g (export "g") (canon lift (core func $core_outer "g"))) +) +(assert_trap (invoke "g") "wasm trap: cannot enter component instance") + +;; also, for now, trap on child-to-parent +(component $Parent + (core module $CoreInner + (func (export "f")) + ) + (core instance $core_inner (instantiate $CoreInner)) + (func $f (canon lift (core func $core_inner "f"))) + + (component $Child + (import "f" (func $f)) + (canon lower (func $f) (core func $f')) + (core module $CoreChild + (import "" "f" (func $f)) + (func (export "g") (call $f)) + ) + (core instance $core_child (instantiate $CoreChild (with "" (instance (export "f" (func $f')))))) + (func (export "g") (canon lift (core func $core_child "g"))) + ) + (instance $child (instantiate $Child (with "f" (func $f)))) + (alias export $child "g" (func $g)) + (export "g" (func $g)) +) +(assert_trap (invoke "g") "wasm trap: cannot enter component instance") diff --git a/packages/jco/test/fixtures/wat/component-model/async/wait-during-callback.wast b/packages/jco/test/fixtures/wat/component-model/async/wait-during-callback.wast new file mode 100644 index 000000000..3f95d5b16 --- /dev/null +++ b/packages/jco/test/fixtures/wat/component-model/async/wait-during-callback.wast @@ -0,0 +1,77 @@ +;; This test calls waitable-set.wait from under an async-callback-lifted export. +(component + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $CM + (import "" "mem" (memory 1)) + (import "" "task.return" (func $task.return (param i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + (import "" "future.new" (func $future.new (result i64))) + (import "" "future.read" (func $future.read (param i32 i32) (result i32))) + (import "" "future.write" (func $future.write (param i32 i32) (result i32))) + + (func $run (export "run") (result i32) + (local $ret i32) (local $ret64 i64) + (local $futr i32) (local $futw i32) (local $ws i32) + (local $event_code i32) (local $retp i32) + + ;; create a future pair + (local.set $ret64 (call $future.new)) + (local.set $futr (i32.wrap_i64 (local.get $ret64))) + (local.set $futw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + + ;; start a pending read that will block + (local.set $ret (call $future.read (local.get $futr) (i32.const 0xdeadbeef))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + + ;; perform a write to make the above read ready + (local.set $ret (call $future.write (local.get $futw) (i32.const 0xdeadbeef))) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (local.get $ret)) + (then unreachable)) + + ;; wait on a waitable set containing our now-ready future.read which + ;; should then immediately resolve + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (local.get $futr) (local.get $ws)) + (local.set $retp (i32.const 0)) + (local.set $event_code (call $waitable-set.wait (local.get $ws) (local.get $retp))) + (if (i32.ne (i32.const 4 (; FUTURE_READ ;)) (local.get $event_code)) + (then unreachable)) + (if (i32.ne (local.get $futr) (i32.load (local.get $retp))) + (then unreachable)) + + ;; return 42 + (call $task.return (i32.const 42)) + (i32.const 0 (; EXIT ;)) + ) + (func (export "run_cb") (param i32 i32 i32) (result i32) + unreachable + ) + ) + (type $FT (future)) + (canon task.return (result u32) (core func $task.return)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.wait (memory $memory "mem") (core func $waitable-set.wait)) + (canon future.new $FT (core func $future.new)) + (canon future.read $FT async (core func $future.read)) + (canon future.write $FT async (core func $future.write)) + (core instance $cm (instantiate $CM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "task.return" (func $task.return)) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.wait" (func $waitable-set.wait)) + (export "future.new" (func $future.new)) + (export "future.read" (func $future.read)) + (export "future.write" (func $future.write)) + )))) + (func (export "run") async (result u32) (canon lift + (core func $cm "run") + async (callback (func $cm "run_cb")) + )) +) +(assert_return (invoke "run") (u32.const 42)) diff --git a/packages/jco/test/fixtures/wat/component-model/async/zero-length.wast b/packages/jco/test/fixtures/wat/component-model/async/zero-length.wast new file mode 100644 index 000000000..fab7765f2 --- /dev/null +++ b/packages/jco/test/fixtures/wat/component-model/async/zero-length.wast @@ -0,0 +1,223 @@ +;; This example defines 3 nested components $Producer, $Consumer and $Parent +;; $Parent imports $Consumer and $Producer, calling $Producer.produce, which +;; returns a stream that $Parent passes to $Consumer.consume. +;; $Producer and $Consumer both start by performing 0-length reads/writes to +;; detect when the other side is ready. Once signalled ready, a 4-byte +;; payload is written/read. +(component + (component $Producer + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $CoreProducer + (import "" "mem" (memory 1)) + (import "" "task.return" (func $task.return (param i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "stream.new" (func $stream.new (result i64))) + (import "" "stream.write" (func $stream.write (param i32 i32 i32) (result i32))) + (import "" "stream.drop-writable" (func $stream.drop-writable (param i32))) + + ;; $ws is waited on by 'produce' + (global $ws (mut i32) (i32.const 0)) + (func $start (global.set $ws (call $waitable-set.new))) + (start $start) + + ;; $outsw is written by 'produce' + (global $outsw (mut i32) (i32.const 0)) + (global $outbufp (mut i32) (i32.const 0x20)) + + (global $state (mut i32) (i32.const 0)) + + (func $produce (export "produce") (result i32) + (local $ret i32) (local $ret64 i64) (local $outsr i32) + + ;; create a new stream r/w pair $outsr/$outsw + (local.set $ret64 (call $stream.new)) + (local.set $outsr (i32.wrap_i64 (local.get $ret64))) + (global.set $outsw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + + ;; return the readable end of the stream to the caller + (call $task.return (local.get $outsr)) + + ;; initiate a zero-length write + (local.set $ret (call $stream.write (global.get $outsw) (i32.const 0xdeadbeef) (i32.const 0))) + (if (i32.ne (i32.const -1) (local.get $ret)) + (then unreachable)) + + ;; wait for the stream.write to complete + (call $waitable.join (global.get $outsw) (global.get $ws)) + (i32.or (i32.const 2 (; WAIT ;)) (i32.shl (global.get $ws) (i32.const 4))) + ) + (func $produce_cb (export "produce_cb") (param $event_code i32) (param $index i32) (param $payload i32) (result i32) + (local $ret i32) + + ;; confirm we're getting a write for the stream $outsw + (if (i32.ne (local.get $event_code) (i32.const 3 (; STREAM_WRITE ;))) + (then unreachable)) + (if (i32.ne (local.get $index) (global.get $outsw)) + (then unreachable)) + + ;; the first call to produce_cb: + (if (i32.eq (global.get $state) (i32.const 0)) (then + ;; confirm we're seeing the zero-length write complete + (if (i32.ne (local.get $payload) (i32.const 0 (; COMPLETED=0 | (0 << 4) ;))) + (then unreachable)) + + ;; issue an async non-zero-length write which should block per spec + (i32.store (i32.const 0) (i32.const 0x12345678)) + (local.set $ret (call $stream.write (global.get $outsw) (i32.const 0) (i32.const 4))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + + (global.set $state (i32.const 1)) + + ;; wait on $ws + (return (i32.or (i32.const 2 (; WAIT ;)) (i32.shl (global.get $ws) (i32.const 4)))) + )) + + ;; the second call to produce_cb: + (if (i32.eq (global.get $state) (i32.const 1)) (then + ;; confirm we're seeing the non-zero-length write complete + (if (i32.ne (local.get $payload) (i32.const 0x41 (; DROPPED=1 | (4 << 4) ;))) + (then unreachable)) + + (call $stream.drop-writable (global.get $outsw)) + (return (i32.const 0 (; EXIT ;))) + )) + + unreachable + ) + ) + (type $ST (stream u8)) + (canon task.return (result $ST) (core func $task.return)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon stream.new $ST (core func $stream.new)) + (canon stream.write $ST async (memory $memory "mem") (core func $stream.write)) + (canon stream.drop-writable $ST (core func $stream.drop-writable)) + (core instance $core_producer (instantiate $CoreProducer (with "" (instance + (export "mem" (memory $memory "mem")) + (export "task.return" (func $task.return)) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "stream.new" (func $stream.new)) + (export "stream.write" (func $stream.write)) + (export "stream.drop-writable" (func $stream.drop-writable)) + )))) + (func (export "produce") async (result (stream u8)) (canon lift + (core func $core_producer "produce") + async (callback (func $core_producer "produce_cb")) + )) + ) + + (component $Consumer + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $CoreConsumer + (import "" "mem" (memory 1)) + (import "" "task.return" (func $task.return (param i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "stream.read" (func $stream.read (param i32 i32 i32) (result i32))) + (import "" "stream.drop-readable" (func $stream.drop-readable (param i32))) + + ;; $ws is waited on by 'consume' + (global $ws (mut i32) (i32.const 0)) + (func $start (global.set $ws (call $waitable-set.new))) + (start $start) + + ;; $insr is read by 'consume' + (global $insr (mut i32) (i32.const 0)) + (global $inbufp (mut i32) (i32.const 0x20)) + + (func $consume (export "consume") (param $insr i32) (result i32) + (local $ret i32) + (global.set $insr (local.get $insr)) + + ;; initiate a zero-length read which will also block (even though there is + ;; a pending write, b/c the pending write is 0-length, per spec) + (local.set $ret (call $stream.read (global.get $insr) (i32.const 0xdeadbeef) (i32.const 0))) + (if (i32.ne (i32.const -1) (local.get $ret)) + (then unreachable)) + + ;; wait for the stream.read to complete + (call $waitable.join (global.get $insr) (global.get $ws)) + (i32.or (i32.const 2 (; WAIT ;)) (i32.shl (global.get $ws) (i32.const 4))) + ) + (func $consume_cb (export "consume_cb") (param $event_code i32) (param $index i32) (param $payload i32) (result i32) + (local $ret i32) + + ;; confirm we're seeing the zero-length read complete + (if (i32.ne (local.get $event_code) (i32.const 2 (; STREAM_READ ;))) + (then unreachable)) + (if (i32.ne (local.get $index) (global.get $insr)) + (then unreachable)) + (if (i32.ne (local.get $payload) (i32.const 0 (; COMPLETED=0 | (0 << 4) ;))) + (then unreachable)) + + ;; perform a non-zero-length read which should succeed without blocking + (local.set $ret (call $stream.read (global.get $insr) (i32.const 0) (i32.const 100))) + (if (i32.ne (i32.const 0x40 (; (4 << 4) | COMPLETED=0 ;)) (local.get $ret)) + (then unreachable)) + (local.set $ret (i32.load (i32.const 0))) + (if (i32.ne (i32.const 0x12345678) (local.get $ret)) + (then unreachable)) + + (call $stream.drop-readable (global.get $insr)) + + ;; return 42 to the top-level assert_return + (call $task.return (i32.const 42)) + (i32.const 0 (; EXIT ;)) + ) + ) + (type $ST (stream u8)) + (canon task.return (result u32) (core func $task.return)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon stream.read $ST async (memory $memory "mem") (core func $stream.read)) + (canon stream.drop-readable $ST (core func $stream.drop-readable)) + (core instance $core_consumer (instantiate $CoreConsumer (with "" (instance + (export "mem" (memory $memory "mem")) + (export "task.return" (func $task.return)) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "stream.read" (func $stream.read)) + (export "stream.drop-readable" (func $stream.drop-readable)) + )))) + (func (export "consume") async (param "in" (stream u8)) (result u32) (canon lift + (core func $core_consumer "consume") + async (callback (func $core_consumer "consume_cb")) + )) + ) + + (component $Parent + (import "produce" (func $produce async (result (stream u8)))) + (import "consume" (func $consume async (param "in" (stream u8)) (result u32))) + + (core module $CoreParent + (import "" "produce" (func $produce (result i32))) + (import "" "consume" (func $consume (param i32) (result i32))) + (memory 1) + (func $run (export "run") (result i32) + (call $consume (call $produce)) + ) + ) + + (canon lower (func $produce) (core func $produce')) + (canon lower (func $consume) (core func $consume')) + (core instance $core_parent (instantiate $CoreParent (with "" (instance + (export "produce" (func $produce')) + (export "consume" (func $consume')) + )))) + (func (export "run") async (result u32) (canon lift (core func $core_parent "run"))) + ) + + (instance $producer (instantiate $Producer)) + (instance $consumer (instantiate $Consumer)) + (instance $parent (instantiate $Parent + (with "produce" (func $producer "produce")) + (with "consume" (func $consumer "consume")) + )) + (func (export "run") (alias export $parent "run")) +) +(assert_return (invoke "run") (u32.const 42)) From 51e170a074ed125bb01e28882bbe8002c398fcc8 Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Mon, 8 Jun 2026 20:09:11 +0900 Subject: [PATCH 2/8] test(jco): add component model WAST test harness --- packages/jco/test/common.js | 5 ++++ .../test/p3/ported/component-model/wast.js | 26 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 packages/jco/test/p3/ported/component-model/wast.js diff --git a/packages/jco/test/common.js b/packages/jco/test/common.js index 3e44d948f..6b39ae8bd 100644 --- a/packages/jco/test/common.js +++ b/packages/jco/test/common.js @@ -32,6 +32,11 @@ export const LOCAL_TEST_COMPONENTS_DIR = join(COMPONENT_FIXTURES_DIR, "../../out /** Path to built extended test components (i.e. output of `just build` run in `jco/test/components`) */ export const EXTENDED_TEST_COMPONENTS_DIR = fileURLToPath(new URL("../../../test/components/output", import.meta.url)); +/** Path to copied component-model repo WAT tests */ +export const COMPONENT_MODEL_FIXTURES_WAST_DIR = fileURLToPath( + new URL("./fixtures/wat/component-model", import.meta.url), +); + /** * Retrieve a list of all component fixtures * diff --git a/packages/jco/test/p3/ported/component-model/wast.js b/packages/jco/test/p3/ported/component-model/wast.js new file mode 100644 index 000000000..2ba303180 --- /dev/null +++ b/packages/jco/test/p3/ported/component-model/wast.js @@ -0,0 +1,26 @@ +import { join, relative } from "node:path"; +import { opendir } from "node:fs/promises"; + +import { suite, test, assert } from "vitest"; + +import { COMPONENT_MODEL_FIXTURES_WAST_DIR } from "../../../common.js"; + +// These tests are ported from the component-model repo +// +// see: https://github.com/WebAssembly/component-model/tree/main/test +// +suite("component-model", async () => { + const walker = await opendir(COMPONENT_MODEL_FIXTURES_WAST_DIR, { recursive: true }); + for await (const dirent of walker) { + if (!dirent.isFile) { + continue; + } + const relPath = relative(COMPONENT_MODEL_FIXTURES_WAST_DIR, join(dirent.parentPath, dirent.name)); + + test.concurrent(relPath, async () => { + // TODO: convert WAST test to WAT + executable JS + console.log(`path [${relPath}]`); + assert.strictEqual(true, true); + }); + } +}); From 7f64fdd34e9ded67032a2e84084b46f44b3eeca5 Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Mon, 8 Jun 2026 21:07:53 +0900 Subject: [PATCH 3/8] test(jco): add machinery for building wast test fixtures --- Cargo.lock | 16 +++- Cargo.toml | 1 + crates/xtask/Cargo.toml | 1 + crates/xtask/src/build/mod.rs | 1 + crates/xtask/src/build/wast_fixtures.rs | 115 ++++++++++++++++++++++++ crates/xtask/src/main.rs | 11 +++ 6 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 crates/xtask/src/build/wast_fixtures.rs diff --git a/Cargo.lock b/Cargo.lock index 51348aac5..edb7ef934 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1918,6 +1918,19 @@ dependencies = [ "wit-parser 0.246.1", ] +[[package]] +name = "wast" +version = "245.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cf1149285569120b8ce39db8b465e8a2b55c34cbb586bd977e43e2bc7300bf" +dependencies = [ + "bumpalo", + "leb128fmt", + "memchr", + "unicode-width 0.2.2", + "wasm-encoder 0.245.1", +] + [[package]] name = "wast" version = "246.0.1" @@ -1937,7 +1950,7 @@ version = "1.246.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "723f2473b47f738c12fc11c8e0bb8b27ce7cf9c78cf1a29dadbc2d34a2513292" dependencies = [ - "wast", + "wast 246.0.1", ] [[package]] @@ -2167,6 +2180,7 @@ dependencies = [ "js-component-bindgen", "semver", "structopt", + "wast 245.0.1", "webidl2wit", "weedle", "wit-component", diff --git a/Cargo.toml b/Cargo.toml index 8ea1a4099..76ab544d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,5 +60,6 @@ wit-bindgen = { version = "0.54.0", default-features = false } wit-bindgen-core = { version = "0.54.0", default-features = false } wit-component = { version = "0.245.1", features = ["dummy-module"] } wit-parser = { version = "0.245.1", default-features = false } +wast = { version = "245.0.1", default-features = false, features = [ "component-model" ] } js-component-bindgen = { version = "1.19.7", path = "./crates/js-component-bindgen" } diff --git a/crates/xtask/Cargo.toml b/crates/xtask/Cargo.toml index 6954344a3..4a6636bcb 100644 --- a/crates/xtask/Cargo.toml +++ b/crates/xtask/Cargo.toml @@ -14,3 +14,4 @@ weedle = "0.13.0" wit-component = { workspace = true } wit-encoder = "0.214.0" xshell = { workspace = true } +wast = { workspace = true } diff --git a/crates/xtask/src/build/mod.rs b/crates/xtask/src/build/mod.rs index c8ae563eb..aa6ee43e7 100644 --- a/crates/xtask/src/build/mod.rs +++ b/crates/xtask/src/build/mod.rs @@ -6,6 +6,7 @@ use xshell::{Shell, cmd}; pub(crate) mod jco; pub(crate) mod test_components; +pub(crate) mod wast_fixtures; pub(crate) mod workspace; pub(crate) static WORKSPACE_DIR: LazyLock = LazyLock::new(|| { diff --git a/crates/xtask/src/build/wast_fixtures.rs b/crates/xtask/src/build/wast_fixtures.rs new file mode 100644 index 000000000..40fc367f3 --- /dev/null +++ b/crates/xtask/src/build/wast_fixtures.rs @@ -0,0 +1,115 @@ +use std::fs::{File, OpenOptions}; +use std::io::{Read, Write}; +use std::path::Path; + +use anyhow::{Context as _, Result, ensure}; + +/// Convert a single WAST file +fn convert_wast_file( + input_wast: &mut File, + input_wast_path: &PathBuf, + output_wasm: &mut File, + output_js: &mut File, +) -> Result<()> { + let mut contents = String::new(); + input_wast + .read_to_string(&mut contents) + .context("failed to read file")?; + let parse_buf = wast::parser::ParseBuffer::new(&contents)?; + let parsed = wast::parser::parse::(&parse_buf).with_context(|| { + format!( + "failed to parse wast directives from [{}]", + input_wast_path.display() + ) + })?; + + // TODO: write JS preamble + + for directive in parsed.directives { + match directive { + wast::WastDirective::Module(mut quote_wat) => { + let encoded = quote_wat.encode().with_context(|| { + format!( + "failed to encode component in WAT [{}]", + input_wast_path.display() + ) + })?; + output_wasm.write_all(&encoded).with_context(|| { + format!( + "failed to write component output in WAT [{}]", + input_wast_path.display() + ) + })?; + } + wast::WastDirective::ModuleDefinition(_) => { + todo!("unsupported directive ModuleDefinition") + } + wast::WastDirective::ModuleInstance { .. } => { + todo!("unsupported directive ModuleInstance") + } + wast::WastDirective::AssertMalformed { .. } => { + todo!("unsupported directive AssertMalformed") + } + wast::WastDirective::AssertInvalid { .. } => { + todo!("unsupported directive AssertInvalid") + } + wast::WastDirective::Register { .. } => { + todo!("unsupported directive Register") + } + wast::WastDirective::Invoke(_) => todo!("unsupported directive Invoke"), + wast::WastDirective::AssertTrap { .. } => todo!("unsupported directive AssertTrap"), + wast::WastDirective::AssertReturn { .. } => todo!("unsupported directive AssertReturn"), + wast::WastDirective::AssertExhaustion { .. } => { + todo!("unsupported directive AssertExhaustion") + } + wast::WastDirective::AssertUnlinkable { .. } => { + todo!("unsupported directive AssertUnlinkable") + } + wast::WastDirective::AssertException { .. } => { + todo!("unsupported directive AssertException") + } + wast::WastDirective::AssertSuspension { .. } => { + todo!("unsupported directive AssertSuspension") + } + wast::WastDirective::Thread(_) => todo!("unsupported directive Thread"), + wast::WastDirective::Wait { .. } => todo!("unsupported directive Wait"), + } + } + + // TODO: output JS file + Ok(()) +} + +/// Build WAST tests that can be used to test p3 host compliance +pub(crate) fn run(wast_path: &Path) -> Result<()> { + let wast_path = wast_path.canonicalize().with_context(|| { + format!( + "failed to canonicalize wast file @ [{}]", + wast_path.display() + ) + })?; + + let mut input_wat = OpenOptions::new() + .read(true) + .open(&wast_path) + .with_context(|| format!("failed to WAST file @ [{}]", wast_path.display()))?; + ensure!(input_wat.metadata()?.is_file(), "wast path must be a file"); + + let mut output_wasm_path = wast_path.clone(); + output_wasm_path.add_extension(".wasm"); + let mut output_wasm = OpenOptions::new() + .write(true) + .truncate(true) + .open(output_wasm_path)?; + + let mut output_js_path = wast_path.clone(); + output_js_path.add_extension(".js"); + let mut output_js = OpenOptions::new() + .write(true) + .truncate(true) + .open(output_js_path)?; + + convert_wast_file(&mut input_wat, &wast_path, &mut output_wasm, &mut output_js)?; + + Ok(()) +} diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs index f35c40805..853bd8a34 100644 --- a/crates/xtask/src/main.rs +++ b/crates/xtask/src/main.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use structopt::StructOpt; mod build; @@ -14,6 +16,11 @@ enum Cmd { Generate(Generate), /// Build test components (i.e. `test-components` crate) BuildTestComponents, + /// Build a single WAST fixture into a Wasm component and executable JS + BuildWastFixture { + /// Path to the given .wast file + path: PathBuf, + }, } #[derive(StructOpt)] @@ -66,6 +73,10 @@ fn main() -> anyhow::Result<()> { build::test_components::run()?; Ok(()) } + Cmd::BuildWastFixture { path } => { + build::wast_fixtures::run(&path)?; + Ok(()) + } Cmd::Test(Platform::Node) => test::run(false), Cmd::Test(Platform::Deno) => test::run(true), Cmd::Generate(Generate::WebidlTests) => generate::webidl_tests::run(), From bf355a7136b52db28b0ffdb43f84e47a3a60cc65 Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Mon, 8 Jun 2026 21:08:18 +0900 Subject: [PATCH 4/8] test(jco): add test setup & checks --- .../test/p3/ported/component-model/wast.js | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/packages/jco/test/p3/ported/component-model/wast.js b/packages/jco/test/p3/ported/component-model/wast.js index 2ba303180..f668b869e 100644 --- a/packages/jco/test/p3/ported/component-model/wast.js +++ b/packages/jco/test/p3/ported/component-model/wast.js @@ -1,9 +1,10 @@ import { join, relative } from "node:path"; import { opendir } from "node:fs/promises"; +import { spawn } from "node:child_process"; -import { suite, test, assert } from "vitest"; +import { suite, test, assert, beforeAll } from "vitest"; -import { COMPONENT_MODEL_FIXTURES_WAST_DIR } from "../../../common.js"; +import { fileExists, COMPONENT_MODEL_FIXTURES_WAST_DIR } from "../../../common.js"; // These tests are ported from the component-model repo // @@ -11,15 +12,39 @@ import { COMPONENT_MODEL_FIXTURES_WAST_DIR } from "../../../common.js"; // suite("component-model", async () => { const walker = await opendir(COMPONENT_MODEL_FIXTURES_WAST_DIR, { recursive: true }); + const metadata = []; for await (const dirent of walker) { if (!dirent.isFile) { continue; } - const relPath = relative(COMPONENT_MODEL_FIXTURES_WAST_DIR, join(dirent.parentPath, dirent.name)); + const wastPath = join(dirent.parentPath, dirent.name); + const wastRelPath = relative(COMPONENT_MODEL_FIXTURES_WAST_DIR, wastPath); + const wasmPath = join(COMPONENT_MODEL_FIXTURES_WAST_DIR, `${dirent.name}.wasm`); + const scriptPath = join(COMPONENT_MODEL_FIXTURES_WAST_DIR, `${dirent.name}.js`); + metadata.push({ + wastRelPath, + wastPath, + wasmPath, + scriptPath, + }); + } + + beforeAll(async () => { + for (const { wastPath } of metadata) { + const fixtureBuild = spawn("cargo", ["xtask", "build-wast-fixture", wastPath], { + detached: false, + stdio: "pipe", + shell: true, + }); + await new Promise(resolve => fixtureBuild.on("exit", resolve)); + } + }); - test.concurrent(relPath, async () => { - // TODO: convert WAST test to WAT + executable JS - console.log(`path [${relPath}]`); + for (const { wastRelPath, wasmPath, scriptPath } of metadata) { + test.concurrent(wastRelPath, async () => { + assert(await fileExists(scriptPath), `missing generated script @ [${scriptPath}]`); + assert(await fileExists(wasmPath), `missing generated wasm component @ [${wasmPath}]`); + // TODO: convert WAST test to WAST + executable JS assert.strictEqual(true, true); }); } From 8f067d32b7578b5c1cf084b2b955e5ed9323118d Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Mon, 8 Jun 2026 21:24:13 +0900 Subject: [PATCH 5/8] fix(jco): fix test file generation --- crates/xtask/src/build/wast_fixtures.rs | 26 +++++++++++++++---- .../fixtures/wat/component-model/.gitignore | 2 ++ .../test/p3/ported/component-model/wast.js | 14 ++++++---- 3 files changed, 32 insertions(+), 10 deletions(-) create mode 100644 packages/jco/test/fixtures/wat/component-model/.gitignore diff --git a/crates/xtask/src/build/wast_fixtures.rs b/crates/xtask/src/build/wast_fixtures.rs index 40fc367f3..ed826a06f 100644 --- a/crates/xtask/src/build/wast_fixtures.rs +++ b/crates/xtask/src/build/wast_fixtures.rs @@ -24,6 +24,12 @@ fn convert_wast_file( })?; // TODO: write JS preamble + writeln!( + output_js, + r#" + import {{ }} from "../"; + "# + )?; for directive in parsed.directives { match directive { @@ -40,6 +46,7 @@ fn convert_wast_file( input_wast_path.display() ) })?; + output_wasm.flush()?; } wast::WastDirective::ModuleDefinition(_) => { todo!("unsupported directive ModuleDefinition") @@ -76,7 +83,7 @@ fn convert_wast_file( } } - // TODO: output JS file + output_js.flush()?; Ok(()) } @@ -96,18 +103,27 @@ pub(crate) fn run(wast_path: &Path) -> Result<()> { ensure!(input_wat.metadata()?.is_file(), "wast path must be a file"); let mut output_wasm_path = wast_path.clone(); - output_wasm_path.add_extension(".wasm"); + output_wasm_path.add_extension("wasm"); let mut output_wasm = OpenOptions::new() .write(true) + .create_new(true) .truncate(true) - .open(output_wasm_path)?; + .open(&output_wasm_path) + .with_context(|| { + format!( + "failed to open output WASM file @ [{}]", + output_wasm_path.display() + ) + })?; let mut output_js_path = wast_path.clone(); - output_js_path.add_extension(".js"); + output_js_path.add_extension("js"); let mut output_js = OpenOptions::new() .write(true) + .create_new(true) .truncate(true) - .open(output_js_path)?; + .open(output_js_path) + .with_context(|| format!("failed to open output JS file @ [{}]", wast_path.display()))?; convert_wast_file(&mut input_wat, &wast_path, &mut output_wasm, &mut output_js)?; diff --git a/packages/jco/test/fixtures/wat/component-model/.gitignore b/packages/jco/test/fixtures/wat/component-model/.gitignore new file mode 100644 index 000000000..3e9568a40 --- /dev/null +++ b/packages/jco/test/fixtures/wat/component-model/.gitignore @@ -0,0 +1,2 @@ +*.wast.wasm +*.wast.js \ No newline at end of file diff --git a/packages/jco/test/p3/ported/component-model/wast.js b/packages/jco/test/p3/ported/component-model/wast.js index f668b869e..fc98a2e1b 100644 --- a/packages/jco/test/p3/ported/component-model/wast.js +++ b/packages/jco/test/p3/ported/component-model/wast.js @@ -4,17 +4,19 @@ import { spawn } from "node:child_process"; import { suite, test, assert, beforeAll } from "vitest"; -import { fileExists, COMPONENT_MODEL_FIXTURES_WAST_DIR } from "../../../common.js"; +import { COMPONENT_MODEL_FIXTURES_WAST_DIR } from "../../../common.js"; +import { fileExists } from "../../../helpers.js"; // These tests are ported from the component-model repo // // see: https://github.com/WebAssembly/component-model/tree/main/test // suite("component-model", async () => { + let metadata = []; const walker = await opendir(COMPONENT_MODEL_FIXTURES_WAST_DIR, { recursive: true }); - const metadata = []; + for await (const dirent of walker) { - if (!dirent.isFile) { + if (!dirent.isFile()) { continue; } const wastPath = join(dirent.parentPath, dirent.name); @@ -29,11 +31,13 @@ suite("component-model", async () => { }); } + metadata = metadata.slice(0,1); + beforeAll(async () => { for (const { wastPath } of metadata) { const fixtureBuild = spawn("cargo", ["xtask", "build-wast-fixture", wastPath], { detached: false, - stdio: "pipe", + stdio: "inherit", shell: true, }); await new Promise(resolve => fixtureBuild.on("exit", resolve)); @@ -42,8 +46,8 @@ suite("component-model", async () => { for (const { wastRelPath, wasmPath, scriptPath } of metadata) { test.concurrent(wastRelPath, async () => { - assert(await fileExists(scriptPath), `missing generated script @ [${scriptPath}]`); assert(await fileExists(wasmPath), `missing generated wasm component @ [${wasmPath}]`); + assert(await fileExists(scriptPath), `missing generated script @ [${scriptPath}]`); // TODO: convert WAST test to WAST + executable JS assert.strictEqual(true, true); }); From 5982c5c65c7ada12e9aeaecca59993758ba3280b Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Mon, 8 Jun 2026 21:31:35 +0900 Subject: [PATCH 6/8] test(jco): add skipped tests to wast --- .../jco/test/p3/ported/component-model/wast.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/jco/test/p3/ported/component-model/wast.js b/packages/jco/test/p3/ported/component-model/wast.js index fc98a2e1b..bfa585cda 100644 --- a/packages/jco/test/p3/ported/component-model/wast.js +++ b/packages/jco/test/p3/ported/component-model/wast.js @@ -7,6 +7,12 @@ import { suite, test, assert, beforeAll } from "vitest"; import { COMPONENT_MODEL_FIXTURES_WAST_DIR } from "../../../common.js"; import { fileExists } from "../../../helpers.js"; +// Relative paths to tests that should be skipped +const TESTS_TO_SKIP = new Set([ + // NOTE: this test must be skipped until we update upstream libs + "validation/implements.wast", +]); + // These tests are ported from the component-model repo // // see: https://github.com/WebAssembly/component-model/tree/main/test @@ -16,11 +22,14 @@ suite("component-model", async () => { const walker = await opendir(COMPONENT_MODEL_FIXTURES_WAST_DIR, { recursive: true }); for await (const dirent of walker) { - if (!dirent.isFile()) { + if (!dirent.isFile() || !dirent.name.endsWith(".wast")) { continue; } + const wastPath = join(dirent.parentPath, dirent.name); const wastRelPath = relative(COMPONENT_MODEL_FIXTURES_WAST_DIR, wastPath); + if (TESTS_TO_SKIP.has(wastRelPath)) { continue; } + const wasmPath = join(COMPONENT_MODEL_FIXTURES_WAST_DIR, `${dirent.name}.wasm`); const scriptPath = join(COMPONENT_MODEL_FIXTURES_WAST_DIR, `${dirent.name}.js`); metadata.push({ @@ -45,7 +54,8 @@ suite("component-model", async () => { }); for (const { wastRelPath, wasmPath, scriptPath } of metadata) { - test.concurrent(wastRelPath, async () => { + const t = TESTS_TO_SKIP.has(wastRelPath) ? test.skip : test.concurrent; + t(wastRelPath, async () => { assert(await fileExists(wasmPath), `missing generated wasm component @ [${wasmPath}]`); assert(await fileExists(scriptPath), `missing generated script @ [${scriptPath}]`); // TODO: convert WAST test to WAST + executable JS From d9313d498d29f4b6b8358899c13a9c55612bac84 Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Mon, 8 Jun 2026 22:33:27 +0900 Subject: [PATCH 7/8] test(jco): wast->js conversion logic --- crates/xtask/src/build/wast_fixtures.rs | 204 ++++++++++++++++-- .../test/p3/ported/component-model/wast.js | 46 +++- 2 files changed, 219 insertions(+), 31 deletions(-) diff --git a/crates/xtask/src/build/wast_fixtures.rs b/crates/xtask/src/build/wast_fixtures.rs index ed826a06f..c811dc113 100644 --- a/crates/xtask/src/build/wast_fixtures.rs +++ b/crates/xtask/src/build/wast_fixtures.rs @@ -1,13 +1,13 @@ +use anyhow::{Context as _, Result, bail, ensure}; use std::fs::{File, OpenOptions}; use std::io::{Read, Write}; use std::path::Path; - -use anyhow::{Context as _, Result, ensure}; +use wast::core::WastRetCore; /// Convert a single WAST file fn convert_wast_file( input_wast: &mut File, - input_wast_path: &PathBuf, + input_wast_path: &Path, output_wasm: &mut File, output_js: &mut File, ) -> Result<()> { @@ -23,11 +23,16 @@ fn convert_wast_file( ) })?; - // TODO: write JS preamble + // Start exported test function writeln!( output_js, r#" - import {{ }} from "../"; + export async function runWastTest(args) {{ + if (!args) {{ throw new Error('missing args'); }} + if (!args.instance) {{ throw new Error('missing loaded wasm instance'); }} + if (!args.assert) {{ throw new Error('missing assert obj'); }} + const {{ instance, assert }} = args; + let res; "# )?; @@ -49,44 +54,182 @@ fn convert_wast_file( output_wasm.flush()?; } wast::WastDirective::ModuleDefinition(_) => { - todo!("unsupported directive ModuleDefinition") + bail!("unsupported directive ModuleDefinition") } wast::WastDirective::ModuleInstance { .. } => { - todo!("unsupported directive ModuleInstance") + bail!("unsupported directive ModuleInstance") } wast::WastDirective::AssertMalformed { .. } => { - todo!("unsupported directive AssertMalformed") + bail!("unsupported directive AssertMalformed") } wast::WastDirective::AssertInvalid { .. } => { - todo!("unsupported directive AssertInvalid") + bail!("unsupported directive AssertInvalid") } wast::WastDirective::Register { .. } => { - todo!("unsupported directive Register") + bail!("unsupported directive Register") + } + wast::WastDirective::Invoke(_) => bail!("unsupported directive Invoke"), + wast::WastDirective::AssertTrap { .. } => bail!("unsupported directive AssertTrap"), + wast::WastDirective::AssertReturn { exec, results, .. } => { + ensure!( + results.len() == 1, + "assert return with multiple results not yet supported" + ); + // TODO: we need to check asyncness for await or not + let (export_name, args) = extract_export_fn(&exec)?; + let check_expr = match results.first() { + Some(ret) => { + format!("assert.strictEqual(res, {});", wast_ret_to_js_param(ret)?) + } + None => "".into(), + }; + writeln!( + output_js, + r#" + res = await instance['{export_name}']({}); + {check_expr} + "#, + args_to_js_params(args)?, + )?; } - wast::WastDirective::Invoke(_) => todo!("unsupported directive Invoke"), - wast::WastDirective::AssertTrap { .. } => todo!("unsupported directive AssertTrap"), - wast::WastDirective::AssertReturn { .. } => todo!("unsupported directive AssertReturn"), wast::WastDirective::AssertExhaustion { .. } => { - todo!("unsupported directive AssertExhaustion") + bail!("unsupported directive AssertExhaustion") } wast::WastDirective::AssertUnlinkable { .. } => { - todo!("unsupported directive AssertUnlinkable") + bail!("unsupported directive AssertUnlinkable") } wast::WastDirective::AssertException { .. } => { - todo!("unsupported directive AssertException") + bail!("unsupported directive AssertException") } wast::WastDirective::AssertSuspension { .. } => { - todo!("unsupported directive AssertSuspension") + bail!("unsupported directive AssertSuspension") } - wast::WastDirective::Thread(_) => todo!("unsupported directive Thread"), - wast::WastDirective::Wait { .. } => todo!("unsupported directive Wait"), + wast::WastDirective::Thread(_) => bail!("unsupported directive Thread"), + wast::WastDirective::Wait { .. } => bail!("unsupported directive Wait"), } } + // Close out the function + writeln!(output_js, "}}",)?; + output_js.flush()?; Ok(()) } +/// Generate a list of JS params +fn args_to_js_params(args: &[wast::WastArg<'_>]) -> Result { + args.iter() + .map(|arg| match arg { + wast::WastArg::Core(v) => core_val_to_js_param(v), + wast::WastArg::Component(v) => cm_val_to_js_param(v), + _ => bail!("unsupported wast arg"), + }) + .collect::>>() + .map(|s| s.join(",")) +} + +/// Convert a Wast core value to a JS value +fn core_val_to_js_param(wast_arg: &wast::core::WastArgCore<'_>) -> Result { + match wast_arg { + wast::core::WastArgCore::I32(v) => Ok(format!("{v}")), + wast::core::WastArgCore::I64(v) => Ok(format!("{v}")), + wast::core::WastArgCore::F32(v) => Ok(format!("{:.8}", f32::from_bits(v.bits))), + wast::core::WastArgCore::F64(v) => Ok(format!("{:.8}", f64::from_bits(v.bits))), + wast::core::WastArgCore::V128(v) => Ok(format!("{}", i128::from_le_bytes(v.to_le_bytes()))), + wast::core::WastArgCore::RefNull(_) => bail!("refs unsupported core args"), + wast::core::WastArgCore::RefExtern(_) => bail!("refs unsupported core args"), + wast::core::WastArgCore::RefHost(_) => bail!("refs unsupported core args"), + } +} + +/// Convert a Wast return value to a JS value +fn wast_ret_to_js_param(wast_ret: &wast::WastRet<'_>) -> Result { + match wast_ret { + wast::WastRet::Core(wast_ret_core) => wast_ret_core_val_to_js_param(wast_ret_core), + wast::WastRet::Component(wast_val) => cm_val_to_js_param(wast_val), + _ => bail!("unsupported wast ret"), + } +} + +/// Convert a Wast CM value to a JS value +fn wast_ret_core_val_to_js_param(wast_ret_core: &WastRetCore<'_>) -> Result { + match wast_ret_core { + WastRetCore::I32(_) => bail!("WastRetCore::I32 not yet supported"), + WastRetCore::I64(_) => bail!("WastRetCore::I64 not yet supported"), + WastRetCore::F32(_nan_pattern) => bail!("WastRetCore::F32 not yet supported"), + WastRetCore::F64(_nan_pattern) => bail!("WastRetCore::F64 not yet supported"), + WastRetCore::V128(_v128_pattern) => bail!("WastRetCore::V128 not yet supported"), + WastRetCore::RefNull(_heap_type) => bail!("WastRetCore::RefNull not yet supported"), + WastRetCore::RefExtern(_) => bail!("WastRetCore::RefExtern not yet supported"), + WastRetCore::RefHost(_) => bail!("WastRetCore::RefHost not yet supported"), + WastRetCore::RefFunc(_index) => bail!("WastRetCore::RefFunc not yet supported"), + WastRetCore::RefAny => bail!("WastRetCore::RefAny not yet supported"), + WastRetCore::RefEq => bail!("WastRetCore::RefEq not yet supported"), + WastRetCore::RefArray => bail!("WastRetCore::RefArray not yet supported"), + WastRetCore::RefStruct => bail!("WastRetCore::RefStruct not yet supported"), + WastRetCore::RefI31 => bail!("WastRetCore::RefI31 not yet supported"), + WastRetCore::RefI31Shared => bail!("WastRetCore::RefI31Shared not yet supported"), + WastRetCore::Either(_wast_ret_cores) => bail!("WastRetCore::Either not yet supported"), + } +} + +/// Convert a Wast CM value to a JS value +fn cm_val_to_js_param(wast_val: &wast::component::WastVal<'_>) -> Result { + match wast_val { + wast::component::WastVal::Bool(v) => Ok(format!("{v}")), + wast::component::WastVal::U8(v) => Ok(format!("{v}")), + wast::component::WastVal::S8(v) => Ok(format!("{v}")), + wast::component::WastVal::U16(v) => Ok(format!("{v}")), + wast::component::WastVal::S16(v) => Ok(format!("{v}")), + wast::component::WastVal::U32(v) => Ok(format!("{v}")), + wast::component::WastVal::S32(v) => Ok(format!("{v}")), + wast::component::WastVal::U64(v) => Ok(format!("{v}")), + wast::component::WastVal::S64(v) => Ok(format!("{v}")), + wast::component::WastVal::F32(v) => Ok(format!("{:.8}", f32::from_bits(v.bits))), + wast::component::WastVal::F64(v) => Ok(format!("{:.8}", f64::from_bits(v.bits))), + wast::component::WastVal::Char(v) => Ok(format!("'{v}'")), + wast::component::WastVal::String(s) => Ok(format!("'{s}'")), + wast::component::WastVal::List(vals) | wast::component::WastVal::Tuple(vals) => vals + .iter() + .map(|v| cm_val_to_js_param(v)) + .collect::>>() + .map(|parts| parts.join(",")) + .map(|v| format!("[{v}]")), + wast::component::WastVal::Record(items) => items + .iter() + .map(|(k, v)| cm_val_to_js_param(v).map(|v| format!("{k}: {v}"))) + .collect::>>() + .map(|parts| parts.join(",")) + .map(|v| format!("{{{v}}}")), + wast::component::WastVal::Variant(tag, wast_val) => match wast_val { + Some(v) => cm_val_to_js_param(v).map(|v| format!("{{ tag: '{tag}', val: {v} }}")), + None => Ok(format!("{{ tag: '{tag}', val: null }}")), + }, + wast::component::WastVal::Enum(v) => Ok(format!("{{ tag: '{v}' }}")), + wast::component::WastVal::Option(wast_val) => match wast_val { + Some(v) => cm_val_to_js_param(v), + None => Ok("null".into()), + }, + wast::component::WastVal::Result(wast_val) => match wast_val { + Ok(v) => match v { + Some(v) => cm_val_to_js_param(v), + None => Ok("{{ tag: 'ok', val: null }}".into()), + }, + Err(e) => match e { + Some(v) => cm_val_to_js_param(v), + None => Ok("{{ tag: 'err', val: null }}".into()), + }, + }, + wast::component::WastVal::Flags(items) => Ok(format!( + "{{{}}}", + items + .iter() + .map(|k| format!("{k}: true")) + .collect::>() + .join(",") + )), + } +} /// Build WAST tests that can be used to test p3 host compliance pub(crate) fn run(wast_path: &Path) -> Result<()> { let wast_path = wast_path.canonicalize().with_context(|| { @@ -106,7 +249,7 @@ pub(crate) fn run(wast_path: &Path) -> Result<()> { output_wasm_path.add_extension("wasm"); let mut output_wasm = OpenOptions::new() .write(true) - .create_new(true) + .create(true) .truncate(true) .open(&output_wasm_path) .with_context(|| { @@ -120,7 +263,7 @@ pub(crate) fn run(wast_path: &Path) -> Result<()> { output_js_path.add_extension("js"); let mut output_js = OpenOptions::new() .write(true) - .create_new(true) + .create(true) .truncate(true) .open(output_js_path) .with_context(|| format!("failed to open output JS file @ [{}]", wast_path.display()))?; @@ -129,3 +272,22 @@ pub(crate) fn run(wast_path: &Path) -> Result<()> { Ok(()) } + +/// Extract the export function from an Exec, along with it's results +fn extract_export_fn<'a>( + exec: &'a wast::WastExecute, +) -> Result<(&'a str, &'a [wast::WastArg<'a>])> { + match exec { + wast::WastExecute::Invoke(wast::WastInvoke { + module, name, args, .. + }) => { + ensure!( + module.is_none(), + "wast invocations with modules not yet supported" + ); + Ok((*name, args)) + } + wast::WastExecute::Wat(_) => bail!("unsupported wast execute type WastExecute::Wat"), + wast::WastExecute::Get { .. } => bail!("unsupported wast execute type WastExecute::Get"), + } +} diff --git a/packages/jco/test/p3/ported/component-model/wast.js b/packages/jco/test/p3/ported/component-model/wast.js index bfa585cda..71ee41f7d 100644 --- a/packages/jco/test/p3/ported/component-model/wast.js +++ b/packages/jco/test/p3/ported/component-model/wast.js @@ -1,11 +1,11 @@ -import { join, relative } from "node:path"; +import { join, relative, basename } from "node:path"; import { opendir } from "node:fs/promises"; import { spawn } from "node:child_process"; import { suite, test, assert, beforeAll } from "vitest"; import { COMPONENT_MODEL_FIXTURES_WAST_DIR } from "../../../common.js"; -import { fileExists } from "../../../helpers.js"; +import { fileExists, setupAsyncTest } from "../../../helpers.js"; // Relative paths to tests that should be skipped const TESTS_TO_SKIP = new Set([ @@ -28,10 +28,12 @@ suite("component-model", async () => { const wastPath = join(dirent.parentPath, dirent.name); const wastRelPath = relative(COMPONENT_MODEL_FIXTURES_WAST_DIR, wastPath); - if (TESTS_TO_SKIP.has(wastRelPath)) { continue; } + if (TESTS_TO_SKIP.has(wastRelPath)) { + continue; + } - const wasmPath = join(COMPONENT_MODEL_FIXTURES_WAST_DIR, `${dirent.name}.wasm`); - const scriptPath = join(COMPONENT_MODEL_FIXTURES_WAST_DIR, `${dirent.name}.js`); + const wasmPath = join(dirent.parentPath, `${dirent.name}.wasm`); + const scriptPath = join(dirent.parentPath, `${dirent.name}.js`); metadata.push({ wastRelPath, wastPath, @@ -40,8 +42,6 @@ suite("component-model", async () => { }); } - metadata = metadata.slice(0,1); - beforeAll(async () => { for (const { wastPath } of metadata) { const fixtureBuild = spawn("cargo", ["xtask", "build-wast-fixture", wastPath], { @@ -49,7 +49,7 @@ suite("component-model", async () => { stdio: "inherit", shell: true, }); - await new Promise(resolve => fixtureBuild.on("exit", resolve)); + await new Promise((resolve) => fixtureBuild.on("exit", resolve)); } }); @@ -58,8 +58,34 @@ suite("component-model", async () => { t(wastRelPath, async () => { assert(await fileExists(wasmPath), `missing generated wasm component @ [${wasmPath}]`); assert(await fileExists(scriptPath), `missing generated script @ [${scriptPath}]`); - // TODO: convert WAST test to WAST + executable JS - assert.strictEqual(true, true); + + let cleanup; + try { + const setup = await setupAsyncTest({ + asyncMode: "jspi", + component: { + name: basename(wastRelPath).replace(".wast.wasm", ""), + path: wasmPath, + }, + jco: { + transpile: { + extraArgs: { + minify: false, + }, + }, + }, + }); + cleanup = setup.cleanup; + const instance = setup.instance; + + const mod = await import(scriptPath); + await mod.runWastTest({ + instance, + assert, + }); + } finally { + await cleanup?.(); + } }); } }); From 78db1d5f038a3bcc57b44b86bc9681a7486c45a2 Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Tue, 9 Jun 2026 00:28:25 +0900 Subject: [PATCH 8/8] test(jco): support WastDirective::AssertTrap --- crates/xtask/src/build/wast_fixtures.rs | 14 ++++++++++++-- .../jco/test/p3/ported/component-model/wast.js | 3 ++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/xtask/src/build/wast_fixtures.rs b/crates/xtask/src/build/wast_fixtures.rs index c811dc113..18c46e605 100644 --- a/crates/xtask/src/build/wast_fixtures.rs +++ b/crates/xtask/src/build/wast_fixtures.rs @@ -31,7 +31,8 @@ fn convert_wast_file( if (!args) {{ throw new Error('missing args'); }} if (!args.instance) {{ throw new Error('missing loaded wasm instance'); }} if (!args.assert) {{ throw new Error('missing assert obj'); }} - const {{ instance, assert }} = args; + if (!args.expect) {{ throw new Error('missing expect obj'); }} + const {{ instance, assert, expect }} = args; let res; "# )?; @@ -69,7 +70,16 @@ fn convert_wast_file( bail!("unsupported directive Register") } wast::WastDirective::Invoke(_) => bail!("unsupported directive Invoke"), - wast::WastDirective::AssertTrap { .. } => bail!("unsupported directive AssertTrap"), + wast::WastDirective::AssertTrap { exec, message, .. } => { + let (export_name, args) = extract_export_fn(&exec)?; + writeln!( + output_js, + r#" + await expect(instance['{export_name}']({})).rejects.toThrow(/trap/, "expected trap with content like [{message}]"); + "#, + args_to_js_params(args)?, + )?; + } wast::WastDirective::AssertReturn { exec, results, .. } => { ensure!( results.len() == 1, diff --git a/packages/jco/test/p3/ported/component-model/wast.js b/packages/jco/test/p3/ported/component-model/wast.js index 71ee41f7d..5ef119ecd 100644 --- a/packages/jco/test/p3/ported/component-model/wast.js +++ b/packages/jco/test/p3/ported/component-model/wast.js @@ -2,7 +2,7 @@ import { join, relative, basename } from "node:path"; import { opendir } from "node:fs/promises"; import { spawn } from "node:child_process"; -import { suite, test, assert, beforeAll } from "vitest"; +import { suite, test, assert, expect, beforeAll } from "vitest"; import { COMPONENT_MODEL_FIXTURES_WAST_DIR } from "../../../common.js"; import { fileExists, setupAsyncTest } from "../../../helpers.js"; @@ -82,6 +82,7 @@ suite("component-model", async () => { await mod.runWastTest({ instance, assert, + expect, }); } finally { await cleanup?.();