From 9ae3e4f0b90a293d8411a9d61a3cb8562144d2fc Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Thu, 7 May 2026 11:34:10 -0300 Subject: [PATCH 01/16] Remove thread from WaitAsyncJob --- .../structured_data/atomics_object.rs | 116 +++++++++--------- .../src/ecmascript/types/spec/data_block.rs | 6 +- 2 files changed, 60 insertions(+), 62 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs index 1a530432b..4d68e938f 100644 --- a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs +++ b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs @@ -6,8 +6,7 @@ use std::{ hint::assert_unchecked, ops::ControlFlow, sync::Arc, - thread::{self, JoinHandle}, - time::Duration, + time::{Duration, Instant}, }; use ecmascript_atomics::Ordering; @@ -1621,52 +1620,65 @@ fn create_wait_result_object<'gc>( .expect("Should perform GC here") } -#[derive(Debug)] struct WaitAsyncJobInner { + data_block: SharedDataBlock, + byte_index_in_buffer: usize, + waiter_record: Arc, promise_to_resolve: Global>, - join_handle: JoinHandle, - _has_timeout: bool, + created_at: Instant, + t: u64, } -#[derive(Debug)] #[repr(transparent)] pub(crate) struct WaitAsyncJob(Box); impl WaitAsyncJob { pub(crate) fn is_finished(&self) -> bool { - self.0.join_handle.is_finished() + let is_notified = self.0.waiter_record.is_notified(); + let timeout_expired = + self.0.t != u64::MAX && self.0.created_at.elapsed() >= Duration::from_millis(self.0.t); + is_notified || timeout_expired } pub(crate) fn _will_halt(&self) -> bool { - self.0._has_timeout + self.0.t != u64::MAX } - // NOTE: The reason for using `GcScope` here even though we could've gotten - // away with `NoGcScope` is that this is essentially a trait impl method, - // but currently without the trait. The job trait will be added eventually - // and we can get rid of this lint exception. - #[allow(unknown_lints, can_use_no_gc_scope)] - pub(crate) fn run<'gc>(self, agent: &mut Agent, gc: GcScope) -> JsResult<'gc, ()> { + pub(crate) fn run<'gc>(self, agent: &mut Agent, gc: GcScope<'gc, '_>) -> JsResult<'gc, ()> { let gc = gc.into_nogc(); - let promise = self.0.promise_to_resolve.take(agent).bind(gc); - let Ok(result) = self.0.join_handle.join() else { - // Foreign thread died; we can never resolve. - return Ok(()); - }; + + // SAFETY: buffer is a cloned SharedDataBlock; non-dangling. + let waiters = unsafe { self.0.data_block.get_or_init_waiters() }; // a. Perform EnterCriticalSection(WL). - // b. If WL.[[Waiters]] contains waiterRecord, then - // i. Let timeOfJobExecution be the time value (UTC) identifying the current time. - // ii. Assert: ℝ(timeOfJobExecution) ≥ waiterRecord.[[TimeoutTime]] (ignoring potential non-monotonicity of time values). - // iii. Set waiterRecord.[[Result]] to "timed-out". - // iv. Perform RemoveWaiter(WL, waiterRecord). - // v. Perform NotifyWaiter(WL, waiterRecord). - // c. Perform LeaveCriticalSection(WL). - let promise_capability = PromiseCapability::from_promise(promise, true); - let result = match result { - WaitResult::Ok => BUILTIN_STRING_MEMORY.ok.into(), - WaitResult::TimedOut => BUILTIN_STRING_MEMORY.timed_out.into(), - }; - unwrap_try(promise_capability.try_resolve(agent, result, gc)); + let mut guard = waiters.lock().unwrap(); + + if self.0.waiter_record.is_notified() { + let promise = self.0.promise_to_resolve.take(agent).bind(gc); + let promise_capability = PromiseCapability::from_promise(promise, true); + unwrap_try(promise_capability.try_resolve(agent, BUILTIN_STRING_MEMORY.ok.into(), gc)); + drop(guard); + } else { + // b. If WL.[[Waiters]] contains waiterRecord, then + // i. Let timeOfJobExecution be the time value (UTC) identifying the current time. + // ii. Assert: ℝ(timeOfJobExecution) ≥ waiterRecord.[[TimeoutTime]] (ignoring potential non-monotonicity of time values). + // iii. Set waiterRecord.[[Result]] to "timed-out". + // iv. Perform RemoveWaiter(WL, waiterRecord). + // v. Perform NotifyWaiter(WL, waiterRecord). + + guard.remove_from_list(self.0.byte_index_in_buffer, self.0.waiter_record); + + let promise = self.0.promise_to_resolve.take(agent).bind(gc); + let promise_capability = PromiseCapability::from_promise(promise, true); + unwrap_try(promise_capability.try_resolve( + agent, + BUILTIN_STRING_MEMORY.timed_out.into(), + gc, + )); + + // c. Perform LeaveCriticalSection(WL). + drop(guard); + } + // d. Return unused. Ok(()) } @@ -1689,40 +1701,22 @@ fn enqueue_atomics_wait_async_job( // 1. Let timeoutJob be a new Job Abstract Closure with no parameters that // captures WL and waiterRecord and performs the following steps when // called: - let handle = thread::spawn(move || { - // SAFETY: buffer is a cloned SharedDataBlock; non-dangling. - let waiters = unsafe { data_block.get_or_init_waiters() }; - let mut guard = waiters.lock().unwrap(); - - if t == u64::MAX { - waiter_record.wait(guard); - } else { - let dur = Duration::from_millis(t); - let (new_guard, timeout) = waiter_record.wait_timeout(guard, dur); - guard = new_guard; - if timeout.timed_out() { - guard.remove_from_list(byte_index_in_buffer, waiter_record); - - // 31. Perform LeaveCriticalSection(WL). - drop(guard); - - // 32. If mode is sync, return waiterRecord.[[Result]]. - return WaitResult::TimedOut; - } - } - WaitResult::Ok - }); + // 2. Let now be the time value (UTC) identifying the current time. + // 3. Let currentRealm be the current Realm Record. + // 4. Perform HostEnqueueTimeoutJob(timeoutJob, currentRealm, 𝔽(waiterRecord.[[TimeoutTime]]) - now). let wait_async_job = Job { realm: Some(Global::new(agent, agent.current_realm(gc).unbind())), inner: InnerJob::WaitAsync(WaitAsyncJob(Box::new(WaitAsyncJobInner { + data_block, + byte_index_in_buffer, + waiter_record, promise_to_resolve: promise, - join_handle: handle, - _has_timeout: t != u64::MAX, + t, + created_at: Instant::now(), }))), }; - // 2. Let now be the time value (UTC) identifying the current time. - // 3. Let currentRealm be the current Realm Record. - // 4. Perform HostEnqueueTimeoutJob(timeoutJob, currentRealm, 𝔽(waiterRecord.[[TimeoutTime]]) - now). - agent.host_hooks.enqueue_generic_job(wait_async_job); + agent.host_hooks.enqueue_timeout_job(wait_async_job, t); + // agent.host_hooks.enqueue_generic_job(wait_async_job); + // 5. Return unused. } diff --git a/nova_vm/src/ecmascript/types/spec/data_block.rs b/nova_vm/src/ecmascript/types/spec/data_block.rs index 5efb6cfdd..189aea3c2 100644 --- a/nova_vm/src/ecmascript/types/spec/data_block.rs +++ b/nova_vm/src/ecmascript/types/spec/data_block.rs @@ -426,7 +426,7 @@ impl SharedDataBlockMaxByteLength { } #[cfg(feature = "shared-array-buffer")] -#[derive(Default)] +#[derive(Default, Debug)] pub(crate) struct WaiterRecord { condvar: Condvar, notified: AtomicBool, @@ -472,6 +472,10 @@ impl WaiterRecord { ), } } + + pub(crate) fn is_notified(&self) -> bool { + self.notified.load(Ordering::Relaxed) + } } /// Result of an `Atomics.wait` or `Atomics.waitAsync` operation. From 7176102f5917bf2a69a83e34bca7002d67f3a7a2 Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Thu, 7 May 2026 11:41:38 -0300 Subject: [PATCH 02/16] Remove unused code --- .../builtins/structured_data/atomics_object.rs | 14 +++++++------- nova_vm/src/ecmascript/types/spec/data_block.rs | 8 -------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs index 4d68e938f..3098d2ed8 100644 --- a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs +++ b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs @@ -17,13 +17,13 @@ use crate::{ BigInt, Builtin, ExceptionType, InnerJob, Job, JsResult, Number, Numeric, OrdinaryObject, Promise, PromiseCapability, Realm, SharedArrayBuffer, SharedDataBlock, SharedTypedArray, String, TryError, TryResult, TypedArrayAbstractOperations, - TypedArrayWithBufferWitnessRecords, Value, WaitResult, WaiterRecord, - builders::OrdinaryObjectBuilder, compare_exchange_in_buffer, for_any_typed_array, - get_modify_set_value_in_buffer, get_value_from_buffer, - make_typed_array_with_buffer_witness_record, number_convert_to_integer_or_infinity, - set_value_in_buffer, to_big_int, to_big_int64, to_big_int64_big_int, to_index, to_int32, - to_int32_number, to_integer_number_or_infinity, to_integer_or_infinity, to_number, - try_result_into_js, try_to_index, unwrap_try, validate_index, validate_typed_array, + TypedArrayWithBufferWitnessRecords, Value, WaiterRecord, builders::OrdinaryObjectBuilder, + compare_exchange_in_buffer, for_any_typed_array, get_modify_set_value_in_buffer, + get_value_from_buffer, make_typed_array_with_buffer_witness_record, + number_convert_to_integer_or_infinity, set_value_in_buffer, to_big_int, to_big_int64, + to_big_int64_big_int, to_index, to_int32, to_int32_number, to_integer_number_or_infinity, + to_integer_or_infinity, to_number, try_result_into_js, try_to_index, unwrap_try, + validate_index, validate_typed_array, }, engine::{Bindable, GcScope, Global, NoGcScope, Scopable}, heap::{ObjectEntry, WellKnownSymbols}, diff --git a/nova_vm/src/ecmascript/types/spec/data_block.rs b/nova_vm/src/ecmascript/types/spec/data_block.rs index 189aea3c2..de1c2a12b 100644 --- a/nova_vm/src/ecmascript/types/spec/data_block.rs +++ b/nova_vm/src/ecmascript/types/spec/data_block.rs @@ -478,14 +478,6 @@ impl WaiterRecord { } } -/// Result of an `Atomics.wait` or `Atomics.waitAsync` operation. -#[derive(Debug)] -#[cfg(feature = "shared-array-buffer")] -pub(crate) enum WaitResult { - Ok, - TimedOut, -} - #[cfg(feature = "shared-array-buffer")] #[derive(Default)] #[repr(transparent)] From 9bc9dd7137fe108ad6bb09376fa8e8617926aaaa Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Thu, 7 May 2026 13:16:18 -0300 Subject: [PATCH 03/16] Implement `enqueue_timeout_job` --- nova_cli/src/lib/child_hooks.rs | 21 ++++++++++++++------- nova_cli/src/lib/host_hooks.rs | 29 +++++++++++++++++++++-------- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/nova_cli/src/lib/child_hooks.rs b/nova_cli/src/lib/child_hooks.rs index 343d21934..06957b469 100644 --- a/nova_cli/src/lib/child_hooks.rs +++ b/nova_cli/src/lib/child_hooks.rs @@ -10,7 +10,7 @@ use std::{ collections::VecDeque, sync::{atomic::AtomicBool, mpsc}, thread, - time::Duration, + time::{Duration, Instant}, }; use nova_vm::ecmascript::{HostHooks, Job}; @@ -19,7 +19,7 @@ use crate::{ChildToHostMessage, HostToChildMessage}; pub struct CliChildHooks { promise_job_queue: RefCell>, - macrotask_queue: RefCell>, + macrotask_queue: RefCell, Job)>>, pub(crate) receiver: mpsc::Receiver, pub(crate) host_sender: mpsc::SyncSender, ready_to_leave: AtomicBool, @@ -78,9 +78,11 @@ impl CliChildHooks { let mut counter = 0u8; while !off_thread_job_queue.is_empty() { counter = counter.wrapping_add(1); - for (i, job) in off_thread_job_queue.iter().enumerate() { - if job.is_finished() { - let job = off_thread_job_queue.swap_remove(i); + let now = Instant::now(); + for (i, (deadline, job)) in off_thread_job_queue.iter().enumerate() { + let deadline_reached = deadline.map_or(true, |d| now >= d); + if deadline_reached && job.is_finished() { + let (_, job) = off_thread_job_queue.swap_remove(i); return Some(job); } } @@ -96,14 +98,19 @@ impl CliChildHooks { impl HostHooks for CliChildHooks { fn enqueue_generic_job(&self, job: Job) { - self.macrotask_queue.borrow_mut().push(job); + self.macrotask_queue.borrow_mut().push((None, job)); } fn enqueue_promise_job(&self, job: Job) { self.promise_job_queue.borrow_mut().push_back(job); } - fn enqueue_timeout_job(&self, _timeout_job: Job, _milliseconds: u64) {} + fn enqueue_timeout_job(&self, timeout_job: Job, milliseconds: u64) { + let deadline = Instant::now() + Duration::from_millis(milliseconds); + self.macrotask_queue + .borrow_mut() + .push((Some(deadline), timeout_job)); + } fn get_host_data(&self) -> &dyn std::any::Any { self diff --git a/nova_cli/src/lib/host_hooks.rs b/nova_cli/src/lib/host_hooks.rs index 71444afaa..545cf5b23 100644 --- a/nova_cli/src/lib/host_hooks.rs +++ b/nova_cli/src/lib/host_hooks.rs @@ -5,8 +5,14 @@ //! The [`HostHooks`] implementation for the main thread. use std::{ - cell::RefCell, collections::VecDeque, fmt::Debug, path::PathBuf, rc::Rc, sync::mpsc, thread, - time::Duration, + cell::RefCell, + collections::VecDeque, + fmt::Debug, + path::PathBuf, + rc::Rc, + sync::mpsc, + thread, + time::{Duration, Instant}, }; use nova_vm::{ @@ -29,7 +35,7 @@ pub enum ChildToHostMessage { pub struct CliHostHooks { promise_job_queue: RefCell>, - macrotask_queue: RefCell>, + macrotask_queue: RefCell, Job)>>, pub(crate) receiver: mpsc::Receiver, pub(crate) own_sender: mpsc::SyncSender, pub(crate) child_senders: RefCell>>, @@ -83,9 +89,11 @@ impl CliHostHooks { let mut counter = 0u8; while !off_thread_job_queue.is_empty() { counter = counter.wrapping_add(1); - for (i, job) in off_thread_job_queue.iter().enumerate() { - if job.is_finished() { - let job = off_thread_job_queue.swap_remove(i); + let now = Instant::now(); + for (i, (deadline, job)) in off_thread_job_queue.iter().enumerate() { + let deadline_reached = deadline.map_or(true, |d| now >= d); + if deadline_reached && job.is_finished() { + let (_, job) = off_thread_job_queue.swap_remove(i); return Some(job); } } @@ -101,14 +109,19 @@ impl CliHostHooks { impl HostHooks for CliHostHooks { fn enqueue_generic_job(&self, job: Job) { - self.macrotask_queue.borrow_mut().push(job); + self.macrotask_queue.borrow_mut().push((None, job)); } fn enqueue_promise_job(&self, job: Job) { self.promise_job_queue.borrow_mut().push_back(job); } - fn enqueue_timeout_job(&self, _timeout_job: Job, _milliseconds: u64) {} + fn enqueue_timeout_job(&self, timeout_job: Job, milliseconds: u64) { + let deadline = Instant::now() + Duration::from_millis(milliseconds); + self.macrotask_queue + .borrow_mut() + .push((Some(deadline), timeout_job)); + } fn load_imported_module<'gc>( &self, From a71802698585f921d500342fb5a5ea92b1c119f2 Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Thu, 7 May 2026 16:41:24 -0300 Subject: [PATCH 04/16] Timeout implemented by host system --- .../structured_data/atomics_object.rs | 144 +++++++++++++----- nova_vm/src/ecmascript/execution/agent.rs | 8 +- .../src/ecmascript/types/spec/data_block.rs | 37 ++++- 3 files changed, 146 insertions(+), 43 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs index 3098d2ed8..ae6054167 100644 --- a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs +++ b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs @@ -17,13 +17,13 @@ use crate::{ BigInt, Builtin, ExceptionType, InnerJob, Job, JsResult, Number, Numeric, OrdinaryObject, Promise, PromiseCapability, Realm, SharedArrayBuffer, SharedDataBlock, SharedTypedArray, String, TryError, TryResult, TypedArrayAbstractOperations, - TypedArrayWithBufferWitnessRecords, Value, WaiterRecord, builders::OrdinaryObjectBuilder, - compare_exchange_in_buffer, for_any_typed_array, get_modify_set_value_in_buffer, - get_value_from_buffer, make_typed_array_with_buffer_witness_record, - number_convert_to_integer_or_infinity, set_value_in_buffer, to_big_int, to_big_int64, - to_big_int64_big_int, to_index, to_int32, to_int32_number, to_integer_number_or_infinity, - to_integer_or_infinity, to_number, try_result_into_js, try_to_index, unwrap_try, - validate_index, validate_typed_array, + TypedArrayWithBufferWitnessRecords, Value, WaitResult, WaiterRecord, + builders::OrdinaryObjectBuilder, compare_exchange_in_buffer, for_any_typed_array, + get_modify_set_value_in_buffer, get_value_from_buffer, + make_typed_array_with_buffer_witness_record, number_convert_to_integer_or_infinity, + set_value_in_buffer, to_big_int, to_big_int64, to_big_int64_big_int, to_index, to_int32, + to_int32_number, to_integer_number_or_infinity, to_integer_or_infinity, to_number, + try_result_into_js, try_to_index, unwrap_try, validate_index, validate_typed_array, }, engine::{Bindable, GcScope, Global, NoGcScope, Scopable}, heap::{ObjectEntry, WellKnownSymbols}, @@ -1625,7 +1625,6 @@ struct WaitAsyncJobInner { byte_index_in_buffer: usize, waiter_record: Arc, promise_to_resolve: Global>, - created_at: Instant, t: u64, } @@ -1634,10 +1633,7 @@ pub(crate) struct WaitAsyncJob(Box); impl WaitAsyncJob { pub(crate) fn is_finished(&self) -> bool { - let is_notified = self.0.waiter_record.is_notified(); - let timeout_expired = - self.0.t != u64::MAX && self.0.created_at.elapsed() >= Duration::from_millis(self.0.t); - is_notified || timeout_expired + self.0.waiter_record.is_notified() } pub(crate) fn _will_halt(&self) -> bool { @@ -1651,34 +1647,85 @@ impl WaitAsyncJob { let waiters = unsafe { self.0.data_block.get_or_init_waiters() }; // a. Perform EnterCriticalSection(WL). let mut guard = waiters.lock().unwrap(); + let waiter_record = self.0.waiter_record; + guard.remove_from_list(self.0.byte_index_in_buffer, waiter_record.clone()); + + let result = match waiter_record.get_result() { + Some(WaitResult::TimedOut) => WaitResult::TimedOut, + Some(WaitResult::Ok) => WaitResult::Ok, + None => { + waiter_record.set_result(WaitResult::Ok); + WaitResult::Ok + } + }; - if self.0.waiter_record.is_notified() { - let promise = self.0.promise_to_resolve.take(agent).bind(gc); - let promise_capability = PromiseCapability::from_promise(promise, true); - unwrap_try(promise_capability.try_resolve(agent, BUILTIN_STRING_MEMORY.ok.into(), gc)); - drop(guard); - } else { - // b. If WL.[[Waiters]] contains waiterRecord, then - // i. Let timeOfJobExecution be the time value (UTC) identifying the current time. - // ii. Assert: ℝ(timeOfJobExecution) ≥ waiterRecord.[[TimeoutTime]] (ignoring potential non-monotonicity of time values). - // iii. Set waiterRecord.[[Result]] to "timed-out". - // iv. Perform RemoveWaiter(WL, waiterRecord). - // v. Perform NotifyWaiter(WL, waiterRecord). - - guard.remove_from_list(self.0.byte_index_in_buffer, self.0.waiter_record); - - let promise = self.0.promise_to_resolve.take(agent).bind(gc); - let promise_capability = PromiseCapability::from_promise(promise, true); - unwrap_try(promise_capability.try_resolve( - agent, - BUILTIN_STRING_MEMORY.timed_out.into(), - gc, - )); + let promise = self.0.promise_to_resolve.take(agent).bind(gc); + let promise_capability = PromiseCapability::from_promise(promise, true); + match result { + WaitResult::Ok => { + unwrap_try(promise_capability.try_resolve( + agent, + BUILTIN_STRING_MEMORY.ok.into(), + gc, + )); + } + WaitResult::TimedOut => { + unwrap_try(promise_capability.try_resolve( + agent, + BUILTIN_STRING_MEMORY.timed_out.into(), + gc, + )); + } + } + // c. Perform LeaveCriticalSection(WL). + drop(guard); + + // d. Return unused. + Ok(()) + } +} + +struct WaitAsyncTimeoutJobInner { + data_block: SharedDataBlock, + byte_index_in_buffer: usize, + waiter_record: Arc, +} - // c. Perform LeaveCriticalSection(WL). - drop(guard); +pub(crate) struct WaitAsyncTimeoutJob(Box); + +impl WaitAsyncTimeoutJob { + pub(crate) fn is_finished(&self) -> bool { + true // Always execute when the timeout is reached + } + + pub(crate) fn run<'gc>(self, agent: &mut Agent, gc: GcScope<'gc, '_>) -> JsResult<'gc, ()> { + let gc = gc.into_nogc(); + + if self.0.waiter_record.get_result().is_some() { + return Ok(()); } + // SAFETY: buffer is a cloned SharedDataBlock; non-dangling. + let waiters = unsafe { self.0.data_block.get_or_init_waiters() }; + // a. Perform EnterCriticalSection(WL). + let mut guard = waiters.lock().unwrap(); + + // b. If WL.[[Waiters]] contains waiterRecord, then + // i. Let timeOfJobExecution be the time value (UTC) identifying the current time. + // ii. Assert: ℝ(timeOfJobExecution) ≥ waiterRecord.[[TimeoutTime]] (ignoring potential non-monotonicity of time values). + // iii. Set waiterRecord.[[Result]] to "timed-out". + self.0.waiter_record.set_result(WaitResult::TimedOut); + + // iv. Perform RemoveWaiter(WL, waiterRecord). + let waiter_record = self.0.waiter_record.clone(); + guard.remove_from_list(self.0.byte_index_in_buffer, self.0.waiter_record); + + // v. Perform NotifyWaiter(WL, waiterRecord). + waiter_record.notify_waiters(); + + // c. Perform LeaveCriticalSection(WL). + drop(guard); + // d. Return unused. Ok(()) } @@ -1704,6 +1751,17 @@ fn enqueue_atomics_wait_async_job( // 2. Let now be the time value (UTC) identifying the current time. // 3. Let currentRealm be the current Realm Record. // 4. Perform HostEnqueueTimeoutJob(timeoutJob, currentRealm, 𝔽(waiterRecord.[[TimeoutTime]]) - now). + + let timeout_job_data = if t != u64::MAX { + Some(WaitAsyncTimeoutJobInner { + data_block: data_block.clone(), + byte_index_in_buffer, + waiter_record: waiter_record.clone(), + }) + } else { + None + }; + let wait_async_job = Job { realm: Some(Global::new(agent, agent.current_realm(gc).unbind())), inner: InnerJob::WaitAsync(WaitAsyncJob(Box::new(WaitAsyncJobInner { @@ -1712,11 +1770,19 @@ fn enqueue_atomics_wait_async_job( waiter_record, promise_to_resolve: promise, t, - created_at: Instant::now(), }))), }; - agent.host_hooks.enqueue_timeout_job(wait_async_job, t); - // agent.host_hooks.enqueue_generic_job(wait_async_job); + agent.host_hooks.enqueue_generic_job(wait_async_job); + + if let Some(inner) = timeout_job_data { + let wait_async_timeout_job = Job { + realm: Some(Global::new(agent, agent.current_realm(gc).unbind())), + inner: InnerJob::WaitAsyncTimeout(WaitAsyncTimeoutJob(Box::new(inner))), + }; + agent + .host_hooks + .enqueue_timeout_job(wait_async_timeout_job, t); + } // 5. Return unused. } diff --git a/nova_vm/src/ecmascript/execution/agent.rs b/nova_vm/src/ecmascript/execution/agent.rs index 1289c6be8..c3535364a 100644 --- a/nova_vm/src/ecmascript/execution/agent.rs +++ b/nova_vm/src/ecmascript/execution/agent.rs @@ -25,10 +25,10 @@ use ahash::AHashMap; use crate::ecmascript::GlobalEnvironment; #[cfg(feature = "shared-array-buffer")] use crate::ecmascript::SharedArrayBuffer; -#[cfg(feature = "atomics")] -use crate::ecmascript::WaitAsyncJob; #[cfg(feature = "weak-refs")] use crate::ecmascript::{FinalizationRegistryCleanupJob, clear_kept_objects}; +#[cfg(feature = "atomics")] +use crate::ecmascript::{WaitAsyncJob, WaitAsyncTimeoutJob}; use crate::{ ecmascript::{ AbstractModuleMethods, Environment, ErrorHeapData, ExecutionContext, Function, @@ -258,6 +258,8 @@ pub(crate) enum InnerJob { PromiseReaction(PromiseReactionJob), #[cfg(feature = "atomics")] WaitAsync(WaitAsyncJob), + #[cfg(feature = "atomics")] + WaitAsyncTimeout(WaitAsyncTimeoutJob), #[cfg(feature = "weak-refs")] FinalizationRegistry(FinalizationRegistryCleanupJob), } @@ -315,6 +317,8 @@ impl Job { InnerJob::PromiseReaction(job) => job.run(agent, gc), #[cfg(feature = "atomics")] InnerJob::WaitAsync(job) => job.run(agent, gc), + #[cfg(feature = "atomics")] + InnerJob::WaitAsyncTimeout(job) => job.run(agent, gc), #[cfg(feature = "weak-refs")] InnerJob::FinalizationRegistry(job) => { job.run(agent, gc); diff --git a/nova_vm/src/ecmascript/types/spec/data_block.rs b/nova_vm/src/ecmascript/types/spec/data_block.rs index de1c2a12b..003baadb1 100644 --- a/nova_vm/src/ecmascript/types/spec/data_block.rs +++ b/nova_vm/src/ecmascript/types/spec/data_block.rs @@ -10,7 +10,7 @@ use std::{ hint::assert_unchecked, sync::{ Arc, Condvar, Mutex, MutexGuard, WaitTimeoutResult, - atomic::{AtomicBool, AtomicPtr, AtomicUsize, Ordering}, + atomic::{AtomicBool, AtomicPtr, AtomicU8, AtomicUsize, Ordering}, }, time::Duration, }; @@ -426,10 +426,22 @@ impl SharedDataBlockMaxByteLength { } #[cfg(feature = "shared-array-buffer")] -#[derive(Default, Debug)] +#[derive(Debug)] pub(crate) struct WaiterRecord { condvar: Condvar, notified: AtomicBool, + result: AtomicU8, +} + +#[cfg(feature = "shared-array-buffer")] +impl Default for WaiterRecord { + fn default() -> Self { + Self { + condvar: Condvar::default(), + notified: AtomicBool::default(), + result: AtomicU8::new(u8::MAX), + } + } } #[cfg(feature = "shared-array-buffer")] @@ -448,6 +460,7 @@ impl WaiterRecord { let lock_result = self .condvar .wait_while(guard, |_| !self.notified.load(Ordering::Relaxed)); + match lock_result { Ok(_) => (), Err(e) => panic!( @@ -476,6 +489,26 @@ impl WaiterRecord { pub(crate) fn is_notified(&self) -> bool { self.notified.load(Ordering::Relaxed) } + + pub(crate) fn set_result(&self, result: WaitResult) { + self.result.store(result as u8, Ordering::Relaxed); + } + + pub(crate) fn get_result(&self) -> Option { + match self.result.load(Ordering::Relaxed) { + 0 => Some(WaitResult::Ok), + 1 => Some(WaitResult::TimedOut), + _ => None, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg(feature = "shared-array-buffer")] +#[repr(u8)] +pub(crate) enum WaitResult { + Ok = 0, + TimedOut = 1, } #[cfg(feature = "shared-array-buffer")] From c0ecf581397dfaf32e295a3e3636a10d232d849b Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Fri, 8 May 2026 07:17:46 -0300 Subject: [PATCH 05/16] Fix linter --- .../structured_data/atomics_object.rs | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs index ae6054167..7faa0caed 100644 --- a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs +++ b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs @@ -2,12 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -use std::{ - hint::assert_unchecked, - ops::ControlFlow, - sync::Arc, - time::{Duration, Instant}, -}; +use std::{hint::assert_unchecked, ops::ControlFlow, sync::Arc, time::Duration}; use ecmascript_atomics::Ordering; @@ -1625,7 +1620,7 @@ struct WaitAsyncJobInner { byte_index_in_buffer: usize, waiter_record: Arc, promise_to_resolve: Global>, - t: u64, + has_timeout: bool, } #[repr(transparent)] @@ -1637,7 +1632,7 @@ impl WaitAsyncJob { } pub(crate) fn _will_halt(&self) -> bool { - self.0.t != u64::MAX + self.0.has_timeout } pub(crate) fn run<'gc>(self, agent: &mut Agent, gc: GcScope<'gc, '_>) -> JsResult<'gc, ()> { @@ -1694,13 +1689,7 @@ struct WaitAsyncTimeoutJobInner { pub(crate) struct WaitAsyncTimeoutJob(Box); impl WaitAsyncTimeoutJob { - pub(crate) fn is_finished(&self) -> bool { - true // Always execute when the timeout is reached - } - - pub(crate) fn run<'gc>(self, agent: &mut Agent, gc: GcScope<'gc, '_>) -> JsResult<'gc, ()> { - let gc = gc.into_nogc(); - + pub(crate) fn run<'gc>(self, _agent: &mut Agent, _gc: GcScope<'gc, '_>) -> JsResult<'gc, ()> { if self.0.waiter_record.get_result().is_some() { return Ok(()); } @@ -1769,7 +1758,7 @@ fn enqueue_atomics_wait_async_job( byte_index_in_buffer, waiter_record, promise_to_resolve: promise, - t, + has_timeout: t != u64::MAX, }))), }; agent.host_hooks.enqueue_generic_job(wait_async_job); From ce4d8353617c4cb49bb616fc2e9e99926376467e Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Fri, 8 May 2026 07:20:45 -0300 Subject: [PATCH 06/16] Fix warn --- .../ecmascript/builtins/structured_data/atomics_object.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs index 7faa0caed..ca6bf2794 100644 --- a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs +++ b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs @@ -1620,7 +1620,7 @@ struct WaitAsyncJobInner { byte_index_in_buffer: usize, waiter_record: Arc, promise_to_resolve: Global>, - has_timeout: bool, + _has_timeout: bool, } #[repr(transparent)] @@ -1632,7 +1632,7 @@ impl WaitAsyncJob { } pub(crate) fn _will_halt(&self) -> bool { - self.0.has_timeout + self.0._has_timeout } pub(crate) fn run<'gc>(self, agent: &mut Agent, gc: GcScope<'gc, '_>) -> JsResult<'gc, ()> { @@ -1758,7 +1758,7 @@ fn enqueue_atomics_wait_async_job( byte_index_in_buffer, waiter_record, promise_to_resolve: promise, - has_timeout: t != u64::MAX, + _has_timeout: t != u64::MAX, }))), }; agent.host_hooks.enqueue_generic_job(wait_async_job); From 3f07edc6abb2ef9738f4641785e1786aa69d7f32 Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Fri, 8 May 2026 07:47:50 -0300 Subject: [PATCH 07/16] Fix linter --- nova_cli/src/lib/child_hooks.rs | 2 +- nova_cli/src/lib/host_hooks.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nova_cli/src/lib/child_hooks.rs b/nova_cli/src/lib/child_hooks.rs index 06957b469..6ebb14970 100644 --- a/nova_cli/src/lib/child_hooks.rs +++ b/nova_cli/src/lib/child_hooks.rs @@ -80,7 +80,7 @@ impl CliChildHooks { counter = counter.wrapping_add(1); let now = Instant::now(); for (i, (deadline, job)) in off_thread_job_queue.iter().enumerate() { - let deadline_reached = deadline.map_or(true, |d| now >= d); + let deadline_reached = deadline.is_none_or(|d| now >= d); if deadline_reached && job.is_finished() { let (_, job) = off_thread_job_queue.swap_remove(i); return Some(job); diff --git a/nova_cli/src/lib/host_hooks.rs b/nova_cli/src/lib/host_hooks.rs index 545cf5b23..09b3bd88f 100644 --- a/nova_cli/src/lib/host_hooks.rs +++ b/nova_cli/src/lib/host_hooks.rs @@ -91,7 +91,7 @@ impl CliHostHooks { counter = counter.wrapping_add(1); let now = Instant::now(); for (i, (deadline, job)) in off_thread_job_queue.iter().enumerate() { - let deadline_reached = deadline.map_or(true, |d| now >= d); + let deadline_reached = deadline.is_none_or(|d| now >= d); if deadline_reached && job.is_finished() { let (_, job) = off_thread_job_queue.swap_remove(i); return Some(job); From aa4dc9abad548d3002233c17068dc0a2a169c15c Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Tue, 9 Jun 2026 20:26:37 -0300 Subject: [PATCH 08/16] Updated comments --- .../builtins/structured_data/atomics_object.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs index ca6bf2794..ac92b8b7a 100644 --- a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs +++ b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs @@ -1635,12 +1635,14 @@ impl WaitAsyncJob { self.0._has_timeout } + /// Implementation of the Job Abstract Closure for [WaitAsyncTimeoutJob](https://tc39.es/ecma262/#sec-enqueueatomicswaitasynctimeoutjob), + /// for the cases where no timeout is specified. pub(crate) fn run<'gc>(self, agent: &mut Agent, gc: GcScope<'gc, '_>) -> JsResult<'gc, ()> { let gc = gc.into_nogc(); // SAFETY: buffer is a cloned SharedDataBlock; non-dangling. let waiters = unsafe { self.0.data_block.get_or_init_waiters() }; - // a. Perform EnterCriticalSection(WL). + let mut guard = waiters.lock().unwrap(); let waiter_record = self.0.waiter_record; guard.remove_from_list(self.0.byte_index_in_buffer, waiter_record.clone()); @@ -1672,10 +1674,8 @@ impl WaitAsyncJob { )); } } - // c. Perform LeaveCriticalSection(WL). - drop(guard); - // d. Return unused. + drop(guard); Ok(()) } } @@ -1735,12 +1735,10 @@ fn enqueue_atomics_wait_async_job( gc: NoGcScope, ) { // 1. Let timeoutJob be a new Job Abstract Closure with no parameters that - // captures WL and waiterRecord and performs the following steps when - // called: + // captures WL and waiterRecord and performs the following steps when called: // 2. Let now be the time value (UTC) identifying the current time. // 3. Let currentRealm be the current Realm Record. // 4. Perform HostEnqueueTimeoutJob(timeoutJob, currentRealm, 𝔽(waiterRecord.[[TimeoutTime]]) - now). - let timeout_job_data = if t != u64::MAX { Some(WaitAsyncTimeoutJobInner { data_block: data_block.clone(), From 91986b1389539441e28a77ba130ad317983d412f Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Tue, 9 Jun 2026 20:40:07 -0300 Subject: [PATCH 09/16] Added `to_string` --- .../builtins/structured_data/atomics_object.rs | 17 +---------------- nova_vm/src/ecmascript/types/spec/data_block.rs | 10 ++++++++++ 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs index ac92b8b7a..bf650b584 100644 --- a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs +++ b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs @@ -1658,22 +1658,7 @@ impl WaitAsyncJob { let promise = self.0.promise_to_resolve.take(agent).bind(gc); let promise_capability = PromiseCapability::from_promise(promise, true); - match result { - WaitResult::Ok => { - unwrap_try(promise_capability.try_resolve( - agent, - BUILTIN_STRING_MEMORY.ok.into(), - gc, - )); - } - WaitResult::TimedOut => { - unwrap_try(promise_capability.try_resolve( - agent, - BUILTIN_STRING_MEMORY.timed_out.into(), - gc, - )); - } - } + unwrap_try(promise_capability.try_resolve(agent, result.to_string().into(), gc)); drop(guard); Ok(()) diff --git a/nova_vm/src/ecmascript/types/spec/data_block.rs b/nova_vm/src/ecmascript/types/spec/data_block.rs index 003baadb1..abbf7a40f 100644 --- a/nova_vm/src/ecmascript/types/spec/data_block.rs +++ b/nova_vm/src/ecmascript/types/spec/data_block.rs @@ -511,6 +511,16 @@ pub(crate) enum WaitResult { TimedOut = 1, } +#[cfg(feature = "shared-array-buffer")] +impl WaitResult { + pub(crate) fn to_string(self) -> crate::ecmascript::String<'static> { + match self { + WaitResult::Ok => crate::ecmascript::BUILTIN_STRING_MEMORY.ok, + WaitResult::TimedOut => crate::ecmascript::BUILTIN_STRING_MEMORY.timed_out, + } + } +} + #[cfg(feature = "shared-array-buffer")] #[derive(Default)] #[repr(transparent)] From efa8ff6e847ad509a225ccfe0a27f6625d160af5 Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Fri, 12 Jun 2026 00:58:31 +0000 Subject: [PATCH 10/16] Added helper function --- .../structured_data/atomics_object.rs | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs index bf650b584..85f6df7aa 100644 --- a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs +++ b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs @@ -2,7 +2,12 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -use std::{hint::assert_unchecked, ops::ControlFlow, sync::Arc, time::Duration}; +use std::{ + hint::assert_unchecked, + ops::ControlFlow, + sync::{Arc, Mutex}, + time::Duration, +}; use ecmascript_atomics::Ordering; @@ -12,7 +17,7 @@ use crate::{ BigInt, Builtin, ExceptionType, InnerJob, Job, JsResult, Number, Numeric, OrdinaryObject, Promise, PromiseCapability, Realm, SharedArrayBuffer, SharedDataBlock, SharedTypedArray, String, TryError, TryResult, TypedArrayAbstractOperations, - TypedArrayWithBufferWitnessRecords, Value, WaitResult, WaiterRecord, + TypedArrayWithBufferWitnessRecords, Value, WaitResult, WaiterLists, WaiterRecord, builders::OrdinaryObjectBuilder, compare_exchange_in_buffer, for_any_typed_array, get_modify_set_value_in_buffer, get_value_from_buffer, make_typed_array_with_buffer_witness_record, number_convert_to_integer_or_infinity, @@ -1615,6 +1620,11 @@ fn create_wait_result_object<'gc>( .expect("Should perform GC here") } +fn get_wait_async_job_waiters(data_block: &SharedDataBlock) -> &Mutex { + // SAFETY: the data block is a non-dangling clone captured in [`do_wait_critical`] after validation. + unsafe { data_block.get_or_init_waiters() } +} + struct WaitAsyncJobInner { data_block: SharedDataBlock, byte_index_in_buffer: usize, @@ -1640,8 +1650,7 @@ impl WaitAsyncJob { pub(crate) fn run<'gc>(self, agent: &mut Agent, gc: GcScope<'gc, '_>) -> JsResult<'gc, ()> { let gc = gc.into_nogc(); - // SAFETY: buffer is a cloned SharedDataBlock; non-dangling. - let waiters = unsafe { self.0.data_block.get_or_init_waiters() }; + let waiters = get_wait_async_job_waiters(&self.0.data_block); let mut guard = waiters.lock().unwrap(); let waiter_record = self.0.waiter_record; @@ -1679,8 +1688,7 @@ impl WaitAsyncTimeoutJob { return Ok(()); } - // SAFETY: buffer is a cloned SharedDataBlock; non-dangling. - let waiters = unsafe { self.0.data_block.get_or_init_waiters() }; + let waiters = get_wait_async_job_waiters(&self.0.data_block); // a. Perform EnterCriticalSection(WL). let mut guard = waiters.lock().unwrap(); From 0111feb66aff6150b6c822026164eace02753140 Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Fri, 12 Jun 2026 07:52:54 -0300 Subject: [PATCH 11/16] Fix dylint version --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d742839c5..7afd0eefb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: - name: Install Dylint uses: taiki-e/install-action@v2 with: - tool: cargo-dylint,dylint-link + tool: cargo-dylint@6.0.1,dylint-link@6.0.1 - name: Check formatting run: cargo fmt --check - name: Clippy From 5d9ea38b2b240e1bf11679fcbd377d235a2eaf01 Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Fri, 12 Jun 2026 12:35:51 +0000 Subject: [PATCH 12/16] Update dylint --- .github/workflows/ci.yml | 14 +++++++++++--- nova_lint/Cargo.toml | 4 ++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7afd0eefb..9b410b91d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,10 +43,18 @@ jobs: uses: Swatinem/rust-cache@v2 with: shared-key: warm - - name: Install Dylint - uses: taiki-e/install-action@v2 + # Build Dylint from crates.io instead of using prebuilt binaries: the + # binaries attached to dylint's GitHub releases (since v6.0.1) are built + # inside the dylint repo and bake in a path dependency on + # /home/runner/work/dylint/dylint/driver, which breaks driver builds. + - name: Install cargo-dylint + uses: taiki-e/cache-cargo-install-action@v2 with: - tool: cargo-dylint@6.0.1,dylint-link@6.0.1 + tool: cargo-dylint@6.0.1 + - name: Install dylint-link + uses: taiki-e/cache-cargo-install-action@v2 + with: + tool: dylint-link@6.0.1 - name: Check formatting run: cargo fmt --check - name: Clippy diff --git a/nova_lint/Cargo.toml b/nova_lint/Cargo.toml index 74ddbc68a..59fd80446 100644 --- a/nova_lint/Cargo.toml +++ b/nova_lint/Cargo.toml @@ -46,11 +46,11 @@ path = "ui/spec_header_level.rs" [dependencies] clippy_utils = { git = "https://github.com/rust-lang/rust-clippy", rev = "c936595d17413c1f08e162e117e504fb4ed126e4" } -dylint_linting = { version = "5.0.0", features = ["constituent"] } +dylint_linting = { version = "6.0.1", features = ["constituent"] } regex = "1" [dev-dependencies] -dylint_testing = "5.0.0" +dylint_testing = "6.0.1" nova_vm = { path = "../nova_vm" } [package.metadata.rust-analyzer] From bd32d1205d0f31c59cf7726fa3ed5b20b8886e3c Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Fri, 12 Jun 2026 13:33:04 +0000 Subject: [PATCH 13/16] Updated toolchain --- .github/workflows/ci.yml | 4 +-- nova_lint/Cargo.toml | 4 ++- nova_lint/rust-toolchain | 2 +- nova_lint/src/utils.rs | 2 +- .../builtins/structured_data/json_object.rs | 12 +++---- .../environments/private_environment.rs | 2 +- .../src/ecmascript/types/spec/data_block.rs | 33 ++++++++++--------- nova_vm/src/heap/heap_bits.rs | 2 +- rust-toolchain.toml | 2 +- 9 files changed, 33 insertions(+), 30 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b410b91d..c05b7ad38 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@nightly with: - toolchain: nightly-2025-10-31 + toolchain: nightly-2026-05-28 components: rustfmt, clippy, llvm-tools-preview, rustc-dev - name: Cache on ${{ github.ref_name }} uses: Swatinem/rust-cache@v2 @@ -60,7 +60,7 @@ jobs: - name: Clippy run: | cargo +stable clippy --all-targets -- -D warnings - cargo +nightly-2025-10-31 clippy --all-targets --all-features -- -D warnings + cargo +nightly-2026-05-28 clippy --all-targets --all-features -- -D warnings - name: Dylint tests working-directory: nova_lint run: cargo test diff --git a/nova_lint/Cargo.toml b/nova_lint/Cargo.toml index 59fd80446..25a865608 100644 --- a/nova_lint/Cargo.toml +++ b/nova_lint/Cargo.toml @@ -45,7 +45,9 @@ name = "spec_header_level" path = "ui/spec_header_level.rs" [dependencies] -clippy_utils = { git = "https://github.com/rust-lang/rust-clippy", rev = "c936595d17413c1f08e162e117e504fb4ed126e4" } +# Must match the nightly toolchain pinned in .github/workflows/ci.yml: each +# clippy_utils release only builds with its contemporary nightly. +clippy_utils = "0.1.98" dylint_linting = { version = "6.0.1", features = ["constituent"] } regex = "1" diff --git a/nova_lint/rust-toolchain b/nova_lint/rust-toolchain index 4e5ce19b2..39b925b85 100644 --- a/nova_lint/rust-toolchain +++ b/nova_lint/rust-toolchain @@ -1,3 +1,3 @@ [toolchain] -channel = "nightly-2025-10-31" +channel = "nightly-2026-05-28" components = ["llvm-tools-preview", "rustc-dev"] diff --git a/nova_lint/src/utils.rs b/nova_lint/src/utils.rs index 3c2ab465f..1a79d2732 100644 --- a/nova_lint/src/utils.rs +++ b/nova_lint/src/utils.rs @@ -51,7 +51,7 @@ pub fn match_def_paths(cx: &LateContext<'_>, did: DefId, syms: &[&[&str]]) -> bo pub fn is_trait_item(cx: &LateContext<'_>, hir_id: HirId) -> bool { if let Node::Item(item) = cx.tcx.parent_hir_node(hir_id) { - matches!(item.kind, ItemKind::Trait(..)) + matches!(item.kind, ItemKind::Trait { .. }) } else { false } diff --git a/nova_vm/src/ecmascript/builtins/structured_data/json_object.rs b/nova_vm/src/ecmascript/builtins/structured_data/json_object.rs index 4819275b3..ed0067470 100644 --- a/nova_vm/src/ecmascript/builtins/structured_data/json_object.rs +++ b/nova_vm/src/ecmascript/builtins/structured_data/json_object.rs @@ -1008,7 +1008,7 @@ fn serialize_json_object<'a, 'b>( // i. Let separator be the string-concatenation of the code unit // 0x002C (COMMA), the code unit 0x000A (LINE FEED), and // state.[[Indent]]. - separator_string = format!(",\n{}", &state.indent).into_boxed_str(); + separator_string = format!(",\n{}", state.indent).into_boxed_str(); // ii. Let properties be the String value formed by concatenating // all the element Strings of partial with each adjacent pair // of Strings separated with separator. The separator String is @@ -1017,8 +1017,8 @@ fn serialize_json_object<'a, 'b>( // iii. Let final be the string-concatenation of "{", the code unit // 0x000A (LINE FEED), state.[[Indent]], properties, the code // unit 0x000A (LINE FEED), stepBack, and "}". - open_string = format!("{{\n{}", &state.indent).into_boxed_str(); - close_string = format!("\n{}}}", &step_back).into_boxed_str(); + open_string = format!("{{\n{}", state.indent).into_boxed_str(); + close_string = format!("\n{}}}", step_back).into_boxed_str(); ( open_string.as_ref(), separator_string.as_ref(), @@ -1170,7 +1170,7 @@ fn serialize_json_array<'a, 'b>( // b. Else, // i. Let separator be the string-concatenation of the code unit 0x002C // (COMMA), the code unit 0x000A (LINE FEED), and state.[[Indent]]. - separator_string = format!(",\n{}", &state.indent).into_boxed_str(); + separator_string = format!(",\n{}", state.indent).into_boxed_str(); // ii. Let properties be the String value formed by concatenating all // the element Strings of partial with each adjacent pair of // Strings separated with separator. The separator String is not @@ -1179,8 +1179,8 @@ fn serialize_json_array<'a, 'b>( // iii. Let final be the string-concatenation of "[", the code unit // 0x000A (LINE FEED), state.[[Indent]], properties, the code unit // 0x000A (LINE FEED), stepBack, and "]". - open_string = format!("[\n{}", &state.indent).into_boxed_str(); - close_string = format!("\n{}]", &step_back).into_boxed_str(); + open_string = format!("[\n{}", state.indent).into_boxed_str(); + close_string = format!("\n{}]", step_back).into_boxed_str(); ( open_string.as_ref(), separator_string.as_ref(), diff --git a/nova_vm/src/ecmascript/execution/environments/private_environment.rs b/nova_vm/src/ecmascript/execution/environments/private_environment.rs index 82aeca3b4..65ded6a24 100644 --- a/nova_vm/src/ecmascript/execution/environments/private_environment.rs +++ b/nova_vm/src/ecmascript/execution/environments/private_environment.rs @@ -485,7 +485,7 @@ impl HeapMarkAndSweep for PrivateEnvironmentRecord { let mut replacements = Vec::new(); // Sweep all binding values, while also sweeping keys and making note // of all changes in them: Those need to be updated in a separate loop. - for (key, _) in names.iter_mut() { + for key in names.keys() { if let String::String(old_key) = key { let old_key = *old_key; let mut new_key = old_key; diff --git a/nova_vm/src/ecmascript/types/spec/data_block.rs b/nova_vm/src/ecmascript/types/spec/data_block.rs index abbf7a40f..9a8aa6849 100644 --- a/nova_vm/src/ecmascript/types/spec/data_block.rs +++ b/nova_vm/src/ecmascript/types/spec/data_block.rs @@ -1219,13 +1219,7 @@ pub(crate) fn create_byte_data_block<'a>( // 1. If size > 2**53 - 1, throw a RangeError exception. if let Some(db) = usize::try_from(size) .ok() - .and_then(|size| { - if size as u64 > DATA_BLOCK_SIZE_LIMIT { - None - } else { - Some(size) - } - }) + .filter(|&size| size as u64 <= DATA_BLOCK_SIZE_LIMIT) .and_then(DataBlock::new) { // 2. Let db be a new Data Block value consisting of size bytes. @@ -1269,13 +1263,7 @@ pub(crate) unsafe fn create_shared_byte_data_block<'a>( // RangeError exception. if let Some(db) = usize::try_from(size) .ok() - .and_then(|size| { - if size as u64 > DATA_BLOCK_SIZE_LIMIT { - None - } else { - Some(size) - } - }) + .filter(|&size| size as u64 <= DATA_BLOCK_SIZE_LIMIT) .and_then(|_| { // SAFETY: function precondition unsafe { @@ -1376,9 +1364,22 @@ pub(crate) fn copy_shared_data_block_bytes( count: usize, ) { // 1. Assert: fromBlock and toBlock are distinct values. + // Note: the pointers must be cast to byte pointers before offsetting; + // offsetting the `NonNull<()>` directly would advance by zero bytes and + // make the non-overlap check vacuously true. debug_assert!(unsafe { - to_block.ptr.as_ptr().add(to_block.max_byte_length()) <= from_block.ptr.as_ptr() - || from_block.ptr.as_ptr().add(from_block.max_byte_length()) <= to_block.ptr.as_ptr() + to_block + .ptr + .as_ptr() + .cast::() + .add(to_block.max_byte_length()) + <= from_block.ptr.as_ptr().cast::() + || from_block + .ptr + .as_ptr() + .cast::() + .add(from_block.max_byte_length()) + <= to_block.ptr.as_ptr().cast::() }); // 2. Let fromSize be the number of bytes in fromBlock. let from_size = from_block.max_byte_length(); diff --git a/nova_vm/src/heap/heap_bits.rs b/nova_vm/src/heap/heap_bits.rs index 0a9593d09..97fb6d998 100644 --- a/nova_vm/src/heap/heap_bits.rs +++ b/nova_vm/src/heap/heap_bits.rs @@ -2294,7 +2294,7 @@ impl Date: Sat, 13 Jun 2026 13:58:59 +0300 Subject: [PATCH 14/16] Remove unnecessary cast, use byte_add instead --- nova_vm/src/ecmascript/types/spec/data_block.rs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/nova_vm/src/ecmascript/types/spec/data_block.rs b/nova_vm/src/ecmascript/types/spec/data_block.rs index 9a8aa6849..3505f2bec 100644 --- a/nova_vm/src/ecmascript/types/spec/data_block.rs +++ b/nova_vm/src/ecmascript/types/spec/data_block.rs @@ -1364,22 +1364,13 @@ pub(crate) fn copy_shared_data_block_bytes( count: usize, ) { // 1. Assert: fromBlock and toBlock are distinct values. - // Note: the pointers must be cast to byte pointers before offsetting; - // offsetting the `NonNull<()>` directly would advance by zero bytes and - // make the non-overlap check vacuously true. debug_assert!(unsafe { - to_block - .ptr - .as_ptr() - .cast::() - .add(to_block.max_byte_length()) - <= from_block.ptr.as_ptr().cast::() + to_block.ptr.as_ptr().byte_add(to_block.max_byte_length()) <= from_block.ptr.as_ptr() || from_block .ptr .as_ptr() - .cast::() - .add(from_block.max_byte_length()) - <= to_block.ptr.as_ptr().cast::() + .byte_add(from_block.max_byte_length()) + <= to_block.ptr.as_ptr() }); // 2. Let fromSize be the number of bytes in fromBlock. let from_size = from_block.max_byte_length(); From 708a7076835c74d69e9b20c766dbd5a15ad0d854 Mon Sep 17 00:00:00 2001 From: Aapo Alasuutari Date: Sat, 13 Jun 2026 14:19:01 +0300 Subject: [PATCH 15/16] Fix lints --- Cargo.toml | 2 +- .../structured_data/atomics_object.rs | 30 ++++++++++--------- .../builtins/temporal/plain_time.rs | 1 - nova_vm/src/ecmascript/execution/agent.rs | 7 +++-- .../src/ecmascript/types/spec/data_block.rs | 6 ++-- 5 files changed, 25 insertions(+), 21 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index beeab8e49..7d304b0e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ console = "=0.16.3" ctrlc = "=3.5.2" ecmascript_atomics = { version = "=0.2.3" } fast-float = "=0.2.0" -hashbrown = "=0.17.0" +hashbrown = "=0.17.1" lexical = { version = "=7.0.5", default-features = false, features = [ "std", "write-integers", diff --git a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs index 85f6df7aa..62fa19368 100644 --- a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs +++ b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs @@ -1503,7 +1503,7 @@ fn do_wait_critical<'gc, const IS_ASYNC: bool, const IS_I64: bool>( let (new_guard, timeout) = waiter_record.wait_timeout(guard, dur); guard = new_guard; if timeout.timed_out() { - guard.remove_from_list(byte_index_in_buffer, waiter_record); + guard.remove_from_list(byte_index_in_buffer, &waiter_record); // 31. Perform LeaveCriticalSection(WL). // 32. If mode is sync, return waiterRecord.[[Result]]. @@ -1645,16 +1645,15 @@ impl WaitAsyncJob { self.0._has_timeout } - /// Implementation of the Job Abstract Closure for [WaitAsyncTimeoutJob](https://tc39.es/ecma262/#sec-enqueueatomicswaitasynctimeoutjob), + /// Implementation of the Job Abstract Closure for + /// [WaitAsyncTimeoutJob](https://tc39.es/ecma262/#sec-enqueueatomicswaitasynctimeoutjob), /// for the cases where no timeout is specified. - pub(crate) fn run<'gc>(self, agent: &mut Agent, gc: GcScope<'gc, '_>) -> JsResult<'gc, ()> { - let gc = gc.into_nogc(); - + pub(crate) fn run<'gc>(self, agent: &mut Agent, gc: NoGcScope<'gc, '_>) -> JsResult<'gc, ()> { let waiters = get_wait_async_job_waiters(&self.0.data_block); let mut guard = waiters.lock().unwrap(); let waiter_record = self.0.waiter_record; - guard.remove_from_list(self.0.byte_index_in_buffer, waiter_record.clone()); + guard.remove_from_list(self.0.byte_index_in_buffer, &waiter_record); let result = match waiter_record.get_result() { Some(WaitResult::TimedOut) => WaitResult::TimedOut, @@ -1683,12 +1682,17 @@ struct WaitAsyncTimeoutJobInner { pub(crate) struct WaitAsyncTimeoutJob(Box); impl WaitAsyncTimeoutJob { - pub(crate) fn run<'gc>(self, _agent: &mut Agent, _gc: GcScope<'gc, '_>) -> JsResult<'gc, ()> { - if self.0.waiter_record.get_result().is_some() { - return Ok(()); + pub(crate) fn run<'gc>(self) { + let WaitAsyncTimeoutJobInner { + data_block, + byte_index_in_buffer, + waiter_record, + } = *self.0; + if waiter_record.get_result().is_some() { + return; } - let waiters = get_wait_async_job_waiters(&self.0.data_block); + let waiters = get_wait_async_job_waiters(&data_block); // a. Perform EnterCriticalSection(WL). let mut guard = waiters.lock().unwrap(); @@ -1696,11 +1700,10 @@ impl WaitAsyncTimeoutJob { // i. Let timeOfJobExecution be the time value (UTC) identifying the current time. // ii. Assert: ℝ(timeOfJobExecution) ≥ waiterRecord.[[TimeoutTime]] (ignoring potential non-monotonicity of time values). // iii. Set waiterRecord.[[Result]] to "timed-out". - self.0.waiter_record.set_result(WaitResult::TimedOut); + waiter_record.set_result(WaitResult::TimedOut); // iv. Perform RemoveWaiter(WL, waiterRecord). - let waiter_record = self.0.waiter_record.clone(); - guard.remove_from_list(self.0.byte_index_in_buffer, self.0.waiter_record); + guard.remove_from_list(byte_index_in_buffer, &waiter_record); // v. Perform NotifyWaiter(WL, waiterRecord). waiter_record.notify_waiters(); @@ -1709,7 +1712,6 @@ impl WaitAsyncTimeoutJob { drop(guard); // d. Return unused. - Ok(()) } } diff --git a/nova_vm/src/ecmascript/builtins/temporal/plain_time.rs b/nova_vm/src/ecmascript/builtins/temporal/plain_time.rs index 56c87c442..2fe27b3fe 100644 --- a/nova_vm/src/ecmascript/builtins/temporal/plain_time.rs +++ b/nova_vm/src/ecmascript/builtins/temporal/plain_time.rs @@ -143,7 +143,6 @@ pub(crate) fn create_temporal_plain_time<'gc>( /// a normal completion containing a Temporal.PlainTime or a throw completion. /// It adds/subtracts temporalDurationLike to/from temporalTime, returning a /// point in time that is in the future/past relative to temporalTime. -/// It performs the following steps when called: fn add_duration_to_time<'gc, const IS_ADD: bool>( agent: &mut Agent, plan_time: TemporalPlainTime, diff --git a/nova_vm/src/ecmascript/execution/agent.rs b/nova_vm/src/ecmascript/execution/agent.rs index c3535364a..3a20cf3ed 100644 --- a/nova_vm/src/ecmascript/execution/agent.rs +++ b/nova_vm/src/ecmascript/execution/agent.rs @@ -316,9 +316,12 @@ impl Job { InnerJob::PromiseResolveThenable(job) => job.run(agent, gc), InnerJob::PromiseReaction(job) => job.run(agent, gc), #[cfg(feature = "atomics")] - InnerJob::WaitAsync(job) => job.run(agent, gc), + InnerJob::WaitAsync(job) => job.run(agent, gc.into_nogc()), #[cfg(feature = "atomics")] - InnerJob::WaitAsyncTimeout(job) => job.run(agent, gc), + InnerJob::WaitAsyncTimeout(job) => { + job.run(); + Ok(()) + } #[cfg(feature = "weak-refs")] InnerJob::FinalizationRegistry(job) => { job.run(agent, gc); diff --git a/nova_vm/src/ecmascript/types/spec/data_block.rs b/nova_vm/src/ecmascript/types/spec/data_block.rs index 3505f2bec..a5f632b45 100644 --- a/nova_vm/src/ecmascript/types/spec/data_block.rs +++ b/nova_vm/src/ecmascript/types/spec/data_block.rs @@ -541,12 +541,12 @@ impl WaiterList { self.waiters.push_back(w); } - pub(crate) fn remove(&mut self, w: Arc) -> bool { + pub(crate) fn remove(&mut self, w: &Arc) -> bool { let Some(index) = self .waiters .iter() .enumerate() - .find(|(_, e)| Arc::ptr_eq(e, &w)) + .find(|(_, e)| Arc::ptr_eq(e, w)) .map(|(i, _)| i) else { return false; @@ -572,7 +572,7 @@ impl WaiterLists { self.map.entry(index).or_default().push(w); } - pub(crate) fn remove_from_list(&mut self, index: usize, w: Arc) { + pub(crate) fn remove_from_list(&mut self, index: usize, w: &Arc) { match self.map.entry(index) { Entry::Occupied(mut entry) => { if entry.get_mut().remove(w) && entry.get().is_empty() { From dca9922def0f8bc53fe58352a7757807a9703f29 Mon Sep 17 00:00:00 2001 From: Aapo Alasuutari Date: Sat, 13 Jun 2026 18:35:19 +0300 Subject: [PATCH 16/16] fix --- .../src/ecmascript/builtins/structured_data/atomics_object.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs index 62fa19368..f885dd903 100644 --- a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs +++ b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs @@ -1682,7 +1682,7 @@ struct WaitAsyncTimeoutJobInner { pub(crate) struct WaitAsyncTimeoutJob(Box); impl WaitAsyncTimeoutJob { - pub(crate) fn run<'gc>(self) { + pub(crate) fn run(self) { let WaitAsyncTimeoutJobInner { data_block, byte_index_in_buffer,