From 308b9fb354a6fccae187b1cf77e2e78e0894fa7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 5 Sep 2026 13:25:13 +0200 Subject: [PATCH 1/4] fix(test): export the strategy-aware provider stream constructor --- changelog.d/9791-provider-stream-constructor.md | 3 +++ tests/fixtures/issue_8075_provider_gc/stdlib-linker.sh | 1 + .../fixtures/issue_8075_provider_gc/stdlib-provider/src/lib.rs | 2 ++ 3 files changed, 6 insertions(+) create mode 100644 changelog.d/9791-provider-stream-constructor.md diff --git a/changelog.d/9791-provider-stream-constructor.md b/changelog.d/9791-provider-stream-constructor.md new file mode 100644 index 0000000000..9785c499c0 --- /dev/null +++ b/changelog.d/9791-provider-stream-constructor.md @@ -0,0 +1,3 @@ +Fix the native-root provider gate's stdlib fixture to retain and export the +strategy-aware ReadableStream constructor used by its compiled Response app. +This lets the app resolve the constructor when loaded as a separate dylib. diff --git a/tests/fixtures/issue_8075_provider_gc/stdlib-linker.sh b/tests/fixtures/issue_8075_provider_gc/stdlib-linker.sh index 0533ce1485..25e1886d26 100755 --- a/tests/fixtures/issue_8075_provider_gc/stdlib-linker.sh +++ b/tests/fixtures/issue_8075_provider_gc/stdlib-linker.sh @@ -18,6 +18,7 @@ stdlib_provider_exports=( js_headers_set js_readable_stream_get_reader_with_options js_readable_stream_new_from_source_object + js_readable_stream_new_with_strategy_and_source_type js_reader_read js_response_body js_response_body_init_ptr diff --git a/tests/fixtures/issue_8075_provider_gc/stdlib-provider/src/lib.rs b/tests/fixtures/issue_8075_provider_gc/stdlib-provider/src/lib.rs index ee860364fe..21645bac66 100644 --- a/tests/fixtures/issue_8075_provider_gc/stdlib-provider/src/lib.rs +++ b/tests/fixtures/issue_8075_provider_gc/stdlib-provider/src/lib.rs @@ -32,6 +32,8 @@ unsafe extern "C" fn pin_issue_8038_response_surface() { let _ = perry_stdlib::js_fetch_response_status_text(0.0); let _ = perry_stdlib::js_response_body(0.0); let _ = perry_stdlib::js_readable_stream_new_from_source_object(0.0, 0.0); + let _ = + perry_stdlib::js_readable_stream_new_with_strategy_and_source_type(0.0, 0.0, 0.0, 0.0, 0.0); let _ = perry_stdlib::js_readable_stream_get_reader_with_options(0.0, 0.0); let _ = perry_stdlib::js_reader_read(0.0); perry_stdlib::js_stdlib_init_dispatch(); From 98895114b9f118cc75cd39b9e103754c86d5b387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 5 Sep 2026 13:40:19 +0200 Subject: [PATCH 2/4] fix(test): retain the Response body initialization reset helper --- changelog.d/9791-provider-stream-constructor.md | 4 ++-- tests/fixtures/issue_8075_provider_gc/stdlib-linker.sh | 1 + .../issue_8075_provider_gc/stdlib-provider/src/lib.rs | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/changelog.d/9791-provider-stream-constructor.md b/changelog.d/9791-provider-stream-constructor.md index 9785c499c0..a424c94205 100644 --- a/changelog.d/9791-provider-stream-constructor.md +++ b/changelog.d/9791-provider-stream-constructor.md @@ -1,3 +1,3 @@ Fix the native-root provider gate's stdlib fixture to retain and export the -strategy-aware ReadableStream constructor used by its compiled Response app. -This lets the app resolve the constructor when loaded as a separate dylib. +strategy-aware ReadableStream constructor and Response body-init reset helper +used by its compiled app, so it can load as a separate dylib. diff --git a/tests/fixtures/issue_8075_provider_gc/stdlib-linker.sh b/tests/fixtures/issue_8075_provider_gc/stdlib-linker.sh index 25e1886d26..277b2b6544 100755 --- a/tests/fixtures/issue_8075_provider_gc/stdlib-linker.sh +++ b/tests/fixtures/issue_8075_provider_gc/stdlib-linker.sh @@ -22,6 +22,7 @@ stdlib_provider_exports=( js_reader_read js_response_body js_response_body_init_ptr + js_response_body_init_reset js_response_get_headers js_response_new js_stdlib_init_dispatch diff --git a/tests/fixtures/issue_8075_provider_gc/stdlib-provider/src/lib.rs b/tests/fixtures/issue_8075_provider_gc/stdlib-provider/src/lib.rs index 21645bac66..3419235ba2 100644 --- a/tests/fixtures/issue_8075_provider_gc/stdlib-provider/src/lib.rs +++ b/tests/fixtures/issue_8075_provider_gc/stdlib-provider/src/lib.rs @@ -25,6 +25,7 @@ unsafe extern "C" fn pin_issue_8038_response_surface() { let _ = perry_stdlib::js_headers_set(0.0, std::ptr::null(), std::ptr::null()); let _ = perry_stdlib::js_headers_append(0.0, std::ptr::null(), std::ptr::null()); let _ = perry_stdlib::js_headers_get(0.0, std::ptr::null()); + let _ = perry_stdlib::js_response_body_init_reset(); let _ = perry_stdlib::js_response_body_init_ptr(0.0); let _ = perry_stdlib::js_response_new(std::ptr::null(), 0.0, std::ptr::null(), 0.0); let _ = perry_stdlib::js_response_get_headers(0.0); From 1dddeb715c76d6b8e644a36c6a4c39f56c19703c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 5 Sep 2026 15:01:48 +0200 Subject: [PATCH 3/4] fix(runtime): share hot TLS declaration identities across providers --- .../9791-provider-stream-constructor.md | 8 +- crates/perry-runtime/src/tls_hot.rs | 195 +++++++++++++++--- 2 files changed, 168 insertions(+), 35 deletions(-) diff --git a/changelog.d/9791-provider-stream-constructor.md b/changelog.d/9791-provider-stream-constructor.md index a424c94205..ac949a649c 100644 --- a/changelog.d/9791-provider-stream-constructor.md +++ b/changelog.d/9791-provider-stream-constructor.md @@ -1,3 +1,5 @@ -Fix the native-root provider gate's stdlib fixture to retain and export the -strategy-aware ReadableStream constructor and Response body-init reset helper -used by its compiled app, so it can load as a separate dylib. +Fix the native-root provider gate's missing ReadableStream and Response helpers, +and prevent separately built runtime providers from assigning different TLS +values to the same shared cache slot. Provider copies of a thread-local now +claim one declaration identity and reuse its existing storage, preserving the +class registry and GC state while streamed Responses run under moving GC. diff --git a/crates/perry-runtime/src/tls_hot.rs b/crates/perry-runtime/src/tls_hot.rs index cffdc9cbc5..01ec3a8fbb 100644 --- a/crates/perry-runtime/src/tls_hot.rs +++ b/crates/perry-runtime/src/tls_hot.rs @@ -564,11 +564,16 @@ pub(crate) fn hot() -> &'static HotTls { /// Claimed once, on the first thread that resolves the declaration, and stable /// for the life of the process — so every thread finds the same declaration at /// the same index in its own cache. -pub struct SlotId(std::sync::atomic::AtomicU32); +pub struct SlotId(std::sync::atomic::AtomicU32, &'static str); impl SlotId { pub const fn new() -> Self { - Self(std::sync::atomic::AtomicU32::new(SLOT_UNASSIGNED)) + Self::named("") + } + + #[doc(hidden)] + pub const fn named(name: &'static str) -> Self { + Self(std::sync::atomic::AtomicU32::new(SLOT_UNASSIGNED), name) } /// The claimed index, or a sentinel `>= HOT_SLOT_CAPACITY`. @@ -592,20 +597,12 @@ impl SlotId { fn claim(&self) -> u32 { use std::sync::atomic::Ordering; maybe_install_stats_hook(); - let mut next = match CLAIM_LOCK.lock() { - Ok(next) => next, - Err(poisoned) => poisoned.into_inner(), - }; let current = self.0.load(Ordering::Relaxed); if current != SLOT_UNASSIGNED { return current; } - let idx = if (*next as usize) < HOT_SLOT_CAPACITY { - let idx = *next; - *next += 1; - idx - } else { - SLOT_OVERFLOW + let idx = unsafe { + js_tls_hot_claim_slot(self.1.as_ptr(), self.1.len(), self as *const Self as usize) }; self.0.store(idx, Ordering::Relaxed); idx @@ -618,21 +615,53 @@ impl Default for SlotId { } } -/// The next index [`SlotId::claim`] will hand out. Also the count of -/// declarations claimed so far, which is what -/// [`claimed_slots`] reports and what the capacity test asserts against. -static CLAIM_LOCK: std::sync::Mutex = std::sync::Mutex::new(0); +/// One slot per logical declaration across provider images. The C entry point +/// below owns this registry even when the runtime's Rust crate hashes differ. +/// Provider images must come from the same source/ABI, as for HotTls itself. +static CLAIM_LOCK: std::sync::Mutex, u32>> = + std::sync::Mutex::new(std::collections::BTreeMap::new()); -/// How many declarations have claimed a slot in this process. +/// The provider images share HotTls, so slot identities must be shared too. +/// Use a C entry point for preemption even when separate runtime builds have +/// different Rust crate hashes. Names identify declarations, never TLS values. /// -/// Instrumentation for the capacity assertion: overflow is silent by design -/// (the declaration keeps working, slowly), so something has to be able to see -/// how close the process is to the ceiling. -pub fn claimed_slots() -> u32 { - match CLAIM_LOCK.lock() { - Ok(next) => *next, - Err(poisoned) => *poisoned.into_inner(), +/// # Safety +/// For a nonempty name, `name` must point to `len` readable bytes. A name must +/// identify the same thread-local declaration (and value type) in every image. +/// For an anonymous declaration, `anonymous` must be its unique static address. +#[no_mangle] +#[inline(never)] // Calls must remain interposable across provider images. +pub unsafe extern "C" fn js_tls_hot_claim_slot( + name: *const u8, + len: usize, + anonymous: usize, +) -> u32 { + let key = if len == 0 { + format!("anonymous:{anonymous}").into_bytes() + } else { + std::slice::from_raw_parts(name, len).to_vec() + }; + let mut slots = CLAIM_LOCK.lock().unwrap_or_else(|p| p.into_inner()); + if let Some(&idx) = slots.get(&key) { + return idx; + } + if slots.len() >= HOT_SLOT_CAPACITY { + return SLOT_OVERFLOW; } + let idx = slots.len() as u32; + slots.insert(key, idx); + idx +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn js_tls_hot_claimed_slots() -> u32 { + CLAIM_LOCK.lock().unwrap_or_else(|p| p.into_inner()).len() as u32 +} + +/// How many declarations have claimed a slot in this process. +pub fn claimed_slots() -> u32 { + js_tls_hot_claimed_slots() } /// How many slots *this thread* has populated. @@ -866,7 +895,8 @@ impl HotKey { self.slot.raw() } - /// `value` is the address of this thread's `T`, published by this key. + /// `value` is this thread's `T`, published by this declaration in one of + /// the compatible provider images sharing the cache. /// /// # Safety /// `value` must have come from this key's slot or from its own `resolve`. @@ -881,19 +911,26 @@ impl HotKey { unsafe { &*(value as *const T) } } - /// Resolve through the real `thread_local!`, claim this declaration's slot - /// if it has none yet, and publish the address for this thread. + /// Claim the shared declaration slot, reuse any published storage, or + /// resolve through the real `thread_local!` and publish it for this thread. #[cold] #[inline(never)] fn resolve_and_cache(&'static self) -> Result<*mut u8, std::thread::AccessError> { - // Resolve first, and outside the claim lock: initialising the value can - // run arbitrary runtime code, including other `perry_thread_local!` - // first touches. - let value = (self.resolve)()?; + // Claim before resolving storage: another provider can already have + // published this declaration in the shared cache. Do not construct or + // overwrite a second copy. The claim lock is released before any TLS + // initializer runs, so nested first touches remain safe. let mut idx = self.slot.raw(); if idx == SLOT_UNASSIGNED { idx = self.slot.claim(); } + if (idx as usize) < HOT_SLOT_CAPACITY { + let cached = hot().slot(idx); + if !cached.is_null() { + return Ok(cached); + } + } + let value = (self.resolve)()?; if (idx as usize) < HOT_SLOT_CAPACITY { // Arm before publishing: after this store any thread-teardown of // the value un-publishes the slot it is about to invalidate. @@ -965,7 +1002,12 @@ macro_rules! __perry_thread_local_one { ($(#[$attr:meta])* $vis:vis $name:ident, $t:ty, $($init:tt)+) => { $(#[$attr])* $vis static $name: $crate::tls_hot::HotKey<$t> = { - static SLOT: $crate::tls_hot::SlotId = $crate::tls_hot::SlotId::new(); + // Module/name alone collide for function-local declarations. + // Avoid file!(): Cargo can use relative vs absolute source paths + // for the same crate in workspace and standalone provider builds. + static SLOT: $crate::tls_hot::SlotId = $crate::tls_hot::SlotId::named(concat!( + module_path!(), "::", stringify!($name), "@", line!(), ":", column!() + )); // `GUARD` is 1 exactly when `$t` has drop glue, so the guard — // and with it the thread-local's destructor — exists exactly when // a cached address could otherwise outlive the value. @@ -1309,6 +1351,95 @@ mod tests { assert_ne!(a, b, "two declarations resolved to one address"); } + /// Model two separately compiled provider copies of one declaration. + /// Merely allocating noncolliding indices is insufficient: both handles + /// must use the same storage and only one initializer/destructor may run. + #[test] + fn provider_copies_share_storage_without_initializing_a_second_value() { + use std::sync::atomic::{AtomicUsize, Ordering}; + static INITIALIZED: AtomicUsize = AtomicUsize::new(0); + static DROPPED: AtomicUsize = AtomicUsize::new(0); + struct Probe(std::cell::Cell); + impl Probe { + fn new() -> Self { + INITIALIZED.fetch_add(1, Ordering::SeqCst); + Self(std::cell::Cell::new(0)) + } + } + impl Drop for Probe { + fn drop(&mut self) { + DROPPED.fetch_add(1, Ordering::SeqCst); + } + } + type Storage = super::HotCell; + thread_local! { + static FIRST_STORAGE: Storage = Storage::new(Probe::new()); + static SECOND_STORAGE: Storage = Storage::new(Probe::new()); + } + static FIRST_SLOT: super::SlotId = super::SlotId::named("provider-test::shared"); + static SECOND_SLOT: super::SlotId = super::SlotId::named("provider-test::shared"); + static FIRST: super::HotKey = super::HotKey::new( + &FIRST_SLOT, + || FIRST_STORAGE.try_with(|c| c.value_addr()), + |idx| { + let _ = FIRST_STORAGE.try_with(|c| c.arm_guard(idx)); + }, + ); + static SECOND: super::HotKey = super::HotKey::new( + &SECOND_SLOT, + || SECOND_STORAGE.try_with(|c| c.value_addr()), + |idx| { + let _ = SECOND_STORAGE.try_with(|c| c.arm_guard(idx)); + }, + ); + // Reverse which provider is touched first, and overlap the threads to + // exercise independent claim atomics and isolate each thread's value. + let barrier = std::sync::Arc::new(std::sync::Barrier::new(8)); + let workers: Vec<_> = (0..8) + .map(|i| { + let barrier = barrier.clone(); + std::thread::spawn(move || { + let (first, second) = if i % 2 == 0 { + (&FIRST, &SECOND) + } else { + (&SECOND, &FIRST) + }; + first.with(|p| p.0.set(i + 100)); + barrier.wait(); + assert_eq!(second.with(|p| p.0.get()), i + 100); + assert_eq!( + first.with(|p| p as *const Probe), + second.with(|p| p as *const Probe) + ); + }) + }) + .collect(); + for worker in workers { + worker.join().expect("provider probe thread panicked"); + } + assert_eq!(FIRST.slot_index(), SECOND.slot_index()); + assert!((FIRST.slot_index() as usize) < super::HOT_SLOT_CAPACITY); + assert_eq!(INITIALIZED.load(Ordering::SeqCst), 8); + assert_eq!(DROPPED.load(Ordering::SeqCst), 8); + } + + /// Function-local declarations can have the same module and identifier; + /// the macro's source coordinates must keep their storage independent. + #[test] + fn same_named_local_declarations_remain_distinct() { + fn first() -> u32 { + crate::perry_thread_local! { static LOCAL: std::cell::Cell = const { std::cell::Cell::new(11) }; } + assert_eq!(LOCAL.with(|p| p.get()), 11); + LOCAL.slot_index() + } + fn second() -> u32 { + crate::perry_thread_local! { static LOCAL: std::cell::Cell = const { std::cell::Cell::new(22) }; } + assert_eq!(LOCAL.with(|p| p.get()), 22); + LOCAL.slot_index() + } + assert_ne!(first(), second()); + } + /// Each thread resolves its own storage, and a worker's slot must not /// leak into the parent's cache. #[test] From bf3427bfc8ac86e8c6f25d521333d5a0c1c52d13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 5 Sep 2026 15:07:50 +0200 Subject: [PATCH 4/4] test(runtime): isolate provider TLS regression probes --- crates/perry-runtime/src/tls_hot.rs | 92 +------------------ .../src/tls_hot/provider_tests.rs | 90 ++++++++++++++++++ 2 files changed, 93 insertions(+), 89 deletions(-) create mode 100644 crates/perry-runtime/src/tls_hot/provider_tests.rs diff --git a/crates/perry-runtime/src/tls_hot.rs b/crates/perry-runtime/src/tls_hot.rs index 01ec3a8fbb..217176590e 100644 --- a/crates/perry-runtime/src/tls_hot.rs +++ b/crates/perry-runtime/src/tls_hot.rs @@ -1039,6 +1039,9 @@ macro_rules! __perry_thread_local_storage { }; } +#[cfg(test)] +mod provider_tests; + #[cfg(test)] mod tests { /// Every cached address must equal the address of the `thread_local!` it @@ -1351,95 +1354,6 @@ mod tests { assert_ne!(a, b, "two declarations resolved to one address"); } - /// Model two separately compiled provider copies of one declaration. - /// Merely allocating noncolliding indices is insufficient: both handles - /// must use the same storage and only one initializer/destructor may run. - #[test] - fn provider_copies_share_storage_without_initializing_a_second_value() { - use std::sync::atomic::{AtomicUsize, Ordering}; - static INITIALIZED: AtomicUsize = AtomicUsize::new(0); - static DROPPED: AtomicUsize = AtomicUsize::new(0); - struct Probe(std::cell::Cell); - impl Probe { - fn new() -> Self { - INITIALIZED.fetch_add(1, Ordering::SeqCst); - Self(std::cell::Cell::new(0)) - } - } - impl Drop for Probe { - fn drop(&mut self) { - DROPPED.fetch_add(1, Ordering::SeqCst); - } - } - type Storage = super::HotCell; - thread_local! { - static FIRST_STORAGE: Storage = Storage::new(Probe::new()); - static SECOND_STORAGE: Storage = Storage::new(Probe::new()); - } - static FIRST_SLOT: super::SlotId = super::SlotId::named("provider-test::shared"); - static SECOND_SLOT: super::SlotId = super::SlotId::named("provider-test::shared"); - static FIRST: super::HotKey = super::HotKey::new( - &FIRST_SLOT, - || FIRST_STORAGE.try_with(|c| c.value_addr()), - |idx| { - let _ = FIRST_STORAGE.try_with(|c| c.arm_guard(idx)); - }, - ); - static SECOND: super::HotKey = super::HotKey::new( - &SECOND_SLOT, - || SECOND_STORAGE.try_with(|c| c.value_addr()), - |idx| { - let _ = SECOND_STORAGE.try_with(|c| c.arm_guard(idx)); - }, - ); - // Reverse which provider is touched first, and overlap the threads to - // exercise independent claim atomics and isolate each thread's value. - let barrier = std::sync::Arc::new(std::sync::Barrier::new(8)); - let workers: Vec<_> = (0..8) - .map(|i| { - let barrier = barrier.clone(); - std::thread::spawn(move || { - let (first, second) = if i % 2 == 0 { - (&FIRST, &SECOND) - } else { - (&SECOND, &FIRST) - }; - first.with(|p| p.0.set(i + 100)); - barrier.wait(); - assert_eq!(second.with(|p| p.0.get()), i + 100); - assert_eq!( - first.with(|p| p as *const Probe), - second.with(|p| p as *const Probe) - ); - }) - }) - .collect(); - for worker in workers { - worker.join().expect("provider probe thread panicked"); - } - assert_eq!(FIRST.slot_index(), SECOND.slot_index()); - assert!((FIRST.slot_index() as usize) < super::HOT_SLOT_CAPACITY); - assert_eq!(INITIALIZED.load(Ordering::SeqCst), 8); - assert_eq!(DROPPED.load(Ordering::SeqCst), 8); - } - - /// Function-local declarations can have the same module and identifier; - /// the macro's source coordinates must keep their storage independent. - #[test] - fn same_named_local_declarations_remain_distinct() { - fn first() -> u32 { - crate::perry_thread_local! { static LOCAL: std::cell::Cell = const { std::cell::Cell::new(11) }; } - assert_eq!(LOCAL.with(|p| p.get()), 11); - LOCAL.slot_index() - } - fn second() -> u32 { - crate::perry_thread_local! { static LOCAL: std::cell::Cell = const { std::cell::Cell::new(22) }; } - assert_eq!(LOCAL.with(|p| p.get()), 22); - LOCAL.slot_index() - } - assert_ne!(first(), second()); - } - /// Each thread resolves its own storage, and a worker's slot must not /// leak into the parent's cache. #[test] diff --git a/crates/perry-runtime/src/tls_hot/provider_tests.rs b/crates/perry-runtime/src/tls_hot/provider_tests.rs new file mode 100644 index 0000000000..eea3fd83f9 --- /dev/null +++ b/crates/perry-runtime/src/tls_hot/provider_tests.rs @@ -0,0 +1,90 @@ +//! Provider declaration identity and shared-storage regressions (#9791). + +/// Model two separately compiled provider copies of one declaration. +/// Merely allocating noncolliding indices is insufficient: both handles +/// must use the same storage and only one initializer/destructor may run. +#[test] +fn provider_copies_share_storage_without_initializing_a_second_value() { + use std::sync::atomic::{AtomicUsize, Ordering}; + static INITIALIZED: AtomicUsize = AtomicUsize::new(0); + static DROPPED: AtomicUsize = AtomicUsize::new(0); + struct Probe(std::cell::Cell); + impl Probe { + fn new() -> Self { + INITIALIZED.fetch_add(1, Ordering::SeqCst); + Self(std::cell::Cell::new(0)) + } + } + impl Drop for Probe { + fn drop(&mut self) { + DROPPED.fetch_add(1, Ordering::SeqCst); + } + } + type Storage = super::HotCell; + thread_local! { + static FIRST_STORAGE: Storage = Storage::new(Probe::new()); + static SECOND_STORAGE: Storage = Storage::new(Probe::new()); + } + static FIRST_SLOT: super::SlotId = super::SlotId::named("provider-test::shared"); + static SECOND_SLOT: super::SlotId = super::SlotId::named("provider-test::shared"); + static FIRST: super::HotKey = super::HotKey::new( + &FIRST_SLOT, + || FIRST_STORAGE.try_with(|c| c.value_addr()), + |idx| { + let _ = FIRST_STORAGE.try_with(|c| c.arm_guard(idx)); + }, + ); + static SECOND: super::HotKey = super::HotKey::new( + &SECOND_SLOT, + || SECOND_STORAGE.try_with(|c| c.value_addr()), + |idx| { + let _ = SECOND_STORAGE.try_with(|c| c.arm_guard(idx)); + }, + ); + // Reverse which provider is touched first, and overlap the threads to + // exercise independent claim atomics and isolate each thread's value. + let barrier = std::sync::Arc::new(std::sync::Barrier::new(8)); + let workers: Vec<_> = (0..8) + .map(|i| { + let barrier = barrier.clone(); + std::thread::spawn(move || { + let (first, second) = if i % 2 == 0 { + (&FIRST, &SECOND) + } else { + (&SECOND, &FIRST) + }; + first.with(|p| p.0.set(i + 100)); + barrier.wait(); + assert_eq!(second.with(|p| p.0.get()), i + 100); + assert_eq!( + first.with(|p| p as *const Probe), + second.with(|p| p as *const Probe) + ); + }) + }) + .collect(); + for worker in workers { + worker.join().expect("provider probe thread panicked"); + } + assert_eq!(FIRST.slot_index(), SECOND.slot_index()); + assert!((FIRST.slot_index() as usize) < super::HOT_SLOT_CAPACITY); + assert_eq!(INITIALIZED.load(Ordering::SeqCst), 8); + assert_eq!(DROPPED.load(Ordering::SeqCst), 8); +} + +/// Function-local declarations can have the same module and identifier; +/// the macro's source coordinates must keep their storage independent. +#[test] +fn same_named_local_declarations_remain_distinct() { + fn first() -> u32 { + crate::perry_thread_local! { static LOCAL: std::cell::Cell = const { std::cell::Cell::new(11) }; } + assert_eq!(LOCAL.with(|p| p.get()), 11); + LOCAL.slot_index() + } + fn second() -> u32 { + crate::perry_thread_local! { static LOCAL: std::cell::Cell = const { std::cell::Cell::new(22) }; } + assert_eq!(LOCAL.with(|p| p.get()), 22); + LOCAL.slot_index() + } + assert_ne!(first(), second()); +}