From f068ecd7843eef0a9ce13f7f3c98555e1111d333 Mon Sep 17 00:00:00 2001 From: Andrew DiZenzo <59515127+andrewtdiz@users.noreply.github.com> Date: Thu, 28 May 2026 08:33:52 -0700 Subject: [PATCH] fix(node:stream): honor Readable.from options --- .../src/lower_call/native_table/net_events.rs | 4 +-- .../src/runtime_decls/stdlib_ffi.rs | 5 ++++ crates/perry-runtime/src/node_stream.rs | 25 +++++++++++++++- .../src/node_stream_keepalive.rs | 3 ++ .../src/node_stream_tests_extra.rs | 29 +++++++++++++++++++ test-parity/known_failures.json | 12 -------- 6 files changed, 63 insertions(+), 15 deletions(-) diff --git a/crates/perry-codegen/src/lower_call/native_table/net_events.rs b/crates/perry-codegen/src/lower_call/native_table/net_events.rs index 32702ec22..af3ba18a2 100644 --- a/crates/perry-codegen/src/lower_call/native_table/net_events.rs +++ b/crates/perry-codegen/src/lower_call/native_table/net_events.rs @@ -686,8 +686,8 @@ pub(super) const NET_EVENTS_ROWS: &[NativeModSig] = &[ has_receiver: false, method: "from", class_filter: None, - runtime: "js_node_stream_readable_from", - args: &[NA_F64], + runtime: "js_node_stream_readable_from_options", + args: &[NA_F64, NA_F64], ret: NR_F64, }, // #1534: static introspection helpers `isDisturbed` and diff --git a/crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs b/crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs index 47b1126ed..a74242eb3 100644 --- a/crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs +++ b/crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs @@ -869,6 +869,11 @@ pub fn declare_stdlib_ffi(module: &mut LlModule) { module.declare_function("js_node_stream_transform_new", DOUBLE, &[DOUBLE]); module.declare_function("js_node_stream_passthrough_new", DOUBLE, &[DOUBLE]); module.declare_function("js_node_stream_readable_from", DOUBLE, &[DOUBLE]); + module.declare_function( + "js_node_stream_readable_from_options", + DOUBLE, + &[DOUBLE, DOUBLE], + ); // #1534: static introspection helpers reflecting tracked stream state. module.declare_function("js_node_stream_is_disturbed", DOUBLE, &[DOUBLE]); module.declare_function("js_node_stream_is_errored", DOUBLE, &[DOUBLE]); diff --git a/crates/perry-runtime/src/node_stream.rs b/crates/perry-runtime/src/node_stream.rs index da2818917..e69b3b477 100644 --- a/crates/perry-runtime/src/node_stream.rs +++ b/crates/perry-runtime/src/node_stream.rs @@ -2994,6 +2994,24 @@ fn normalize_readable_from_input(iterable: f64) -> f64 { box_pointer(arr as *const u8) } +fn readable_from_options(opts: f64) -> f64 { + let merged = crate::object::js_object_alloc(0, 2); + let object_mode = !get_hidden_value(opts, hidden_key(b"objectMode")) + .is_some_and(|v| v.to_bits() == TAG_FALSE); + set_hidden_value( + box_pointer(merged as *const u8), + hidden_key(b"objectMode"), + f64::from_bits(if object_mode { TAG_TRUE } else { TAG_FALSE }), + ); + let hwm = opt_number(opts, b"highWaterMark").unwrap_or(1.0); + set_hidden_value( + box_pointer(merged as *const u8), + hidden_key(b"highWaterMark"), + hwm, + ); + box_pointer(merged as *const u8) +} + fn append_string_bytes(value: f64, out: &mut Vec) { let ptr = crate::value::js_get_string_pointer_unified(value) as *const crate::StringHeader; append_string_ptr_bytes(ptr, out); @@ -3741,12 +3759,17 @@ pub extern "C" fn js_node_stream_passthrough_new(opts: f64) -> f64 { /// `node:stream/consumers` can drain the current stub stream surface. #[no_mangle] pub extern "C" fn js_node_stream_readable_from(iterable: f64) -> f64 { + js_node_stream_readable_from_options(iterable, f64::from_bits(TAG_UNDEFINED)) +} + +#[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(); } - let readable = js_node_stream_readable_new(f64::from_bits(TAG_UNDEFINED)); + let readable = js_node_stream_readable_new(readable_from_options(opts)); let raw = raw_ptr_from_value(readable); if raw >= 0x10000 { let chunks = normalize_readable_from_input(iterable); diff --git a/crates/perry-runtime/src/node_stream_keepalive.rs b/crates/perry-runtime/src/node_stream_keepalive.rs index 77197669e..2df04b7a2 100644 --- a/crates/perry-runtime/src/node_stream_keepalive.rs +++ b/crates/perry-runtime/src/node_stream_keepalive.rs @@ -142,6 +142,9 @@ static KEEP_NS_PASSTHROUGH_NEW: extern "C" fn(f64) -> f64 = super::js_node_strea #[used] static KEEP_NS_READABLE_FROM: extern "C" fn(f64) -> f64 = super::js_node_stream_readable_from; #[used] +static KEEP_NS_READABLE_FROM_OPTIONS: extern "C" fn(f64, f64) -> f64 = + super::js_node_stream_readable_from_options; +#[used] static KEEP_NS_IS_DISTURBED: extern "C" fn(f64) -> f64 = super::js_node_stream_is_disturbed; #[used] static KEEP_NS_IS_ERRORED: extern "C" fn(f64) -> f64 = super::js_node_stream_is_errored; diff --git a/crates/perry-runtime/src/node_stream_tests_extra.rs b/crates/perry-runtime/src/node_stream_tests_extra.rs index 15b1e24b6..84aff27b6 100644 --- a/crates/perry-runtime/src/node_stream_tests_extra.rs +++ b/crates/perry-runtime/src/node_stream_tests_extra.rs @@ -623,6 +623,35 @@ fn stream_object_mode_fields_reflect_defaults_and_options() { ); } +#[test] +fn readable_from_uses_node_object_mode_and_high_water_mark_defaults() { + let mut default_chunks = crate::array::js_array_alloc(1); + default_chunks = crate::array::js_array_push_f64(default_chunks, string_value("a")); + let default_readable = js_node_stream_readable_from(box_pointer(default_chunks as *const u8)); + let default_handle = raw_ptr_from_value(default_readable) as i64; + assert_eq!( + js_node_stream_method_readable_object_mode(default_handle).to_bits(), + TAG_TRUE + ); + assert_eq!(js_node_stream_method_readable_hwm(default_handle), 1.0); + + let mut byte_chunks = crate::array::js_array_alloc(1); + byte_chunks = crate::array::js_array_push_f64(byte_chunks, string_value("b")); + let opts = crate::object::js_object_alloc(0, 2); + js_object_set_field_by_name(opts, hidden_key(b"objectMode"), f64::from_bits(TAG_FALSE)); + js_object_set_field_by_name(opts, hidden_key(b"highWaterMark"), 1.0); + let byte_readable = js_node_stream_readable_from_options( + box_pointer(byte_chunks as *const u8), + box_pointer(opts as *const u8), + ); + let byte_handle = raw_ptr_from_value(byte_readable) as i64; + assert_eq!( + js_node_stream_method_readable_object_mode(byte_handle).to_bits(), + TAG_FALSE + ); + assert_eq!(js_node_stream_method_readable_hwm(byte_handle), 1.0); +} + #[test] fn writable_corked_counter_tracks_cork_balance() { let stream = js_node_stream_writable_new(f64::from_bits(TAG_UNDEFINED)); diff --git a/test-parity/known_failures.json b/test-parity/known_failures.json index b14ed558c..e5d04e798 100644 --- a/test-parity/known_failures.json +++ b/test-parity/known_failures.json @@ -596,12 +596,6 @@ "category": "bug-open", "reason": "node:stream: pipe() called twice with same destination duplicates writes (should be no-op for second call). Flips to PASS when #1532 lands." }, - "node-suite/stream/readable/from-with-options": { - "issue": "1532", - "added": "2026-05-24", - "category": "bug-open", - "reason": "node:stream: Readable.from(iter, options) — 2nd-arg stream options (objectMode/highWaterMark) ignored. Flips to PASS when #1532 lands." - }, "node-suite/stream/pipe/end-false-option": { "issue": "1532", "added": "2026-05-24", @@ -1352,12 +1346,6 @@ "category": "bug-open", "reason": "node:stream: compose(string) — does not throw TypeError. Flips to PASS when #1531 lands." }, - "node-suite/stream/readable/static-from-with-options": { - "issue": "1532", - "added": "2026-05-24", - "category": "bug-open", - "reason": "node:stream: Readable.from(iter, opts) — objectMode/hwm 2nd-arg not applied. Flips to PASS when #1532 lands." - }, "node-suite/stream/compose/two-stage-error-stops": { "issue": "1531", "added": "2026-05-24",