From 7972558862cf0d336623ebeb8f477b181b57954a Mon Sep 17 00:00:00 2001 From: Andrew DiZenzo Date: Thu, 4 Jun 2026 16:50:25 +0000 Subject: [PATCH] fix(runtime): align Atomics wait notify receivers --- crates/perry-runtime/src/atomics.rs | 58 +++++++++++++++++-- crates/perry-runtime/src/typedarray/mod.rs | 19 ++++++ crates/perry-runtime/src/typedarray_view.rs | 3 + .../globals/atomics-wait-notify-receivers.ts | 37 ++++++++++++ 4 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 test-parity/node-suite/globals/atomics-wait-notify-receivers.ts diff --git a/crates/perry-runtime/src/atomics.rs b/crates/perry-runtime/src/atomics.rs index 8c2e674979..f3b73cc114 100644 --- a/crates/perry-runtime/src/atomics.rs +++ b/crates/perry-runtime/src/atomics.rs @@ -74,6 +74,17 @@ impl AtomicView { bigint_integer_kind(self.kind()) } + fn has_shared_backing(&self) -> bool { + match self { + AtomicView::TypedArray { ptr, .. } => { + crate::typedarray::typed_array_has_shared_backing(*ptr) + } + AtomicView::Uint8ArrayBuffer(ptr) => { + crate::buffer::is_shared_array_buffer(*ptr as usize) + } + } + } + fn length(&self) -> i32 { match self { AtomicView::TypedArray { ptr, .. } => js_typed_array_length(*ptr), @@ -161,6 +172,24 @@ fn atomics_int32_view_arg(value: f64) -> AtomicView { throw_type_error(b"Atomics wait/notify requires an Int32Array"); } +fn atomics_wait_notify_view_arg(value: f64) -> AtomicView { + let js = JSValue::from_bits(value.to_bits()); + if !js.is_pointer() { + throw_type_error(b"Atomics wait/notify requires an Int32Array or BigInt64Array"); + } + let raw = clean_ta_ptr(js.as_pointer::()) as usize; + if raw == 0 { + throw_type_error(b"Atomics wait/notify requires an Int32Array or BigInt64Array"); + } + match lookup_typed_array_kind(raw) { + Some(kind @ (KIND_INT32 | KIND_BIGINT64)) => AtomicView::TypedArray { + ptr: raw as *mut TypedArrayHeader, + kind, + }, + _ => throw_type_error(b"Atomics wait/notify requires an Int32Array or BigInt64Array"), + } +} + fn atomics_to_index(index: f64, length: i32) -> i32 { let mut n = JSValue::from_bits(index.to_bits()).to_number(); if n.is_nan() { @@ -375,6 +404,12 @@ fn int32_slot(view: f64, index: f64) -> (AtomicView, i32) { (view, idx) } +fn wait_notify_slot(view: f64, index: f64) -> (AtomicView, i32) { + let view = atomics_wait_notify_view_arg(view); + let idx = atomics_to_index(index, view.length()); + (view, idx) +} + fn wait_async_result(async_value: bool, value: f64) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); let obj = crate::object::js_object_alloc(0, 2); @@ -581,8 +616,11 @@ pub extern "C" fn js_atomics_notify( index: f64, count: f64, ) -> f64 { - let (_view, _idx) = int32_slot(view, index); + let (view, _idx) = wait_notify_slot(view, index); let _ = numeric_arg(count); + if !view.has_shared_backing() { + return 0.0; + } 0.0 } @@ -594,10 +632,20 @@ pub extern "C" fn js_atomics_wait( expected: f64, timeout: f64, ) -> f64 { - let (view, idx) = int32_slot(view, index); - let expected = coerce_for_kind(KIND_INT32, expected); - if view.get_numeric(idx) != expected { - return string_value(b"not-equal"); + let (view, idx) = wait_notify_slot(view, index); + if !view.has_shared_backing() { + throw_type_error(b"Atomics.wait requires a shared typed array"); + } + if view.kind() == KIND_BIGINT64 { + let expected = bigint_bits(expected); + if view.get_bigint_bits(idx) != expected { + return string_value(b"not-equal"); + } + } else { + let expected = coerce_for_kind(KIND_INT32, expected); + if view.get_numeric(idx) != expected { + return string_value(b"not-equal"); + } } let _ = number_arg(timeout); diff --git a/crates/perry-runtime/src/typedarray/mod.rs b/crates/perry-runtime/src/typedarray/mod.rs index f6318fad14..67b0f7850e 100644 --- a/crates/perry-runtime/src/typedarray/mod.rs +++ b/crates/perry-runtime/src/typedarray/mod.rs @@ -142,6 +142,11 @@ thread_local! { /// (~1.0% leaf samples on perf-comprehensive). static TYPED_ARRAY_REGISTRY: RefCell> = RefCell::new(crate::fast_hash::new_ptr_hash_map()); + /// Perry currently materializes typed-array views over ArrayBuffer storage + /// as owning TypedArrayHeader values. Track which views came from + /// SharedArrayBuffer so Atomics.wait can apply Node's shared-buffer guard. + static TYPED_ARRAY_SHARED_BACKING: RefCell> = + RefCell::new(crate::fast_hash::new_ptr_hash_set()); } pub fn register_typed_array(ptr: *const TypedArrayHeader, kind: u8) { @@ -155,6 +160,9 @@ pub fn unregister_typed_array(ptr: *const TypedArrayHeader) { TYPED_ARRAY_REGISTRY.with(|r| { r.borrow_mut().remove(&owner); }); + TYPED_ARRAY_SHARED_BACKING.with(|r| { + r.borrow_mut().remove(&owner); + }); crate::typedarray_props::typed_array_clear_own_props(owner); } @@ -164,6 +172,17 @@ pub fn lookup_typed_array_kind(addr: usize) -> Option { TYPED_ARRAY_REGISTRY.with(|r| r.borrow().get(&addr).copied()) } +pub(crate) fn mark_typed_array_shared_backing(ptr: *const TypedArrayHeader) { + TYPED_ARRAY_SHARED_BACKING.with(|r| { + r.borrow_mut().insert(ptr as usize); + }); +} + +pub(crate) fn typed_array_has_shared_backing(ptr: *const TypedArrayHeader) -> bool { + let ptr = clean_ta_ptr(ptr); + TYPED_ARRAY_SHARED_BACKING.with(|r| r.borrow().contains(&(ptr as usize))) +} + #[inline] fn strip_nanbox(p: u64) -> usize { let top16 = p >> 48; diff --git a/crates/perry-runtime/src/typedarray_view.rs b/crates/perry-runtime/src/typedarray_view.rs index 7a1904f197..20c7659cb9 100644 --- a/crates/perry-runtime/src/typedarray_view.rs +++ b/crates/perry-runtime/src/typedarray_view.rs @@ -109,6 +109,9 @@ pub extern "C" fn js_typed_array_view( // so copy the backing bytes at `offset` directly to preserve element values. let count = elem_count.max(0) as u32; let ta = typed_array_alloc(kind, count); + if crate::buffer::is_shared_array_buffer(addr) { + crate::typedarray::mark_typed_array_shared_backing(ta); + } if count > 0 { unsafe { let src_data = crate::buffer::buffer_data(src).add(offset as usize); diff --git a/test-parity/node-suite/globals/atomics-wait-notify-receivers.ts b/test-parity/node-suite/globals/atomics-wait-notify-receivers.ts new file mode 100644 index 0000000000..e46868e984 --- /dev/null +++ b/test-parity/node-suite/globals/atomics-wait-notify-receivers.ts @@ -0,0 +1,37 @@ +// @ts-nocheck +function show(label, value) { + console.log(label + ":" + String(value)); +} + +function showErr(label, fn) { + try { + show(label, fn()); + } catch (err) { + console.log(label + ":" + err.name); + } +} + +const sab = new SharedArrayBuffer(8); +const ab = new ArrayBuffer(8); + +const sharedI32 = new Int32Array(sab); +const localI32 = new Int32Array(ab); +const sharedBigI64 = new BigInt64Array(sab); +const localBigI64 = new BigInt64Array(ab); +const sharedBigU64 = new BigUint64Array(sab); + +show("wait i32 shared mismatch", Atomics.wait(sharedI32, 0, 1, 0)); +showErr("wait i32 nonshared", () => Atomics.wait(localI32, 0, 0, 0)); +show("notify i32 nonshared", Atomics.notify(localI32, 0)); +showErr("notify i32 nonshared oob", () => Atomics.notify(localI32, 9)); + +show("wait b64 shared mismatch", Atomics.wait(sharedBigI64, 0, 1n, 0)); +show("wait b64 shared timeout", Atomics.wait(sharedBigI64, 0, 0n, 0)); +showErr("wait b64 number expected", () => Atomics.wait(sharedBigI64, 0, 0, 0)); +showErr("wait b64 nonshared", () => Atomics.wait(localBigI64, 0, 0n, 0)); +show("notify b64 shared", Atomics.notify(sharedBigI64, 0)); +show("notify b64 nonshared", Atomics.notify(localBigI64, 0)); +showErr("notify b64 nonshared oob", () => Atomics.notify(localBigI64, 9)); + +showErr("wait bu64 shared", () => Atomics.wait(sharedBigU64, 0, 0n, 0)); +showErr("notify bu64 shared", () => Atomics.notify(sharedBigU64, 0));