From 2c4526e88ce63e4a9887759a1fc3c5f514c4f62c Mon Sep 17 00:00:00 2001 From: TheHypnoo Date: Thu, 30 Jul 2026 12:39:41 +0200 Subject: [PATCH 1/8] fix(stream): align iteration and pipeline semantics --- crates/perry-runtime/src/exception.rs | 12 +- crates/perry-runtime/src/fs/validate.rs | 3 + crates/perry-runtime/src/node_stream.rs | 40 ++- .../src/node_stream/async_iterator.rs | 119 ++++++- .../src/node_stream_constructors/builders.rs | 22 +- .../src/node_stream_constructors/pipeline.rs | 7 +- .../src/node_stream_destroy_state.rs | 4 +- .../perry-runtime/src/node_stream_dispatch.rs | 6 + .../src/node_stream_iter_helpers.rs | 311 ++++++++++++++++++ .../perry-runtime/src/node_stream_pipeline.rs | 101 +++++- .../src/node_stream_readwrite.rs | 49 ++- crates/perry-runtime/src/node_stream_tests.rs | 196 +++++++++++ .../src/node_stream_tests_extra.rs | 26 +- .../src/node_submodules/diagnostics.rs | 5 +- .../src/os/os_process_emitter.rs | 5 +- .../perry-runtime/src/promise/microtasks.rs | 4 + crates/perry-runtime/src/symbol/iterator.rs | 11 + .../src/generator/per_iteration.rs | 59 +++- 18 files changed, 909 insertions(+), 71 deletions(-) diff --git a/crates/perry-runtime/src/exception.rs b/crates/perry-runtime/src/exception.rs index e571e7e1af..af6072e1fb 100644 --- a/crates/perry-runtime/src/exception.rs +++ b/crates/perry-runtime/src/exception.rs @@ -364,7 +364,17 @@ pub(crate) fn print_uncaught(value: f64) { // for this header. When the stack is empty (defensive), fall // back to the bare `: ` line. if !stack_str.is_empty() { - eprintln!("{}", stack_str); + if let Some(code) = + crate::node_submodules::error_code_for_message(unsafe { (*eh).message }) + { + let frames = stack_str + .split_once('\n') + .map(|(_, frames)| format!("\n{frames}")) + .unwrap_or_default(); + eprintln!("{name_display} [{code}]: {msg_str}{frames}"); + } else { + eprintln!("{}", stack_str); + } } else if msg_str.is_empty() { eprintln!("{}", name_display); } else { diff --git a/crates/perry-runtime/src/fs/validate.rs b/crates/perry-runtime/src/fs/validate.rs index 02a73ecccf..d8c01b3973 100644 --- a/crates/perry-runtime/src/fs/validate.rs +++ b/crates/perry-runtime/src/fs/validate.rs @@ -192,6 +192,9 @@ pub fn describe_received(value: f64) -> String { } return format!("type number ({})", n); } + if crate::promise::js_value_is_promise(value) != 0 { + return "an instance of Promise".to_string(); + } if !super::stream::extract_closure_ptr(value).is_null() { return "function ".to_string(); } diff --git a/crates/perry-runtime/src/node_stream.rs b/crates/perry-runtime/src/node_stream.rs index 703221c709..df673b81a8 100644 --- a/crates/perry-runtime/src/node_stream.rs +++ b/crates/perry-runtime/src/node_stream.rs @@ -531,6 +531,16 @@ fn push_chunk(stream: f64, chunk: f64) -> f64 { let Some(chunk) = decode_readable_chunk_for_encoding(stream, chunk) else { return f64::from_bits(TAG_TRUE); }; + let chunk = if !readable_object_mode(stream) + && readable_encoding_tag(stream).is_none() + && JSValue::from_bits(chunk.to_bits()).is_any_string() + { + let mut bytes = Vec::new(); + append_chunk_bytes(chunk, &mut bytes, 0); + buffer_value_from_bytes(&bytes) + } else { + chunk + }; push_chunk_backpressure_result(stream, append_readable_output_chunk(stream, chunk)) } @@ -819,7 +829,7 @@ extern "C" fn pipe_unpipe_callback(closure: *const ClosureHeader, src: f64) -> f f64::from_bits(TAG_UNDEFINED) } -extern "C" fn pipe_error_callback(closure: *const ClosureHeader, _err: f64) -> f64 { +extern "C" fn pipe_error_callback(closure: *const ClosureHeader, err: f64) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -828,6 +838,9 @@ extern "C" fn pipe_error_callback(closure: *const ClosureHeader, _err: f64) -> f if !unpipe_destination(src, dest) { cleanup_pipe_listeners_from_closure(closure); } + if stream_listener_count_for_event(dest, string_value(b"error")) == 0 { + crate::exception::js_throw(err); + } f64::from_bits(TAG_UNDEFINED) } @@ -1086,7 +1099,7 @@ fn invoke_writable_write(stream: f64, chunk: f64, enc: f64, len: f64, callback: } crate::object::js_implicit_this_set(prev_this); } else { - complete_writable_write(stream, len, callback, f64::from_bits(TAG_UNDEFINED)); + throw_missing_stream_method("The _write() method is not implemented"); } } @@ -1146,8 +1159,15 @@ fn invoke_transform_write(stream: f64, chunk: f64, enc: f64, len: f64, callback: crate::object::js_implicit_this_set(prev_this); return; } - emit_writable_chunk(stream, chunk); - complete_writable_write(stream, len, callback, f64::from_bits(TAG_UNDEFINED)); + throw_missing_stream_method("The _transform() method is not implemented"); +} + +#[cold] +fn throw_missing_stream_method(message: &str) -> ! { + let s = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); + crate::node_submodules::register_error_code_pub(s, "ERR_METHOD_NOT_IMPLEMENTED"); + let err = crate::error::js_error_new_with_message(s); + crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)) } #[cold] @@ -1160,12 +1180,12 @@ fn throw_writable_null_chunk() -> ! { } #[cold] -fn throw_readable_from_invalid_iterable() -> ! { - let msg = b"The \"iterable\" argument must be an instance of Iterable."; - let s = crate::string::js_string_from_bytes(msg.as_ptr(), msg.len() as u32); - crate::node_submodules::register_error_code_pub(s, "ERR_INVALID_ARG_TYPE"); - let err = crate::error::js_typeerror_new(s); - crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)) +fn throw_readable_from_invalid_iterable(value: f64) -> ! { + let message = format!( + "The \"iterable\" argument must be an instance of Iterable. Received {}", + crate::fs::validate::describe_received(value) + ); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE") } fn normalize_write_args(stream: f64, chunk: f64, enc: f64, cb: f64) -> (f64, f64, f64) { diff --git a/crates/perry-runtime/src/node_stream/async_iterator.rs b/crates/perry-runtime/src/node_stream/async_iterator.rs index cd18b0d7c3..95aa03b458 100644 --- a/crates/perry-runtime/src/node_stream/async_iterator.rs +++ b/crates/perry-runtime/src/node_stream/async_iterator.rs @@ -66,6 +66,43 @@ extern "C" fn ns_readable_iterator_chunk_rejected( f64::from_bits(TAG_UNDEFINED) } +extern "C" fn ns_readable_source_iterator_fulfilled( + closure: *const ClosureHeader, + result: f64, +) -> f64 { + let iterator = js_closure_get_capture_f64(closure, 0); + let done = object_ptr_from_value(result).is_none_or(|obj| { + crate::value::js_is_truthy(crate::object::js_object_get_field_by_name_f64( + obj as *const crate::object::ObjectHeader, + hidden_key(b"done"), + )) != 0 + }); + if done { + iterator_mark_done(iterator); + iterator_set_stream_ended(iterator); + if let Some(stream) = get_hidden_value(iterator, hidden_key(READABLE_ITERATOR_STREAM_KEY)) { + mark_stream_ended(stream); + } + } else { + note_yield(iterator); + } + result +} + +extern "C" fn ns_readable_source_iterator_rejected( + closure: *const ClosureHeader, + reason: f64, +) -> f64 { + let iterator = js_closure_get_capture_f64(closure, 0); + iterator_mark_done(iterator); + iterator_set_stream_ended(iterator); + if let Some(stream) = get_hidden_value(iterator, hidden_key(READABLE_ITERATOR_STREAM_KEY)) { + call_source_iterator_return(stream); + destroy_stream(stream, reason); + } + rejected_promise(reason) +} + /// Wrap a yielded chunk in a resolved `{value,done:false}` promise. A chunk that /// is itself a Promise (e.g. `Readable.from([Promise.resolve(x)])` whose element /// reached the queue unresolved) is awaited so the consumer sees the settled @@ -285,6 +322,9 @@ extern "C" fn ns_readable_iter_on_data(closure: *const ClosureHeader, chunk: f64 } else { iterator_enqueue(iterator, chunk); } + if let Some(stream) = get_hidden_value(iterator, hidden_key(READABLE_ITERATOR_STREAM_KEY)) { + pause_readable_stream(stream); + } f64::from_bits(TAG_UNDEFINED) } @@ -384,9 +424,11 @@ fn iterator_ensure_attached(iterator: f64, stream: f64) { // Already-terminal-before-attach: no future event will reach our listeners, // so seed the terminal state directly. - if let Some(err) = readable_hidden_error(stream) { - iterator_set_error(iterator, err); - return; + if !readable_chunks_nonempty(stream) { + if let Some(err) = readable_hidden_error(stream) { + iterator_set_error(iterator, err); + return; + } } if has_truthy_hidden(stream, hidden_end_emitted_key()) || stream_destroyed(stream) { iterator_set_stream_ended(iterator); @@ -451,7 +493,7 @@ fn settle_iterator_return_value(value: f64) { } } -fn call_source_iterator_return(stream: f64) { +pub(super) fn call_source_iterator_return(stream: f64) { let Some(source_iterator) = get_hidden_value(stream, hidden_key(READABLE_SOURCE_ITERATOR_KEY)) else { return; @@ -477,6 +519,42 @@ extern "C" fn ns_readable_iterator_next(closure: *const ClosureHeader) -> f64 { return readable_iterator_done(); }; + if !readable_chunks_nonempty(stream) { + if let Some(source_iterator) = + get_hidden_value(stream, hidden_key(READABLE_SOURCE_ITERATOR_KEY)) + { + let next = match catch_pipeline_throw(|| unsafe { + crate::object::js_native_call_method( + source_iterator, + b"next".as_ptr() as *const i8, + 4, + std::ptr::null(), + 0, + ) + }) { + Ok(next) => next, + Err(reason) => { + iterator_mark_done(iterator); + call_source_iterator_return(stream); + destroy_stream(stream, reason); + return rejected_promise(reason); + } + }; + let promise = if crate::promise::js_value_is_promise(next) != 0 { + crate::value::js_nanbox_get_pointer(next) as *mut crate::promise::Promise + } else { + crate::promise::js_promise_resolved(next) + }; + let fulfilled = js_closure_alloc(ns_readable_source_iterator_fulfilled as *const u8, 1); + let rejected = js_closure_alloc(ns_readable_source_iterator_rejected as *const u8, 1); + js_closure_set_capture_f64(fulfilled, 0, iterator); + js_closure_set_capture_f64(rejected, 0, iterator); + return box_pointer( + crate::promise::js_promise_then(promise, fulfilled, rejected) as *const u8, + ); + } + } + // First pull: attach persistent listeners + start flow. Listeners deliver // asynchronously (resume schedules microtasks), so nothing arrives // synchronously here — no event-loop re-entrancy. @@ -488,6 +566,14 @@ extern "C" fn ns_readable_iterator_next(closure: *const ClosureHeader) -> f64 { return readable_iterator_chunk_result(chunk); } + if !readable_chunks_nonempty(stream) { + if let Some(err) = readable_hidden_error(stream) { + iterator_mark_done(iterator); + iterator_remove_listeners(iterator); + return rejected_promise(err); + } + } + // A stored error surfaces (once) as a rejection, then the iterator is done. if let Some(err) = iterator_stored_error(iterator) { iterator_mark_done(iterator); @@ -507,6 +593,7 @@ extern "C" fn ns_readable_iterator_next(closure: *const ClosureHeader) -> f64 { // their own promise (FIFO) — none is overwritten or dropped. let promise = crate::promise::js_promise_new(); iterator_push_pending(iterator, promise); + resume_readable_stream(stream); box_pointer(promise as *const u8) } @@ -660,6 +747,11 @@ pub(super) fn register_arities() { crate::closure::js_register_closure_arity(ns_readable_iterator_self as *const u8, 0); crate::closure::js_register_closure_arity(ns_readable_iterator_chunk_fulfilled as *const u8, 1); crate::closure::js_register_closure_arity(ns_readable_iterator_chunk_rejected as *const u8, 1); + crate::closure::js_register_closure_arity( + ns_readable_source_iterator_fulfilled as *const u8, + 1, + ); + crate::closure::js_register_closure_arity(ns_readable_source_iterator_rejected as *const u8, 1); crate::closure::js_register_closure_arity(ns_readable_iter_on_data as *const u8, 1); crate::closure::js_register_closure_arity(ns_readable_iter_on_end as *const u8, 0); crate::closure::js_register_closure_arity(ns_readable_iter_on_error as *const u8, 1); @@ -760,4 +852,23 @@ mod fifo_pending_tests { ); assert!(crate::value::js_is_truthy(result_field(p2, b"done")) != 0); } + + #[test] + fn retained_source_rejection_finishes_iterator_and_destroys_stream() { + let chunks = crate::array::js_array_alloc(0); + let stream = js_node_stream_readable_from(box_pointer(chunks as *const u8)); + let iterator = build_readable_async_iterator(stream, true); + let rejected = js_closure_alloc(ns_readable_source_iterator_rejected as *const u8, 1); + js_closure_set_capture_f64(rejected, 0, iterator); + + let result = ns_readable_source_iterator_rejected(rejected, 7.0); + let promise = crate::value::js_nanbox_get_pointer(result) as *mut crate::promise::Promise; + assert!(iterator_is_done(iterator)); + assert!(stream_destroyed(stream)); + assert_eq!( + unsafe { (*promise).state }, + crate::promise::PromiseState::Rejected + ); + assert_eq!(unsafe { (*promise).reason }, 7.0); + } } diff --git a/crates/perry-runtime/src/node_stream_constructors/builders.rs b/crates/perry-runtime/src/node_stream_constructors/builders.rs index df0bb5e3dc..503b94e2ba 100644 --- a/crates/perry-runtime/src/node_stream_constructors/builders.rs +++ b/crates/perry-runtime/src/node_stream_constructors/builders.rs @@ -427,10 +427,8 @@ pub extern "C" fn js_node_stream_readable_from(iterable: f64) -> f64 { #[no_mangle] pub extern "C" fn js_node_stream_readable_from_options(iterable: f64, opts: f64) -> f64 { - if matches!(iterable.to_bits(), TAG_NULL | TAG_UNDEFINED) - || is_non_iterable_primitive_for_readable_from(iterable) - { - throw_readable_from_invalid_iterable(); + if is_invalid_readable_from_input(iterable) { + throw_readable_from_invalid_iterable(iterable); } let readable = js_node_stream_readable_new(readable_from_options(opts)); let raw = raw_ptr_from_value(readable); @@ -462,19 +460,3 @@ pub extern "C" fn js_node_stream_readable_from_options(iterable: f64, opts: f64) } readable } - -fn initialize_readable_from_buffered_length(readable: f64, chunks: f64) { - let mut values = Vec::new(); - push_chunk_values(chunks, &mut values, 0); - let length = if readable_object_mode(readable) { - values.len() as f64 - } else { - let mut bytes = Vec::new(); - for value in values { - append_chunk_bytes(value, &mut bytes, 0); - } - bytes.len() as f64 - }; - set_hidden_value(readable, hidden_buffered_key(), length); - set_hidden_value(readable, hidden_key(b"readableLength"), length); -} diff --git a/crates/perry-runtime/src/node_stream_constructors/pipeline.rs b/crates/perry-runtime/src/node_stream_constructors/pipeline.rs index d9b6e81034..5bb157094f 100644 --- a/crates/perry-runtime/src/node_stream_constructors/pipeline.rs +++ b/crates/perry-runtime/src/node_stream_constructors/pipeline.rs @@ -245,17 +245,16 @@ pub extern "C" fn js_node_stream_pipeline(args: *const crate::array::ArrayHeader let callback = *args.last().unwrap_or(&f64::from_bits(TAG_UNDEFINED)); if !is_callable_value(callback) { - throw_pipeline_callback_required(); + throw_pipeline_callback_required(callback); } args.pop(); - let mut options = PipelineOptions { + let options = PipelineOptions { end_final: true, signal: None, }; if args.last().copied().is_some_and(is_pipeline_options_arg) { - let option_arg = args.pop().unwrap_or(f64::from_bits(TAG_UNDEFINED)); - options = pipeline_options_from_arg(option_arg); + throw_pipeline_invalid_body(*args.last().unwrap()); } if args.len() == 1 && is_array_like_value(args[0]) { diff --git a/crates/perry-runtime/src/node_stream_destroy_state.rs b/crates/perry-runtime/src/node_stream_destroy_state.rs index bd4ddb8743..3b591207a2 100644 --- a/crates/perry-runtime/src/node_stream_destroy_state.rs +++ b/crates/perry-runtime/src/node_stream_destroy_state.rs @@ -19,9 +19,7 @@ pub(super) extern "C" fn ns_destroy_error_microtask(closure: *const ClosureHeade if bits != TAG_UNDEFINED && bits != TAG_NULL { set_hidden_value(stream, hidden_error_key(), err); let error = super::string_value(b"error"); - if super::event_emitter::stream_listener_count_for_event(stream, error) > 0 { - let _ = super::event_emitter::emit_stream_event(stream, error, &[err]); - } + let _ = super::event_emitter::emit_stream_event(stream, error, &[err]); } super::mark_stream_closed_and_emit_close(stream); f64::from_bits(TAG_UNDEFINED) diff --git a/crates/perry-runtime/src/node_stream_dispatch.rs b/crates/perry-runtime/src/node_stream_dispatch.rs index 926747d69e..f664c961d7 100644 --- a/crates/perry-runtime/src/node_stream_dispatch.rs +++ b/crates/perry-runtime/src/node_stream_dispatch.rs @@ -338,6 +338,12 @@ pub(super) fn register_stub_arities() { register(ns_iter_every as *const u8, 2); register(ns_iter_flat_map as *const u8, 2); register(ns_iter_take as *const u8, 1); + register(ns_take_source_next as *const u8, 0); + register(ns_take_source_return as *const u8, 0); + register(ns_take_source_fulfilled as *const u8, 1); + register(ns_take_source_rejected as *const u8, 1); + register(ns_take_limit_fulfilled as *const u8, 1); + register(ns_take_limit_rejected as *const u8, 1); register(ns_iter_drop as *const u8, 1); register_consume_arities(); async_iterator::register_arities(); diff --git a/crates/perry-runtime/src/node_stream_iter_helpers.rs b/crates/perry-runtime/src/node_stream_iter_helpers.rs index ac1b503d88..9eefa1a74b 100644 --- a/crates/perry-runtime/src/node_stream_iter_helpers.rs +++ b/crates/perry-runtime/src/node_stream_iter_helpers.rs @@ -908,8 +908,186 @@ pub(super) fn flatten_async_iterable_value(value: f64) -> Option<*mut crate::arr flatten_async_iterable_with_source(value).map(|(chunks, _)| chunks) } +const TAKE_SOURCE_STREAM_KEY: &[u8] = b"__perryTakeSourceStream"; +const TAKE_RESULT_STREAM_KEY: &[u8] = b"__perryTakeResultStream"; +const TAKE_SOURCE_REMAINING_KEY: &[u8] = b"__perryTakeSourceRemaining"; +const TAKE_SOURCE_DONE_KEY: &[u8] = b"__perryTakeSourceDone"; + +fn take_source_done_result() -> f64 { + let result = crate::object::js_object_alloc(0, 2); + js_object_set_field_by_name(result, hidden_key(b"value"), f64::from_bits(TAG_UNDEFINED)); + js_object_set_field_by_name(result, hidden_key(b"done"), f64::from_bits(TAG_TRUE)); + box_pointer(result as *const u8) +} + +fn finish_take_source(iterator: f64, reason: Option, close_source: bool) { + if has_truthy_hidden(iterator, hidden_key(TAKE_SOURCE_DONE_KEY)) { + return; + } + set_hidden_value( + iterator, + hidden_key(TAKE_SOURCE_DONE_KEY), + f64::from_bits(TAG_TRUE), + ); + let Some(source) = get_hidden_value(iterator, hidden_key(TAKE_SOURCE_STREAM_KEY)) else { + return; + }; + if close_source { + async_iterator::call_source_iterator_return(source); + } + let terminal = reason.unwrap_or_else(|| f64::from_bits(TAG_UNDEFINED)); + destroy_stream(source, terminal); + if let Some(result) = get_hidden_value(iterator, hidden_key(TAKE_RESULT_STREAM_KEY)) { + destroy_stream(result, terminal); + } +} + +fn take_source_fulfilled(iterator: f64, result: f64) -> f64 { + match pipeline_iterator_result(result) { + Some((false, _)) => { + let remaining = + get_hidden_value(iterator, hidden_key(TAKE_SOURCE_REMAINING_KEY)).unwrap_or(0.0); + set_hidden_value( + iterator, + hidden_key(TAKE_SOURCE_REMAINING_KEY), + (remaining - 1.0).max(0.0), + ); + } + _ => finish_take_source(iterator, None, false), + } + result +} + +pub(super) extern "C" fn ns_take_source_fulfilled( + closure: *const ClosureHeader, + result: f64, +) -> f64 { + take_source_fulfilled(js_closure_get_capture_f64(closure, 0), result) +} + +pub(super) extern "C" fn ns_take_source_rejected( + closure: *const ClosureHeader, + reason: f64, +) -> f64 { + finish_take_source(js_closure_get_capture_f64(closure, 0), Some(reason), true); + rejected_promise(reason) +} + +pub(super) extern "C" fn ns_take_limit_fulfilled( + closure: *const ClosureHeader, + result: f64, +) -> f64 { + let iterator = js_closure_get_capture_f64(closure, 0); + let close_source = matches!(pipeline_iterator_result(result), Some((false, _))); + finish_take_source(iterator, None, close_source); + take_source_done_result() +} + +pub(super) extern "C" fn ns_take_limit_rejected( + closure: *const ClosureHeader, + _reason: f64, +) -> f64 { + finish_take_source(js_closure_get_capture_f64(closure, 0), None, true); + take_source_done_result() +} + +pub(super) extern "C" fn ns_take_source_next(closure: *const ClosureHeader) -> f64 { + let iterator = this_value(closure); + if has_truthy_hidden(iterator, hidden_key(TAKE_SOURCE_DONE_KEY)) { + return take_source_done_result(); + } + let remaining = + get_hidden_value(iterator, hidden_key(TAKE_SOURCE_REMAINING_KEY)).unwrap_or(0.0); + let Some(source) = get_hidden_value(iterator, hidden_key(TAKE_SOURCE_STREAM_KEY)) else { + finish_take_source(iterator, None, false); + return take_source_done_result(); + }; + let Some(source_iterator) = get_hidden_value(source, hidden_key(READABLE_SOURCE_ITERATOR_KEY)) + else { + finish_take_source(iterator, None, false); + return take_source_done_result(); + }; + let next = match catch_pipeline_throw(|| unsafe { + crate::object::js_native_call_method( + source_iterator, + b"next".as_ptr() as *const i8, + 4, + std::ptr::null(), + 0, + ) + }) { + Ok(next) => next, + Err(reason) => { + if remaining <= 0.0 { + finish_take_source(iterator, None, true); + return take_source_done_result(); + } + finish_take_source(iterator, Some(reason), true); + return rejected_promise(reason); + } + }; + if remaining <= 0.0 { + if crate::promise::js_value_is_promise(next) == 0 { + let close_source = matches!(pipeline_iterator_result(next), Some((false, _))); + finish_take_source(iterator, None, close_source); + return take_source_done_result(); + } + let promise = crate::value::js_nanbox_get_pointer(next) as *mut crate::promise::Promise; + let fulfilled = js_closure_alloc(ns_take_limit_fulfilled as *const u8, 1); + let rejected = js_closure_alloc(ns_take_limit_rejected as *const u8, 1); + js_closure_set_capture_f64(fulfilled, 0, iterator); + js_closure_set_capture_f64(rejected, 0, iterator); + return box_pointer( + crate::promise::js_promise_then(promise, fulfilled, rejected) as *const u8, + ); + } + if crate::promise::js_value_is_promise(next) == 0 { + return take_source_fulfilled(iterator, next); + } + let promise = crate::value::js_nanbox_get_pointer(next) as *mut crate::promise::Promise; + let fulfilled = js_closure_alloc(ns_take_source_fulfilled as *const u8, 1); + let rejected = js_closure_alloc(ns_take_source_rejected as *const u8, 1); + js_closure_set_capture_f64(fulfilled, 0, iterator); + js_closure_set_capture_f64(rejected, 0, iterator); + box_pointer(crate::promise::js_promise_then(promise, fulfilled, rejected) as *const u8) +} + +pub(super) extern "C" fn ns_take_source_return(closure: *const ClosureHeader) -> f64 { + finish_take_source(this_value(closure), None, true); + take_source_done_result() +} + +fn take_source_iterator(source: f64, result: f64, count: u32) -> f64 { + let methods = [ + ("next", cast0(ns_take_source_next)), + ("return", cast0(ns_take_source_return)), + ]; + let iterator = box_pointer(build_object(&methods, 0x7FFF_FF70) as *const u8); + set_hidden_value(iterator, hidden_key(TAKE_SOURCE_STREAM_KEY), source); + set_hidden_value(iterator, hidden_key(TAKE_RESULT_STREAM_KEY), result); + set_hidden_value( + iterator, + hidden_key(TAKE_SOURCE_REMAINING_KEY), + count as f64, + ); + iterator +} + pub(super) extern "C" fn ns_iter_take(closure: *const ClosureHeader, count: f64) -> f64 { let this = this_value(closure); + if !readable_chunks_nonempty(this) { + if let Some(_source_iterator) = + get_hidden_value(this, hidden_key(READABLE_SOURCE_ITERATOR_KEY)) + { + let result = readable_from_chunks(crate::array::js_array_alloc(0)); + set_hidden_value( + result, + hidden_key(READABLE_SOURCE_ITERATOR_KEY), + take_source_iterator(this, result, count_arg(count)), + ); + return result; + } + } prepare_readable_for_iteration(this); let arr = readable_chunks_array(this); let mut out = crate::array::js_array_alloc(0); @@ -940,3 +1118,136 @@ pub(super) extern "C" fn ns_iter_drop(closure: *const ClosureHeader, count: f64) propagate_stream_state(this, f64::from_bits(TAG_UNDEFINED), result); result } + +#[cfg(test)] +mod take_tests { + use super::*; + + extern "C" fn source_next(closure: *const ClosureHeader) -> f64 { + let source = this_value(closure); + let value = get_hidden_value(source, hidden_key(b"count")).unwrap_or(0.0) + 1.0; + set_hidden_value(source, hidden_key(b"count"), value); + if value > 2.0 && has_truthy_hidden(source, hidden_key(b"failAfterLimit")) { + if has_truthy_hidden(source, hidden_key(b"rejectAfterLimit")) { + return rejected_promise(9.0); + } + crate::exception::js_throw(9.0); + } + let done = get_hidden_value(source, hidden_key(b"doneAfter")) + .is_some_and(|done_after| value > done_after); + let result = crate::object::js_object_alloc(0, 2); + js_object_set_field_by_name(result, hidden_key(b"value"), value); + js_object_set_field_by_name( + result, + hidden_key(b"done"), + f64::from_bits(if done { TAG_TRUE } else { TAG_FALSE }), + ); + box_pointer(result as *const u8) + } + + extern "C" fn source_return(closure: *const ClosureHeader) -> f64 { + let source = this_value(closure); + set_hidden_value(source, hidden_key(b"returned"), f64::from_bits(TAG_TRUE)); + f64::from_bits(TAG_UNDEFINED) + } + + fn pull(iterator: f64) -> (bool, f64) { + let next = unsafe { + crate::object::js_native_call_method( + iterator, + b"next".as_ptr() as *const i8, + 4, + std::ptr::null(), + 0, + ) + }; + pipeline_iterator_result(settle_pipeline_value(next).unwrap()).unwrap() + } + + #[test] + fn take_closes_retained_source_after_limit() { + let methods = [ + ("next", cast0(source_next)), + ("return", cast0(source_return)), + ]; + let source = box_pointer(build_object(&methods, 0x7FFF_FF72) as *const u8); + let chunks = crate::array::js_array_alloc(0); + let stream = js_node_stream_readable_from(box_pointer(chunks as *const u8)); + set_hidden_value(stream, hidden_key(READABLE_SOURCE_ITERATOR_KEY), source); + let take = js_closure_alloc(ns_iter_take as *const u8, 1); + js_closure_set_capture_ptr(take, 0, stream.to_bits() as i64); + + let result = ns_iter_take(take, 2.0); + let iterator = get_hidden_value(result, hidden_key(READABLE_SOURCE_ITERATOR_KEY)).unwrap(); + + assert!(!has_truthy_hidden(source, hidden_key(b"returned"))); + assert!(!stream_destroyed(stream)); + assert_eq!(pull(iterator), (false, 1.0)); + assert_eq!(pull(iterator), (false, 2.0)); + assert!(pull(iterator).0); + assert!(has_truthy_hidden(source, hidden_key(b"returned"))); + assert!(stream_destroyed(stream)); + assert!(stream_destroyed(result)); + assert_eq!(get_hidden_value(source, hidden_key(b"count")), Some(3.0)); + } + + #[test] + fn take_does_not_return_source_that_finished_naturally() { + let methods = [ + ("next", cast0(source_next)), + ("return", cast0(source_return)), + ]; + let source = box_pointer(build_object(&methods, 0x7FFF_FF73) as *const u8); + set_hidden_value(source, hidden_key(b"doneAfter"), 1.0); + let chunks = crate::array::js_array_alloc(0); + let stream = js_node_stream_readable_from(box_pointer(chunks as *const u8)); + set_hidden_value(stream, hidden_key(READABLE_SOURCE_ITERATOR_KEY), source); + let take = js_closure_alloc(ns_iter_take as *const u8, 1); + js_closure_set_capture_ptr(take, 0, stream.to_bits() as i64); + + let result = ns_iter_take(take, 2.0); + let iterator = get_hidden_value(result, hidden_key(READABLE_SOURCE_ITERATOR_KEY)).unwrap(); + + assert!(!stream_destroyed(stream)); + assert_eq!(pull(iterator), (false, 1.0)); + assert!(pull(iterator).0); + assert!(!has_truthy_hidden(source, hidden_key(b"returned"))); + assert!(stream_destroyed(stream)); + assert!(stream_destroyed(result)); + } + + #[test] + fn take_suppresses_sync_and_async_lookahead_errors() { + for reject in [false, true] { + let methods = [ + ("next", cast0(source_next)), + ("return", cast0(source_return)), + ]; + let source = box_pointer(build_object(&methods, 0x7FFF_FF74) as *const u8); + set_hidden_value( + source, + hidden_key(b"failAfterLimit"), + f64::from_bits(TAG_TRUE), + ); + if reject { + set_hidden_value( + source, + hidden_key(b"rejectAfterLimit"), + f64::from_bits(TAG_TRUE), + ); + } + let chunks = crate::array::js_array_alloc(0); + let stream = js_node_stream_readable_from(box_pointer(chunks as *const u8)); + set_hidden_value(stream, hidden_key(READABLE_SOURCE_ITERATOR_KEY), source); + let result = readable_from_chunks(crate::array::js_array_alloc(0)); + let iterator = take_source_iterator(stream, result, 2); + + assert_eq!(pull(iterator), (false, 1.0)); + assert_eq!(pull(iterator), (false, 2.0)); + assert!(pull(iterator).0); + assert!(has_truthy_hidden(source, hidden_key(b"returned"))); + assert!(stream_destroyed(stream)); + assert!(stream_destroyed(result)); + } + } +} diff --git a/crates/perry-runtime/src/node_stream_pipeline.rs b/crates/perry-runtime/src/node_stream_pipeline.rs index 77edd421b8..713419c95b 100644 --- a/crates/perry-runtime/src/node_stream_pipeline.rs +++ b/crates/perry-runtime/src/node_stream_pipeline.rs @@ -400,6 +400,15 @@ pub(super) fn collect_pipeline_chunks(value: f64) -> Result { TAG_UNDEFINED | TAG_NULL => return Ok(pipeline_empty_chunks()), _ => {} } + if !readable_chunks_nonempty(value) { + if let Some(source_iterator) = + get_hidden_value(value, hidden_key(READABLE_SOURCE_ITERATOR_KEY)) + { + if let Some(chunks) = collect_pipeline_iterator_chunks(source_iterator)? { + return Ok(chunks); + } + } + } if let Some(result) = js_node_stream_collect_chunks_result(value) { return result; } @@ -468,6 +477,11 @@ pub(super) fn call_pipeline_function_stage( stage: f64, source: f64, ) -> Result { + let source = if is_array_like_value(source) { + js_node_stream_readable_from(source) + } else { + source + }; let args = [source]; let result = catch_pipeline_throw(|| unsafe { crate::closure::js_native_call_value(stage, args.as_ptr(), args.len()) @@ -517,6 +531,24 @@ pub(super) fn fail_collected_pipeline(stages: &[f64], callback: f64, err: f64) { } } +extern "C" fn collected_pipeline_error_noop(_closure: *const ClosureHeader, _err: f64) -> f64 { + f64::from_bits(TAG_UNDEFINED) +} + +fn install_collected_pipeline_error_guards(stages: &[f64]) { + crate::closure::js_register_closure_arity(collected_pipeline_error_noop as *const u8, 1); + for stage in stages { + if is_pipeline_stream(*stage) { + let listener = js_closure_alloc(collected_pipeline_error_noop as *const u8, 0); + add_stream_listener_for_event( + *stage, + string_value(b"error"), + box_pointer(listener as *const u8), + ); + } + } +} + pub(super) fn complete_collected_pipeline(callback: f64, value: f64) { if is_callable_value(callback) { call_listener_args( @@ -542,6 +574,7 @@ pub(super) fn run_collected_pipeline( callback: f64, options: PipelineOptions, ) -> f64 { + install_collected_pipeline_error_guards(stages); let last = *stages.last().unwrap_or(&f64::from_bits(TAG_UNDEFINED)); let first = stages[0]; let mut chunks = if is_callable_value(first) { @@ -818,6 +851,14 @@ fn fail_composed_duplex(composite: f64, source: f64, stages: f64, err: f64) { if stream_destroyed(composite) { return; } + if has_truthy_hidden(composite, hidden_key(b"__perryStreamComposePriming")) { + set_hidden_value( + composite, + hidden_key(b"__perryStreamComposePendingError"), + err, + ); + return; + } compose_destroy_stage_list(stages, err); if is_pipeline_stream(source) { destroy_stream(source, err); @@ -972,6 +1013,12 @@ fn install_compose_source_listeners(composite: f64, source: f64, stages: f64) { js_closure_set_capture_f64(end, 0, composite); add_stream_listener_for_event(source, string_value(b"end"), box_pointer(end as *const u8)); + install_compose_source_error_listener(composite, source, stages); + + start_pipeline_readable(source); +} + +fn install_compose_source_error_listener(composite: f64, source: f64, stages: f64) { let error = js_closure_alloc(compose_source_error_callback as *const u8, 3); js_closure_set_capture_f64(error, 0, composite); js_closure_set_capture_f64(error, 1, source); @@ -981,8 +1028,6 @@ fn install_compose_source_listeners(composite: f64, source: f64, stages: f64) { string_value(b"error"), box_pointer(error as *const u8), ); - - start_pipeline_readable(source); } fn install_composed_duplex_callbacks(composite: f64, stages: f64, source: f64, writable: bool) { @@ -1050,13 +1095,31 @@ fn new_composed_duplex(stages: &[f64], source: Option, writable: bool) -> f let source_value = source.unwrap_or_else(|| f64::from_bits(TAG_UNDEFINED)); install_composed_duplex_callbacks(composite, stages_value, source_value, writable); if let Some(source) = source { + install_compose_stage_error_listeners(composite, source_value, stages_value); if !compose_source_has_snapshot(source) { - install_compose_stage_error_listeners(composite, source_value, stages_value); install_compose_source_listeners(composite, source, stages_value); } else { + install_compose_source_error_listener(composite, source, stages_value); + set_hidden_value( + composite, + hidden_key(b"__perryStreamComposePriming"), + f64::from_bits(TAG_TRUE), + ); prime_composed_duplex_from_source(composite, source, stages_value); - if !stream_destroyed(composite) { - install_compose_stage_error_listeners(composite, source_value, stages_value); + set_hidden_value( + composite, + hidden_key(b"__perryStreamComposePriming"), + f64::from_bits(TAG_FALSE), + ); + if let Some(err) = + get_hidden_value(composite, hidden_key(b"__perryStreamComposePendingError")) + { + set_hidden_value( + composite, + hidden_key(b"__perryStreamComposePendingError"), + f64::from_bits(TAG_UNDEFINED), + ); + fail_composed_duplex(composite, source, stages_value, err); } } } else { @@ -1091,16 +1154,32 @@ pub(super) fn build_node_stream_compose(args: Vec) -> f64 { #[cold] pub(super) fn throw_pipeline_missing_streams() -> ! { - crate::node_submodules::diagnostics::throw_type_error_no_code( - b"The \"streams\" argument must be specified", + crate::fs::validate::throw_type_error_with_code( + "The \"streams\" argument must be specified", + "ERR_MISSING_ARGS", ) } #[cold] -pub(super) fn throw_pipeline_callback_required() -> ! { - crate::node_submodules::diagnostics::throw_type_error_no_code( - b"The \"streams[stream.length - 1]\" property must be of type function", - ) +pub(super) fn throw_pipeline_callback_required(callback: f64) -> ! { + let received = ["PassThrough", "Transform", "Duplex", "Writable", "Readable"] + .into_iter() + .find(|name| is_classic_stream_instance_of(callback, name)) + .map(|name| format!("an instance of {name}")) + .unwrap_or_else(|| crate::fs::validate::describe_received(callback)); + let message = format!( + "The \"streams[stream.length - 1]\" property must be of type function. Received {received}" + ); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE") +} + +#[cold] +pub(super) fn throw_pipeline_invalid_body(body: f64) -> ! { + let message = format!( + "The \"body\" argument must be of type function or an instance of Blob, ReadableStream, WritableStream, Stream, Iterable, AsyncIterable, or Promise or {{ readable, writable }} pair. Received {}", + crate::fs::validate::describe_received(body) + ); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE") } #[cold] diff --git a/crates/perry-runtime/src/node_stream_readwrite.rs b/crates/perry-runtime/src/node_stream_readwrite.rs index 92e9130110..8fb13f56b1 100644 --- a/crates/perry-runtime/src/node_stream_readwrite.rs +++ b/crates/perry-runtime/src/node_stream_readwrite.rs @@ -873,6 +873,23 @@ pub(super) fn drain_readable_from_events(stream: f64) { { return; } + if !readable_chunks_nonempty(stream) { + if let Some(source_iterator) = + get_hidden_value(stream, hidden_key(READABLE_SOURCE_ITERATOR_KEY)) + { + match collect_pipeline_iterator_chunks(source_iterator) { + Ok(Some(chunks)) => { + set_hidden_value(stream, hidden_chunks_key(), chunks); + initialize_readable_from_buffered_length(stream, chunks); + } + Ok(None) => {} + Err(err) => { + destroy_stream(stream, err); + return; + } + } + } + } if let Some(chunks) = readable_hidden_chunks(stream) { let mut values = Vec::new(); push_chunk_values(chunks, &mut values, 0); @@ -993,7 +1010,8 @@ pub(super) fn writable_hidden_final(value: f64) -> Option { } pub(super) fn is_transform_stream(stream: f64) -> bool { - transform_hidden_callback(stream).is_some() + is_classic_stream_instance_of(stream, "Transform") + || transform_hidden_callback(stream).is_some() || transform_hidden_flush(stream).is_some() || has_truthy_hidden(stream, hidden_transform_passthrough_key()) } @@ -1378,6 +1396,13 @@ pub(super) fn is_non_iterable_primitive_for_readable_from(value: f64) -> bool { (jsval.is_number() || jsval.is_int32() || jsval.is_bool()) && !jsval.is_any_string() } +pub(super) fn is_invalid_readable_from_input(value: f64) -> bool { + matches!(value.to_bits(), TAG_NULL | TAG_UNDEFINED) + || is_non_iterable_primitive_for_readable_from(value) + || is_callable_value(value) + || crate::promise::js_value_is_promise(value) != 0 +} + pub(super) fn uint8array_byte_chunks(raw: usize) -> f64 { let arr = crate::array::js_array_alloc(0); if raw < 0x10000 || !crate::buffer::is_registered_buffer(raw) { @@ -1471,6 +1496,12 @@ pub(super) fn normalize_readable_from_input(iterable: f64) -> NormalizedReadable let arr = crate::array::js_array_push_f64(arr, iterable); return normalized_readable_chunks(box_pointer(arr as *const u8)); } + if let Some(source_iterator) = crate::array::call_symbol_async_iterator_for_flat_map(iterable) { + return NormalizedReadableInput { + chunks: box_pointer(crate::array::js_array_alloc(0) as *const u8), + source_iterator: Some(source_iterator), + }; + } if let Some((chunks, source_iterator)) = flatten_async_iterable_with_source(iterable) { return NormalizedReadableInput { chunks: box_pointer(chunks as *const u8), @@ -1488,6 +1519,22 @@ pub(super) fn normalize_readable_from_input(iterable: f64) -> NormalizedReadable normalized_readable_chunks(box_pointer(arr as *const u8)) } +pub(super) fn initialize_readable_from_buffered_length(readable: f64, chunks: f64) { + let mut values = Vec::new(); + push_chunk_values(chunks, &mut values, 0); + let length = if readable_object_mode(readable) { + values.len() as f64 + } else { + let mut bytes = Vec::new(); + for value in values { + append_chunk_bytes(value, &mut bytes, 0); + } + bytes.len() as f64 + }; + set_hidden_value(readable, hidden_buffered_key(), length); + set_hidden_value(readable, hidden_key(b"readableLength"), length); +} + fn flatten_sync_iterable_value( value: f64, ) -> Option<(*mut crate::array::ArrayHeader, Option)> { diff --git a/crates/perry-runtime/src/node_stream_tests.rs b/crates/perry-runtime/src/node_stream_tests.rs index f91a6f5697..01aba12758 100644 --- a/crates/perry-runtime/src/node_stream_tests.rs +++ b/crates/perry-runtime/src/node_stream_tests.rs @@ -3,6 +3,7 @@ use super::*; use std::cell::RefCell; +use std::os::raw::c_int; thread_local! { pub(super) static WRITE_CAPTURED: RefCell>> = const { RefCell::new(Vec::new()) }; @@ -27,6 +28,21 @@ thread_local! { pub(super) static PENDING_WRITE_CALLBACK: RefCell> = const { RefCell::new(None) }; static TRANSFORM_THIS_HAS_STREAM_STATE: RefCell> = const { RefCell::new(Vec::new()) }; static TRANSFORM_FLUSH_COUNT: RefCell = const { RefCell::new(0) }; + static UNCAUGHT_STREAM_ERROR_COUNT: RefCell = const { RefCell::new(0) }; +} + +fn catches_runtime_throw(f: impl FnOnce()) -> bool { + let env = crate::exception::js_try_push(); + let jumped = unsafe { crate::ffi::setjmp::setjmp(env as *mut c_int) }; + if jumped == 0 { + f(); + crate::exception::js_try_end(); + false + } else { + crate::exception::js_try_end(); + crate::exception::js_clear_exception(); + true + } } pub(super) fn string_value(s: &str) -> f64 { @@ -47,6 +63,123 @@ fn buffer_value(bytes: &[u8]) -> f64 { box_pointer(buf as *const u8) } +#[test] +fn readable_from_validation_rejects_promises_and_functions() { + let promise = crate::promise::js_promise_new(); + let function = crate::closure::js_closure_alloc(write_capture as *const u8, 0); + + assert!(is_invalid_readable_from_input(box_pointer( + promise as *const u8 + ))); + assert_eq!( + crate::fs::validate::describe_received(box_pointer(promise as *const u8)), + "an instance of Promise" + ); + assert!(is_invalid_readable_from_input(box_pointer( + function as *const u8 + ))); +} + +#[test] +fn bare_writable_and_transform_reject_missing_methods() { + let undefined = f64::from_bits(TAG_UNDEFINED); + let chunk = string_value("x"); + let writable = js_node_stream_writable_new(undefined); + let transform = js_node_stream_transform_new(undefined); + + assert!(catches_runtime_throw(|| { + write_writable_chunk(writable, chunk, undefined, undefined); + })); + assert!(catches_runtime_throw(|| { + write_writable_chunk(transform, chunk, undefined, undefined); + })); +} + +extern "C" fn capture_uncaught_stream_error(_closure: *const ClosureHeader, _error: f64) -> f64 { + UNCAUGHT_STREAM_ERROR_COUNT.with(|count| *count.borrow_mut() += 1); + f64::from_bits(TAG_UNDEFINED) +} + +#[test] +fn destroy_with_unhandled_error_reaches_process_uncaught_exception() { + crate::os::test_clear_process_event_listeners(); + UNCAUGHT_STREAM_ERROR_COUNT.with(|count| *count.borrow_mut() = 0); + let listener = js_closure_alloc(capture_uncaught_stream_error as *const u8, 0); + crate::closure::js_register_closure_arity(capture_uncaught_stream_error as *const u8, 1); + let event = string_value("uncaughtException"); + let listener = box_pointer(listener as *const u8); + let _ = crate::os::js_process_on(event.to_bits() as i64, listener.to_bits() as i64); + + let stream = js_node_stream_readable_new(f64::from_bits(TAG_UNDEFINED)); + let message = crate::string::js_string_from_bytes(b"boom".as_ptr(), 4); + let error = + crate::value::js_nanbox_pointer(crate::error::js_error_new_with_message(message) as i64); + + destroy_stream(stream, error); + let _ = crate::promise::js_promise_run_microtasks(); + + UNCAUGHT_STREAM_ERROR_COUNT.with(|count| assert_eq!(*count.borrow(), 1)); + crate::os::test_clear_process_event_listeners(); +} + +#[test] +fn pipe_cleanup_does_not_swallow_destination_error() { + crate::os::test_clear_process_event_listeners(); + UNCAUGHT_STREAM_ERROR_COUNT.with(|count| *count.borrow_mut() = 0); + let listener = js_closure_alloc(capture_uncaught_stream_error as *const u8, 0); + crate::closure::js_register_closure_arity(capture_uncaught_stream_error as *const u8, 1); + let event = string_value("uncaughtException"); + let listener = box_pointer(listener as *const u8); + let _ = crate::os::js_process_on(event.to_bits() as i64, listener.to_bits() as i64); + + let source = js_node_stream_readable_new(f64::from_bits(TAG_UNDEFINED)); + let destination = js_node_stream_writable_new(f64::from_bits(TAG_UNDEFINED)); + let _ = js_node_stream_method_pipe( + raw_ptr_from_value(source) as i64, + destination, + f64::from_bits(TAG_UNDEFINED), + ); + let message = crate::string::js_string_from_bytes(b"boom".as_ptr(), 4); + let error = + crate::value::js_nanbox_pointer(crate::error::js_error_new_with_message(message) as i64); + destroy_stream(destination, error); + let _ = crate::promise::js_promise_run_microtasks(); + + UNCAUGHT_STREAM_ERROR_COUNT.with(|count| assert_eq!(*count.borrow(), 1)); + crate::os::test_clear_process_event_listeners(); +} + +extern "C" fn return_pipeline_source(_closure: *const ClosureHeader, source: f64) -> f64 { + source +} + +#[test] +fn pipeline_function_stage_receives_async_iterable_stream() { + let mut chunks = crate::array::js_array_alloc(1); + chunks = crate::array::js_array_push_f64(chunks, string_value("x")); + let stage = js_closure_alloc(return_pipeline_source as *const u8, 0); + crate::closure::js_register_closure_arity(return_pipeline_source as *const u8, 1); + + let result = call_pipeline_function_stage( + box_pointer(stage as *const u8), + box_pointer(chunks as *const u8), + ) + .unwrap(); + + assert!(is_pipeline_stream(result.value)); + assert!(!is_array_like_value(result.value)); +} + +#[test] +fn synchronous_iteration_rejects_async_only_readable() { + let chunks = crate::array::js_array_alloc(0); + let readable = js_node_stream_readable_from(box_pointer(chunks as *const u8)); + + assert!(catches_runtime_throw(|| { + let _ = crate::array::js_for_of_to_array(readable); + })); +} + fn string_contents(value: f64) -> String { let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; let Some((ptr, len)) = crate::string::str_bytes_from_jsvalue(value, &mut scratch) else { @@ -355,6 +488,19 @@ extern "C" fn transform_identity_callback( f64::from_bits(TAG_UNDEFINED) } +extern "C" fn transform_error_callback( + closure: *const ClosureHeader, + _chunk: f64, + _enc: f64, + cb: f64, +) -> f64 { + let err = crate::closure::js_closure_get_capture_f64(closure, 0); + unsafe { + let _ = crate::closure::js_native_call_value(cb, [err].as_ptr(), 1); + } + f64::from_bits(TAG_UNDEFINED) +} + extern "C" fn transform_push_pair_callback( _closure: *const ClosureHeader, _chunk: f64, @@ -647,6 +793,56 @@ fn compose_source_transform_applies_stage_snapshot() { .with(|matches| assert_eq!(matches.borrow().as_slice(), &[true])); } +#[test] +fn compose_snapshot_stage_error_reaches_only_composite_listener() { + crate::os::test_clear_process_event_listeners(); + ERROR_COUNT.with(|count| *count.borrow_mut() = 0); + UNCAUGHT_STREAM_ERROR_COUNT.with(|count| *count.borrow_mut() = 0); + let uncaught = js_closure_alloc(capture_uncaught_stream_error as *const u8, 0); + crate::closure::js_register_closure_arity(capture_uncaught_stream_error as *const u8, 1); + let _ = crate::os::js_process_on( + string_value("uncaughtException").to_bits() as i64, + box_pointer(uncaught as *const u8).to_bits() as i64, + ); + + let mut chunks = crate::array::js_array_alloc(1); + chunks = crate::array::js_array_push_f64(chunks, string_value("x")); + let source = js_node_stream_readable_from(box_pointer(chunks as *const u8)); + + let message = crate::string::js_string_from_bytes(b"compose-fail".as_ptr(), 12); + let error = + crate::value::js_nanbox_pointer(crate::error::js_error_new_with_message(message) as i64); + let options = crate::object::js_object_alloc(0, 1); + let transform = js_closure_alloc(transform_error_callback as *const u8, 1); + crate::closure::js_register_closure_arity(transform_error_callback as *const u8, 3); + crate::closure::js_closure_set_capture_f64(transform, 0, error); + js_object_set_field_by_name( + options, + hidden_key(b"transform"), + box_pointer(transform as *const u8), + ); + let stage = js_node_stream_transform_new(box_pointer(options as *const u8)); + + let mut args = crate::array::js_array_alloc(2); + args = crate::array::js_array_push_f64(args, source); + args = crate::array::js_array_push_f64(args, stage); + let composite = js_node_stream_compose_args(args); + + let listener = js_closure_alloc(capture_error_listener as *const u8, 0); + crate::closure::js_register_closure_arity(capture_error_listener as *const u8, 1); + let _ = js_node_stream_method_on( + raw_ptr_from_value(composite) as i64, + string_value("error"), + box_pointer(listener as *const u8), + ); + let _ = crate::promise::js_promise_run_microtasks(); + + let errors = ERROR_COUNT.with(|count| *count.borrow()); + let uncaught = UNCAUGHT_STREAM_ERROR_COUNT.with(|count| *count.borrow()); + assert_eq!((errors, uncaught), (1, 0)); + crate::os::test_clear_process_event_listeners(); +} + #[test] fn compose_single_transform_returns_stage() { READABLE_DATA_CAPTURED.with(|captured| captured.borrow_mut().clear()); diff --git a/crates/perry-runtime/src/node_stream_tests_extra.rs b/crates/perry-runtime/src/node_stream_tests_extra.rs index 520b19a28c..d2ad99fa1e 100644 --- a/crates/perry-runtime/src/node_stream_tests_extra.rs +++ b/crates/perry-runtime/src/node_stream_tests_extra.rs @@ -920,7 +920,15 @@ fn writable_lifecycle_flags_reflect_end_and_finish() { WRITABLE_FINISH_COUNT.with(|count| *count.borrow_mut() = 0); WRITABLE_CLOSE_COUNT.with(|count| *count.borrow_mut() = 0); - let stream = js_node_stream_writable_new(f64::from_bits(TAG_UNDEFINED)); + let opts = crate::object::js_object_alloc(0, 1); + let closure = js_closure_alloc(write_capture as *const u8, 0); + crate::closure::js_register_closure_arity(write_capture as *const u8, 3); + js_object_set_field_by_name( + opts, + hidden_key(b"write"), + f64::from_bits(JSValue::pointer(closure as *const u8).bits()), + ); + let stream = js_node_stream_writable_new(box_pointer(opts as *const u8)); let handle = raw_ptr_from_value(stream) as i64; let obj = raw_ptr_from_value(stream) as *const ObjectHeader; @@ -1033,16 +1041,16 @@ fn readable_auto_destroy_false_does_not_close_after_end() { #[test] fn stream_destroy_with_error_marks_errored_state() { let stream = js_node_stream_readable_new(f64::from_bits(TAG_UNDEFINED)); + let handle = raw_ptr_from_value(stream) as i64; let destroy = js_object_get_field_by_name_f64( raw_ptr_from_value(stream) as *const ObjectHeader, hidden_key(b"destroy"), ); let err = string_value("boom"); + let error_listener = box_pointer(js_closure_alloc(noop_listener as *const u8, 0) as *const u8); + let _ = js_node_stream_method_on(handle, string_value("error"), error_listener); - assert_eq!( - js_node_stream_method_errored(raw_ptr_from_value(stream) as i64).to_bits(), - TAG_NULL - ); + assert_eq!(js_node_stream_method_errored(handle).to_bits(), TAG_NULL); let ret = unsafe { crate::closure::js_native_call_value(destroy, &err, 1) }; assert_eq!(ret.to_bits(), stream.to_bits()); @@ -1050,7 +1058,7 @@ fn stream_destroy_with_error_marks_errored_state() { let _ = crate::promise::js_promise_run_microtasks(); assert_eq!(js_node_stream_is_errored(stream).to_bits(), TAG_TRUE); assert_eq!( - js_node_stream_method_errored(raw_ptr_from_value(stream) as i64).to_bits(), + js_node_stream_method_errored(handle).to_bits(), err.to_bits() ); } @@ -1090,6 +1098,8 @@ fn readable_aborted_reflects_destroy_before_end() { let handle = raw_ptr_from_value(stream) as i64; let obj = raw_ptr_from_value(stream) as *const ObjectHeader; let err = string_value("abort"); + let error_listener = box_pointer(js_closure_alloc(noop_listener as *const u8, 0) as *const u8); + let _ = js_node_stream_method_on(handle, string_value("error"), error_listener); assert_eq!( js_node_stream_method_readable_aborted(handle).to_bits(), @@ -1117,6 +1127,8 @@ fn readable_aborted_reflects_destroy_before_end() { let ended = js_node_stream_readable_new(f64::from_bits(TAG_UNDEFINED)); let ended_handle = raw_ptr_from_value(ended) as i64; + let error_listener = box_pointer(js_closure_alloc(noop_listener as *const u8, 0) as *const u8); + let _ = js_node_stream_method_on(ended_handle, string_value("error"), error_listener); let _ = js_node_stream_method_push(ended_handle, f64::from_bits(TAG_NULL)); let _ = js_node_stream_method_destroy(ended_handle, err); assert_eq!( @@ -1146,6 +1158,8 @@ fn stream_native_receiver_methods_update_hidden_state() { let stream = js_node_stream_passthrough_new(f64::from_bits(TAG_UNDEFINED)); let handle = raw_ptr_from_value(stream) as i64; + let cb = box_pointer(js_closure_alloc(noop_listener as *const u8, 0) as *const u8); + let _ = js_node_stream_method_on(handle, string_value("error"), cb); let _ = js_node_stream_method_destroy(handle, err); assert!(readable_hidden_error(stream).is_none()); let _ = crate::promise::js_promise_run_microtasks(); diff --git a/crates/perry-runtime/src/node_submodules/diagnostics.rs b/crates/perry-runtime/src/node_submodules/diagnostics.rs index 0f23f8ffcd..2282ecf6d8 100644 --- a/crates/perry-runtime/src/node_submodules/diagnostics.rs +++ b/crates/perry-runtime/src/node_submodules/diagnostics.rs @@ -772,7 +772,10 @@ pub fn diagnostics_channel_drain_uncaught() { } let pending = DIAG_PENDING_UNCAUGHT.with(|q| std::mem::take(&mut *q.borrow_mut())); for err in pending { - crate::os::emit_process_uncaught_exception(err); + if !crate::os::emit_process_event("uncaughtException", &[err]) { + crate::exception::print_uncaught(err); + crate::process::exit_after_current_thread_collection_teardown(1); + } } } diff --git a/crates/perry-runtime/src/os/os_process_emitter.rs b/crates/perry-runtime/src/os/os_process_emitter.rs index ccc9be70ac..15542dc70d 100644 --- a/crates/perry-runtime/src/os/os_process_emitter.rs +++ b/crates/perry-runtime/src/os/os_process_emitter.rs @@ -653,7 +653,10 @@ pub(crate) fn test_process_event_listener_root_snapshot() -> usize { } pub fn emit_process_uncaught_exception(error: f64) { - emit_process_event("uncaughtException", &[error]); + if !emit_process_event("uncaughtException", &[error]) { + crate::exception::print_uncaught(error); + crate::process::exit_after_current_thread_collection_teardown(1); + } } /// process.nextTick(callback, ...args) — schedule callback as a tick, diff --git a/crates/perry-runtime/src/promise/microtasks.rs b/crates/perry-runtime/src/promise/microtasks.rs index 5a6d8d249b..be1bb7893a 100644 --- a/crates/perry-runtime/src/promise/microtasks.rs +++ b/crates/perry-runtime/src/promise/microtasks.rs @@ -234,6 +234,9 @@ fn run_microtasks(mode: MicrotaskDrainMode) -> i32 { if !prev.trap_next.is_null() { js_promise_reject(prev.trap_next, exc); ran += 1; + } else { + crate::node_submodules::diagnostics::schedule_uncaught(exc); + ran += 1; } } } @@ -826,6 +829,7 @@ fn run_microtasks(mode: MicrotaskDrainMode) -> i32 { } crate::exception::js_try_end(); + crate::node_submodules::diagnostics_channel_drain_uncaught(); let _ = crate::gc::gc_runtime_safepoint(); diff --git a/crates/perry-runtime/src/symbol/iterator.rs b/crates/perry-runtime/src/symbol/iterator.rs index 730fa74919..16cb97114c 100644 --- a/crates/perry-runtime/src/symbol/iterator.rs +++ b/crates/perry-runtime/src/symbol/iterator.rs @@ -445,6 +445,17 @@ pub extern "C" fn js_get_iterator(val_f64: f64) -> f64 { if is_registered_class_ref { throw_value_not_iterable(); } + let async_iter_wk = well_known_symbol("asyncIterator"); + if !async_iter_wk.is_null() { + let sym_f64 = + f64::from_bits(crate::value::JSValue::pointer(async_iter_wk as *const u8).bits()); + let async_iter_fn = unsafe { js_object_get_symbol_property(val_f64, sym_f64) }; + if async_iter_fn.to_bits() != TAG_UNDEFINED + && async_iter_fn.to_bits() != crate::value::TAG_NULL + { + throw_value_not_iterable(); + } + } val_f64 } diff --git a/crates/perry-transform/src/generator/per_iteration.rs b/crates/perry-transform/src/generator/per_iteration.rs index 393d562fa8..69a66d7f6b 100644 --- a/crates/perry-transform/src/generator/per_iteration.rs +++ b/crates/perry-transform/src/generator/per_iteration.rs @@ -74,7 +74,6 @@ //! would lose its value on resume, which is a worse bug than the one being //! fixed. -use super::hoist_yields::expr_contains_yield; use crate::unroll::escape_analysis::{ count_local_refs_expr, count_local_refs_stmt, count_local_refs_stmts, }; @@ -150,7 +149,7 @@ fn analyze_loop( .. } = init.as_ref() { - if !init_expr.as_ref().is_some_and(expr_contains_yield) && block_scoped(*id) { + if !init_expr.as_ref().is_some_and(expr_contains_suspend) && block_scoped(*id) { out.insert(*id); } } @@ -176,7 +175,7 @@ fn classify_block( // A `let __tmp = yield …;` IS the state split: the linearizer // assigns it in the resumed state, so it must stay a boxed // cross-state local. - let splits_state = init.as_ref().is_some_and(expr_contains_yield); + let splits_state = init.as_ref().is_some_and(expr_contains_suspend); if !splits_state && block_scoped(*id) && !used_after_suspend(*id, &block[i + 1..]) { out.insert(*id); } @@ -243,12 +242,12 @@ fn loop_contains_suspend(stmt: &Stmt) -> bool { body, } => { init.as_ref().is_some_and(|i| stmt_contains_suspend(i)) - || condition.as_ref().is_some_and(expr_contains_yield) - || update.as_ref().is_some_and(expr_contains_yield) + || condition.as_ref().is_some_and(expr_contains_suspend) + || update.as_ref().is_some_and(expr_contains_suspend) || body.iter().any(stmt_contains_suspend) } Stmt::While { condition, body } | Stmt::DoWhile { body, condition } => { - expr_contains_yield(condition) || body.iter().any(stmt_contains_suspend) + expr_contains_suspend(condition) || body.iter().any(stmt_contains_suspend) } _ => false, } @@ -257,12 +256,12 @@ fn loop_contains_suspend(stmt: &Stmt) -> bool { /// Conservative "does this statement suspend?" — scans every expression it /// owns (not just the normalized `yield` statement shapes `body_contains_yield` /// recognizes) and recurses through all nested blocks, including nested loops. -/// `expr_contains_yield` stops at `Expr::Closure`, so a nested async arrow's +/// `expr_contains_suspend` stops at `Expr::Closure`, so a nested async arrow's /// own `await`s correctly do not count as a suspend of THIS function. fn stmt_contains_suspend(stmt: &Stmt) -> bool { let mut found = false; each_expr(stmt, &mut |e| { - if !found && expr_contains_yield(e) { + if !found && expr_contains_suspend(e) { found = true; } }); @@ -278,6 +277,22 @@ fn stmt_contains_suspend(stmt: &Stmt) -> bool { nested } +fn expr_contains_suspend(expr: &Expr) -> bool { + if matches!(expr, Expr::Yield { .. } | Expr::Await(_)) { + return true; + } + if matches!(expr, Expr::Closure { .. }) { + return false; + } + let mut found = false; + walk_expr_children(expr, &mut |child| { + if !found && expr_contains_suspend(child) { + found = true; + } + }); + found +} + /// Invoke `f` on each expression directly owned by `stmt` (not those inside /// nested statement lists — `each_child_stmt_list` covers those). fn each_expr(stmt: &Stmt, f: &mut F) { @@ -903,7 +918,7 @@ fn classify_written_block( ) { for (i, stmt) in block.iter().enumerate() { if let Stmt::Let { id, init, .. } = stmt { - let splits_state = init.as_ref().is_some_and(expr_contains_yield); + let splits_state = init.as_ref().is_some_and(expr_contains_suspend); if !splits_state && mutably_captured.contains(id) && block_scoped(*id) @@ -1054,3 +1069,29 @@ fn rewrite_cells_in_expr(e: &mut Expr, cells: &HashSet) { } walk_expr_children_mut(e, &mut |child| rewrite_cells_in_expr(child, cells)); } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn loop_local_used_after_await_stays_hoisted() { + let id = 1; + let body = vec![Stmt::While { + condition: Expr::Bool(true), + body: vec![ + Stmt::Let { + id, + name: "value".into(), + ty: Type::Any, + mutable: false, + init: Some(Expr::String("x".into())), + }, + Stmt::Expr(Expr::Await(Box::new(Expr::Integer(0)))), + Stmt::Expr(Expr::LocalGet(id)), + ], + }]; + + assert!(!collect_per_iteration_ids(&body).contains(&id)); + } +} From d27adfecccbba9a4b0c31e3a3e5a6f991088fc70 Mon Sep 17 00:00:00 2001 From: TheHypnoo Date: Thu, 30 Jul 2026 12:40:05 +0200 Subject: [PATCH 2/8] fix(stream): complete web stream state propagation --- crates/perry-codegen/src/ext_registry.rs | 1 + .../perry-codegen/src/lower_call/builtin.rs | 2 +- .../src/lower_call/options/fetch.rs | 21 +- .../src/runtime_decls/strings_part2.rs | 5 + .../node_stream_constructors/web_adapter.rs | 2 +- crates/perry-stdlib/src/streams.rs | 233 +++++- crates/perry-stdlib/src/streams/idalloc.rs | 8 + crates/perry-stdlib/src/streams/pipe.rs | 747 ++++++++++++++---- crates/perry-stdlib/src/streams/subclass.rs | 26 +- crates/perry-stdlib/src/streams/tests.rs | 87 +- crates/perry-stdlib/src/streams/transform.rs | 175 +++- crates/perry-stdlib/src/streams/writable.rs | 67 +- 12 files changed, 1166 insertions(+), 208 deletions(-) diff --git a/crates/perry-codegen/src/ext_registry.rs b/crates/perry-codegen/src/ext_registry.rs index 1cf57df2cb..7280a4abe9 100644 --- a/crates/perry-codegen/src/ext_registry.rs +++ b/crates/perry-codegen/src/ext_registry.rs @@ -116,6 +116,7 @@ const FFI_REGISTRY: &[(&str, OwnerKind)] = &[ ("js_readable_stream_tee", OwnerKind::Stdlib { feature: Some("bundled-streams") }), ("js_readable_stream_pipe_to", OwnerKind::Stdlib { feature: Some("bundled-streams") }), ("js_readable_stream_pipe_through", OwnerKind::Stdlib { feature: Some("bundled-streams") }), + ("js_readable_stream_pipe_through_validate", OwnerKind::Stdlib { feature: Some("bundled-streams") }), ("js_readable_stream_from_blob", OwnerKind::Stdlib { feature: Some("bundled-streams") }), ("js_readable_stream_from_response", OwnerKind::Stdlib { feature: Some("bundled-streams") }), ("js_readable_stream_from_iterable", OwnerKind::Stdlib { feature: Some("bundled-streams") }), diff --git a/crates/perry-codegen/src/lower_call/builtin.rs b/crates/perry-codegen/src/lower_call/builtin.rs index e94416c701..7abad6dd9d 100644 --- a/crates/perry-codegen/src/lower_call/builtin.rs +++ b/crates/perry-codegen/src/lower_call/builtin.rs @@ -1444,7 +1444,7 @@ pub(super) fn lower_builtin_new( // each may be a plain highWaterMark number or a strategy object; // the runtime parses either form. let mut writable_strategy = hwm; - let mut readable_strategy = double_literal(1.0); + let mut readable_strategy = double_literal(0.0); if args.len() >= 2 { writable_strategy = lower_expr(ctx, &args[1])?; } diff --git a/crates/perry-codegen/src/lower_call/options/fetch.rs b/crates/perry-codegen/src/lower_call/options/fetch.rs index 166e49bf44..e4384c0355 100644 --- a/crates/perry-codegen/src/lower_call/options/fetch.rs +++ b/crates/perry-codegen/src/lower_call/options/fetch.rs @@ -961,16 +961,31 @@ pub(in crate::lower_call) fn lower_fetch_native_method( "js_transform_stream_readable", &[(DOUBLE, &transform)], ); - let new_h = ctx.block().call( + let options = if args.len() >= 2 { + lower_expr(ctx, &args[1])? + } else { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + let _ = ctx.block().call( DOUBLE, - "js_readable_stream_pipe_through", + "js_readable_stream_pipe_through_validate", &[ (DOUBLE, &recv_handle), (DOUBLE, &writable), (DOUBLE, &readable), + (DOUBLE, &options), + ], + ); + let _ = ctx.block().call( + I64, + "js_readable_stream_pipe_to", + &[ + (DOUBLE, &recv_handle), + (DOUBLE, &writable), + (DOUBLE, &options), ], ); - return Ok(Some(new_h)); + return Ok(Some(readable)); } "locked" => { let v = ctx.block().call( diff --git a/crates/perry-codegen/src/runtime_decls/strings_part2.rs b/crates/perry-codegen/src/runtime_decls/strings_part2.rs index 6edb3addde..f8494699d8 100644 --- a/crates/perry-codegen/src/runtime_decls/strings_part2.rs +++ b/crates/perry-codegen/src/runtime_decls/strings_part2.rs @@ -1111,6 +1111,11 @@ pub(crate) fn declare_phase_b_strings_part2(module: &mut LlModule) { DOUBLE, &[DOUBLE, DOUBLE, DOUBLE], ); + module.declare_function( + "js_readable_stream_pipe_through_validate", + DOUBLE, + &[DOUBLE, DOUBLE, DOUBLE, DOUBLE], + ); module.declare_function( "js_readable_stream_controller_enqueue", DOUBLE, diff --git a/crates/perry-runtime/src/node_stream_constructors/web_adapter.rs b/crates/perry-runtime/src/node_stream_constructors/web_adapter.rs index fbfd78eeca..cc653bd900 100644 --- a/crates/perry-runtime/src/node_stream_constructors/web_adapter.rs +++ b/crates/perry-runtime/src/node_stream_constructors/web_adapter.rs @@ -149,7 +149,7 @@ fn build_enumerable_object(fields: &[(&[u8], f64)]) -> f64 { } fn build_web_read_result(value: f64, done: bool) -> f64 { - build_enumerable_object(&[(b"value", value), (b"done", bool_value(done))]) + build_enumerable_object(&[(b"done", bool_value(done)), (b"value", value)]) } fn property_value(value: f64, name: &[u8]) -> f64 { diff --git a/crates/perry-stdlib/src/streams.rs b/crates/perry-stdlib/src/streams.rs index 59eacc131b..c1f61684bc 100644 --- a/crates/perry-stdlib/src/streams.rs +++ b/crates/perry-stdlib/src/streams.rs @@ -46,6 +46,45 @@ pub(crate) fn internal_promise() -> *mut Promise { p } +unsafe fn try_call_stream_action(callback: i64, reason: f64) -> Result { + let trap_buf = perry_runtime::exception::js_try_push(); + let jumped = perry_runtime::ffi::setjmp::setjmp(trap_buf as *mut c_int); + if jumped == 0 { + let result = js_closure_call1(callback as *const ClosureHeader, reason); + perry_runtime::exception::js_try_end(); + Ok(result) + } else { + let error = perry_runtime::exception::js_get_exception(); + perry_runtime::exception::js_clear_exception(); + perry_runtime::exception::js_try_end(); + Err(error.to_bits()) + } +} + +unsafe fn stream_action_promise(result: f64) -> Option<*mut Promise> { + let adopted = perry_runtime::promise::js_assimilate_thenable(result); + if perry_runtime::promise::js_value_is_promise(adopted) == 0 { + return None; + } + let promise = js_nanbox_get_pointer(adopted) as *mut Promise; + (!promise.is_null()).then_some(promise) +} + +unsafe fn settle_stream_action_promise(promise: *mut Promise, actions: &[*mut Promise]) { + match actions { + [] => js_promise_resolve(promise, f64::from_bits(TAG_UNDEFINED)), + [action] => perry_runtime::promise::js_promise_resolve_with_promise(promise, *action), + _ => { + let values = js_array_alloc(actions.len() as u32); + for action in actions { + js_array_push(values, JSValue::pointer(*action as *const u8)); + } + let all = perry_runtime::promise::js_promise_all(values); + perry_runtime::promise::js_promise_resolve_with_promise(promise, all); + } + } +} + mod byob; mod expando; mod idalloc; @@ -214,6 +253,7 @@ struct TransformStreamData { transform_cb: i64, flush_cb: i64, native: Option, + backpressure: bool, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -387,6 +427,15 @@ fn scan_stream_roots_mut(visitor: &mut perry_runtime::gc::RuntimeRootVisitor<'_> *slot = p as usize; } } + if let Ok(mut map) = transform::TRANSFORM_BACKPRESSURED_JOBS.lock() { + for jobs in map.values_mut() { + for slot in jobs.iter_mut() { + let mut job = *slot as *mut ClosureHeader; + visitor.visit_raw_mut_ptr_slot(&mut job); + *slot = job as usize; + } + } + } if let Ok(mut map) = READERS.lock() { for r in map.values_mut() { visitor.visit_raw_mut_ptr_slot(&mut r.closed_promise); @@ -436,13 +485,13 @@ unsafe fn stream_object_closure(object: f64, name: &[u8]) -> i64 { unsafe fn build_iter_result(value_bits: u64, done: bool) -> u64 { let obj = js_object_alloc(0, 2); let keys = js_array_alloc(2); - let k_value = js_string_from_bytes(b"value".as_ptr(), 5); let k_done = js_string_from_bytes(b"done".as_ptr(), 4); - js_array_push(keys, JSValue::string_ptr(k_value)); + let k_value = js_string_from_bytes(b"value".as_ptr(), 5); js_array_push(keys, JSValue::string_ptr(k_done)); - js_object_set_field(obj, 0, JSValue::from_bits(value_bits)); + js_array_push(keys, JSValue::string_ptr(k_value)); let done_bits = if done { TAG_TRUE } else { TAG_FALSE }; - js_object_set_field(obj, 1, JSValue::from_bits(done_bits)); + js_object_set_field(obj, 0, JSValue::from_bits(done_bits)); + js_object_set_field(obj, 1, JSValue::from_bits(value_bits)); js_object_set_keys(obj, keys); JSValue::object_ptr(obj as *mut u8).bits() } @@ -708,17 +757,80 @@ extern "C" fn readable_pull_microtask(closure: *const ClosureHeader) -> f64 { let pull_outcome = perry_runtime::exception::js_call_catching(|| { if pull_returns_byte_chunk { pull_deferred_byte_chunk(stream_id, cb); + f64::from_bits(TAG_UNDEFINED) } else { - js_closure_call1(cb as *const ClosureHeader, stream_id as f64); + js_closure_call1(cb as *const ClosureHeader, stream_id as f64) } - f64::from_bits(TAG_UNDEFINED) }); - if let Some(s) = READABLE_STREAMS.lock().unwrap().get_mut(&stream_id) { - s.pulling = false; + match pull_outcome { + Ok(result) => { + if perry_runtime::promise::js_value_is_promise(result) != 0 { + let promise = + perry_runtime::value::js_nanbox_get_pointer(result) as *mut Promise; + if !promise.is_null() { + let fulfilled = readable_pull_settled_closure( + readable_pull_fulfilled as *const u8, + stream_id, + ); + let rejected = readable_pull_settled_closure( + readable_pull_rejected as *const u8, + stream_id, + ); + let _ = perry_runtime::promise::js_promise_then( + promise, fulfilled, rejected, + ); + return f64::from_bits(TAG_UNDEFINED); + } + } + if let Some(s) = READABLE_STREAMS.lock().unwrap().get_mut(&stream_id) { + s.pulling = false; + } + } + Err(exc) => { + if let Some(s) = READABLE_STREAMS.lock().unwrap().get_mut(&stream_id) { + s.pulling = false; + } + error_readable_stream(stream_id, exc.to_bits()); + } } - if let Err(exc) = pull_outcome { - error_readable_stream(stream_id, exc.to_bits()); + } + } + f64::from_bits(TAG_UNDEFINED) +} + +fn readable_pull_settled_closure(func: *const u8, stream_id: usize) -> *mut ClosureHeader { + perry_runtime::closure::js_register_closure_arity(func, 1); + let closure = perry_runtime::closure::js_closure_alloc(func, 1); + perry_runtime::closure::js_closure_set_capture_ptr(closure, 0, stream_id as i64); + closure +} + +extern "C" fn readable_pull_fulfilled(closure: *const ClosureHeader, _value: f64) -> f64 { + unsafe { + let stream_id = perry_runtime::closure::js_closure_get_capture_ptr(closure, 0) as usize; + if let Some(s) = READABLE_STREAMS.lock().unwrap().get_mut(&stream_id) { + s.pulling = false; + } + maybe_pull(stream_id); + } + f64::from_bits(TAG_UNDEFINED) +} + +extern "C" fn readable_pull_rejected(closure: *const ClosureHeader, reason: f64) -> f64 { + unsafe { + let stream_id = perry_runtime::closure::js_closure_get_capture_ptr(closure, 0) as usize; + let should_error = { + let mut streams = READABLE_STREAMS.lock().unwrap(); + match streams.get_mut(&stream_id) { + Some(stream) => { + stream.pulling = false; + stream.state == ReadableState::Readable + } + None => false, } + }; + if should_error { + error_readable_stream(stream_id, reason.to_bits()); } } f64::from_bits(TAG_UNDEFINED) @@ -1142,14 +1254,29 @@ unsafe fn js_readable_stream_cancel_inner( reject_type_error(promise, "ReadableStream is locked"); return promise; } + let mut actions = [std::ptr::null_mut(); 2]; + let mut action_count = 0; if let Some(writable_id) = transform_writable_for_readable(id) { - let _ = js_writable_stream_abort_inner(writable_id as f64, reason, true); + actions[action_count] = js_writable_stream_abort_inner(writable_id as f64, reason, true); + action_count += 1; } if cb != 0 { - js_closure_call1(cb as *const ClosureHeader, reason); + match try_call_stream_action(cb, reason) { + Ok(result) => { + if let Some(action) = stream_action_promise(result) { + actions[action_count] = action; + action_count += 1; + } + } + Err(error) => { + close_pending(id); + js_promise_reject(promise, f64::from_bits(error)); + return promise; + } + } } close_pending(id); - js_promise_resolve(promise, f64::from_bits(TAG_UNDEFINED)); + settle_stream_action_promise(promise, &actions[..action_count]); promise } @@ -1512,6 +1639,14 @@ unsafe fn throw_invalid_readable_strategy_size(stream_id: usize, size: f64) -> ! perry_runtime::exception::js_throw(f64::from_bits(err)) } +fn readable_controller_enqueue_error(stream_id: usize) -> Option<&'static str> { + let streams = READABLE_STREAMS.lock().unwrap(); + match streams.get(&stream_id) { + Some(stream) if stream.state == ReadableState::Readable => None, + _ => Some("Invalid state: Controller is already closed"), + } +} + // ───────────────────────────────────────────────────────────────────── // ReadableStreamDefaultController FFI (controller is the stream handle) // ───────────────────────────────────────────────────────────────────── @@ -1533,6 +1668,9 @@ pub unsafe extern "C" fn js_readable_stream_controller_enqueue( } } let id = stream_handle as usize; + if let Some(message) = readable_controller_enqueue_error(id) { + throw_type_error_with_code(message, "ERR_INVALID_STATE"); + } let chunk_bits = chunk.to_bits(); let is_byte_stream = { let g = READABLE_STREAMS.lock().unwrap(); @@ -1992,20 +2130,77 @@ pub unsafe extern "C" fn js_reader_cancel(reader_handle: f64, reason: f64) -> *m // tee / pipeTo / pipeThrough // ───────────────────────────────────────────────────────────────────── -/// `readable.pipeThrough({readable, writable})` — pipeTo into the -/// transform's writable side, return its readable side. Caller already -/// destructured the TransformStream into its readable / writable -/// handles. +fn pipe_through_validation_error( + readable_id: usize, + writable_id: usize, + output_id: usize, +) -> Option<&'static str> { + let readable = READABLE_STREAMS.lock().unwrap(); + match readable.get(&readable_id) { + Some(stream) if stream.reader_handle.is_some() => return Some("ReadableStream is locked"), + Some(_) => {} + None => return Some("Invalid ReadableStream"), + } + if !readable.contains_key(&output_id) { + return Some("Invalid transform readable"); + } + drop(readable); + match WRITABLE_STREAMS.lock().unwrap().get(&writable_id) { + Some(stream) if stream.writer_handle.is_some() => Some("WritableStream is locked"), + Some(_) => None, + None => Some("Invalid transform writable"), + } +} + +unsafe fn pipe_through_options_error(options: f64) -> Option<&'static str> { + let signal = perry_runtime::value::js_get_property(options, b"signal".as_ptr() as i64, 6); + pipe_through_signal_error(signal) +} + +fn pipe_through_signal_error(signal: f64) -> Option<&'static str> { + (signal.to_bits() != TAG_UNDEFINED + && perry_runtime::url::js_abort_signal_resolve_ptr(signal).is_null()) + .then_some("The options.signal property must be an AbortSignal") +} + +#[no_mangle] +pub unsafe extern "C" fn js_readable_stream_pipe_through_validate( + readable_handle: f64, + transform_writable_handle: f64, + transform_readable_handle: f64, + options: f64, +) -> f64 { + if let Some(message) = pipe_through_validation_error( + readable_handle as usize, + transform_writable_handle as usize, + transform_readable_handle as usize, + ) { + throw_type_error(message); + } + if let Some(message) = pipe_through_options_error(options) { + throw_type_error(message); + } + transform_readable_handle +} + +/// Legacy three-argument ABI shared with the external streams library. #[no_mangle] pub unsafe extern "C" fn js_readable_stream_pipe_through( readable_handle: f64, transform_writable_handle: f64, transform_readable_handle: f64, ) -> f64 { - let _ = js_readable_stream_pipe_to( + let output = js_readable_stream_pipe_through_validate( readable_handle, transform_writable_handle, + transform_readable_handle, f64::from_bits(TAG_UNDEFINED), ); - transform_readable_handle + let pipe = js_readable_stream_pipe_to( + readable_handle, + transform_writable_handle, + f64::from_bits(TAG_UNDEFINED), + ); + js_promise_mark_internally_handled(pipe); + output } diff --git a/crates/perry-stdlib/src/streams/idalloc.rs b/crates/perry-stdlib/src/streams/idalloc.rs index 82655d59c3..15afda8672 100644 --- a/crates/perry-stdlib/src/streams/idalloc.rs +++ b/crates/perry-stdlib/src/streams/idalloc.rs @@ -239,6 +239,14 @@ fn evict_ids(batch: &[usize]) { g.remove(id); } } + { + let mut g = super::transform::TRANSFORM_BACKPRESSURED_JOBS + .lock() + .unwrap(); + for id in batch { + g.remove(id); + } + } byob::evict_ids(batch); tee::evict_ids(batch); for &id in batch { diff --git a/crates/perry-stdlib/src/streams/pipe.rs b/crates/perry-stdlib/src/streams/pipe.rs index c1c896a362..a9deef471e 100644 --- a/crates/perry-stdlib/src/streams/pipe.rs +++ b/crates/perry-stdlib/src/streams/pipe.rs @@ -2,8 +2,8 @@ use super::{ box_promise, idalloc, js_writable_stream_close, maybe_pull, reject_type_error, transform_close, - writable_stream_write, ReadableState, READABLE_STREAMS, TAG_UNDEFINED, TRANSFORM_PAIRS, - WRITABLE_STREAMS, + writable_stream_write, ReadableState, WritableState, READABLE_STREAMS, TAG_UNDEFINED, + TRANSFORM_PAIRS, WRITABLE_STREAMS, }; use perry_runtime::{ js_nanbox_get_pointer, js_object_get_field_by_name, js_promise_new, js_promise_reject, @@ -19,6 +19,16 @@ struct PipeLockIds { writer_id: usize, } +#[derive(Clone, Copy)] +struct PipeState { + locks: PipeLockIds, + prevent_close: bool, + prevent_abort: bool, + prevent_cancel: bool, + signal: f64, + abort_listener: f64, +} + fn acquire_pipe_locks(readable_id: usize, writable_id: usize) -> Result { let reader_id = idalloc::next_pipe_lock_id(); let writer_id = idalloc::next_pipe_lock_id(); @@ -114,17 +124,26 @@ fn capture_f64(closure: *const ClosureHeader, idx: u32) -> f64 { f64::from_bits(bits) } +fn pipe_state_from_capture(closure: *const ClosureHeader) -> PipeState { + PipeState { + locks: PipeLockIds { + reader_id: capture_f64(closure, 3) as usize, + writer_id: capture_f64(closure, 4) as usize, + }, + prevent_close: perry_runtime::value::js_is_truthy(capture_f64(closure, 5)) != 0, + prevent_abort: perry_runtime::value::js_is_truthy(capture_f64(closure, 6)) != 0, + prevent_cancel: perry_runtime::value::js_is_truthy(capture_f64(closure, 7)) != 0, + signal: capture_f64(closure, 8), + abort_listener: capture_f64(closure, 9), + } +} + extern "C" fn readable_stream_pipe_to_microtask(closure: *const ClosureHeader) -> f64 { unsafe { let r_id = capture_f64(closure, 0) as usize; let w_id = capture_f64(closure, 1) as usize; let promise = promise_from_capture(closure, 2); - let locks = PipeLockIds { - reader_id: capture_f64(closure, 3) as usize, - writer_id: capture_f64(closure, 4) as usize, - }; - let prevent_close = perry_runtime::value::js_is_truthy(capture_f64(closure, 5)) != 0; - pipe_step(r_id, w_id, promise, locks, prevent_close); + pipe_step(r_id, w_id, promise, pipe_state_from_capture(closure)); } f64::from_bits(TAG_UNDEFINED) } @@ -137,20 +156,14 @@ extern "C" fn readable_stream_pipe_to_read_fulfilled( let r_id = capture_f64(closure, 0) as usize; let w_id = capture_f64(closure, 1) as usize; let promise = promise_from_capture(closure, 2); - let locks = PipeLockIds { - reader_id: capture_f64(closure, 3) as usize, - writer_id: capture_f64(closure, 4) as usize, - }; - let prevent_close = perry_runtime::value::js_is_truthy(capture_f64(closure, 5)) != 0; + let state = pipe_state_from_capture(closure); if perry_runtime::promise::js_promise_state(promise) != 0 { return f64::from_bits(TAG_UNDEFINED); } match pipe_iter_result(result) { - Some((true, _)) => finish_pipe(r_id, w_id, promise, locks, prevent_close), - Some((false, value)) => { - pipe_write_then_continue(r_id, w_id, promise, locks, prevent_close, value) - } - None => reject_pipe(r_id, w_id, promise, locks, result.to_bits()), + Some((true, _)) => finish_pipe(r_id, w_id, promise, state), + Some((false, value)) => pipe_write_then_continue(r_id, w_id, promise, state, value), + None => abort_destination_and_reject(r_id, w_id, promise, state, result.to_bits()), } } f64::from_bits(TAG_UNDEFINED) @@ -164,12 +177,10 @@ extern "C" fn readable_stream_pipe_to_write_fulfilled( let r_id = capture_f64(closure, 0) as usize; let w_id = capture_f64(closure, 1) as usize; let promise = promise_from_capture(closure, 2); - let locks = PipeLockIds { - reader_id: capture_f64(closure, 3) as usize, - writer_id: capture_f64(closure, 4) as usize, - }; - let prevent_close = perry_runtime::value::js_is_truthy(capture_f64(closure, 5)) != 0; - pipe_step(r_id, w_id, promise, locks, prevent_close); + if perry_runtime::promise::js_promise_state(promise) != 0 { + return f64::from_bits(TAG_UNDEFINED); + } + pipe_step(r_id, w_id, promise, pipe_state_from_capture(closure)); } f64::from_bits(TAG_UNDEFINED) } @@ -178,28 +189,88 @@ extern "C" fn readable_stream_pipe_to_close_fulfilled( closure: *const ClosureHeader, _value: f64, ) -> f64 { - let r_id = capture_f64(closure, 0) as usize; - let w_id = capture_f64(closure, 1) as usize; - let promise = promise_from_capture(closure, 2); - let locks = PipeLockIds { - reader_id: capture_f64(closure, 3) as usize, - writer_id: capture_f64(closure, 4) as usize, - }; - release_pipe_locks(r_id, w_id, locks); - js_promise_resolve(promise, f64::from_bits(TAG_UNDEFINED)); + unsafe { + let r_id = capture_f64(closure, 0) as usize; + let w_id = capture_f64(closure, 1) as usize; + let promise = promise_from_capture(closure, 2); + if perry_runtime::promise::js_promise_state(promise) != 0 { + return f64::from_bits(TAG_UNDEFINED); + } + let state = pipe_state_from_capture(closure); + cleanup_pipe_signal(state); + release_pipe_locks(r_id, w_id, state.locks); + js_promise_resolve(promise, f64::from_bits(TAG_UNDEFINED)); + } f64::from_bits(TAG_UNDEFINED) } -extern "C" fn readable_stream_pipe_to_rejected(closure: *const ClosureHeader, reason: f64) -> f64 { +extern "C" fn readable_stream_pipe_to_read_rejected( + closure: *const ClosureHeader, + reason: f64, +) -> f64 { unsafe { let r_id = capture_f64(closure, 0) as usize; let w_id = capture_f64(closure, 1) as usize; let promise = promise_from_capture(closure, 2); - let locks = PipeLockIds { - reader_id: capture_f64(closure, 3) as usize, - writer_id: capture_f64(closure, 4) as usize, - }; - reject_pipe(r_id, w_id, promise, locks, reason.to_bits()); + if perry_runtime::promise::js_promise_state(promise) != 0 { + return f64::from_bits(TAG_UNDEFINED); + } + abort_destination_and_reject( + r_id, + w_id, + promise, + pipe_state_from_capture(closure), + reason.to_bits(), + ); + } + f64::from_bits(TAG_UNDEFINED) +} + +extern "C" fn readable_stream_pipe_to_write_rejected( + closure: *const ClosureHeader, + reason: f64, +) -> f64 { + unsafe { + let r_id = capture_f64(closure, 0) as usize; + let w_id = capture_f64(closure, 1) as usize; + let promise = promise_from_capture(closure, 2); + if perry_runtime::promise::js_promise_state(promise) != 0 { + return f64::from_bits(TAG_UNDEFINED); + } + cancel_source_and_reject( + r_id, + w_id, + promise, + pipe_state_from_capture(closure), + reason.to_bits(), + ); + } + f64::from_bits(TAG_UNDEFINED) +} + +extern "C" fn readable_stream_pipe_to_aborted(closure: *const ClosureHeader) -> f64 { + unsafe { + let promise = promise_from_capture(closure, 2); + if perry_runtime::promise::js_promise_state(promise) != 0 { + return f64::from_bits(TAG_UNDEFINED); + } + let r_id = capture_f64(closure, 0) as usize; + let w_id = capture_f64(closure, 1) as usize; + let state = pipe_state_from_capture(closure); + let reason = pipe_signal_reason(state.signal); + let mut actions = [std::ptr::null_mut(); 2]; + let mut action_count = 0; + if !state.prevent_abort && writable_can_abort(w_id) { + actions[action_count] = + super::js_writable_stream_abort_inner(w_id as f64, f64::from_bits(reason), true); + action_count += 1; + } + if !state.prevent_cancel && readable_can_cancel(r_id) { + actions[action_count] = + super::js_readable_stream_cancel_inner(r_id as f64, f64::from_bits(reason), true); + action_count += 1; + } + wait_for_shutdown_actions(r_id, w_id, promise, state, reason, &actions[..action_count]); } f64::from_bits(TAG_UNDEFINED) } @@ -233,12 +304,15 @@ unsafe fn pipe_step( readable_id: usize, writable_id: usize, promise: *mut Promise, - locks: PipeLockIds, - prevent_close: bool, + state: PipeState, ) { if perry_runtime::promise::js_promise_state(promise) != 0 { return; } + if let Some(reason) = writable_pipe_error(writable_id) { + cancel_source_and_reject(readable_id, writable_id, promise, state, reason); + return; + } let step = pipe_next_read(readable_id); // Pipe progress on this readable is consumer progress: release // transform writes parked on backpressure (chained @@ -247,23 +321,16 @@ unsafe fn pipe_step( super::transform::transform_release_writes(readable_id); match step { PipeReadStep::Chunk(chunk) => { - pipe_write_then_continue( - readable_id, - writable_id, - promise, - locks, - prevent_close, - chunk, - ); + pipe_write_then_continue(readable_id, writable_id, promise, state, chunk); } PipeReadStep::Done => { - finish_pipe(readable_id, writable_id, promise, locks, prevent_close); + finish_pipe(readable_id, writable_id, promise, state); } PipeReadStep::Error(reason) => { - reject_pipe(readable_id, writable_id, promise, locks, reason); + abort_destination_and_reject(readable_id, writable_id, promise, state, reason); } PipeReadStep::Pending => { - wait_for_next_read(readable_id, writable_id, promise, locks, prevent_close); + wait_for_next_read(readable_id, writable_id, promise, state); } } } @@ -272,11 +339,11 @@ unsafe fn finish_pipe( readable_id: usize, writable_id: usize, promise: *mut Promise, - locks: PipeLockIds, - prevent_close: bool, + state: PipeState, ) { - if prevent_close { - release_pipe_locks(readable_id, writable_id, locks); + if state.prevent_close { + cleanup_pipe_signal(state); + release_pipe_locks(readable_id, writable_id, state.locks); js_promise_resolve(promise, f64::from_bits(TAG_UNDEFINED)); return; } @@ -291,23 +358,21 @@ unsafe fn finish_pipe( readable_id, writable_id, promise, - locks, - prevent_close, + state, ); let rejected = pipe_closure( - readable_stream_pipe_to_rejected as *const u8, + readable_stream_pipe_to_write_rejected as *const u8, readable_id, writable_id, promise, - locks, - prevent_close, + state, ); perry_runtime::closure::js_register_closure_arity( readable_stream_pipe_to_close_fulfilled as *const u8, 1, ); perry_runtime::closure::js_register_closure_arity( - readable_stream_pipe_to_rejected as *const u8, + readable_stream_pipe_to_write_rejected as *const u8, 1, ); let _ = perry_runtime::promise::js_promise_then(close_promise, fulfilled, rejected); @@ -317,57 +382,193 @@ unsafe fn reject_pipe( readable_id: usize, writable_id: usize, promise: *mut Promise, - locks: PipeLockIds, + state: PipeState, reason: u64, ) { - release_pipe_locks(readable_id, writable_id, locks); + cleanup_pipe_signal(state); + release_pipe_locks(readable_id, writable_id, state.locks); js_promise_reject(promise, f64::from_bits(reason)); } -/// Queue the next pipe cycle directly as a microtask (one tick), mirroring the -/// pipeTo entry's initial scheduling. -unsafe fn schedule_pipe_step( +unsafe fn abort_destination_and_reject( readable_id: usize, writable_id: usize, promise: *mut Promise, - locks: PipeLockIds, - prevent_close: bool, + state: PipeState, + reason: u64, ) { - let closure = - perry_runtime::closure::js_closure_alloc(readable_stream_pipe_to_microtask as *const u8, 6); - perry_runtime::closure::js_register_closure_arity( - readable_stream_pipe_to_microtask as *const u8, - 0, + if !state.prevent_abort && writable_can_abort(writable_id) { + let action = + super::js_writable_stream_abort_inner(writable_id as f64, f64::from_bits(reason), true); + wait_for_shutdown_actions(readable_id, writable_id, promise, state, reason, &[action]); + return; + } + reject_pipe(readable_id, writable_id, promise, state, reason); +} + +unsafe fn cancel_source_and_reject( + readable_id: usize, + writable_id: usize, + promise: *mut Promise, + state: PipeState, + reason: u64, +) { + if !state.prevent_cancel && readable_can_cancel(readable_id) { + let action = super::js_readable_stream_cancel_inner( + readable_id as f64, + f64::from_bits(reason), + true, + ); + wait_for_shutdown_actions(readable_id, writable_id, promise, state, reason, &[action]); + return; + } + reject_pipe(readable_id, writable_id, promise, state, reason); +} + +extern "C" fn readable_stream_pipe_to_shutdown_fulfilled( + closure: *const ClosureHeader, + _value: f64, +) -> f64 { + unsafe { + let promise = promise_from_capture(closure, 2); + if perry_runtime::promise::js_promise_state(promise) != 0 { + return f64::from_bits(TAG_UNDEFINED); + } + reject_pipe( + capture_f64(closure, 0) as usize, + capture_f64(closure, 1) as usize, + promise, + pipe_state_from_capture(closure), + capture_f64(closure, 10).to_bits(), + ); + } + f64::from_bits(TAG_UNDEFINED) +} + +extern "C" fn readable_stream_pipe_to_shutdown_rejected( + closure: *const ClosureHeader, + reason: f64, +) -> f64 { + unsafe { + let promise = promise_from_capture(closure, 2); + if perry_runtime::promise::js_promise_state(promise) != 0 { + return f64::from_bits(TAG_UNDEFINED); + } + reject_pipe( + capture_f64(closure, 0) as usize, + capture_f64(closure, 1) as usize, + promise, + pipe_state_from_capture(closure), + reason.to_bits(), + ); + } + f64::from_bits(TAG_UNDEFINED) +} + +unsafe fn wait_for_shutdown_actions( + readable_id: usize, + writable_id: usize, + promise: *mut Promise, + state: PipeState, + reason: u64, + actions: &[*mut Promise], +) { + if actions.is_empty() { + reject_pipe(readable_id, writable_id, promise, state, reason); + return; + } + let action = if actions.len() == 1 { + actions[0] + } else { + let values = perry_runtime::js_array_alloc(actions.len() as u32); + for action in actions { + perry_runtime::js_array_push(values, JSValue::pointer(*action as *const u8)); + } + perry_runtime::promise::js_promise_all(values) + }; + let fulfilled = pipe_closure_with_reason( + readable_stream_pipe_to_shutdown_fulfilled as *const u8, + readable_id, + writable_id, + promise, + state, + reason, ); - perry_runtime::closure::js_closure_set_capture_ptr( - closure, - 0, - (readable_id as f64).to_bits() as i64, + let rejected = pipe_closure_with_reason( + readable_stream_pipe_to_shutdown_rejected as *const u8, + readable_id, + writable_id, + promise, + state, + reason, ); - perry_runtime::closure::js_closure_set_capture_ptr( - closure, + perry_runtime::closure::js_register_closure_arity( + readable_stream_pipe_to_shutdown_fulfilled as *const u8, 1, - (writable_id as f64).to_bits() as i64, - ); - perry_runtime::closure::js_closure_set_capture_ptr( - closure, - 2, - box_promise(promise).to_bits() as i64, ); - perry_runtime::closure::js_closure_set_capture_ptr( - closure, - 3, - (locks.reader_id as f64).to_bits() as i64, + perry_runtime::closure::js_register_closure_arity( + readable_stream_pipe_to_shutdown_rejected as *const u8, + 1, ); - perry_runtime::closure::js_closure_set_capture_ptr( - closure, - 4, - (locks.writer_id as f64).to_bits() as i64, + let _ = perry_runtime::promise::js_promise_then(action, fulfilled, rejected); +} + +fn readable_can_cancel(readable_id: usize) -> bool { + READABLE_STREAMS + .lock() + .unwrap() + .get(&readable_id) + .map(|stream| stream.state == ReadableState::Readable) + .unwrap_or(false) +} + +fn writable_can_abort(writable_id: usize) -> bool { + WRITABLE_STREAMS + .lock() + .unwrap() + .get(&writable_id) + .map(|stream| { + matches!( + stream.state, + WritableState::Writable | WritableState::Closing + ) + }) + .unwrap_or(false) +} + +unsafe fn writable_pipe_error(writable_id: usize) -> Option { + let state = WRITABLE_STREAMS + .lock() + .unwrap() + .get(&writable_id) + .map(|stream| (stream.state, stream.error_value)); + match state { + Some((WritableState::Errored, reason)) => Some(reason), + Some((WritableState::Closing | WritableState::Closed, _)) => Some( + super::make_type_error_with_message("Invalid state: WritableStream is closed"), + ), + _ => None, + } +} + +/// Queue the next pipe cycle directly as a microtask (one tick), mirroring the +/// pipeTo entry's initial scheduling. +unsafe fn schedule_pipe_step( + readable_id: usize, + writable_id: usize, + promise: *mut Promise, + state: PipeState, +) { + let closure = pipe_closure( + readable_stream_pipe_to_microtask as *const u8, + readable_id, + writable_id, + promise, + state, ); - perry_runtime::closure::js_closure_set_capture_ptr( - closure, - 5, - (if prevent_close { 1.0 } else { 0.0f64 }).to_bits() as i64, + perry_runtime::closure::js_register_closure_arity( + readable_stream_pipe_to_microtask as *const u8, + 0, ); perry_runtime::builtins::js_queue_microtask(closure as i64); } @@ -376,11 +577,11 @@ unsafe fn pipe_write_then_continue( readable_id: usize, writable_id: usize, promise: *mut Promise, - locks: PipeLockIds, - prevent_close: bool, + state: PipeState, chunk: u64, ) { - let write_promise = writable_stream_write(writable_id, locks.writer_id, f64::from_bits(chunk)); + let write_promise = + writable_stream_write(writable_id, state.locks.writer_id, f64::from_bits(chunk)); // Spec ReadableStreamPipeTo awaits only BACKPRESSURE (writer.ready), not // each write's completion. A sink that accepted the chunk synchronously // (write promise already fulfilled) must not cost an extra reaction tick — @@ -404,9 +605,9 @@ unsafe fn pipe_write_then_continue( .unwrap_or(false) }; if park_now { - wait_for_next_read(readable_id, writable_id, promise, locks, prevent_close); + wait_for_next_read(readable_id, writable_id, promise, state); } else { - schedule_pipe_step(readable_id, writable_id, promise, locks, prevent_close); + schedule_pipe_step(readable_id, writable_id, promise, state); } return; } @@ -415,23 +616,21 @@ unsafe fn pipe_write_then_continue( readable_id, writable_id, promise, - locks, - prevent_close, + state, ); let rejected = pipe_closure( - readable_stream_pipe_to_rejected as *const u8, + readable_stream_pipe_to_write_rejected as *const u8, readable_id, writable_id, promise, - locks, - prevent_close, + state, ); perry_runtime::closure::js_register_closure_arity( readable_stream_pipe_to_write_fulfilled as *const u8, 1, ); perry_runtime::closure::js_register_closure_arity( - readable_stream_pipe_to_rejected as *const u8, + readable_stream_pipe_to_write_rejected as *const u8, 1, ); let _ = perry_runtime::promise::js_promise_then(write_promise, fulfilled, rejected); @@ -441,8 +640,7 @@ unsafe fn wait_for_next_read( readable_id: usize, writable_id: usize, promise: *mut Promise, - locks: PipeLockIds, - prevent_close: bool, + state: PipeState, ) { let read_promise = js_promise_new(); if let Some(s) = READABLE_STREAMS.lock().unwrap().get_mut(&readable_id) { @@ -465,23 +663,21 @@ unsafe fn wait_for_next_read( readable_id, writable_id, promise, - locks, - prevent_close, + state, ); let rejected = pipe_closure( - readable_stream_pipe_to_rejected as *const u8, + readable_stream_pipe_to_read_rejected as *const u8, readable_id, writable_id, promise, - locks, - prevent_close, + state, ); perry_runtime::closure::js_register_closure_arity( readable_stream_pipe_to_read_fulfilled as *const u8, 1, ); perry_runtime::closure::js_register_closure_arity( - readable_stream_pipe_to_rejected as *const u8, + readable_stream_pipe_to_read_rejected as *const u8, 1, ); let _ = perry_runtime::promise::js_promise_then(read_promise, fulfilled, rejected); @@ -528,10 +724,32 @@ fn pipe_closure( readable_id: usize, writable_id: usize, promise: *mut Promise, - locks: PipeLockIds, - prevent_close: bool, + state: PipeState, +) -> *mut perry_runtime::ClosureHeader { + pipe_closure_with_extra(func, readable_id, writable_id, promise, state, None) +} + +fn pipe_closure_with_reason( + func: *const u8, + readable_id: usize, + writable_id: usize, + promise: *mut Promise, + state: PipeState, + reason: u64, +) -> *mut perry_runtime::ClosureHeader { + pipe_closure_with_extra(func, readable_id, writable_id, promise, state, Some(reason)) +} + +fn pipe_closure_with_extra( + func: *const u8, + readable_id: usize, + writable_id: usize, + promise: *mut Promise, + state: PipeState, + reason: Option, ) -> *mut perry_runtime::ClosureHeader { - let closure = perry_runtime::closure::js_closure_alloc(func, 6); + let closure = + perry_runtime::closure::js_closure_alloc(func, if reason.is_some() { 11 } else { 10 }); perry_runtime::closure::js_closure_set_capture_ptr( closure, 0, @@ -550,18 +768,37 @@ fn pipe_closure( perry_runtime::closure::js_closure_set_capture_ptr( closure, 3, - (locks.reader_id as f64).to_bits() as i64, + (state.locks.reader_id as f64).to_bits() as i64, ); perry_runtime::closure::js_closure_set_capture_ptr( closure, 4, - (locks.writer_id as f64).to_bits() as i64, + (state.locks.writer_id as f64).to_bits() as i64, ); perry_runtime::closure::js_closure_set_capture_ptr( closure, 5, - (if prevent_close { 1.0 } else { 0.0f64 }).to_bits() as i64, + (if state.prevent_close { 1.0 } else { 0.0f64 }).to_bits() as i64, + ); + perry_runtime::closure::js_closure_set_capture_ptr( + closure, + 6, + (if state.prevent_abort { 1.0 } else { 0.0f64 }).to_bits() as i64, + ); + perry_runtime::closure::js_closure_set_capture_ptr( + closure, + 7, + (if state.prevent_cancel { 1.0 } else { 0.0f64 }).to_bits() as i64, + ); + perry_runtime::closure::js_closure_set_capture_ptr(closure, 8, state.signal.to_bits() as i64); + perry_runtime::closure::js_closure_set_capture_ptr( + closure, + 9, + state.abort_listener.to_bits() as i64, ); + if let Some(reason) = reason { + perry_runtime::closure::js_closure_set_capture_ptr(closure, 10, reason as i64); + } closure } @@ -579,9 +816,13 @@ pub unsafe extern "C" fn js_readable_stream_pipe_to( let promise = js_promise_new(); let r_id = readable_handle as usize; let w_id = writable_handle as usize; - let prevent_close = pipe_option_truthy(options, b"preventClose"); - if pipe_signal_is_aborted(options) { - js_promise_reject(promise, perry_runtime::url::js_abort_error_value()); + let signal = pipe_option_value(options, b"signal"); + + if signal.to_bits() != TAG_UNDEFINED && pipe_signal_ptr(signal).is_none() { + reject_type_error( + promise, + "The options.signal property must be an AbortSignal", + ); return promise; } @@ -593,50 +834,90 @@ pub unsafe extern "C" fn js_readable_stream_pipe_to( } }; - let closure = - perry_runtime::closure::js_closure_alloc(readable_stream_pipe_to_microtask as *const u8, 6); + let mut state = PipeState { + locks, + prevent_close: pipe_option_truthy(options, b"preventClose"), + prevent_abort: pipe_option_truthy(options, b"preventAbort"), + prevent_cancel: pipe_option_truthy(options, b"preventCancel"), + signal, + abort_listener: f64::from_bits(TAG_UNDEFINED), + }; + + if let Some(signal_ptr) = pipe_signal_ptr(signal) { + let listener = pipe_closure( + readable_stream_pipe_to_aborted as *const u8, + r_id, + w_id, + promise, + state, + ); + perry_runtime::closure::js_register_closure_arity( + readable_stream_pipe_to_aborted as *const u8, + 0, + ); + state.abort_listener = f64::from_bits(JSValue::pointer(listener as *const u8).bits()); + perry_runtime::closure::js_closure_set_capture_ptr( + listener, + 9, + state.abort_listener.to_bits() as i64, + ); + let abort = js_string_from_bytes(b"abort".as_ptr(), 5); + let abort_value = f64::from_bits(JSValue::string_ptr(abort).bits()); + perry_runtime::url::js_abort_signal_add_listener( + signal_ptr, + abort_value, + state.abort_listener, + ); + if perry_runtime::url::js_abort_signal_is_aborted(signal_ptr) != 0 { + readable_stream_pipe_to_aborted(listener); + return promise; + } + } + + let closure = pipe_closure( + readable_stream_pipe_to_microtask as *const u8, + r_id, + w_id, + promise, + state, + ); perry_runtime::closure::js_register_closure_arity( readable_stream_pipe_to_microtask as *const u8, 0, ); - perry_runtime::closure::js_closure_set_capture_ptr(closure, 0, (r_id as f64).to_bits() as i64); - perry_runtime::closure::js_closure_set_capture_ptr(closure, 1, (w_id as f64).to_bits() as i64); - perry_runtime::closure::js_closure_set_capture_ptr( - closure, - 2, - box_promise(promise).to_bits() as i64, - ); - perry_runtime::closure::js_closure_set_capture_ptr( - closure, - 3, - (locks.reader_id as f64).to_bits() as i64, - ); - perry_runtime::closure::js_closure_set_capture_ptr( - closure, - 4, - (locks.writer_id as f64).to_bits() as i64, - ); - perry_runtime::closure::js_closure_set_capture_ptr( - closure, - 5, - (if prevent_close { 1.0 } else { 0.0f64 }).to_bits() as i64, - ); perry_runtime::builtins::js_queue_microtask(closure as i64); promise } -unsafe fn pipe_signal_is_aborted(options: f64) -> bool { - let signal = pipe_option_value(options, b"signal"); - let jsval = JSValue::from_bits(signal.to_bits()); - if !jsval.is_pointer() { - return false; +unsafe fn pipe_signal_ptr(signal: f64) -> Option<*mut ObjectHeader> { + let ptr = perry_runtime::url::js_abort_signal_resolve_ptr(signal); + (!ptr.is_null()).then_some(ptr) +} + +unsafe fn pipe_signal_reason(signal: f64) -> u64 { + let reason = perry_runtime::value::js_get_property(signal, b"reason".as_ptr() as i64, 6); + if reason.to_bits() == TAG_UNDEFINED { + perry_runtime::url::js_abort_error_value().to_bits() + } else { + reason.to_bits() } - let signal_ptr = js_nanbox_get_pointer(signal) as *mut ObjectHeader; - if signal_ptr.is_null() { - return false; +} + +unsafe fn cleanup_pipe_signal(state: PipeState) { + let Some(signal_ptr) = pipe_signal_ptr(state.signal) else { + return; + }; + if state.abort_listener.to_bits() == TAG_UNDEFINED { + return; } - perry_runtime::url::js_abort_signal_is_aborted(signal_ptr) != 0 + let abort = js_string_from_bytes(b"abort".as_ptr(), 5); + let abort_value = f64::from_bits(JSValue::string_ptr(abort).bits()); + perry_runtime::url::js_abort_signal_remove_listener( + signal_ptr, + abort_value, + state.abort_listener, + ); } unsafe fn pipe_option_truthy(options: f64, name: &[u8]) -> bool { @@ -647,3 +928,151 @@ unsafe fn pipe_option_truthy(options: f64, name: &[u8]) -> bool { unsafe fn pipe_option_value(options: f64, name: &[u8]) -> f64 { perry_runtime::value::js_get_property(options, name.as_ptr() as i64, name.len() as i64) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::streams::{alloc_readable, alloc_writable}; + + extern "C" fn pending_abort_action(closure: *const ClosureHeader, _reason: f64) -> f64 { + let promise = + perry_runtime::closure::js_closure_get_capture_ptr(closure, 0) as *mut Promise; + box_promise(promise) + } + + #[test] + fn source_error_respects_prevent_abort() { + let _serial = crate::streams::tests::serial_guard(); + for prevent_abort in [false, true] { + let readable = alloc_readable(0, 0, 0, 1.0); + let writable = alloc_writable(0, 0, 0, 1.0); + let locks = acquire_pipe_locks(readable, writable).unwrap(); + let promise = js_promise_new(); + let state = PipeState { + locks, + prevent_close: false, + prevent_abort, + prevent_cancel: false, + signal: f64::from_bits(TAG_UNDEFINED), + abort_listener: f64::from_bits(TAG_UNDEFINED), + }; + + unsafe { + abort_destination_and_reject(readable, writable, promise, state, TAG_UNDEFINED) + }; + + if prevent_abort { + assert_eq!(perry_runtime::promise::js_promise_state(promise), 2); + } else { + assert_eq!(perry_runtime::promise::js_promise_state(promise), 0); + assert_eq!( + READABLE_STREAMS + .lock() + .unwrap() + .get(&readable) + .unwrap() + .reader_handle, + Some(locks.reader_id) + ); + assert_eq!( + WRITABLE_STREAMS + .lock() + .unwrap() + .get(&writable) + .unwrap() + .writer_handle, + Some(locks.writer_id) + ); + perry_runtime::promise::js_promise_run_microtasks(); + assert_eq!(perry_runtime::promise::js_promise_state(promise), 2); + assert!(READABLE_STREAMS + .lock() + .unwrap() + .get(&readable) + .unwrap() + .reader_handle + .is_none()); + assert!(WRITABLE_STREAMS + .lock() + .unwrap() + .get(&writable) + .unwrap() + .writer_handle + .is_none()); + } + let writable_state = WRITABLE_STREAMS + .lock() + .unwrap() + .get(&writable) + .unwrap() + .state; + assert!(if prevent_abort { + writable_state == WritableState::Writable + } else { + writable_state == WritableState::Errored + }); + } + } + + #[test] + fn pipe_keeps_locks_until_async_abort_settles() { + let _serial = crate::streams::tests::serial_guard(); + let action = js_promise_new(); + let callback = + perry_runtime::closure::js_closure_alloc(pending_abort_action as *const u8, 1); + perry_runtime::closure::js_register_closure_arity(pending_abort_action as *const u8, 1); + perry_runtime::closure::js_closure_set_capture_ptr(callback, 0, action as i64); + let readable = alloc_readable(0, 0, 0, 1.0); + let writable = alloc_writable(0, 0, callback as i64, 1.0); + let locks = acquire_pipe_locks(readable, writable).unwrap(); + let promise = js_promise_new(); + let state = PipeState { + locks, + prevent_close: false, + prevent_abort: false, + prevent_cancel: false, + signal: f64::from_bits(TAG_UNDEFINED), + abort_listener: f64::from_bits(TAG_UNDEFINED), + }; + + unsafe { abort_destination_and_reject(readable, writable, promise, state, TAG_UNDEFINED) }; + perry_runtime::promise::js_promise_run_microtasks(); + assert_eq!(perry_runtime::promise::js_promise_state(promise), 0); + assert_eq!( + READABLE_STREAMS + .lock() + .unwrap() + .get(&readable) + .unwrap() + .reader_handle, + Some(locks.reader_id) + ); + assert_eq!( + WRITABLE_STREAMS + .lock() + .unwrap() + .get(&writable) + .unwrap() + .writer_handle, + Some(locks.writer_id) + ); + + js_promise_resolve(action, f64::from_bits(TAG_UNDEFINED)); + perry_runtime::promise::js_promise_run_microtasks(); + assert_eq!(perry_runtime::promise::js_promise_state(promise), 2); + assert!(READABLE_STREAMS + .lock() + .unwrap() + .get(&readable) + .unwrap() + .reader_handle + .is_none()); + assert!(WRITABLE_STREAMS + .lock() + .unwrap() + .get(&writable) + .unwrap() + .writer_handle + .is_none()); + } +} diff --git a/crates/perry-stdlib/src/streams/subclass.rs b/crates/perry-stdlib/src/streams/subclass.rs index a8cb3cc424..f19c405751 100644 --- a/crates/perry-stdlib/src/streams/subclass.rs +++ b/crates/perry-stdlib/src/streams/subclass.rs @@ -204,15 +204,28 @@ pub(crate) unsafe fn dispatch_stream_method( let transform = js_stream_unwrap_handle(arg0); let writable = js_transform_stream_writable(transform); let readable = js_transform_stream_readable(transform); - return Some(js_readable_stream_pipe_through(handle, writable, readable)); + let output = + js_readable_stream_pipe_through_validate(handle, writable, readable, arg1); + let pipe = js_readable_stream_pipe_to(handle, writable, arg1); + js_promise_mark_internally_handled(pipe); + return Some(output); } // #1644: a readable handle is also its own controller. The // start/transform/flush callbacks receive it as `controller`, so // `controller.enqueue/close/error/terminate` dispatch here when the - // controller param is generically typed. `terminate()` ends the - // readable side (TransformStreamDefaultController.terminate). + // controller param is generically typed. "enqueue" => return Some(js_readable_stream_controller_enqueue(handle, arg0)), - "close" | "terminate" => return Some(js_readable_stream_controller_close(handle)), + "close" => return Some(js_readable_stream_controller_close(handle)), + "terminate" => { + let result = js_readable_stream_controller_close(handle); + if let Some(writable_id) = transform_writable_for_readable(id) { + let reason = f64::from_bits(make_type_error_with_message( + "Invalid state: TransformStream has been terminated", + )); + let _ = js_writable_stream_abort_inner(writable_id as f64, reason, true); + } + return Some(result); + } "error" => return Some(js_readable_stream_controller_error(handle, arg0)), _ => return None, } @@ -257,6 +270,11 @@ pub(crate) unsafe fn dispatch_stream_property(handle: f64, name: &str) -> f64 { } (2, "locked") => return js_writable_stream_locked(handle), (3, "closed") => return box_promise(js_reader_closed(handle)), + (4, "closed") => return box_promise(js_writer_closed(handle)), + (4, "ready") => return box_promise(js_writer_ready(handle)), + (4, "desiredSize") => return js_writer_desired_size(handle), + (5, "readable") => return js_transform_stream_readable(handle), + (5, "writable") => return js_transform_stream_writable(handle), _ => {} } // #5437: expando properties stored via `stream.prop = v` (React's diff --git a/crates/perry-stdlib/src/streams/tests.rs b/crates/perry-stdlib/src/streams/tests.rs index 7d6b1ddd33..b25936081b 100644 --- a/crates/perry-stdlib/src/streams/tests.rs +++ b/crates/perry-stdlib/src/streams/tests.rs @@ -6,7 +6,7 @@ use super::*; /// interleave allocations between a retire and its recycle assertion. static ALLOCATOR_TEST_SERIAL: Mutex<()> = Mutex::new(()); -fn serial_guard() -> std::sync::MutexGuard<'static, ()> { +pub(super) fn serial_guard() -> std::sync::MutexGuard<'static, ()> { ALLOCATOR_TEST_SERIAL .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) @@ -214,3 +214,88 @@ fn utf8_split_prefix_tracks_incomplete_sequence() { assert_eq!(split_utf8_prefix(&[0xc3, 0xa9]).unwrap(), (2, false)); assert!(split_utf8_prefix(&[0xff]).is_err()); } + +#[test] +fn transform_terminate_closes_readable_and_errors_writable() { + let _serial = serial_guard(); + let undefined = f64::from_bits(TAG_UNDEFINED); + let transform = + unsafe { js_transform_stream_new(undefined, undefined, undefined, undefined, undefined) }; + let readable = unsafe { js_transform_stream_readable(transform) }; + let writable = unsafe { js_transform_stream_writable(transform) }; + + assert!(unsafe { dispatch_stream_method(readable, "terminate", &[]) }.is_some()); + assert!(matches!( + READABLE_STREAMS + .lock() + .unwrap() + .get(&(readable as usize)) + .unwrap() + .state, + ReadableState::Closed + )); + assert!(matches!( + WRITABLE_STREAMS + .lock() + .unwrap() + .get(&(writable as usize)) + .unwrap() + .state, + WritableState::Errored + )); +} + +#[test] +fn enqueue_rejects_closed_readable_controller() { + let _serial = serial_guard(); + let readable = alloc_closed_readable(); + + assert_eq!( + readable_controller_enqueue_error(readable), + Some("Invalid state: Controller is already closed") + ); +} + +#[test] +fn pipe_through_rejects_locked_endpoints_before_starting() { + let _serial = serial_guard(); + let source = alloc_readable(0, 0, 0, 1.0); + let output = alloc_readable(0, 0, 0, 1.0); + let destination = alloc_writable(0, 0, 0, 1.0); + + READABLE_STREAMS + .lock() + .unwrap() + .get_mut(&source) + .unwrap() + .reader_handle = Some(1); + assert_eq!( + pipe_through_validation_error(source, destination, output), + Some("ReadableStream is locked") + ); + + READABLE_STREAMS + .lock() + .unwrap() + .get_mut(&source) + .unwrap() + .reader_handle = None; + WRITABLE_STREAMS + .lock() + .unwrap() + .get_mut(&destination) + .unwrap() + .writer_handle = Some(2); + assert_eq!( + pipe_through_validation_error(source, destination, output), + Some("WritableStream is locked") + ); + + let invalid_signal = js_object_alloc(0, 0); + assert_eq!( + pipe_through_signal_error(f64::from_bits( + JSValue::object_ptr(invalid_signal as *mut u8).bits(), + )), + Some("The options.signal property must be an AbortSignal") + ); +} diff --git a/crates/perry-stdlib/src/streams/transform.rs b/crates/perry-stdlib/src/streams/transform.rs index e20851b4a7..2c8d1c85c4 100644 --- a/crates/perry-stdlib/src/streams/transform.rs +++ b/crates/perry-stdlib/src/streams/transform.rs @@ -25,7 +25,11 @@ pub unsafe extern "C" fn js_transform_stream_new( let transform_cb = closure_from_bits(transform_bits.to_bits()); let flush_cb = closure_from_bits(flush_bits.to_bits()); let writable = parse_strategy_value(writable_strategy); - let readable = parse_strategy_value(readable_strategy); + let readable = if readable_strategy.to_bits() == TAG_UNDEFINED { + (0.0, 0) + } else { + parse_strategy_value(readable_strategy) + }; alloc_transform_stream_with_strategies( start_cb, transform_cb, @@ -69,6 +73,7 @@ unsafe fn alloc_transform_stream_with_strategies( let mut g = READABLE_STREAMS.lock().unwrap(); if let Some(s) = g.get_mut(&readable_id) { s.started = true; + s.high_water_mark = if r_hwm.is_nan() { 0.0 } else { r_hwm.max(0.0) }; } } @@ -115,6 +120,7 @@ unsafe fn alloc_transform_stream_with_strategies( transform_cb, flush_cb, native, + backpressure: r_hwm <= 0.0, }, ); TRANSFORM_PAIRS.lock().unwrap().insert(writable_id, id); @@ -258,7 +264,16 @@ pub(super) unsafe fn transform_write(writable_id: usize, chunk: f64) -> *mut Pro .unwrap() .entry(writable_id) .or_insert(0) += 1; - perry_runtime::builtins::js_queue_microtask(job as i64); + if transform_initial_backpressure(readable_id) { + TRANSFORM_BACKPRESSURED_JOBS + .lock() + .unwrap() + .entry(readable_id) + .or_default() + .push(job as usize); + } else { + perry_runtime::builtins::js_queue_microtask(job as i64); + } promise } @@ -351,11 +366,12 @@ unsafe fn settle_transform_write(readable_id: usize, promise: *mut Promise) { Park, Resolve, } + let initial_backpressure = transform_initial_backpressure(readable_id); let settle = { let g = super::READABLE_STREAMS.lock().unwrap(); match g.get(&readable_id) { Some(s) if s.state == ReadableState::Errored => Settle::Errored(s.error_value), - Some(s) if !s.chunks.is_empty() => Settle::Park, + Some(s) if transform_has_backpressure(s, initial_backpressure) => Settle::Park, _ => Settle::Resolve, } }; @@ -383,6 +399,8 @@ lazy_static::lazy_static! { /// (async transformers count until their returned promise settles). static ref TRANSFORM_PENDING_WRITES: Mutex> = Mutex::new(HashMap::new()); + pub(super) static ref TRANSFORM_BACKPRESSURED_JOBS: Mutex>> = + Mutex::new(HashMap::new()); /// #6607: transform writable id -> close-request promise (as address) /// deferred until the pending write jobs above drain. pub(super) static ref TRANSFORM_PENDING_CLOSE: Mutex> = @@ -465,17 +483,27 @@ extern "C" fn transform_write_settle_rejected(closure: *const ClosureHeader, rea /// promises once its queue has drained (spec: the source pull algorithm sets /// backpressure = false). Called from the reader-read paths in `streams.rs`. pub(super) unsafe fn transform_release_writes(readable_id: usize) { - let (drained, errored) = { + set_transform_initial_backpressure(readable_id, false); + if let Some(jobs) = TRANSFORM_BACKPRESSURED_JOBS + .lock() + .unwrap() + .remove(&readable_id) + { + for job in jobs { + perry_runtime::builtins::js_queue_microtask(job as i64); + } + } + let (backpressured, errored) = { let g = super::READABLE_STREAMS.lock().unwrap(); match g.get(&readable_id) { Some(s) => ( - s.chunks.is_empty(), + transform_has_backpressure(s, false), (s.state == ReadableState::Errored).then_some(s.error_value), ), - None => (true, None), + None => (false, None), } }; - if !drained && errored.is_none() { + if backpressured && errored.is_none() { return; } let parked = TRANSFORM_WRITE_RELEASES @@ -492,6 +520,139 @@ pub(super) unsafe fn transform_release_writes(readable_id: usize) { } } +pub(super) unsafe fn transform_abort_pending_writes(writable_id: usize, reason: f64) { + let readable_id = TRANSFORM_PAIRS + .lock() + .unwrap() + .get(&writable_id) + .and_then(|transform_id| { + TRANSFORM_STREAMS + .lock() + .unwrap() + .get(transform_id) + .map(|stream| stream.readable_handle) + }); + let Some(readable_id) = readable_id else { + return; + }; + if let Some(jobs) = TRANSFORM_BACKPRESSURED_JOBS + .lock() + .unwrap() + .remove(&readable_id) + { + for job in jobs { + let closure = job as *const ClosureHeader; + let promise = + perry_runtime::closure::js_closure_get_capture_ptr(closure, 3) as *mut Promise; + js_promise_reject(promise, reason); + transform_write_job_done(writable_id); + } + } +} + +fn transform_has_backpressure(stream: &ReadableStreamData, initial: bool) -> bool { + initial || (!stream.chunks.is_empty() && stream.queue_total_size >= stream.high_water_mark) +} + +fn transform_initial_backpressure(readable_id: usize) -> bool { + TRANSFORM_STREAMS + .lock() + .unwrap() + .values() + .find_map(|stream| (stream.readable_handle == readable_id).then_some(stream.backpressure)) + .unwrap_or(false) +} + +fn set_transform_initial_backpressure(readable_id: usize, value: bool) { + if let Some(stream) = TRANSFORM_STREAMS + .lock() + .unwrap() + .values_mut() + .find(|stream| stream.readable_handle == readable_id) + { + stream.backpressure = value; + } +} + +#[cfg(test)] +mod backpressure_tests { + use super::*; + use std::sync::atomic::{AtomicUsize, Ordering}; + + static TRANSFORM_CALLS: AtomicUsize = AtomicUsize::new(0); + + extern "C" fn capture_transform( + _closure: *const ClosureHeader, + _chunk: f64, + _controller: f64, + ) -> f64 { + TRANSFORM_CALLS.fetch_add(1, Ordering::Relaxed); + f64::from_bits(TAG_UNDEFINED) + } + + #[test] + fn default_transform_starts_backpressured_until_read_demand() { + let _serial = crate::streams::tests::serial_guard(); + let undefined = f64::from_bits(TAG_UNDEFINED); + let transform = unsafe { + js_transform_stream_new(undefined, undefined, undefined, undefined, undefined) + }; + let readable = unsafe { js_transform_stream_readable(transform) } as usize; + + assert!(transform_initial_backpressure(readable)); + unsafe { transform_release_writes(readable) }; + assert!(!transform_initial_backpressure(readable)); + } + + #[test] + fn initial_backpressure_defers_transform_until_read_demand() { + let _serial = crate::streams::tests::serial_guard(); + TRANSFORM_CALLS.store(0, Ordering::Relaxed); + let transform = perry_runtime::closure::js_closure_alloc(capture_transform as *const u8, 0); + perry_runtime::closure::js_register_closure_arity(capture_transform as *const u8, 2); + let transform_value = f64::from_bits(JSValue::pointer(transform as *const u8).bits()); + let undefined = f64::from_bits(TAG_UNDEFINED); + let stream = unsafe { + js_transform_stream_new(undefined, transform_value, undefined, undefined, undefined) + }; + let readable = unsafe { js_transform_stream_readable(stream) } as usize; + let writable = unsafe { js_transform_stream_writable(stream) } as usize; + let write = unsafe { transform_write(writable, 1.0) }; + + perry_runtime::promise::js_promise_run_microtasks(); + assert_eq!(TRANSFORM_CALLS.load(Ordering::Relaxed), 0); + assert_eq!(perry_runtime::promise::js_promise_state(write), 0); + + unsafe { transform_release_writes(readable) }; + perry_runtime::promise::js_promise_run_microtasks(); + assert_eq!(TRANSFORM_CALLS.load(Ordering::Relaxed), 1); + assert_eq!(perry_runtime::promise::js_promise_state(write), 1); + } + + #[test] + fn abort_rejects_transform_waiting_for_read_demand() { + let _serial = crate::streams::tests::serial_guard(); + TRANSFORM_CALLS.store(0, Ordering::Relaxed); + let transform = perry_runtime::closure::js_closure_alloc(capture_transform as *const u8, 0); + perry_runtime::closure::js_register_closure_arity(capture_transform as *const u8, 2); + let transform_value = f64::from_bits(JSValue::pointer(transform as *const u8).bits()); + let undefined = f64::from_bits(TAG_UNDEFINED); + let stream = unsafe { + js_transform_stream_new(undefined, transform_value, undefined, undefined, undefined) + }; + let writable = unsafe { js_transform_stream_writable(stream) } as usize; + let write = unsafe { transform_write(writable, 1.0) }; + + unsafe { + super::writable::js_writable_stream_abort_inner(writable as f64, 7.0, true); + } + perry_runtime::promise::js_promise_run_microtasks(); + assert_eq!(TRANSFORM_CALLS.load(Ordering::Relaxed), 0); + assert_eq!(perry_runtime::promise::js_promise_state(write), 2); + assert_eq!(perry_runtime::promise::js_promise_reason(write), 7.0); + } +} + pub(super) unsafe fn transform_close(writable_id: usize) -> *mut Promise { // #6607 (WHATWG TransformStreamDefaultSinkCloseAlgorithm ordering): the // sink close runs only after queued writes complete. `transform_write` diff --git a/crates/perry-stdlib/src/streams/writable.rs b/crates/perry-stdlib/src/streams/writable.rs index 7cd185bd6c..e7476851d8 100644 --- a/crates/perry-stdlib/src/streams/writable.rs +++ b/crates/perry-stdlib/src/streams/writable.rs @@ -208,19 +208,34 @@ pub(super) unsafe fn js_writable_stream_abort_inner( reject_type_error(promise, "Invalid state: WritableStream is locked"); return promise; } - if cb != 0 { - js_closure_call1(cb as *const ClosureHeader, reason); - } + let action = if cb != 0 { + match try_call_stream_action(cb, reason) { + Ok(result) => stream_action_promise(result), + Err(error) => { + js_promise_reject(promise, f64::from_bits(error)); + None + } + } + } else { + None + }; if !closed_p.is_null() { js_promise_reject(closed_p, reason); } if !close_request.is_null() { js_promise_reject(close_request, reason); } + transform::transform_abort_pending_writes(id, reason); // #5437: writable stream aborted (terminal Errored) — drop expandos. super::expando::stream_expando_clear(id); super::idalloc::retire_writable_terminal(id); - js_promise_resolve(promise, f64::from_bits(TAG_UNDEFINED)); + if perry_runtime::promise::js_promise_state(promise) == 0 { + if let Some(action) = action { + settle_stream_action_promise(promise, &[action]); + } else { + js_promise_resolve(promise, f64::from_bits(TAG_UNDEFINED)); + } + } promise } @@ -258,6 +273,24 @@ fn writable_capture_promise(closure: *const ClosureHeader, idx: u32) -> *mut Pro perry_runtime::closure::js_closure_get_capture_ptr(closure, idx) as *mut Promise } +extern "C" fn writable_write_start_microtask(closure: *const ClosureHeader) -> f64 { + unsafe { + let stream_id = writable_capture_usize(closure, 0); + let writer_id = writable_capture_usize(closure, 1); + let cb = perry_runtime::closure::js_closure_get_capture_ptr(closure, 2); + let chunk_bits = perry_runtime::closure::js_closure_get_capture_ptr(closure, 3) as u64; + let write_promise = writable_capture_promise(closure, 4); + run_writable_write( + stream_id, + writer_id, + cb, + f64::from_bits(chunk_bits), + write_promise, + ); + } + f64::from_bits(TAG_UNDEFINED) +} + extern "C" fn writable_write_fulfilled(closure: *const ClosureHeader, _value: f64) -> f64 { unsafe { let stream_id = writable_capture_usize(closure, 0); @@ -570,15 +603,23 @@ pub(super) unsafe fn writable_stream_write( install_writable_backpressure_ready(stream_id, writer_id); } if let Some((cb, chunk, write_promise)) = start_write { - // Spec/Node tick parity: with no write in flight, `writer.write(chunk)` - // invokes the sink's `write()` SYNCHRONOUSLY in the same job - // (WritableStreamDefaultControllerProcessWrite) — deferring it through - // a microtask cost one tick per write, which let a tee sibling's - // reader outrun a pipeTo pump (teepipe.js; Next.js cold-start head - // reorder). All registry locks are released above; `run_writable_write` - // re-enters the FFI safely (it is the same body the queued microtask - // ran). - run_writable_write(stream_id, writer_id, cb, chunk, write_promise); + let job_fn = writable_write_start_microtask as *const u8; + perry_runtime::closure::js_register_closure_arity(job_fn, 0); + let job = perry_runtime::closure::js_closure_alloc(job_fn, 5); + perry_runtime::closure::js_closure_set_capture_ptr( + job, + 0, + (stream_id as f64).to_bits() as i64, + ); + perry_runtime::closure::js_closure_set_capture_ptr( + job, + 1, + (writer_id as f64).to_bits() as i64, + ); + perry_runtime::closure::js_closure_set_capture_ptr(job, 2, cb); + perry_runtime::closure::js_closure_set_capture_ptr(job, 3, chunk.to_bits() as i64); + perry_runtime::closure::js_closure_set_capture_ptr(job, 4, write_promise as i64); + perry_runtime::builtins::js_queue_microtask(job as i64); } promise } From efeec4060f6cff2150c752ea1e6bf45e8d6d69b5 Mon Sep 17 00:00:00 2001 From: TheHypnoo Date: Thu, 30 Jul 2026 12:40:25 +0200 Subject: [PATCH 3/8] test(parity): distinguish Node oracle failures --- run_parity_tests.sh | 89 +++++++++++++++++++++++++++++++++------------ 1 file changed, 65 insertions(+), 24 deletions(-) diff --git a/run_parity_tests.sh b/run_parity_tests.sh index a1e32864cc..17b121b281 100755 --- a/run_parity_tests.sh +++ b/run_parity_tests.sh @@ -146,7 +146,7 @@ else fi # Function to run with optional timeout -# Describe an abnormal Perry exit, or print nothing when the exit is normal. +# Describe an abnormal process exit, or print nothing when the exit is normal. # Bash reports a signal death as 128+signo; `timeout` reports 124. perry_abnormal_exit() { local code=$1 @@ -283,9 +283,9 @@ stop_tls_upgrade_server() { # net.createConnection(host, port) vs Node.js's (port, host)). For these, # instead of comparing to Node.js, we compare Perry's output against a # stored expected file in test-parity/expected/.txt. -# Node.js is still run; if it exits non-zero we record NODE_FAIL and skip; -# if it exits 0 but with a different output we fall through to the expected- -# file comparison (not a parity fail — the incompatibility is intentional). +# Node.js is still run; an ordinary non-zero exit remains a valid oracle result +# and is compared with Perry. Timeouts and signal deaths are NODE_FAIL because +# they do not produce a completed reference result. EXPECTED_DIR="$SCRIPT_DIR/test-parity/expected" EXPECTED_EXIT_DIR="$SCRIPT_DIR/test-parity/expected-exit" @@ -418,8 +418,16 @@ normalize_output() { local decoded decoded=$(printf '%s' "$input" | "$PYTHON_CMD" -c ' import sys +skip_unsettled_context = 0 for raw in sys.stdin: line = raw.rstrip("\n").rstrip("\r") + if skip_unsettled_context: + skip_unsettled_context -= 1 + continue + if line.startswith("Warning: Detected unsettled top-level await at "): + print("Warning: Detected unsettled top-level await") + skip_unsettled_context = 2 + continue if line.startswith(""): hex_part = line[len(" node_fail + skip above), so an - # abnormal exit here is unambiguously Perry's. + # #6271 zlib SIGSEGV hid behind a green-looking label. Node completed + # normally to reach this point, although an intentional uncaught throw may + # have produced an ordinary non-zero exit. # # Exception: a test carrying its own expected-output file may legitimately # assert a non-zero exit; only *signals*/timeouts are treated as crashes, @@ -1022,7 +1056,7 @@ for test_file in "${TEST_FILES[@]}"; do perry_crash=$(perry_abnormal_exit "$perry_exit") if [[ -n "$perry_crash" ]]; then echo -e "${RED}CRASH${NC} $test_id (${perry_crash})" - echo " Perry died after printing $(printf '%s' "$perry_output" | grep -c '' || true) line(s); Node exited 0." + echo " Perry died after printing $(printf '%s' "$perry_output" | grep -c '' || true) line(s); Node exited $node_exit." echo " Last Perry line: $(printf '%s' "$perry_output" | tail -1)" ((CRASH_FAIL++)) CRASH_FAILURES+=("$test_id") @@ -1057,9 +1091,14 @@ for test_file in "${TEST_FILES[@]}"; do # Normalize both outputs for comparison node_normalized=$(normalize_output "$node_output") perry_normalized=$(normalize_output "$perry_output") + if [[ "$node_exit" -ne 0 ]]; then + node_normalized=$(normalize_failure_output "$node_normalized") + perry_normalized=$(normalize_failure_output "$perry_normalized") + fi - # Compare outputs - if [[ "$node_normalized" == "$perry_normalized" ]]; then + # Exit status is observable behavior too. Comparing only output let a + # swallowed throw pass whenever both sides printed the same prefix. + if [[ "$node_exit" == "$perry_exit" && "$node_normalized" == "$perry_normalized" ]]; then echo -e "${GREEN}PASS${NC} $test_id" ((PARITY_PASS++)) status="pass" @@ -1070,6 +1109,8 @@ for test_file in "${TEST_FILES[@]}"; do status="parity_fail" # Show diff for failures (first few lines) + echo " Node exit: $node_exit" + echo " Perry exit: $perry_exit" echo " Node.js: $(echo "$node_output" | head -1)" echo " Perry: $(echo "$perry_output" | head -1)" fi From 50761c5437e8a8f52206c719a846b1ef5a96acc6 Mon Sep 17 00:00:00 2001 From: TheHypnoo Date: Thu, 30 Jul 2026 12:40:44 +0200 Subject: [PATCH 4/8] test(stream): settle cancel-during-pipeto fixture --- .../node-suite/stream/web/cancel-during-pipeto.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/test-parity/node-suite/stream/web/cancel-during-pipeto.ts b/test-parity/node-suite/stream/web/cancel-during-pipeto.ts index 813dc3191f..7bdec19f49 100644 --- a/test-parity/node-suite/stream/web/cancel-during-pipeto.ts +++ b/test-parity/node-suite/stream/web/cancel-during-pipeto.ts @@ -1,7 +1,11 @@ import { ReadableStream, WritableStream } from "node:stream/web"; -// rs.cancel() during an active pipeTo — the pipeTo promise rejects (locked stream). +// rs.cancel() during an active pipeTo rejects because the readable is locked. const rs = new ReadableStream({ - pull(c) { setTimeout(() => c.enqueue("x"), 50); }, + async pull(c) { + await new Promise((resolve) => setTimeout(resolve, 50)); + c.enqueue("x"); + c.close(); + }, }); const ws = new WritableStream({ write() {} }); const p = rs.pipeTo(ws); @@ -13,5 +17,4 @@ try { cancelErr = e && e.name; } console.log("cancel-on-locked rejected:", cancelErr); -// pipeTo will likely still be pending -p.catch(() => {}); +await p; From aa326b2ac585424c7fb97c1e9ce2b1ccea3aaa20 Mon Sep 17 00:00:00 2001 From: TheHypnoo Date: Thu, 30 Jul 2026 16:20:38 +0200 Subject: [PATCH 5/8] fix(stream): address review findings --- .../src/lower_call/options/fetch.rs | 4 +- .../src/runtime_decls/strings_part2.rs | 1 + crates/perry-runtime/src/array/iterator.rs | 11 +- crates/perry-runtime/src/array/mod.rs | 2 +- .../tests/runtime_roots/callback_scanners.rs | 7 +- .../src/node_stream/async_iterator.rs | 103 +++-- .../src/node_stream_iter_helpers.rs | 228 +++++++--- .../perry-runtime/src/node_stream_pipeline.rs | 394 ++++++++++++------ .../src/node_stream_readwrite.rs | 2 +- .../src/node_stream_tests_extra.rs | 102 +++-- .../perry-runtime/src/node_submodules/mod.rs | 18 +- .../perry-runtime/src/promise/async_step.rs | 2 +- .../src/generator/per_iteration.rs | 23 + 13 files changed, 623 insertions(+), 274 deletions(-) diff --git a/crates/perry-codegen/src/lower_call/options/fetch.rs b/crates/perry-codegen/src/lower_call/options/fetch.rs index e4384c0355..9c2eaa2f8a 100644 --- a/crates/perry-codegen/src/lower_call/options/fetch.rs +++ b/crates/perry-codegen/src/lower_call/options/fetch.rs @@ -976,7 +976,7 @@ pub(in crate::lower_call) fn lower_fetch_native_method( (DOUBLE, &options), ], ); - let _ = ctx.block().call( + let pipe = ctx.block().call( I64, "js_readable_stream_pipe_to", &[ @@ -985,6 +985,8 @@ pub(in crate::lower_call) fn lower_fetch_native_method( (DOUBLE, &options), ], ); + ctx.block() + .call_void("js_promise_mark_internally_handled", &[(I64, &pipe)]); return Ok(Some(readable)); } "locked" => { diff --git a/crates/perry-codegen/src/runtime_decls/strings_part2.rs b/crates/perry-codegen/src/runtime_decls/strings_part2.rs index f8494699d8..6fd8e9d4f5 100644 --- a/crates/perry-codegen/src/runtime_decls/strings_part2.rs +++ b/crates/perry-codegen/src/runtime_decls/strings_part2.rs @@ -879,6 +879,7 @@ pub(crate) fn declare_phase_b_strings_part2(module: &mut LlModule) { ); module.declare_function("js_promise_resolve", VOID, &[I64, DOUBLE]); module.declare_function("js_promise_reject", VOID, &[I64, DOUBLE]); + module.declare_function("js_promise_mark_internally_handled", VOID, &[I64]); module.declare_function("js_promise_resolved", I64, &[DOUBLE]); module.declare_function("js_async_fn_result", I64, &[DOUBLE]); module.declare_function("js_promise_rejected", I64, &[DOUBLE]); diff --git a/crates/perry-runtime/src/array/iterator.rs b/crates/perry-runtime/src/array/iterator.rs index 140ad2deef..31388a7394 100644 --- a/crates/perry-runtime/src/array/iterator.rs +++ b/crates/perry-runtime/src/array/iterator.rs @@ -988,12 +988,9 @@ pub extern "C" fn js_iterator_close_if_not_done(iter_f64: f64, done_f64: f64) -> f64::from_bits(crate::value::TAG_UNDEFINED) } -/// Issue #1572 — node:stream uses this from `node_stream::ns_iter_flat_map` -/// to drive an async-iterable mapper result (an `async function*` return -/// value) without re-deriving the `Symbol.asyncIterator` lookup + -/// implicit-this dance. -pub(crate) fn call_symbol_async_iterator_for_flat_map(value: f64) -> Option { - call_symbol_async_iterator(value) +/// Resolve `Symbol.asyncIterator` and invoke it with the iterable as `this`. +pub(crate) fn call_symbol_async_iterator(value: f64) -> Option { + call_symbol_async_iterator_impl(value) } /// Issue #1572 — same as `js_async_iterator_to_array` but reachable from @@ -1076,7 +1073,7 @@ pub(crate) fn sync_iterator_to_array_if_not_async(iter_f64: f64) -> Option<*mut Some(result) } -fn call_symbol_async_iterator(value: f64) -> Option { +fn call_symbol_async_iterator_impl(value: f64) -> Option { let sym = crate::symbol::well_known_symbol("asyncIterator"); if sym.is_null() { return None; diff --git a/crates/perry-runtime/src/array/mod.rs b/crates/perry-runtime/src/array/mod.rs index b18c5e8c08..2e9dc01552 100644 --- a/crates/perry-runtime/src/array/mod.rs +++ b/crates/perry-runtime/src/array/mod.rs @@ -126,7 +126,7 @@ pub use self::subclass::{ // protocol instead of being appended as a single chunk. pub(crate) use self::iterator::{ async_from_sync_wrap_iterator, async_iterator_to_array_for_flat_map, - call_symbol_async_iterator_for_flat_map, entries_array_for_small_handle_id, has_iterator_next, + call_symbol_async_iterator, entries_array_for_small_handle_id, has_iterator_next, sync_iterator_to_array_if_not_async, }; pub use self::jsvalue_api::{ diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/callback_scanners.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/callback_scanners.rs index c114c08f70..462d5cab18 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/callback_scanners.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/callback_scanners.rs @@ -1205,7 +1205,12 @@ fn test_gc_init_mutable_scanner_families_rewrite_runtime_slots() { ); assert_eq!( crate::node_submodules::test_node_submodule_roots(), - (fixture.old_addr(), fixture.old_addr(), fixture.old_addr()) + ( + fixture.old_addr(), + fixture.old_addr(), + fixture.old_addr(), + fixture.old_bits, + ) ); assert_eq!( crate::os::test_process_event_listener_root_snapshot(), diff --git a/crates/perry-runtime/src/node_stream/async_iterator.rs b/crates/perry-runtime/src/node_stream/async_iterator.rs index 95aa03b458..5bec16a018 100644 --- a/crates/perry-runtime/src/node_stream/async_iterator.rs +++ b/crates/perry-runtime/src/node_stream/async_iterator.rs @@ -323,7 +323,11 @@ extern "C" fn ns_readable_iter_on_data(closure: *const ClosureHeader, chunk: f64 iterator_enqueue(iterator, chunk); } if let Some(stream) = get_hidden_value(iterator, hidden_key(READABLE_ITERATOR_STREAM_KEY)) { - pause_readable_stream(stream); + if iterator_has_pending(iterator) { + resume_readable_stream(stream); + } else { + pause_readable_stream(stream); + } } f64::from_bits(TAG_UNDEFINED) } @@ -511,21 +515,28 @@ pub(super) fn call_source_iterator_return(stream: f64) { } extern "C" fn ns_readable_iterator_next(closure: *const ClosureHeader) -> f64 { - let iterator = this_value(closure); - if iterator_is_done(iterator) { + let scope = crate::gc::RuntimeHandleScope::new(); + let iterator = scope.root_nanbox_f64(this_value(closure)); + if iterator_is_done(iterator.get_nanbox_f64()) { return readable_iterator_done(); } - let Some(stream) = get_hidden_value(iterator, hidden_key(READABLE_ITERATOR_STREAM_KEY)) else { + let Some(stream) = get_hidden_value( + iterator.get_nanbox_f64(), + hidden_key(READABLE_ITERATOR_STREAM_KEY), + ) else { return readable_iterator_done(); }; - - if !readable_chunks_nonempty(stream) { - if let Some(source_iterator) = - get_hidden_value(stream, hidden_key(READABLE_SOURCE_ITERATOR_KEY)) - { + let stream = scope.root_nanbox_f64(stream); + + if !readable_chunks_nonempty(stream.get_nanbox_f64()) { + if let Some(source_iterator) = get_hidden_value( + stream.get_nanbox_f64(), + hidden_key(READABLE_SOURCE_ITERATOR_KEY), + ) { + let source_iterator = scope.root_nanbox_f64(source_iterator); let next = match catch_pipeline_throw(|| unsafe { crate::object::js_native_call_method( - source_iterator, + source_iterator.get_nanbox_f64(), b"next".as_ptr() as *const i8, 4, std::ptr::null(), @@ -534,57 +545,65 @@ extern "C" fn ns_readable_iterator_next(closure: *const ClosureHeader) -> f64 { }) { Ok(next) => next, Err(reason) => { - iterator_mark_done(iterator); - call_source_iterator_return(stream); - destroy_stream(stream, reason); - return rejected_promise(reason); + let reason = scope.root_nanbox_f64(reason); + iterator_mark_done(iterator.get_nanbox_f64()); + call_source_iterator_return(stream.get_nanbox_f64()); + destroy_stream(stream.get_nanbox_f64(), reason.get_nanbox_f64()); + return rejected_promise(reason.get_nanbox_f64()); } }; - let promise = if crate::promise::js_value_is_promise(next) != 0 { - crate::value::js_nanbox_get_pointer(next) as *mut crate::promise::Promise + let next = scope.root_nanbox_f64(next); + let promise = if crate::promise::js_value_is_promise(next.get_nanbox_f64()) != 0 { + crate::value::js_nanbox_get_pointer(next.get_nanbox_f64()) + as *mut crate::promise::Promise } else { - crate::promise::js_promise_resolved(next) + crate::promise::js_promise_resolved(next.get_nanbox_f64()) }; + let promise = scope.root_raw_mut_ptr(promise); let fulfilled = js_closure_alloc(ns_readable_source_iterator_fulfilled as *const u8, 1); + let fulfilled = scope.root_raw_mut_ptr(fulfilled); let rejected = js_closure_alloc(ns_readable_source_iterator_rejected as *const u8, 1); - js_closure_set_capture_f64(fulfilled, 0, iterator); - js_closure_set_capture_f64(rejected, 0, iterator); - return box_pointer( - crate::promise::js_promise_then(promise, fulfilled, rejected) as *const u8, - ); + let rejected = scope.root_raw_mut_ptr(rejected); + js_closure_set_capture_f64(fulfilled.get_raw_mut_ptr(), 0, iterator.get_nanbox_f64()); + js_closure_set_capture_f64(rejected.get_raw_mut_ptr(), 0, iterator.get_nanbox_f64()); + return box_pointer(crate::promise::js_promise_then( + promise.get_raw_mut_ptr(), + fulfilled.get_raw_mut_ptr(), + rejected.get_raw_mut_ptr(), + ) as *const u8); } } // First pull: attach persistent listeners + start flow. Listeners deliver // asynchronously (resume schedules microtasks), so nothing arrives // synchronously here — no event-loop re-entrancy. - iterator_ensure_attached(iterator, stream); + iterator_ensure_attached(iterator.get_nanbox_f64(), stream.get_nanbox_f64()); // A chunk is already buffered → resolve immediately. - if let Some(chunk) = iterator_dequeue(iterator) { - note_yield(iterator); + if let Some(chunk) = iterator_dequeue(iterator.get_nanbox_f64()) { + note_yield(iterator.get_nanbox_f64()); return readable_iterator_chunk_result(chunk); } - if !readable_chunks_nonempty(stream) { - if let Some(err) = readable_hidden_error(stream) { - iterator_mark_done(iterator); - iterator_remove_listeners(iterator); + if !readable_chunks_nonempty(stream.get_nanbox_f64()) { + if let Some(err) = readable_hidden_error(stream.get_nanbox_f64()) { + iterator_mark_done(iterator.get_nanbox_f64()); + iterator_remove_listeners(iterator.get_nanbox_f64()); return rejected_promise(err); } } // A stored error surfaces (once) as a rejection, then the iterator is done. - if let Some(err) = iterator_stored_error(iterator) { - iterator_mark_done(iterator); - iterator_remove_listeners(iterator); + if let Some(err) = iterator_stored_error(iterator.get_nanbox_f64()) { + iterator_mark_done(iterator.get_nanbox_f64()); + iterator_remove_listeners(iterator.get_nanbox_f64()); return rejected_promise(err); } // The stream has ended and the queue is drained → done. - if iterator_stream_ended(iterator) { - iterator_mark_done(iterator); - iterator_remove_listeners(iterator); + if iterator_stream_ended(iterator.get_nanbox_f64()) { + iterator_mark_done(iterator.get_nanbox_f64()); + iterator_remove_listeners(iterator.get_nanbox_f64()); return readable_iterator_done(); } @@ -592,9 +611,10 @@ extern "C" fn ns_readable_iterator_next(closure: *const ClosureHeader) -> f64 { // `data`/`end`/`error` event settles. Concurrent `next()` calls each enqueue // their own promise (FIFO) — none is overwritten or dropped. let promise = crate::promise::js_promise_new(); - iterator_push_pending(iterator, promise); - resume_readable_stream(stream); - box_pointer(promise as *const u8) + let promise = scope.root_raw_mut_ptr(promise); + iterator_push_pending(iterator.get_nanbox_f64(), promise.get_raw_mut_ptr()); + resume_readable_stream(stream.get_nanbox_f64()); + box_pointer(promise.get_raw_const_ptr()) } extern "C" fn ns_readable_iterator_return(closure: *const ClosureHeader) -> f64 { @@ -775,7 +795,8 @@ mod fifo_pending_tests { #[test] fn pending_pulls_settle_in_fifo_order_on_data() { - let iterator = new_iterator(); + let stream = readable_from_chunks(crate::array::js_array_alloc(0)); + let iterator = build_readable_async_iterator(stream, true); let p1 = crate::promise::js_promise_new(); let p2 = crate::promise::js_promise_new(); // Two `next()` pulls made while the queue is empty: both must be @@ -787,9 +808,11 @@ mod fifo_pending_tests { let data_cb = js_closure_alloc(ns_readable_iter_on_data as *const u8, 1); js_closure_set_capture_f64(data_cb, 0, iterator); - // Two data events resolve p1 then p2 in FIFO order. + set_readable_flowing(stream, f64::from_bits(TAG_TRUE)); ns_readable_iter_on_data(data_cb, 1.0); + assert!(readable_is_flowing(stream)); ns_readable_iter_on_data(data_cb, 2.0); + assert!(readable_is_paused(stream)); assert_eq!( unsafe { (*p1).state }, diff --git a/crates/perry-runtime/src/node_stream_iter_helpers.rs b/crates/perry-runtime/src/node_stream_iter_helpers.rs index 9eefa1a74b..d6d81e3a92 100644 --- a/crates/perry-runtime/src/node_stream_iter_helpers.rs +++ b/crates/perry-runtime/src/node_stream_iter_helpers.rs @@ -661,7 +661,7 @@ fn consume_stream(stream: f64, callback: f64, op: f64, initial: f64, opts: f64) consume_op_default(op) }; - let Some(iter) = crate::array::call_symbol_async_iterator_for_flat_map(stream) else { + let Some(iter) = crate::array::call_symbol_async_iterator(stream) else { // Not async-iterable (shouldn't happen for a readable): empty result. if op == CONSUME_OP_REDUCE && !has_initial { return rejected_promise(reduce_missing_initial_error()); @@ -876,11 +876,10 @@ pub(super) fn flatten_async_iterable_with_source( value: f64, ) -> Option<(*mut crate::array::ArrayHeader, Option)> { use crate::array::{ - async_iterator_to_array_for_flat_map, call_symbol_async_iterator_for_flat_map, - has_iterator_next, + async_iterator_to_array_for_flat_map, call_symbol_async_iterator, has_iterator_next, }; use crate::symbol::js_get_iterator; - if let Some(async_iter) = call_symbol_async_iterator_for_flat_map(value) { + if let Some(async_iter) = call_symbol_async_iterator(value) { return Some(( async_iterator_to_array_for_flat_map(async_iter), Some(async_iter), @@ -921,41 +920,63 @@ fn take_source_done_result() -> f64 { } fn finish_take_source(iterator: f64, reason: Option, close_source: bool) { - if has_truthy_hidden(iterator, hidden_key(TAKE_SOURCE_DONE_KEY)) { + let scope = crate::gc::RuntimeHandleScope::new(); + let iterator = scope.root_nanbox_f64(iterator); + let reason = reason.map(|reason| scope.root_nanbox_f64(reason)); + let iterator_value = iterator.get_nanbox_f64(); + if has_truthy_hidden(iterator_value, hidden_key(TAKE_SOURCE_DONE_KEY)) { return; } set_hidden_value( - iterator, + iterator_value, hidden_key(TAKE_SOURCE_DONE_KEY), f64::from_bits(TAG_TRUE), ); - let Some(source) = get_hidden_value(iterator, hidden_key(TAKE_SOURCE_STREAM_KEY)) else { + let Some(source) = get_hidden_value(iterator_value, hidden_key(TAKE_SOURCE_STREAM_KEY)) else { return; }; + let source = scope.root_nanbox_f64(source); if close_source { - async_iterator::call_source_iterator_return(source); + async_iterator::call_source_iterator_return(source.get_nanbox_f64()); } - let terminal = reason.unwrap_or_else(|| f64::from_bits(TAG_UNDEFINED)); - destroy_stream(source, terminal); - if let Some(result) = get_hidden_value(iterator, hidden_key(TAKE_RESULT_STREAM_KEY)) { - destroy_stream(result, terminal); + let terminal = reason + .as_ref() + .map(|reason| reason.get_nanbox_f64()) + .unwrap_or_else(|| f64::from_bits(TAG_UNDEFINED)); + destroy_stream(source.get_nanbox_f64(), terminal); + if let Some(result) = get_hidden_value( + iterator.get_nanbox_f64(), + hidden_key(TAKE_RESULT_STREAM_KEY), + ) { + let result = scope.root_nanbox_f64(result); + let terminal = reason + .as_ref() + .map(|reason| reason.get_nanbox_f64()) + .unwrap_or_else(|| f64::from_bits(TAG_UNDEFINED)); + destroy_stream(result.get_nanbox_f64(), terminal); } } fn take_source_fulfilled(iterator: f64, result: f64) -> f64 { - match pipeline_iterator_result(result) { + let scope = crate::gc::RuntimeHandleScope::new(); + let iterator = scope.root_nanbox_f64(iterator); + let result = scope.root_nanbox_f64(result); + match pipeline_iterator_result(result.get_nanbox_f64()) { Some((false, _)) => { - let remaining = - get_hidden_value(iterator, hidden_key(TAKE_SOURCE_REMAINING_KEY)).unwrap_or(0.0); + let remaining = get_hidden_value( + iterator.get_nanbox_f64(), + hidden_key(TAKE_SOURCE_REMAINING_KEY), + ) + .unwrap_or(0.0); set_hidden_value( - iterator, + iterator.get_nanbox_f64(), hidden_key(TAKE_SOURCE_REMAINING_KEY), (remaining - 1.0).max(0.0), ); } - _ => finish_take_source(iterator, None, false), + _ => finish_take_source(iterator.get_nanbox_f64(), None, false), } - result + result.get_nanbox_f64() } pub(super) extern "C" fn ns_take_source_fulfilled( @@ -969,8 +990,14 @@ pub(super) extern "C" fn ns_take_source_rejected( closure: *const ClosureHeader, reason: f64, ) -> f64 { - finish_take_source(js_closure_get_capture_f64(closure, 0), Some(reason), true); - rejected_promise(reason) + let scope = crate::gc::RuntimeHandleScope::new(); + let reason = scope.root_nanbox_f64(reason); + finish_take_source( + js_closure_get_capture_f64(closure, 0), + Some(reason.get_nanbox_f64()), + true, + ); + rejected_promise(reason.get_nanbox_f64()) } pub(super) extern "C" fn ns_take_limit_fulfilled( @@ -992,24 +1019,35 @@ pub(super) extern "C" fn ns_take_limit_rejected( } pub(super) extern "C" fn ns_take_source_next(closure: *const ClosureHeader) -> f64 { - let iterator = this_value(closure); - if has_truthy_hidden(iterator, hidden_key(TAKE_SOURCE_DONE_KEY)) { + let scope = crate::gc::RuntimeHandleScope::new(); + let iterator = scope.root_nanbox_f64(this_value(closure)); + if has_truthy_hidden(iterator.get_nanbox_f64(), hidden_key(TAKE_SOURCE_DONE_KEY)) { return take_source_done_result(); } - let remaining = - get_hidden_value(iterator, hidden_key(TAKE_SOURCE_REMAINING_KEY)).unwrap_or(0.0); - let Some(source) = get_hidden_value(iterator, hidden_key(TAKE_SOURCE_STREAM_KEY)) else { - finish_take_source(iterator, None, false); + let remaining = get_hidden_value( + iterator.get_nanbox_f64(), + hidden_key(TAKE_SOURCE_REMAINING_KEY), + ) + .unwrap_or(0.0); + let Some(source) = get_hidden_value( + iterator.get_nanbox_f64(), + hidden_key(TAKE_SOURCE_STREAM_KEY), + ) else { + finish_take_source(iterator.get_nanbox_f64(), None, false); return take_source_done_result(); }; - let Some(source_iterator) = get_hidden_value(source, hidden_key(READABLE_SOURCE_ITERATOR_KEY)) - else { - finish_take_source(iterator, None, false); + let source = scope.root_nanbox_f64(source); + let Some(source_iterator) = get_hidden_value( + source.get_nanbox_f64(), + hidden_key(READABLE_SOURCE_ITERATOR_KEY), + ) else { + finish_take_source(iterator.get_nanbox_f64(), None, false); return take_source_done_result(); }; + let source_iterator = scope.root_nanbox_f64(source_iterator); let next = match catch_pipeline_throw(|| unsafe { crate::object::js_native_call_method( - source_iterator, + source_iterator.get_nanbox_f64(), b"next".as_ptr() as *const i8, 4, std::ptr::null(), @@ -1018,38 +1056,59 @@ pub(super) extern "C" fn ns_take_source_next(closure: *const ClosureHeader) -> f }) { Ok(next) => next, Err(reason) => { + let reason = scope.root_nanbox_f64(reason); if remaining <= 0.0 { - finish_take_source(iterator, None, true); + finish_take_source(iterator.get_nanbox_f64(), None, true); return take_source_done_result(); } - finish_take_source(iterator, Some(reason), true); - return rejected_promise(reason); + finish_take_source( + iterator.get_nanbox_f64(), + Some(reason.get_nanbox_f64()), + true, + ); + return rejected_promise(reason.get_nanbox_f64()); } }; + let next = scope.root_nanbox_f64(next); if remaining <= 0.0 { - if crate::promise::js_value_is_promise(next) == 0 { - let close_source = matches!(pipeline_iterator_result(next), Some((false, _))); - finish_take_source(iterator, None, close_source); + if crate::promise::js_value_is_promise(next.get_nanbox_f64()) == 0 { + let close_source = matches!( + pipeline_iterator_result(next.get_nanbox_f64()), + Some((false, _)) + ); + finish_take_source(iterator.get_nanbox_f64(), None, close_source); return take_source_done_result(); } - let promise = crate::value::js_nanbox_get_pointer(next) as *mut crate::promise::Promise; let fulfilled = js_closure_alloc(ns_take_limit_fulfilled as *const u8, 1); + let fulfilled = scope.root_raw_mut_ptr(fulfilled); let rejected = js_closure_alloc(ns_take_limit_rejected as *const u8, 1); - js_closure_set_capture_f64(fulfilled, 0, iterator); - js_closure_set_capture_f64(rejected, 0, iterator); - return box_pointer( - crate::promise::js_promise_then(promise, fulfilled, rejected) as *const u8, - ); + let rejected = scope.root_raw_mut_ptr(rejected); + js_closure_set_capture_f64(fulfilled.get_raw_mut_ptr(), 0, iterator.get_nanbox_f64()); + js_closure_set_capture_f64(rejected.get_raw_mut_ptr(), 0, iterator.get_nanbox_f64()); + let promise = crate::value::js_nanbox_get_pointer(next.get_nanbox_f64()) + as *mut crate::promise::Promise; + return box_pointer(crate::promise::js_promise_then( + promise, + fulfilled.get_raw_mut_ptr(), + rejected.get_raw_mut_ptr(), + ) as *const u8); } - if crate::promise::js_value_is_promise(next) == 0 { - return take_source_fulfilled(iterator, next); + if crate::promise::js_value_is_promise(next.get_nanbox_f64()) == 0 { + return take_source_fulfilled(iterator.get_nanbox_f64(), next.get_nanbox_f64()); } - let promise = crate::value::js_nanbox_get_pointer(next) as *mut crate::promise::Promise; let fulfilled = js_closure_alloc(ns_take_source_fulfilled as *const u8, 1); + let fulfilled = scope.root_raw_mut_ptr(fulfilled); let rejected = js_closure_alloc(ns_take_source_rejected as *const u8, 1); - js_closure_set_capture_f64(fulfilled, 0, iterator); - js_closure_set_capture_f64(rejected, 0, iterator); - box_pointer(crate::promise::js_promise_then(promise, fulfilled, rejected) as *const u8) + let rejected = scope.root_raw_mut_ptr(rejected); + js_closure_set_capture_f64(fulfilled.get_raw_mut_ptr(), 0, iterator.get_nanbox_f64()); + js_closure_set_capture_f64(rejected.get_raw_mut_ptr(), 0, iterator.get_nanbox_f64()); + let promise = + crate::value::js_nanbox_get_pointer(next.get_nanbox_f64()) as *mut crate::promise::Promise; + box_pointer(crate::promise::js_promise_then( + promise, + fulfilled.get_raw_mut_ptr(), + rejected.get_raw_mut_ptr(), + ) as *const u8) } pub(super) extern "C" fn ns_take_source_return(closure: *const ClosureHeader) -> f64 { @@ -1058,13 +1117,24 @@ pub(super) extern "C" fn ns_take_source_return(closure: *const ClosureHeader) -> } fn take_source_iterator(source: f64, result: f64, count: u32) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let source = scope.root_nanbox_f64(source); + let result = scope.root_nanbox_f64(result); let methods = [ ("next", cast0(ns_take_source_next)), ("return", cast0(ns_take_source_return)), ]; let iterator = box_pointer(build_object(&methods, 0x7FFF_FF70) as *const u8); - set_hidden_value(iterator, hidden_key(TAKE_SOURCE_STREAM_KEY), source); - set_hidden_value(iterator, hidden_key(TAKE_RESULT_STREAM_KEY), result); + set_hidden_value( + iterator, + hidden_key(TAKE_SOURCE_STREAM_KEY), + source.get_nanbox_f64(), + ); + set_hidden_value( + iterator, + hidden_key(TAKE_RESULT_STREAM_KEY), + result.get_nanbox_f64(), + ); set_hidden_value( iterator, hidden_key(TAKE_SOURCE_REMAINING_KEY), @@ -1074,22 +1144,37 @@ fn take_source_iterator(source: f64, result: f64, count: u32) -> f64 { } pub(super) extern "C" fn ns_iter_take(closure: *const ClosureHeader, count: f64) -> f64 { - let this = this_value(closure); - if !readable_chunks_nonempty(this) { - if let Some(_source_iterator) = - get_hidden_value(this, hidden_key(READABLE_SOURCE_ITERATOR_KEY)) + let scope = crate::gc::RuntimeHandleScope::new(); + let this = scope.root_nanbox_f64(this_value(closure)); + if !readable_chunks_nonempty(this.get_nanbox_f64()) { + if get_hidden_value( + this.get_nanbox_f64(), + hidden_key(READABLE_SOURCE_ITERATOR_KEY), + ) + .is_some() { let result = readable_from_chunks(crate::array::js_array_alloc(0)); + let result = scope.root_nanbox_f64(result); + propagate_stream_state( + this.get_nanbox_f64(), + f64::from_bits(TAG_UNDEFINED), + result.get_nanbox_f64(), + ); + let iterator = take_source_iterator( + this.get_nanbox_f64(), + result.get_nanbox_f64(), + count_arg(count), + ); set_hidden_value( - result, + result.get_nanbox_f64(), hidden_key(READABLE_SOURCE_ITERATOR_KEY), - take_source_iterator(this, result, count_arg(count)), + iterator, ); - return result; + return result.get_nanbox_f64(); } } - prepare_readable_for_iteration(this); - let arr = readable_chunks_array(this); + prepare_readable_for_iteration(this.get_nanbox_f64()); + let arr = readable_chunks_array(this.get_nanbox_f64()); let mut out = crate::array::js_array_alloc(0); if !arr.is_null() { let len = crate::array::js_array_length(arr); @@ -1099,7 +1184,7 @@ pub(super) extern "C" fn ns_iter_take(closure: *const ClosureHeader, count: f64) } } let result = readable_from_chunks(out); - propagate_stream_state(this, f64::from_bits(TAG_UNDEFINED), result); + propagate_stream_state(this.get_nanbox_f64(), f64::from_bits(TAG_UNDEFINED), result); result } @@ -1216,6 +1301,29 @@ mod take_tests { assert!(stream_destroyed(result)); } + #[test] + fn take_propagates_retained_source_state() { + let source = box_pointer(build_object( + &[ + ("next", cast0(source_next)), + ("return", cast0(source_return)), + ], + 0x7FFF_FF75, + ) as *const u8); + let stream = + js_node_stream_readable_from(box_pointer(crate::array::js_array_alloc(0) as *const u8)); + set_hidden_value(stream, hidden_key(READABLE_SOURCE_ITERATOR_KEY), source); + set_hidden_value(stream, hidden_error_key(), 7.0); + set_hidden_value(stream, hidden_signal_key(), 8.0); + let take = js_closure_alloc(ns_iter_take as *const u8, 1); + js_closure_set_capture_ptr(take, 0, stream.to_bits() as i64); + + let result = ns_iter_take(take, 2.0); + + assert_eq!(readable_hidden_error(result), Some(7.0)); + assert_eq!(get_hidden_value(result, hidden_signal_key()), Some(8.0)); + } + #[test] fn take_suppresses_sync_and_async_lookahead_errors() { for reject in [false, true] { diff --git a/crates/perry-runtime/src/node_stream_pipeline.rs b/crates/perry-runtime/src/node_stream_pipeline.rs index 713419c95b..c789b4d21a 100644 --- a/crates/perry-runtime/src/node_stream_pipeline.rs +++ b/crates/perry-runtime/src/node_stream_pipeline.rs @@ -134,11 +134,16 @@ pub(super) fn normalize_pipeline_source(value: f64, index: usize) -> f64 { } pub(super) fn pipeline_stage_array(stages: &[f64]) -> f64 { - let mut arr = crate::array::js_array_alloc(stages.len() as u32); - for stage in stages { - arr = crate::array::js_array_push_f64(arr, *stage); + let scope = crate::gc::RuntimeHandleScope::new(); + let stages = scope.root_nanbox_f64_slice(stages); + let arr = scope.root_raw_mut_ptr(crate::array::js_array_alloc(stages.len() as u32)); + for stage in &stages { + arr.set_raw_mut_ptr(crate::array::js_array_push_f64( + arr.get_raw_mut_ptr(), + stage.get_nanbox_f64(), + )); } - box_pointer(arr as *const u8) + box_pointer(arr.get_raw_const_ptr()) } pub(super) fn new_pipeline_callback_state() -> f64 { @@ -396,39 +401,46 @@ pub(super) fn catch_pipeline_throw(call: impl FnOnce() -> f64) -> Result Result { let value = settle_pipeline_value(value)?; - match value.to_bits() { + let scope = crate::gc::RuntimeHandleScope::new(); + let value = scope.root_nanbox_f64(value); + match value.get_nanbox_f64().to_bits() { TAG_UNDEFINED | TAG_NULL => return Ok(pipeline_empty_chunks()), _ => {} } - if !readable_chunks_nonempty(value) { - if let Some(source_iterator) = - get_hidden_value(value, hidden_key(READABLE_SOURCE_ITERATOR_KEY)) - { - if let Some(chunks) = collect_pipeline_iterator_chunks(source_iterator)? { + if !readable_chunks_nonempty(value.get_nanbox_f64()) { + if let Some(source_iterator) = get_hidden_value( + value.get_nanbox_f64(), + hidden_key(READABLE_SOURCE_ITERATOR_KEY), + ) { + let source_iterator = scope.root_nanbox_f64(source_iterator); + if let Some(chunks) = + collect_pipeline_iterator_chunks(source_iterator.get_nanbox_f64())? + { return Ok(chunks); } } } - if let Some(result) = js_node_stream_collect_chunks_result(value) { + if let Some(result) = js_node_stream_collect_chunks_result(value.get_nanbox_f64()) { return result; } - let raw = raw_ptr_from_value(value); + let raw = raw_ptr_from_value(value.get_nanbox_f64()); if let Some(chunks) = collection_iterable_chunks(raw) { return Ok(chunks); } - if let Some(chunks) = collect_pipeline_iterator_chunks(value)? { + if let Some(chunks) = collect_pipeline_iterator_chunks(value.get_nanbox_f64())? { return Ok(chunks); } - if object_ptr_from_value(value).is_some() { + if object_ptr_from_value(value.get_nanbox_f64()).is_some() { let undefined = f64::from_bits(crate::value::TAG_UNDEFINED); - let collected = crate::promise::js_array_from_async(value, undefined, undefined); + let collected = + crate::promise::js_array_from_async(value.get_nanbox_f64(), undefined, undefined); let settled = settle_pipeline_value(collected)?; if is_array_like_value(settled) { return Ok(settled); } } - if is_single_chunk_value(value) { - return Ok(pipeline_single_chunk(value)); + if is_single_chunk_value(value.get_nanbox_f64()) { + return Ok(pipeline_single_chunk(value.get_nanbox_f64())); } Ok(pipeline_empty_chunks()) } @@ -450,11 +462,13 @@ pub(super) fn collect_pipeline_iterator_chunks(iterable: f64) -> Result Result Result { - let source = if is_array_like_value(source) { - js_node_stream_readable_from(source) - } else { - source - }; - let args = [source]; + let scope = crate::gc::RuntimeHandleScope::new(); + let stage = scope.root_nanbox_f64(stage); + let source = scope.root_nanbox_f64(source); + if is_array_like_value(source.get_nanbox_f64()) { + source.set_nanbox_f64(js_node_stream_readable_from(source.get_nanbox_f64())); + } + let args = [source.get_nanbox_f64()]; let result = catch_pipeline_throw(|| unsafe { - crate::closure::js_native_call_value(stage, args.as_ptr(), args.len()) + crate::closure::js_native_call_value(stage.get_nanbox_f64(), args.as_ptr(), args.len()) })?; settle_pipeline_value_with_origin(result) } @@ -537,13 +557,17 @@ extern "C" fn collected_pipeline_error_noop(_closure: *const ClosureHeader, _err fn install_collected_pipeline_error_guards(stages: &[f64]) { crate::closure::js_register_closure_arity(collected_pipeline_error_noop as *const u8, 1); - for stage in stages { - if is_pipeline_stream(*stage) { + let scope = crate::gc::RuntimeHandleScope::new(); + let stages = scope.root_nanbox_f64_slice(stages); + let error = scope.root_nanbox_f64(string_value(b"error")); + for stage in &stages { + if is_pipeline_stream(stage.get_nanbox_f64()) { let listener = js_closure_alloc(collected_pipeline_error_noop as *const u8, 0); + let listener = scope.root_raw_mut_ptr(listener); add_stream_listener_for_event( - *stage, - string_value(b"error"), - box_pointer(listener as *const u8), + stage.get_nanbox_f64(), + error.get_nanbox_f64(), + box_pointer(listener.get_raw_const_ptr()), ); } } @@ -757,55 +781,63 @@ fn compose_copy_chunks(chunks: f64) -> f64 { } fn compose_take_stage_output(stage: f64) -> Result { - drain_compose_stream_stage(stage); - if let Some(err) = readable_hidden_error(stage) { + let scope = crate::gc::RuntimeHandleScope::new(); + let stage = scope.root_nanbox_f64(stage); + drain_compose_stream_stage(stage.get_nanbox_f64()); + if let Some(err) = readable_hidden_error(stage.get_nanbox_f64()) { return Err(err); } - let chunks = readable_hidden_chunks(stage) + let chunks = readable_hidden_chunks(stage.get_nanbox_f64()) .map(compose_copy_chunks) .unwrap_or_else(compose_empty_chunks); - clear_readable_buffer(stage); - clear_pending_readable_chunks(stage); - if let Some(err) = readable_hidden_error(stage) { + let chunks = scope.root_nanbox_f64(chunks); + clear_readable_buffer(stage.get_nanbox_f64()); + clear_pending_readable_chunks(stage.get_nanbox_f64()); + if let Some(err) = readable_hidden_error(stage.get_nanbox_f64()) { Err(err) } else { - Ok(chunks) + Ok(chunks.get_nanbox_f64()) } } fn compose_process_stream_stage(stage: f64, chunks: f64, end_stage: bool) -> Result { - clear_readable_buffer(stage); - clear_pending_readable_chunks(stage); - for chunk in pipeline_chunks_vec(chunks) { + let scope = crate::gc::RuntimeHandleScope::new(); + let stage = scope.root_nanbox_f64(stage); + let chunks = scope.root_nanbox_f64(chunks); + clear_readable_buffer(stage.get_nanbox_f64()); + clear_pending_readable_chunks(stage.get_nanbox_f64()); + let values = pipeline_chunks_vec(chunks.get_nanbox_f64()); + let values = scope.root_nanbox_f64_slice(&values); + for chunk in &values { catch_pipeline_throw(|| { write_writable_chunk( - stage, - chunk, + stage.get_nanbox_f64(), + chunk.get_nanbox_f64(), f64::from_bits(TAG_UNDEFINED), f64::from_bits(TAG_UNDEFINED), ) })?; - drain_compose_stream_stage(stage); - if let Some(err) = readable_hidden_error(stage) { + drain_compose_stream_stage(stage.get_nanbox_f64()); + if let Some(err) = readable_hidden_error(stage.get_nanbox_f64()) { return Err(err); } } if end_stage { catch_pipeline_throw(|| { finish_stream_with_args( - stage, + stage.get_nanbox_f64(), f64::from_bits(TAG_UNDEFINED), f64::from_bits(TAG_UNDEFINED), f64::from_bits(TAG_UNDEFINED), ); f64::from_bits(TAG_UNDEFINED) })?; - drain_compose_stream_stage(stage); - if let Some(err) = readable_hidden_error(stage) { + drain_compose_stream_stage(stage.get_nanbox_f64()); + if let Some(err) = readable_hidden_error(stage.get_nanbox_f64()) { return Err(err); } } - compose_take_stage_output(stage) + compose_take_stage_output(stage.get_nanbox_f64()) } fn compose_process_callable_stage(stage: f64, chunks: f64) -> Result { @@ -814,25 +846,39 @@ fn compose_process_callable_stage(stage: f64, chunks: f64) -> Result { } fn compose_process_stages(stages: &[f64], input: f64, end_stages: bool) -> Result { - let mut chunks = input; - for stage in stages { - if is_callable_value(*stage) { - chunks = compose_process_callable_stage(*stage, chunks)?; + let scope = crate::gc::RuntimeHandleScope::new(); + let stages = scope.root_nanbox_f64_slice(stages); + let chunks = scope.root_nanbox_f64(input); + for stage in &stages { + if is_callable_value(stage.get_nanbox_f64()) { + chunks.set_nanbox_f64(compose_process_callable_stage( + stage.get_nanbox_f64(), + chunks.get_nanbox_f64(), + )?); continue; } - if is_pipeline_stream(*stage) { - chunks = compose_process_stream_stage(*stage, chunks, end_stages)?; + if is_pipeline_stream(stage.get_nanbox_f64()) { + chunks.set_nanbox_f64(compose_process_stream_stage( + stage.get_nanbox_f64(), + chunks.get_nanbox_f64(), + end_stages, + )?); continue; } - chunks = collect_pipeline_chunks(*stage)?; + chunks.set_nanbox_f64(collect_pipeline_chunks(stage.get_nanbox_f64())?); } - Ok(chunks) + Ok(chunks.get_nanbox_f64()) } fn compose_push_output(composite: f64, chunks: f64) -> Result<(), f64> { - for chunk in pipeline_chunks_vec(chunks) { - let _ = push_chunk(composite, chunk); - if let Some(err) = readable_hidden_error(composite) { + let scope = crate::gc::RuntimeHandleScope::new(); + let composite = scope.root_nanbox_f64(composite); + let chunks = scope.root_nanbox_f64(chunks); + let values = pipeline_chunks_vec(chunks.get_nanbox_f64()); + let values = scope.root_nanbox_f64_slice(&values); + for chunk in &values { + let _ = push_chunk(composite.get_nanbox_f64(), chunk.get_nanbox_f64()); + if let Some(err) = readable_hidden_error(composite.get_nanbox_f64()) { return Err(err); } } @@ -984,81 +1030,122 @@ pub(super) extern "C" fn compose_duplex_final_callback( } fn install_compose_stage_error_listeners(composite: f64, source: f64, stages: f64) { - let error_event = string_value(b"error"); - for stage in compose_stage_values(stages) { - if !is_pipeline_stream(stage) { + let scope = crate::gc::RuntimeHandleScope::new(); + let composite = scope.root_nanbox_f64(composite); + let source = scope.root_nanbox_f64(source); + let stages = scope.root_nanbox_f64(stages); + let stage_values = compose_stage_values(stages.get_nanbox_f64()); + let stage_values = scope.root_nanbox_f64_slice(&stage_values); + let error_event = scope.root_nanbox_f64(string_value(b"error")); + for stage in &stage_values { + if !is_pipeline_stream(stage.get_nanbox_f64()) { continue; } let listener = js_closure_alloc(compose_stage_error_callback as *const u8, 3); - js_closure_set_capture_f64(listener, 0, composite); - js_closure_set_capture_f64(listener, 1, source); - js_closure_set_capture_f64(listener, 2, stages); - add_stream_listener_for_event(stage, error_event, box_pointer(listener as *const u8)); + let listener = scope.root_raw_mut_ptr(listener); + js_closure_set_capture_f64(listener.get_raw_mut_ptr(), 0, composite.get_nanbox_f64()); + js_closure_set_capture_f64(listener.get_raw_mut_ptr(), 1, source.get_nanbox_f64()); + js_closure_set_capture_f64(listener.get_raw_mut_ptr(), 2, stages.get_nanbox_f64()); + add_stream_listener_for_event( + stage.get_nanbox_f64(), + error_event.get_nanbox_f64(), + box_pointer(listener.get_raw_const_ptr()), + ); } } fn install_compose_source_listeners(composite: f64, source: f64, stages: f64) { - if !is_pipeline_stream(source) { + let scope = crate::gc::RuntimeHandleScope::new(); + let composite = scope.root_nanbox_f64(composite); + let source = scope.root_nanbox_f64(source); + let stages = scope.root_nanbox_f64(stages); + if !is_pipeline_stream(source.get_nanbox_f64()) { return; } let data = js_closure_alloc(compose_source_data_callback as *const u8, 1); - js_closure_set_capture_f64(data, 0, composite); + let data = scope.root_raw_mut_ptr(data); + js_closure_set_capture_f64(data.get_raw_mut_ptr(), 0, composite.get_nanbox_f64()); add_stream_listener_for_event( - source, + source.get_nanbox_f64(), string_value(b"data"), - box_pointer(data as *const u8), + box_pointer(data.get_raw_const_ptr()), ); let end = js_closure_alloc(compose_source_end_callback as *const u8, 1); - js_closure_set_capture_f64(end, 0, composite); - add_stream_listener_for_event(source, string_value(b"end"), box_pointer(end as *const u8)); + let end = scope.root_raw_mut_ptr(end); + js_closure_set_capture_f64(end.get_raw_mut_ptr(), 0, composite.get_nanbox_f64()); + add_stream_listener_for_event( + source.get_nanbox_f64(), + string_value(b"end"), + box_pointer(end.get_raw_const_ptr()), + ); - install_compose_source_error_listener(composite, source, stages); + install_compose_source_error_listener( + composite.get_nanbox_f64(), + source.get_nanbox_f64(), + stages.get_nanbox_f64(), + ); - start_pipeline_readable(source); + start_pipeline_readable(source.get_nanbox_f64()); } fn install_compose_source_error_listener(composite: f64, source: f64, stages: f64) { + let scope = crate::gc::RuntimeHandleScope::new(); + let composite = scope.root_nanbox_f64(composite); + let source = scope.root_nanbox_f64(source); + let stages = scope.root_nanbox_f64(stages); let error = js_closure_alloc(compose_source_error_callback as *const u8, 3); - js_closure_set_capture_f64(error, 0, composite); - js_closure_set_capture_f64(error, 1, source); - js_closure_set_capture_f64(error, 2, stages); + let error = scope.root_raw_mut_ptr(error); + js_closure_set_capture_f64(error.get_raw_mut_ptr(), 0, composite.get_nanbox_f64()); + js_closure_set_capture_f64(error.get_raw_mut_ptr(), 1, source.get_nanbox_f64()); + js_closure_set_capture_f64(error.get_raw_mut_ptr(), 2, stages.get_nanbox_f64()); add_stream_listener_for_event( - source, + source.get_nanbox_f64(), string_value(b"error"), - box_pointer(error as *const u8), + box_pointer(error.get_raw_const_ptr()), ); } fn install_composed_duplex_callbacks(composite: f64, stages: f64, source: f64, writable: bool) { - let raw = raw_ptr_from_value(composite); + let scope = crate::gc::RuntimeHandleScope::new(); + let composite = scope.root_nanbox_f64(composite); + let stages = scope.root_nanbox_f64(stages); + let source = scope.root_nanbox_f64(source); + let raw = raw_ptr_from_value(composite.get_nanbox_f64()); if raw < 0x10000 { return; } - let obj = raw as *mut ObjectHeader; let write = js_closure_alloc(compose_duplex_write_callback as *const u8, 3); - js_closure_set_capture_f64(write, 0, composite); - js_closure_set_capture_f64(write, 1, stages); - js_closure_set_capture_f64(write, 2, source); - js_object_set_field_by_name(obj, hidden_write_key(), box_pointer(write as *const u8)); + let write = scope.root_raw_mut_ptr(write); + js_closure_set_capture_f64(write.get_raw_mut_ptr(), 0, composite.get_nanbox_f64()); + js_closure_set_capture_f64(write.get_raw_mut_ptr(), 1, stages.get_nanbox_f64()); + js_closure_set_capture_f64(write.get_raw_mut_ptr(), 2, source.get_nanbox_f64()); + let obj = raw_ptr_from_value(composite.get_nanbox_f64()) as *mut ObjectHeader; + js_object_set_field_by_name( + obj, + hidden_write_key(), + box_pointer(write.get_raw_const_ptr()), + ); let final_cb = js_closure_alloc(compose_duplex_final_callback as *const u8, 3); - js_closure_set_capture_f64(final_cb, 0, composite); - js_closure_set_capture_f64(final_cb, 1, stages); - js_closure_set_capture_f64(final_cb, 2, source); + let final_cb = scope.root_raw_mut_ptr(final_cb); + js_closure_set_capture_f64(final_cb.get_raw_mut_ptr(), 0, composite.get_nanbox_f64()); + js_closure_set_capture_f64(final_cb.get_raw_mut_ptr(), 1, stages.get_nanbox_f64()); + js_closure_set_capture_f64(final_cb.get_raw_mut_ptr(), 2, source.get_nanbox_f64()); + let obj = raw_ptr_from_value(composite.get_nanbox_f64()) as *mut ObjectHeader; js_object_set_field_by_name( obj, hidden_writable_final_key(), - box_pointer(final_cb as *const u8), + box_pointer(final_cb.get_raw_const_ptr()), ); set_hidden_value( - composite, + composite.get_nanbox_f64(), hidden_key(b"writableCustomSink"), f64::from_bits(TAG_TRUE), ); if !writable { - set_visible_writable(composite, false); + set_visible_writable(composite.get_nanbox_f64(), false); } } @@ -1067,65 +1154,126 @@ fn compose_source_has_snapshot(source: f64) -> bool { } fn prime_composed_duplex_from_source(composite: f64, source: f64, stages: f64) -> bool { - prepare_readable_for_iteration(source); - let chunks = match collect_pipeline_chunks(source) { + let scope = crate::gc::RuntimeHandleScope::new(); + let composite = scope.root_nanbox_f64(composite); + let source = scope.root_nanbox_f64(source); + let stages = scope.root_nanbox_f64(stages); + prepare_readable_for_iteration(source.get_nanbox_f64()); + let chunks = match collect_pipeline_chunks(source.get_nanbox_f64()) { Ok(chunks) => chunks, Err(err) => { - fail_composed_duplex(composite, source, stages, err); + fail_composed_duplex( + composite.get_nanbox_f64(), + source.get_nanbox_f64(), + stages.get_nanbox_f64(), + err, + ); return true; } }; - let stage_values = compose_stage_values(stages); - match compose_process_stages(&stage_values, chunks, true) - .and_then(|chunks| compose_push_output(composite, chunks)) + let chunks = scope.root_nanbox_f64(chunks); + let stage_values = compose_stage_values(stages.get_nanbox_f64()); + let stage_values = scope.root_nanbox_f64_slice(&stage_values); + match compose_process_stages( + &crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&stage_values), + chunks.get_nanbox_f64(), + true, + ) + .and_then(|chunks| compose_push_output(composite.get_nanbox_f64(), chunks)) { Ok(()) => { - schedule_readable_end(composite); + schedule_readable_end(composite.get_nanbox_f64()); } Err(err) => { - fail_composed_duplex(composite, source, stages, err); + fail_composed_duplex( + composite.get_nanbox_f64(), + source.get_nanbox_f64(), + stages.get_nanbox_f64(), + err, + ); } } true } fn new_composed_duplex(stages: &[f64], source: Option, writable: bool) -> f64 { - let composite = js_node_stream_duplex_new(readable_from_options(f64::from_bits(TAG_UNDEFINED))); - let stages_value = pipeline_stage_array(stages); - let source_value = source.unwrap_or_else(|| f64::from_bits(TAG_UNDEFINED)); - install_composed_duplex_callbacks(composite, stages_value, source_value, writable); - if let Some(source) = source { - install_compose_stage_error_listeners(composite, source_value, stages_value); - if !compose_source_has_snapshot(source) { - install_compose_source_listeners(composite, source, stages_value); + let scope = crate::gc::RuntimeHandleScope::new(); + let stages = scope.root_nanbox_f64_slice(stages); + let source = source.map(|source| scope.root_nanbox_f64(source)); + let composite = scope.root_nanbox_f64(js_node_stream_duplex_new(readable_from_options( + f64::from_bits(TAG_UNDEFINED), + ))); + let stages_value = scope.root_nanbox_f64(pipeline_stage_array( + &crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&stages), + )); + let source_value = source + .as_ref() + .map(|source| source.get_nanbox_f64()) + .unwrap_or_else(|| f64::from_bits(TAG_UNDEFINED)); + install_composed_duplex_callbacks( + composite.get_nanbox_f64(), + stages_value.get_nanbox_f64(), + source_value, + writable, + ); + if let Some(source) = source.as_ref() { + install_compose_stage_error_listeners( + composite.get_nanbox_f64(), + source.get_nanbox_f64(), + stages_value.get_nanbox_f64(), + ); + if !compose_source_has_snapshot(source.get_nanbox_f64()) { + install_compose_source_listeners( + composite.get_nanbox_f64(), + source.get_nanbox_f64(), + stages_value.get_nanbox_f64(), + ); } else { - install_compose_source_error_listener(composite, source, stages_value); + install_compose_source_error_listener( + composite.get_nanbox_f64(), + source.get_nanbox_f64(), + stages_value.get_nanbox_f64(), + ); set_hidden_value( - composite, + composite.get_nanbox_f64(), hidden_key(b"__perryStreamComposePriming"), f64::from_bits(TAG_TRUE), ); - prime_composed_duplex_from_source(composite, source, stages_value); + prime_composed_duplex_from_source( + composite.get_nanbox_f64(), + source.get_nanbox_f64(), + stages_value.get_nanbox_f64(), + ); set_hidden_value( - composite, + composite.get_nanbox_f64(), hidden_key(b"__perryStreamComposePriming"), f64::from_bits(TAG_FALSE), ); - if let Some(err) = - get_hidden_value(composite, hidden_key(b"__perryStreamComposePendingError")) - { + if let Some(err) = get_hidden_value( + composite.get_nanbox_f64(), + hidden_key(b"__perryStreamComposePendingError"), + ) { set_hidden_value( - composite, + composite.get_nanbox_f64(), hidden_key(b"__perryStreamComposePendingError"), f64::from_bits(TAG_UNDEFINED), ); - fail_composed_duplex(composite, source, stages_value, err); + fail_composed_duplex( + composite.get_nanbox_f64(), + source.get_nanbox_f64(), + stages_value.get_nanbox_f64(), + err, + ); } } } else { - install_compose_stage_error_listeners(composite, source_value, stages_value); + install_compose_stage_error_listeners( + composite.get_nanbox_f64(), + source_value, + stages_value.get_nanbox_f64(), + ); } - composite + composite.get_nanbox_f64() } pub(super) fn build_node_stream_compose(args: Vec) -> f64 { diff --git a/crates/perry-runtime/src/node_stream_readwrite.rs b/crates/perry-runtime/src/node_stream_readwrite.rs index 8fb13f56b1..23f6c40ea2 100644 --- a/crates/perry-runtime/src/node_stream_readwrite.rs +++ b/crates/perry-runtime/src/node_stream_readwrite.rs @@ -1496,7 +1496,7 @@ pub(super) fn normalize_readable_from_input(iterable: f64) -> NormalizedReadable let arr = crate::array::js_array_push_f64(arr, iterable); return normalized_readable_chunks(box_pointer(arr as *const u8)); } - if let Some(source_iterator) = crate::array::call_symbol_async_iterator_for_flat_map(iterable) { + if let Some(source_iterator) = crate::array::call_symbol_async_iterator(iterable) { return NormalizedReadableInput { chunks: box_pointer(crate::array::js_array_alloc(0) as *const u8), source_iterator: Some(source_iterator), diff --git a/crates/perry-runtime/src/node_stream_tests_extra.rs b/crates/perry-runtime/src/node_stream_tests_extra.rs index d2ad99fa1e..6d3922502e 100644 --- a/crates/perry-runtime/src/node_stream_tests_extra.rs +++ b/crates/perry-runtime/src/node_stream_tests_extra.rs @@ -920,15 +920,16 @@ fn writable_lifecycle_flags_reflect_end_and_finish() { WRITABLE_FINISH_COUNT.with(|count| *count.borrow_mut() = 0); WRITABLE_CLOSE_COUNT.with(|count| *count.borrow_mut() = 0); - let opts = crate::object::js_object_alloc(0, 1); + let scope = crate::gc::RuntimeHandleScope::new(); + let opts = scope.root_raw_mut_ptr(crate::object::js_object_alloc(0, 1)); let closure = js_closure_alloc(write_capture as *const u8, 0); crate::closure::js_register_closure_arity(write_capture as *const u8, 3); js_object_set_field_by_name( - opts, + opts.get_raw_mut_ptr(), hidden_key(b"write"), f64::from_bits(JSValue::pointer(closure as *const u8).bits()), ); - let stream = js_node_stream_writable_new(box_pointer(opts as *const u8)); + let stream = js_node_stream_writable_new(box_pointer(opts.get_raw_const_ptr())); let handle = raw_ptr_from_value(stream) as i64; let obj = raw_ptr_from_value(stream) as *const ObjectHeader; @@ -1040,26 +1041,36 @@ fn readable_auto_destroy_false_does_not_close_after_end() { #[test] fn stream_destroy_with_error_marks_errored_state() { - let stream = js_node_stream_readable_new(f64::from_bits(TAG_UNDEFINED)); - let handle = raw_ptr_from_value(stream) as i64; - let destroy = js_object_get_field_by_name_f64( - raw_ptr_from_value(stream) as *const ObjectHeader, + let scope = crate::gc::RuntimeHandleScope::new(); + let stream = scope.root_nanbox_f64(js_node_stream_readable_new(f64::from_bits(TAG_UNDEFINED))); + let destroy = scope.root_nanbox_f64(js_object_get_field_by_name_f64( + raw_ptr_from_value(stream.get_nanbox_f64()) as *const ObjectHeader, hidden_key(b"destroy"), - ); - let err = string_value("boom"); + )); + let err = scope.root_nanbox_f64(string_value("boom")); let error_listener = box_pointer(js_closure_alloc(noop_listener as *const u8, 0) as *const u8); + let handle = raw_ptr_from_value(stream.get_nanbox_f64()) as i64; let _ = js_node_stream_method_on(handle, string_value("error"), error_listener); assert_eq!(js_node_stream_method_errored(handle).to_bits(), TAG_NULL); - let ret = unsafe { crate::closure::js_native_call_value(destroy, &err, 1) }; + let args = [err.get_nanbox_f64()]; + let ret = unsafe { + crate::closure::js_native_call_value(destroy.get_nanbox_f64(), args.as_ptr(), args.len()) + }; - assert_eq!(ret.to_bits(), stream.to_bits()); - assert_eq!(js_node_stream_is_errored(stream).to_bits(), TAG_FALSE); + assert_eq!(ret.to_bits(), stream.get_nanbox_f64().to_bits()); + assert_eq!( + js_node_stream_is_errored(stream.get_nanbox_f64()).to_bits(), + TAG_FALSE + ); let _ = crate::promise::js_promise_run_microtasks(); - assert_eq!(js_node_stream_is_errored(stream).to_bits(), TAG_TRUE); + assert_eq!( + js_node_stream_is_errored(stream.get_nanbox_f64()).to_bits(), + TAG_TRUE + ); assert_eq!( js_node_stream_method_errored(handle).to_bits(), - err.to_bits() + err.get_nanbox_f64().to_bits() ); } @@ -1094,11 +1105,11 @@ fn readable_exposes_async_dispose_symbol_method() { #[test] fn readable_aborted_reflects_destroy_before_end() { - let stream = js_node_stream_readable_new(f64::from_bits(TAG_UNDEFINED)); - let handle = raw_ptr_from_value(stream) as i64; - let obj = raw_ptr_from_value(stream) as *const ObjectHeader; - let err = string_value("abort"); + let scope = crate::gc::RuntimeHandleScope::new(); + let stream = scope.root_nanbox_f64(js_node_stream_readable_new(f64::from_bits(TAG_UNDEFINED))); + let err = scope.root_nanbox_f64(string_value("abort")); let error_listener = box_pointer(js_closure_alloc(noop_listener as *const u8, 0) as *const u8); + let handle = raw_ptr_from_value(stream.get_nanbox_f64()) as i64; let _ = js_node_stream_method_on(handle, string_value("error"), error_listener); assert_eq!( @@ -1106,17 +1117,25 @@ fn readable_aborted_reflects_destroy_before_end() { TAG_FALSE ); assert_eq!( - js_object_get_field_by_name_f64(obj, hidden_key(b"readableAborted")).to_bits(), + js_object_get_field_by_name_f64( + raw_ptr_from_value(stream.get_nanbox_f64()) as *const ObjectHeader, + hidden_key(b"readableAborted"), + ) + .to_bits(), TAG_FALSE ); - let _ = js_node_stream_method_destroy(handle, err); + let _ = js_node_stream_method_destroy(handle, err.get_nanbox_f64()); assert_eq!( js_node_stream_method_readable_aborted(handle).to_bits(), TAG_TRUE ); assert_eq!( - js_object_get_field_by_name_f64(obj, hidden_key(b"readableAborted")).to_bits(), + js_object_get_field_by_name_f64( + raw_ptr_from_value(stream.get_nanbox_f64()) as *const ObjectHeader, + hidden_key(b"readableAborted"), + ) + .to_bits(), TAG_TRUE ); let _ = crate::promise::js_promise_run_microtasks(); @@ -1125,12 +1144,12 @@ fn readable_aborted_reflects_destroy_before_end() { TAG_TRUE ); - let ended = js_node_stream_readable_new(f64::from_bits(TAG_UNDEFINED)); - let ended_handle = raw_ptr_from_value(ended) as i64; + let ended = scope.root_nanbox_f64(js_node_stream_readable_new(f64::from_bits(TAG_UNDEFINED))); let error_listener = box_pointer(js_closure_alloc(noop_listener as *const u8, 0) as *const u8); + let ended_handle = raw_ptr_from_value(ended.get_nanbox_f64()) as i64; let _ = js_node_stream_method_on(ended_handle, string_value("error"), error_listener); let _ = js_node_stream_method_push(ended_handle, f64::from_bits(TAG_NULL)); - let _ = js_node_stream_method_destroy(ended_handle, err); + let _ = js_node_stream_method_destroy(ended_handle, err.get_nanbox_f64()); assert_eq!( js_node_stream_method_readable_aborted(ended_handle).to_bits(), TAG_FALSE @@ -1139,31 +1158,40 @@ fn readable_aborted_reflects_destroy_before_end() { #[test] fn stream_native_receiver_methods_update_hidden_state() { - let stream = js_node_stream_passthrough_new(f64::from_bits(TAG_UNDEFINED)); - let handle = raw_ptr_from_value(stream) as i64; - let err = string_value("boom"); + let scope = crate::gc::RuntimeHandleScope::new(); + let stream = scope.root_nanbox_f64(js_node_stream_passthrough_new(f64::from_bits( + TAG_UNDEFINED, + ))); + let err = scope.root_nanbox_f64(string_value("boom")); let cb = box_pointer(js_closure_alloc(noop_listener as *const u8, 0) as *const u8); + let handle = raw_ptr_from_value(stream.get_nanbox_f64()) as i64; let _ = js_node_stream_method_on(handle, string_value("error"), cb); assert_eq!( - js_node_stream_method_emit(handle, string_value("error"), err).to_bits(), + js_node_stream_method_emit(handle, string_value("error"), err.get_nanbox_f64()).to_bits(), TAG_TRUE ); - assert!(js_node_stream_hidden_error_after_read(stream).is_some()); + assert!(js_node_stream_hidden_error_after_read(stream.get_nanbox_f64()).is_some()); - let stream = js_node_stream_passthrough_new(f64::from_bits(TAG_UNDEFINED)); - let handle = raw_ptr_from_value(stream) as i64; + let stream = scope.root_nanbox_f64(js_node_stream_passthrough_new(f64::from_bits( + TAG_UNDEFINED, + ))); + let handle = raw_ptr_from_value(stream.get_nanbox_f64()) as i64; let _ = js_node_stream_method_end(handle, f64::from_bits(TAG_UNDEFINED)); - assert!(js_node_stream_is_stub_ended_after_read(stream)); + assert!(js_node_stream_is_stub_ended_after_read( + stream.get_nanbox_f64() + )); - let stream = js_node_stream_passthrough_new(f64::from_bits(TAG_UNDEFINED)); - let handle = raw_ptr_from_value(stream) as i64; + let stream = scope.root_nanbox_f64(js_node_stream_passthrough_new(f64::from_bits( + TAG_UNDEFINED, + ))); let cb = box_pointer(js_closure_alloc(noop_listener as *const u8, 0) as *const u8); + let handle = raw_ptr_from_value(stream.get_nanbox_f64()) as i64; let _ = js_node_stream_method_on(handle, string_value("error"), cb); - let _ = js_node_stream_method_destroy(handle, err); - assert!(readable_hidden_error(stream).is_none()); + let _ = js_node_stream_method_destroy(handle, err.get_nanbox_f64()); + assert!(readable_hidden_error(stream.get_nanbox_f64()).is_none()); let _ = crate::promise::js_promise_run_microtasks(); - assert!(js_node_stream_hidden_error_after_read(stream).is_some()); + assert!(js_node_stream_hidden_error_after_read(stream.get_nanbox_f64()).is_some()); } #[test] diff --git a/crates/perry-runtime/src/node_submodules/mod.rs b/crates/perry-runtime/src/node_submodules/mod.rs index 06c54e7b6a..b3e7ecccf7 100644 --- a/crates/perry-runtime/src/node_submodules/mod.rs +++ b/crates/perry-runtime/src/node_submodules/mod.rs @@ -1420,6 +1420,11 @@ pub fn scan_node_submodule_singleton_roots(mark: &mut dyn FnMut(f64)) { } pub fn scan_node_submodule_singleton_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { + DIAG_PENDING_UNCAUGHT.with(|pending| { + for err in pending.borrow_mut().iter_mut() { + visitor.visit_nanbox_f64_slot(err); + } + }); if ANY_SINGLETON_ALLOCATED.load(Ordering::Acquire) == 0 { return; } @@ -1497,11 +1502,18 @@ pub(crate) fn test_seed_node_submodule_roots( DIAG_NOOP_CLOSURE.with(|slot| { *slot.borrow_mut() = Some(diag_noop); }); + DIAG_PENDING_UNCAUGHT.with(|pending| { + let mut pending = pending.borrow_mut(); + pending.clear(); + pending.push(f64::from_bits( + JSValue::pointer(namespace as *const u8).bits(), + )); + }); ANY_SINGLETON_ALLOCATED.store(1, Ordering::Release); } #[cfg(test)] -pub(crate) fn test_node_submodule_roots() -> (usize, usize, usize) { +pub(crate) fn test_node_submodule_roots() -> (usize, usize, usize, u64) { let closure = EXPORT_SINGLETONS.with(|m| { m.borrow() .get(&(1, 2)) @@ -1512,7 +1524,9 @@ pub(crate) fn test_node_submodule_roots() -> (usize, usize, usize) { NAMESPACE_SINGLETONS.with(|m| m.borrow().get(&3).map(|ptr| *ptr as usize).unwrap_or(0)); let diag = DIAG_NOOP_CLOSURE.with(|slot| slot.borrow().as_ref().map(|ptr| *ptr as usize).unwrap_or(0)); - (closure, namespace, diag) + let pending = DIAG_PENDING_UNCAUGHT + .with(|pending| pending.borrow_mut().pop().map(f64::to_bits).unwrap_or(0)); + (closure, namespace, diag, pending) } // ----- FFI entry points ----- diff --git a/crates/perry-runtime/src/promise/async_step.rs b/crates/perry-runtime/src/promise/async_step.rs index d1574436d8..30dd532905 100644 --- a/crates/perry-runtime/src/promise/async_step.rs +++ b/crates/perry-runtime/src/promise/async_step.rs @@ -933,7 +933,7 @@ fn array_value_ptr(input: f64) -> Option<*const crate::array::ArrayHeader> { } fn iterator_value_for_from_async(input: f64) -> Option { - if let Some(iter) = crate::array::call_symbol_async_iterator_for_flat_map(input) { + if let Some(iter) = crate::array::call_symbol_async_iterator(input) { return Some(iter); } if crate::object::is_async_generator_instance_value(input) { diff --git a/crates/perry-transform/src/generator/per_iteration.rs b/crates/perry-transform/src/generator/per_iteration.rs index 69a66d7f6b..bc5ea252dd 100644 --- a/crates/perry-transform/src/generator/per_iteration.rs +++ b/crates/perry-transform/src/generator/per_iteration.rs @@ -1094,4 +1094,27 @@ mod tests { assert!(!collect_per_iteration_ids(&body).contains(&id)); } + + #[test] + fn for_loop_local_used_after_await_stays_hoisted() { + let id = 1; + let body = vec![Stmt::For { + init: None, + condition: Some(Expr::Bool(true)), + update: None, + body: vec![ + Stmt::Let { + id, + name: "value".into(), + ty: Type::Any, + mutable: false, + init: Some(Expr::String("x".into())), + }, + Stmt::Expr(Expr::Await(Box::new(Expr::Integer(0)))), + Stmt::Expr(Expr::LocalGet(id)), + ], + }]; + + assert!(!collect_per_iteration_ids(&body).contains(&id)); + } } From 3204bba49c32f3ec7009162440dc368d2a9e64d6 Mon Sep 17 00:00:00 2001 From: TheHypnoo Date: Fri, 31 Jul 2026 11:44:09 +0200 Subject: [PATCH 6/8] fix(stream): address follow-up review findings --- crates/perry-runtime/src/array/iterator.rs | 8 +- .../src/node_stream/async_iterator.rs | 3 +- .../perry-runtime/src/node_stream_pipeline.rs | 27 ++- crates/perry-runtime/src/node_stream_tests.rs | 59 +++++++ .../src/node_stream_tests_extra.rs | 158 +++++++++++------- 5 files changed, 184 insertions(+), 71 deletions(-) diff --git a/crates/perry-runtime/src/array/iterator.rs b/crates/perry-runtime/src/array/iterator.rs index 31388a7394..5cf1fa3e9a 100644 --- a/crates/perry-runtime/src/array/iterator.rs +++ b/crates/perry-runtime/src/array/iterator.rs @@ -988,11 +988,6 @@ pub extern "C" fn js_iterator_close_if_not_done(iter_f64: f64, done_f64: f64) -> f64::from_bits(crate::value::TAG_UNDEFINED) } -/// Resolve `Symbol.asyncIterator` and invoke it with the iterable as `this`. -pub(crate) fn call_symbol_async_iterator(value: f64) -> Option { - call_symbol_async_iterator_impl(value) -} - /// Issue #1572 — same as `js_async_iterator_to_array` but reachable from /// the node_stream crate path so flatMap can flatten an `async function*` /// mapper result without duplicating the next()/done/value loop. @@ -1073,7 +1068,8 @@ pub(crate) fn sync_iterator_to_array_if_not_async(iter_f64: f64) -> Option<*mut Some(result) } -fn call_symbol_async_iterator_impl(value: f64) -> Option { +/// Resolve `Symbol.asyncIterator` and invoke it with the iterable as `this`. +pub(crate) fn call_symbol_async_iterator(value: f64) -> Option { let sym = crate::symbol::well_known_symbol("asyncIterator"); if sym.is_null() { return None; diff --git a/crates/perry-runtime/src/node_stream/async_iterator.rs b/crates/perry-runtime/src/node_stream/async_iterator.rs index 5bec16a018..4cdd0ec2ec 100644 --- a/crates/perry-runtime/src/node_stream/async_iterator.rs +++ b/crates/perry-runtime/src/node_stream/async_iterator.rs @@ -878,8 +878,7 @@ mod fifo_pending_tests { #[test] fn retained_source_rejection_finishes_iterator_and_destroys_stream() { - let chunks = crate::array::js_array_alloc(0); - let stream = js_node_stream_readable_from(box_pointer(chunks as *const u8)); + let stream = readable_from_chunks(crate::array::js_array_alloc(0)); let iterator = build_readable_async_iterator(stream, true); let rejected = js_closure_alloc(ns_readable_source_iterator_rejected as *const u8, 1); js_closure_set_capture_f64(rejected, 0, iterator); diff --git a/crates/perry-runtime/src/node_stream_pipeline.rs b/crates/perry-runtime/src/node_stream_pipeline.rs index c789b4d21a..c0d2c7b3d4 100644 --- a/crates/perry-runtime/src/node_stream_pipeline.rs +++ b/crates/perry-runtime/src/node_stream_pipeline.rs @@ -1239,11 +1239,25 @@ fn new_composed_duplex(stages: &[f64], source: Option, writable: bool) -> f hidden_key(b"__perryStreamComposePriming"), f64::from_bits(TAG_TRUE), ); - prime_composed_duplex_from_source( - composite.get_nanbox_f64(), - source.get_nanbox_f64(), - stages_value.get_nanbox_f64(), - ); + let previous_this = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); + let primed = catch_pipeline_throw(|| { + prime_composed_duplex_from_source( + composite.get_nanbox_f64(), + source.get_nanbox_f64(), + stages_value.get_nanbox_f64(), + ); + f64::from_bits(TAG_UNDEFINED) + }); + crate::object::js_implicit_this_set(previous_this.get_nanbox_f64()); + if let Err(err) = primed { + let err = scope.root_nanbox_f64(err); + fail_composed_duplex( + composite.get_nanbox_f64(), + source.get_nanbox_f64(), + stages_value.get_nanbox_f64(), + err.get_nanbox_f64(), + ); + } set_hidden_value( composite.get_nanbox_f64(), hidden_key(b"__perryStreamComposePriming"), @@ -1253,6 +1267,7 @@ fn new_composed_duplex(stages: &[f64], source: Option, writable: bool) -> f composite.get_nanbox_f64(), hidden_key(b"__perryStreamComposePendingError"), ) { + let err = scope.root_nanbox_f64(err); set_hidden_value( composite.get_nanbox_f64(), hidden_key(b"__perryStreamComposePendingError"), @@ -1262,7 +1277,7 @@ fn new_composed_duplex(stages: &[f64], source: Option, writable: bool) -> f composite.get_nanbox_f64(), source.get_nanbox_f64(), stages_value.get_nanbox_f64(), - err, + err.get_nanbox_f64(), ); } } diff --git a/crates/perry-runtime/src/node_stream_tests.rs b/crates/perry-runtime/src/node_stream_tests.rs index 01aba12758..4c264bfb88 100644 --- a/crates/perry-runtime/src/node_stream_tests.rs +++ b/crates/perry-runtime/src/node_stream_tests.rs @@ -452,6 +452,10 @@ extern "C" fn read_records_this(closure: *const ClosureHeader) -> f64 { f64::from_bits(TAG_UNDEFINED) } +extern "C" fn read_throws(closure: *const ClosureHeader, _size: f64) -> f64 { + crate::exception::js_throw(crate::closure::js_closure_get_capture_f64(closure, 0)) +} + extern "C" fn transform_upper_callback( _closure: *const ClosureHeader, chunk: f64, @@ -843,6 +847,61 @@ fn compose_snapshot_stage_error_reaches_only_composite_listener() { crate::os::test_clear_process_event_listeners(); } +#[test] +fn compose_snapshot_read_throw_returns_errored_composite() { + ERROR_COUNT.with(|count| *count.borrow_mut() = 0); + let scope = crate::gc::RuntimeHandleScope::new(); + let message = scope.root_string_ptr(crate::string::js_string_from_bytes( + b"read-boom".as_ptr(), + 9, + )); + let error = scope.root_nanbox_f64(crate::value::js_nanbox_pointer( + crate::error::js_error_new_with_message(message.get_raw_mut_ptr()) as i64, + )); + let options = scope.root_raw_mut_ptr(crate::object::js_object_alloc(0, 1)); + let read = scope.root_raw_mut_ptr(js_closure_alloc(read_throws as *const u8, 1)); + crate::closure::js_register_closure_arity(read_throws as *const u8, 1); + crate::closure::js_closure_set_capture_f64(read.get_raw_mut_ptr(), 0, error.get_nanbox_f64()); + js_object_set_field_by_name( + options.get_raw_mut_ptr(), + hidden_key(b"read"), + box_pointer(read.get_raw_const_ptr()), + ); + let source = scope.root_nanbox_f64(js_node_stream_readable_new(box_pointer( + options.get_raw_const_ptr(), + ))); + let chunk = scope.root_nanbox_f64(string_value("x")); + let _ = push_chunk(source.get_nanbox_f64(), chunk.get_nanbox_f64()); + let stage = scope.root_nanbox_f64(js_node_stream_passthrough_new(f64::from_bits( + TAG_UNDEFINED, + ))); + let mut args = crate::array::js_array_alloc(2); + args = crate::array::js_array_push_f64(args, source.get_nanbox_f64()); + args = crate::array::js_array_push_f64(args, stage.get_nanbox_f64()); + let args = scope.root_raw_mut_ptr(args); + + let composite = catch_pipeline_throw(|| js_node_stream_compose_args(args.get_raw_const_ptr())) + .expect("compose should convert a source read throw into a stream error"); + let composite = scope.root_nanbox_f64(composite); + assert!(!has_truthy_hidden( + composite.get_nanbox_f64(), + hidden_key(b"__perryStreamComposePriming") + )); + + let listener = scope.root_raw_mut_ptr(js_closure_alloc(capture_error_listener as *const u8, 0)); + crate::closure::js_register_closure_arity(capture_error_listener as *const u8, 1); + let error_event = scope.root_nanbox_f64(string_value("error")); + let _ = js_node_stream_method_on( + raw_ptr_from_value(composite.get_nanbox_f64()) as i64, + error_event.get_nanbox_f64(), + box_pointer(listener.get_raw_const_ptr()), + ); + let _ = crate::promise::js_promise_run_microtasks(); + + ERROR_COUNT.with(|count| assert_eq!(*count.borrow(), 1)); + assert!(stream_destroyed(composite.get_nanbox_f64())); +} + #[test] fn compose_single_transform_returns_stage() { READABLE_DATA_CAPTURED.with(|captured| captured.borrow_mut().clear()); diff --git a/crates/perry-runtime/src/node_stream_tests_extra.rs b/crates/perry-runtime/src/node_stream_tests_extra.rs index 6d3922502e..88c53fa42e 100644 --- a/crates/perry-runtime/src/node_stream_tests_extra.rs +++ b/crates/perry-runtime/src/node_stream_tests_extra.rs @@ -922,63 +922,78 @@ fn writable_lifecycle_flags_reflect_end_and_finish() { let scope = crate::gc::RuntimeHandleScope::new(); let opts = scope.root_raw_mut_ptr(crate::object::js_object_alloc(0, 1)); - let closure = js_closure_alloc(write_capture as *const u8, 0); + let closure = scope.root_raw_mut_ptr(js_closure_alloc(write_capture as *const u8, 0)); crate::closure::js_register_closure_arity(write_capture as *const u8, 3); js_object_set_field_by_name( opts.get_raw_mut_ptr(), hidden_key(b"write"), - f64::from_bits(JSValue::pointer(closure as *const u8).bits()), + box_pointer(closure.get_raw_const_ptr()), ); - let stream = js_node_stream_writable_new(box_pointer(opts.get_raw_const_ptr())); - let handle = raw_ptr_from_value(stream) as i64; - let obj = raw_ptr_from_value(stream) as *const ObjectHeader; + let stream = scope.root_nanbox_f64(js_node_stream_writable_new(box_pointer( + opts.get_raw_const_ptr(), + ))); + let handle = || raw_ptr_from_value(stream.get_nanbox_f64()) as i64; + let object = || raw_ptr_from_value(stream.get_nanbox_f64()) as *const ObjectHeader; - assert_eq!(js_node_stream_method_writable(handle).to_bits(), TAG_TRUE); + assert_eq!(js_node_stream_method_writable(handle()).to_bits(), TAG_TRUE); assert_eq!( - js_object_get_field_by_name_f64(obj, hidden_key(b"writable")).to_bits(), + js_object_get_field_by_name_f64(object(), hidden_key(b"writable")).to_bits(), TAG_TRUE ); assert_eq!( - js_object_get_field_by_name_f64(obj, hidden_key(b"closed")).to_bits(), + js_object_get_field_by_name_f64(object(), hidden_key(b"closed")).to_bits(), TAG_FALSE ); assert_eq!( - js_node_stream_method_writable_ended(handle).to_bits(), + js_node_stream_method_writable_ended(handle()).to_bits(), TAG_FALSE ); assert_eq!( - js_node_stream_method_writable_finished(handle).to_bits(), + js_node_stream_method_writable_finished(handle()).to_bits(), TAG_FALSE ); - let finish = - box_pointer(js_closure_alloc(capture_finish_listener as *const u8, 0) as *const u8); - let close = box_pointer(js_closure_alloc(capture_close_listener as *const u8, 0) as *const u8); - let _ = js_node_stream_method_on(handle, string_value("finish"), finish); - let _ = js_node_stream_method_on(handle, string_value("close"), close); + let finish = scope.root_raw_mut_ptr(js_closure_alloc(capture_finish_listener as *const u8, 0)); + let close = scope.root_raw_mut_ptr(js_closure_alloc(capture_close_listener as *const u8, 0)); + let finish_event = scope.root_nanbox_f64(string_value("finish")); + let close_event = scope.root_nanbox_f64(string_value("close")); + let _ = js_node_stream_method_on( + handle(), + finish_event.get_nanbox_f64(), + box_pointer(finish.get_raw_const_ptr()), + ); + let _ = js_node_stream_method_on( + handle(), + close_event.get_nanbox_f64(), + box_pointer(close.get_raw_const_ptr()), + ); - let _ = js_node_stream_method_end(handle, string_value("done")); - assert_eq!(js_node_stream_method_writable(handle).to_bits(), TAG_FALSE); + let done = scope.root_nanbox_f64(string_value("done")); + let _ = js_node_stream_method_end(handle(), done.get_nanbox_f64()); + assert_eq!( + js_node_stream_method_writable(handle()).to_bits(), + TAG_FALSE + ); assert_eq!( - js_node_stream_method_writable_ended(handle).to_bits(), + js_node_stream_method_writable_ended(handle()).to_bits(), TAG_TRUE ); assert_eq!( - js_node_stream_method_writable_finished(handle).to_bits(), + js_node_stream_method_writable_finished(handle()).to_bits(), TAG_FALSE ); let _ = crate::promise::js_promise_run_microtasks(); assert_eq!( - js_node_stream_method_writable_finished(handle).to_bits(), + js_node_stream_method_writable_finished(handle()).to_bits(), TAG_TRUE ); assert_eq!( - js_object_get_field_by_name_f64(obj, hidden_key(b"writableFinished")).to_bits(), + js_object_get_field_by_name_f64(object(), hidden_key(b"writableFinished")).to_bits(), TAG_TRUE ); assert_eq!( - js_object_get_field_by_name_f64(obj, hidden_key(b"closed")).to_bits(), + js_object_get_field_by_name_f64(object(), hidden_key(b"closed")).to_bits(), TAG_TRUE ); WRITABLE_FINISH_COUNT.with(|count| assert_eq!(*count.borrow(), 1)); @@ -1048,11 +1063,16 @@ fn stream_destroy_with_error_marks_errored_state() { hidden_key(b"destroy"), )); let err = scope.root_nanbox_f64(string_value("boom")); - let error_listener = box_pointer(js_closure_alloc(noop_listener as *const u8, 0) as *const u8); - let handle = raw_ptr_from_value(stream.get_nanbox_f64()) as i64; - let _ = js_node_stream_method_on(handle, string_value("error"), error_listener); + let error_listener = scope.root_raw_mut_ptr(js_closure_alloc(noop_listener as *const u8, 0)); + let error_event = scope.root_nanbox_f64(string_value("error")); + let handle = || raw_ptr_from_value(stream.get_nanbox_f64()) as i64; + let _ = js_node_stream_method_on( + handle(), + error_event.get_nanbox_f64(), + box_pointer(error_listener.get_raw_const_ptr()), + ); - assert_eq!(js_node_stream_method_errored(handle).to_bits(), TAG_NULL); + assert_eq!(js_node_stream_method_errored(handle()).to_bits(), TAG_NULL); let args = [err.get_nanbox_f64()]; let ret = unsafe { crate::closure::js_native_call_value(destroy.get_nanbox_f64(), args.as_ptr(), args.len()) @@ -1069,7 +1089,7 @@ fn stream_destroy_with_error_marks_errored_state() { TAG_TRUE ); assert_eq!( - js_node_stream_method_errored(handle).to_bits(), + js_node_stream_method_errored(handle()).to_bits(), err.get_nanbox_f64().to_bits() ); } @@ -1108,12 +1128,17 @@ fn readable_aborted_reflects_destroy_before_end() { let scope = crate::gc::RuntimeHandleScope::new(); let stream = scope.root_nanbox_f64(js_node_stream_readable_new(f64::from_bits(TAG_UNDEFINED))); let err = scope.root_nanbox_f64(string_value("abort")); - let error_listener = box_pointer(js_closure_alloc(noop_listener as *const u8, 0) as *const u8); - let handle = raw_ptr_from_value(stream.get_nanbox_f64()) as i64; - let _ = js_node_stream_method_on(handle, string_value("error"), error_listener); + let error_listener = scope.root_raw_mut_ptr(js_closure_alloc(noop_listener as *const u8, 0)); + let error_event = scope.root_nanbox_f64(string_value("error")); + let handle = || raw_ptr_from_value(stream.get_nanbox_f64()) as i64; + let _ = js_node_stream_method_on( + handle(), + error_event.get_nanbox_f64(), + box_pointer(error_listener.get_raw_const_ptr()), + ); assert_eq!( - js_node_stream_method_readable_aborted(handle).to_bits(), + js_node_stream_method_readable_aborted(handle()).to_bits(), TAG_FALSE ); assert_eq!( @@ -1125,9 +1150,9 @@ fn readable_aborted_reflects_destroy_before_end() { TAG_FALSE ); - let _ = js_node_stream_method_destroy(handle, err.get_nanbox_f64()); + let _ = js_node_stream_method_destroy(handle(), err.get_nanbox_f64()); assert_eq!( - js_node_stream_method_readable_aborted(handle).to_bits(), + js_node_stream_method_readable_aborted(handle()).to_bits(), TAG_TRUE ); assert_eq!( @@ -1140,18 +1165,23 @@ fn readable_aborted_reflects_destroy_before_end() { ); let _ = crate::promise::js_promise_run_microtasks(); assert_eq!( - js_node_stream_method_readable_aborted(handle).to_bits(), + js_node_stream_method_readable_aborted(handle()).to_bits(), TAG_TRUE ); let ended = scope.root_nanbox_f64(js_node_stream_readable_new(f64::from_bits(TAG_UNDEFINED))); - let error_listener = box_pointer(js_closure_alloc(noop_listener as *const u8, 0) as *const u8); - let ended_handle = raw_ptr_from_value(ended.get_nanbox_f64()) as i64; - let _ = js_node_stream_method_on(ended_handle, string_value("error"), error_listener); - let _ = js_node_stream_method_push(ended_handle, f64::from_bits(TAG_NULL)); - let _ = js_node_stream_method_destroy(ended_handle, err.get_nanbox_f64()); + let ended_error_listener = + scope.root_raw_mut_ptr(js_closure_alloc(noop_listener as *const u8, 0)); + let ended_handle = || raw_ptr_from_value(ended.get_nanbox_f64()) as i64; + let _ = js_node_stream_method_on( + ended_handle(), + error_event.get_nanbox_f64(), + box_pointer(ended_error_listener.get_raw_const_ptr()), + ); + let _ = js_node_stream_method_push(ended_handle(), f64::from_bits(TAG_NULL)); + let _ = js_node_stream_method_destroy(ended_handle(), err.get_nanbox_f64()); assert_eq!( - js_node_stream_method_readable_aborted(ended_handle).to_bits(), + js_node_stream_method_readable_aborted(ended_handle()).to_bits(), TAG_FALSE ); } @@ -1159,39 +1189,53 @@ fn readable_aborted_reflects_destroy_before_end() { #[test] fn stream_native_receiver_methods_update_hidden_state() { let scope = crate::gc::RuntimeHandleScope::new(); - let stream = scope.root_nanbox_f64(js_node_stream_passthrough_new(f64::from_bits( + let emitting = scope.root_nanbox_f64(js_node_stream_passthrough_new(f64::from_bits( TAG_UNDEFINED, ))); let err = scope.root_nanbox_f64(string_value("boom")); - let cb = box_pointer(js_closure_alloc(noop_listener as *const u8, 0) as *const u8); - let handle = raw_ptr_from_value(stream.get_nanbox_f64()) as i64; - let _ = js_node_stream_method_on(handle, string_value("error"), cb); + let error_event = scope.root_nanbox_f64(string_value("error")); + let emit_listener = scope.root_raw_mut_ptr(js_closure_alloc(noop_listener as *const u8, 0)); + let emitting_handle = || raw_ptr_from_value(emitting.get_nanbox_f64()) as i64; + let _ = js_node_stream_method_on( + emitting_handle(), + error_event.get_nanbox_f64(), + box_pointer(emit_listener.get_raw_const_ptr()), + ); assert_eq!( - js_node_stream_method_emit(handle, string_value("error"), err.get_nanbox_f64()).to_bits(), + js_node_stream_method_emit( + emitting_handle(), + error_event.get_nanbox_f64(), + err.get_nanbox_f64(), + ) + .to_bits(), TAG_TRUE ); - assert!(js_node_stream_hidden_error_after_read(stream.get_nanbox_f64()).is_some()); + assert!(js_node_stream_hidden_error_after_read(emitting.get_nanbox_f64()).is_some()); - let stream = scope.root_nanbox_f64(js_node_stream_passthrough_new(f64::from_bits( + let ended = scope.root_nanbox_f64(js_node_stream_passthrough_new(f64::from_bits( TAG_UNDEFINED, ))); - let handle = raw_ptr_from_value(stream.get_nanbox_f64()) as i64; - let _ = js_node_stream_method_end(handle, f64::from_bits(TAG_UNDEFINED)); + let ended_handle = || raw_ptr_from_value(ended.get_nanbox_f64()) as i64; + let _ = js_node_stream_method_end(ended_handle(), f64::from_bits(TAG_UNDEFINED)); assert!(js_node_stream_is_stub_ended_after_read( - stream.get_nanbox_f64() + ended.get_nanbox_f64() )); - let stream = scope.root_nanbox_f64(js_node_stream_passthrough_new(f64::from_bits( + let destroyed = scope.root_nanbox_f64(js_node_stream_passthrough_new(f64::from_bits( TAG_UNDEFINED, ))); - let cb = box_pointer(js_closure_alloc(noop_listener as *const u8, 0) as *const u8); - let handle = raw_ptr_from_value(stream.get_nanbox_f64()) as i64; - let _ = js_node_stream_method_on(handle, string_value("error"), cb); - let _ = js_node_stream_method_destroy(handle, err.get_nanbox_f64()); - assert!(readable_hidden_error(stream.get_nanbox_f64()).is_none()); + let destroy_listener = scope.root_raw_mut_ptr(js_closure_alloc(noop_listener as *const u8, 0)); + let destroyed_handle = || raw_ptr_from_value(destroyed.get_nanbox_f64()) as i64; + let _ = js_node_stream_method_on( + destroyed_handle(), + error_event.get_nanbox_f64(), + box_pointer(destroy_listener.get_raw_const_ptr()), + ); + let _ = js_node_stream_method_destroy(destroyed_handle(), err.get_nanbox_f64()); + assert!(readable_hidden_error(destroyed.get_nanbox_f64()).is_none()); let _ = crate::promise::js_promise_run_microtasks(); - assert!(js_node_stream_hidden_error_after_read(stream.get_nanbox_f64()).is_some()); + assert!(js_node_stream_hidden_error_after_read(destroyed.get_nanbox_f64()).is_some()); } #[test] From 591481cb9149ba4baeb11d9b93a9c891becab6df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 1 Aug 2026 07:29:33 +0200 Subject: [PATCH 7/8] fix(stream): remove obsolete pipeline options helper --- changelog.d/7067-node-stream-node-26-parity.md | 1 + crates/perry-runtime/src/node_stream_pipeline.rs | 10 ---------- 2 files changed, 1 insertion(+), 10 deletions(-) create mode 100644 changelog.d/7067-node-stream-node-26-parity.md diff --git a/changelog.d/7067-node-stream-node-26-parity.md b/changelog.d/7067-node-stream-node-26-parity.md new file mode 100644 index 0000000000..e8505c1ac5 --- /dev/null +++ b/changelog.d/7067-node-stream-node-26-parity.md @@ -0,0 +1 @@ +Completed Node.js 26.5 stream parity across classic streams, async iteration, iterator helpers, pipelines, composition, and Web Streams, including lifecycle, backpressure, cancellation, error propagation, and microtask ordering. diff --git a/crates/perry-runtime/src/node_stream_pipeline.rs b/crates/perry-runtime/src/node_stream_pipeline.rs index c0d2c7b3d4..1b5116388a 100644 --- a/crates/perry-runtime/src/node_stream_pipeline.rs +++ b/crates/perry-runtime/src/node_stream_pipeline.rs @@ -106,16 +106,6 @@ pub(super) fn is_pipeline_options_arg(value: f64) -> bool { && !is_array_like_value(value) } -pub(super) fn pipeline_options_from_arg(value: f64) -> PipelineOptions { - let end_final = get_hidden_value(value, hidden_key(b"end")) - .map(|v| v.to_bits() != TAG_FALSE) - .unwrap_or(true); - PipelineOptions { - end_final, - signal: options_signal(value), - } -} - pub(super) fn pipe_options_end(value: f64) -> bool { get_hidden_value(value, hidden_key(b"end")) .map(|v| v.to_bits() != TAG_FALSE) From 73f50f00d6d7bc8f1f7f479a1578bc4594bdea91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 1 Aug 2026 07:29:33 +0200 Subject: [PATCH 8/8] chore: bump version for PR 7067 --- CLAUDE.md | 2 +- Cargo.lock | 152 ++++++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- 3 files changed, 78 insertions(+), 78 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a642c4bad7..353cbb05ef 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.1266 +**Current Version:** 0.5.1267 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 8f7e7875d7..787d48bfd5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5503,7 +5503,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "anyhow", "base64", @@ -5563,14 +5563,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "cc", "libc", @@ -5578,7 +5578,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "anyhow", "log", @@ -5592,7 +5592,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "anyhow", "perry-hir", @@ -5600,7 +5600,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "anyhow", "perry-hir", @@ -5608,7 +5608,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "anyhow", "perry-dispatch", @@ -5617,7 +5617,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "anyhow", "perry-hir", @@ -5625,7 +5625,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "anyhow", "base64", @@ -5637,7 +5637,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "anyhow", "perry-hir", @@ -5645,7 +5645,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "anyhow", "async-trait", @@ -5674,14 +5674,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "serde", "serde_json", @@ -5689,7 +5689,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1266" +version = "0.5.1267" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5700,7 +5700,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "anyhow", "clap", @@ -5715,7 +5715,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "block2", "objc2", @@ -5725,7 +5725,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "argon2", "perry-ffi", @@ -5733,7 +5733,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "perry-ffi", "reqwest", @@ -5742,7 +5742,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "bcrypt", "perry-ffi", @@ -5750,7 +5750,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "perry-ffi", "rusqlite", @@ -5758,7 +5758,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "perry-ffi", "scraper", @@ -5766,7 +5766,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "perry-ffi", "perry-runtime", @@ -5774,7 +5774,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "chrono", "cron", @@ -5784,7 +5784,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "chrono", "perry-ffi", @@ -5792,7 +5792,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "perry-ffi", "rust_decimal", @@ -5800,7 +5800,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "perry-ffi", "serde_json", @@ -5808,7 +5808,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5816,7 +5816,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "perry-ffi", "perry-runtime", @@ -5824,14 +5824,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "bytes", "http-body-util", @@ -5849,7 +5849,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "bytes", "lazy_static", @@ -5862,7 +5862,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "bytes", "h2", @@ -5886,7 +5886,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "lazy_static", "perry-ffi", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "base64", "jsonwebtoken", @@ -5907,7 +5907,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "lru", "perry-ffi", @@ -5915,7 +5915,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "chrono", "perry-ffi", @@ -5923,7 +5923,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "bson", "futures-util", @@ -5935,7 +5935,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "chrono", "perry-ffi", @@ -5945,7 +5945,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "nanoid", "perry-ffi", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "bytes", "perry-ffi", @@ -5967,7 +5967,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -5986,7 +5986,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "lettre", "perry-ffi", @@ -5996,7 +5996,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "perry-ffi", "printpdf", @@ -6004,7 +6004,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "perry-ffi", "sqlx", @@ -6013,7 +6013,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "governor", "perry-ffi", @@ -6021,7 +6021,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "fast_image_resize", "image", @@ -6031,14 +6031,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "lazy_static", "perry-ffi", @@ -6047,7 +6047,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "perry-ffi", "perry-runtime", @@ -6056,7 +6056,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "perry-ffi", "uuid", @@ -6064,7 +6064,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "perry-ffi", "regex", @@ -6074,7 +6074,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "futures-util", "lazy_static", @@ -6087,7 +6087,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "brotli", "flate2", @@ -6097,7 +6097,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "dashmap", "once_cell", @@ -6106,7 +6106,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "anyhow", "perry-api-manifest", @@ -6124,7 +6124,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "anyhow", "perry-diagnostics", @@ -6136,7 +6136,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "anyhow", "base64", @@ -6177,14 +6177,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6279,14 +6279,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "anyhow", "perry-hir", @@ -6295,14 +6295,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "base64", "itoa", @@ -6319,7 +6319,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "rand 0.10.1", "serde", @@ -6329,7 +6329,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6352,7 +6352,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "base64", "block2", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "base64", "block2", @@ -6383,7 +6383,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1266" +version = "0.5.1267" [[package]] name = "perry-ui-test" @@ -6394,11 +6394,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1266" +version = "0.5.1267" [[package]] name = "perry-ui-tvos" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "base64", "block2", @@ -6414,7 +6414,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "base64", "block2", @@ -6430,7 +6430,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "block2", "libc", @@ -6443,7 +6443,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "base64", "libc", @@ -6460,14 +6460,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "anyhow", "base64", @@ -6483,7 +6483,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1266" +version = "0.5.1267" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 19b4d94ebb..cd9cad1cbd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -292,7 +292,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1266" +version = "0.5.1267" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry"