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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions crates/js-component-bindgen/src/intrinsics/lift.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ use crate::intrinsics::component::ComponentIntrinsic;
use crate::intrinsics::p3::async_future::AsyncFutureIntrinsic;
use crate::intrinsics::p3::async_stream::AsyncStreamIntrinsic;
use crate::intrinsics::p3::error_context::ErrCtxIntrinsic;
use crate::intrinsics::p3::{
CANNOT_LIFT_FUTURE_IN_WAITABLE_SET, CANNOT_LIFT_STREAM_IN_WAITABLE_SET,
};
use crate::intrinsics::string::StringIntrinsic;
use crate::intrinsics::{Intrinsic, RenderIntrinsicsArgs};
use crate::source::Source;
Expand Down Expand Up @@ -1376,7 +1379,7 @@ impl LiftIntrinsic {
throw new {runtime_error_class}('cannot lift future after previous read succeeded');
}}
if (!futureEnd.isIdleState()) {{ throw new Error('futures must be in idle state'); }}
if (futureEnd.isInSet()) {{ throw new {runtime_error_class}('futures in waitable sets cannot be lifted'); }}
if (futureEnd.isInSet()) {{ throw new {runtime_error_class}({CANNOT_LIFT_FUTURE_IN_WAITABLE_SET:?}); }}

return [ futureEnd.promise(), ctx ];
}};
Expand Down Expand Up @@ -1447,7 +1450,7 @@ impl LiftIntrinsic {
throw new {runtime_error_class}('cannot lift stream after being notified that the writable end dropped');
}}
if (!streamEnd.isIdleState()) {{ throw new Error('streams must be in idle state'); }}
if (streamEnd.isInSet()) {{ throw new {runtime_error_class}('streams in waitable sets cannot be lifted'); }}
if (streamEnd.isInSet()) {{ throw new {runtime_error_class}({CANNOT_LIFT_STREAM_IN_WAITABLE_SET:?}); }}

const stream = new {external_stream_class}({{
globalRep: streamEnd.globalStreamMapRep(),
Expand Down
50 changes: 50 additions & 0 deletions crates/js-component-bindgen/src/intrinsics/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2024,6 +2024,56 @@ mod tests {
}
}

#[test]
fn future_and_stream_waitable_membership_traps_before_transfer() {
for (intrinsic, check, remove, message) in [
(
Intrinsic::AsyncFuture(AsyncFutureIntrinsic::FutureTransfer),
"if (futureEnd.isInSet()) {",
"const removedFutureEnd = removeFutureEndFromTable(",
"cannot lift future while it's in a waitable set",
),
(
Intrinsic::AsyncStream(AsyncStreamIntrinsic::StreamTransfer),
"if (streamEnd.isInSet()) {",
"const removedStreamEnd = removeStreamEndFromTable(",
"cannot lift stream while it's in a waitable set",
),
] {
let source = render_intrinsic_body(intrinsic);
let check = source.find(check).expect("waitable membership check");
let remove = source.find(remove).expect("endpoint table removal");

assert!(check < remove, "membership validation must precede removal");
assert!(source.contains(&format!(
"throw new WebAssemblyRuntimeError(\"{message}\");"
)));
}

for (intrinsic, message) in [
(
Intrinsic::AsyncFuture(AsyncFutureIntrinsic::HostFutureClass),
"cannot lift future while it's in a waitable set",
),
(
Intrinsic::AsyncStream(AsyncStreamIntrinsic::HostStreamClass),
"cannot lift stream while it's in a waitable set",
),
(
Intrinsic::Lift(LiftIntrinsic::LiftFlatFuture),
"cannot lift future while it's in a waitable set",
),
(
Intrinsic::Lift(LiftIntrinsic::LiftFlatStream),
"cannot lift stream while it's in a waitable set",
),
] {
let source = render_intrinsic_body(intrinsic);
assert!(source.contains(&format!("\"{message}\"")));
assert!(!source.contains("in waitable sets cannot be lifted"));
}
}

#[test]
fn future_ends_track_own_and_peer_drop_state_separately() {
let mut intrinsics = BTreeSet::from([Intrinsic::AsyncFuture(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::intrinsics::{Intrinsic, RenderIntrinsicsArgs};
use crate::source::Source;
use crate::uwriteln;

use super::async_task::AsyncTaskIntrinsic;
use super::{CANNOT_LIFT_FUTURE_IN_WAITABLE_SET, async_task::AsyncTaskIntrinsic};

/// This enum contains intrinsics that enable Futures
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
Expand Down Expand Up @@ -665,7 +665,7 @@ impl AsyncFutureIntrinsic {
if (futureEnd.isDoneState()) {{
throw new {runtime_error_class}('cannot lift future after previous read succeeded');
}}
if (futureEnd.isInSet()) {{ throw new {runtime_error_class}('futures in waitable sets cannot be lifted'); }}
if (futureEnd.isInSet()) {{ throw new {runtime_error_class}({CANNOT_LIFT_FUTURE_IN_WAITABLE_SET:?}); }}

return futureEnd.promise();
}}
Expand Down Expand Up @@ -1758,6 +1758,9 @@ impl AsyncFutureIntrinsic {
if (futureEnd.isDoneState()) {{
throw new {runtime_error_class}('cannot lift future after previous read succeeded');
}}
if (futureEnd.isInSet()) {{
throw new {runtime_error_class}({CANNOT_LIFT_FUTURE_IN_WAITABLE_SET:?});
}}

const removedFutureEnd = {remove_future_end_from_table_fn}({{ tableIdx: srcTableIdx, futureWaitableIdx: srcFutureWaitableIdx }});
if (removedFutureEnd !== futureEnd) {{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::{
source::Source,
};

use super::async_task::AsyncTaskIntrinsic;
use super::{CANNOT_LIFT_STREAM_IN_WAITABLE_SET, async_task::AsyncTaskIntrinsic};

/// This enum contains intrinsics that enable Stream
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
Expand Down Expand Up @@ -1816,7 +1816,7 @@ impl AsyncStreamIntrinsic {
if (streamEnd.isDoneState()) {{
throw new {runtime_error_class}('cannot lift stream after being notified that the writable end dropped');
}}
if (streamEnd.isInSet()) {{ throw new {runtime_error_class}('streams in waitable sets cannot be lifted'); }}
if (streamEnd.isInSet()) {{ throw new {runtime_error_class}({CANNOT_LIFT_STREAM_IN_WAITABLE_SET:?}); }}

return new {external_stream_class}({{
isReadable: streamEnd.isReadable(),
Expand Down Expand Up @@ -2438,6 +2438,9 @@ impl AsyncStreamIntrinsic {
if (streamEnd.isDoneState()) {{
throw new {runtime_error_class}('cannot lift stream after being notified that the writable end dropped');
}}
if (streamEnd.isInSet()) {{
throw new {runtime_error_class}({CANNOT_LIFT_STREAM_IN_WAITABLE_SET:?});
}}

const removedStreamEnd = {remove_stream_end_from_table_fn}({{ tableIdx: srcTableIdx, streamWaitableIdx: srcStreamWaitableIdx }});
if (removedStreamEnd !== streamEnd) {{
Expand Down
5 changes: 5 additions & 0 deletions crates/js-component-bindgen/src/intrinsics/p3/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@ pub(crate) mod async_task;
pub(crate) mod error_context;
pub(crate) mod host;
pub(crate) mod waitable;

pub(crate) const CANNOT_LIFT_FUTURE_IN_WAITABLE_SET: &str =
"cannot lift future while it's in a waitable set";
pub(crate) const CANNOT_LIFT_STREAM_IN_WAITABLE_SET: &str =
"cannot lift stream while it's in a waitable set";
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
;; This test contains a single component $Tester which creates a stream and
;; future, joins the readable end into a waitable set, and then attempts to
;; lift the readable end by returning it from an export. Because the readable
;; end is in a waitable set, lifting must trap.

(component definition $Tester
(core module $Memory (memory (export "mem") 1))
(core instance $memory (instantiate $Memory))
(core module $M
(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 "" "stream.new" (func $stream.new (result i64)))

(func $return-future-in-set (export "return-future-in-set") (result i32)
(local $ret64 i64) (local $rx i32) (local $ws i32)
(local.set $ret64 (call $future.new))
(local.set $rx (i32.wrap_i64 (local.get $ret64)))
(local.set $ws (call $waitable-set.new))
(call $waitable.join (local.get $rx) (local.get $ws))
(local.get $rx)
)
(func $return-stream-in-set (export "return-stream-in-set") (result i32)
(local $ret64 i64) (local $rx i32) (local $ws i32)
(local.set $ret64 (call $stream.new))
(local.set $rx (i32.wrap_i64 (local.get $ret64)))
(local.set $ws (call $waitable-set.new))
(call $waitable.join (local.get $rx) (local.get $ws))
(local.get $rx)
)
)
(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 future.new $FT (core func $future.new))
(canon stream.new $ST (core func $stream.new))
(core instance $m (instantiate $M (with "" (instance
(export "waitable.join" (func $waitable.join))
(export "waitable-set.new" (func $waitable-set.new))
(export "future.new" (func $future.new))
(export "stream.new" (func $stream.new))
))))
(func (export "return-future-in-set") async (result $FT) (canon lift (core func $m "return-future-in-set")))
(func (export "return-stream-in-set") async (result $ST) (canon lift (core func $m "return-stream-in-set")))
)

(component instance $i1 $Tester)
(assert_trap (invoke "return-future-in-set") "cannot lift future while it's in a waitable set")
(component instance $i2 $Tester)
(assert_trap (invoke "return-stream-in-set") "cannot lift stream while it's in a waitable set")
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@
"name": "trap-if-done.wast",
"revision": "7479890cb506d0f8f687595a4d41361ff8a2a194"
},
{
"name": "trap-if-transfer-in-waitable-set.wast",
"revision": "c956fde25a4baba42187761a8c7954dd40d3d4a4"
},
{
"name": "trap-on-reenter.wast",
"revision": "4769c405eda54e99ea28292ebd007fcbe53c7b40"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const WAST_TESTS: readonly WastTest[] = [
{ relPath: 'async/validate-no-stream-char.wast' },
{ relPath: 'async/validate-no-async-abi-for-sync-type.wast' },
{ relPath: 'async/builtin-trap-poisons-instance.wast' },
{ relPath: 'async/trap-if-transfer-in-waitable-set.wast' },

// Skipped tests
// TODO: Revisit this fixture once stackful async support is implemented.
Expand Down
Loading