Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions changelog.d/8019-exotic-expando-runtime-state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
### Complete the #6759 exotic-expando RuntimeState migration

Date, RegExp, Promise, Map, Set, and Temporal expando values now share the
owning thread's explicit `RuntimeState` with the other hot object-model tables,
instead of resolving a separate table TLS key plus an independent in-use gate.
GC root scanning, owner rekeying, dead-owner pruning, and worker isolation keep
their existing semantics; the shape-tree implementation record now also names
which stronger Phase B/C RFC goals remain separate work.
194 changes: 122 additions & 72 deletions crates/perry-runtime/src/object/exotic_expando.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,56 +101,68 @@ pub(crate) fn exotic_expando_kind_of_value(value: f64) -> Option<(usize, ExoticK
exotic_expando_kind(addr).map(|kind| (addr, kind))
}

crate::perry_thread_local! {
/// addr -> insertion-ordered (key, nanboxed value bits) pairs (Date/RegExp).
static EXOTIC_EXPANDO: RefCell<HashMap<usize, Vec<(String, u64)>>> =
RefCell::new(HashMap::new());
/// #6759 Phase A: exotic-cell expando storage grouped under the owning
/// thread's [`crate::state::RuntimeState`]. Keeping the gate beside the map
/// lets each operation fetch the runtime state once and reuse it, rather than
/// resolving two independent TLS keys on every first store or guarded lookup.
pub(crate) struct ExoticExpandoTables {
/// addr -> insertion-ordered (key, nanboxed value bits) pairs for the
/// non-Error exotic cells handled by this module.
entries: RefCell<HashMap<usize, Vec<(String, u64)>>>,
/// Fast-path gate so hot get/set paths skip the map lookup until the
/// first expando is installed on this thread.
static EXPANDO_IN_USE: Cell<bool> = const { Cell::new(false) };
in_use: Cell<bool>,
}

impl ExoticExpandoTables {
pub(crate) fn new() -> Self {
Self {
entries: RefCell::new(HashMap::new()),
in_use: Cell::new(false),
}
}
}

pub(crate) fn expando_in_use() -> bool {
EXPANDO_IN_USE.with(|c| c.get())
crate::state::state().exotic_expando.in_use.get()
}

fn expando_store(addr: usize, key: &str, bits: u64) {
EXPANDO_IN_USE.with(|c| c.set(true));
EXOTIC_EXPANDO.with(|m| {
let mut map = m.borrow_mut();
let entries = map.entry(addr).or_default();
if let Some(slot) = entries.iter_mut().find(|(k, _)| k == key) {
slot.1 = bits;
} else {
entries.push((key.to_string(), bits));
}
});
let tables = &crate::state::state().exotic_expando;
tables.in_use.set(true);
let mut map = tables.entries.borrow_mut();
let entries = map.entry(addr).or_default();
if let Some(slot) = entries.iter_mut().find(|(k, _)| k == key) {
slot.1 = bits;
} else {
entries.push((key.to_string(), bits));
}
}

fn expando_lookup(addr: usize, key: &str) -> Option<u64> {
if !expando_in_use() {
let tables = &crate::state::state().exotic_expando;
if !tables.in_use.get() {
return None;
}
EXOTIC_EXPANDO.with(|m| {
m.borrow()
.get(&addr)
.and_then(|entries| entries.iter().find(|(k, _)| k == key).map(|(_, v)| *v))
})
tables
.entries
.borrow()
.get(&addr)
.and_then(|entries| entries.iter().find(|(k, _)| k == key).map(|(_, v)| *v))
}

fn expando_remove(addr: usize, key: &str) -> bool {
if !expando_in_use() {
let tables = &crate::state::state().exotic_expando;
if !tables.in_use.get() {
return false;
}
EXOTIC_EXPANDO.with(|m| {
let mut map = m.borrow_mut();
if let Some(entries) = map.get_mut(&addr) {
let before = entries.len();
entries.retain(|(k, _)| k != key);
return entries.len() != before;
}
false
})
let mut map = tables.entries.borrow_mut();
if let Some(entries) = map.get_mut(&addr) {
let before = entries.len();
entries.retain(|(k, _)| k != key);
return entries.len() != before;
}
false
}

/// Kind-dispatched own data-property store: Error delegates to the
Expand Down Expand Up @@ -187,15 +199,16 @@ fn value_keys(kind: ExoticKind, addr: usize) -> Vec<String> {
.map(|(k, _)| k)
.collect(),
_ => {
if !expando_in_use() {
let tables = &crate::state::state().exotic_expando;
if !tables.in_use.get() {
return Vec::new();
}
EXOTIC_EXPANDO.with(|m| {
m.borrow()
.get(&addr)
.map(|entries| entries.iter().map(|(k, _)| k.clone()).collect())
.unwrap_or_default()
})
tables
.entries
.borrow()
.get(&addr)
.map(|entries| entries.iter().map(|(k, _)| k.clone()).collect())
.unwrap_or_default()
}
}
}
Expand All @@ -204,12 +217,11 @@ fn value_keys(kind: ExoticKind, addr: usize) -> Vec<String> {
/// cell. Called from Date / RegExp allocation so address reuse can't leak
/// the old instance's properties onto the new one.
pub(crate) fn expando_clear_on_alloc(addr: usize) {
if !expando_in_use() {
let tables = &crate::state::state().exotic_expando;
if !tables.in_use.get() {
return;
}
EXOTIC_EXPANDO.with(|m| {
m.borrow_mut().remove(&addr);
});
tables.entries.borrow_mut().remove(&addr);
}

/// Death pruning (2026-07-09 GC audit wave 2): the root scanner
Expand All @@ -222,31 +234,35 @@ pub(crate) fn expando_clear_on_alloc(addr: usize) {
/// Temporal cells that die PINNED are skipped by the predicate's pinned
/// check and remain covered by the clear-on-alloc path.
pub(crate) fn prune_dead_exotic_expando_owners(is_dead_owner: &dyn Fn(usize) -> bool) {
if !expando_in_use() {
let tables = &crate::state::state().exotic_expando;
if !tables.in_use.get() {
return;
}
EXOTIC_EXPANDO.with(|m| {
let mut map = m.borrow_mut();
if !map.is_empty() {
map.retain(|owner, _| !is_dead_owner(*owner));
}
});
let mut map = tables.entries.borrow_mut();
if !map.is_empty() {
map.retain(|owner, _| !is_dead_owner(*owner));
}
}

#[cfg(test)]
pub(crate) fn test_seed_exotic_expando_entry(addr: usize, key: &str, value_bits: u64) {
EXPANDO_IN_USE.with(|c| c.set(true));
EXOTIC_EXPANDO.with(|m| {
m.borrow_mut()
.entry(addr)
.or_default()
.push((key.to_string(), value_bits));
});
let tables = &crate::state::state().exotic_expando;
tables.in_use.set(true);
tables
.entries
.borrow_mut()
.entry(addr)
.or_default()
.push((key.to_string(), value_bits));
}

#[cfg(test)]
pub(crate) fn test_exotic_expando_entry_exists(addr: usize) -> bool {
EXOTIC_EXPANDO.with(|m| m.borrow().contains_key(&addr))
crate::state::state()
.exotic_expando
.entries
.borrow()
.contains_key(&addr)
}

/// Rekey a movable exotic cell's expando entry after the GC relocates it from
Expand All @@ -257,15 +273,14 @@ pub(crate) fn test_exotic_expando_entry_exists(addr: usize) -> bool {
/// are already rewritten by `scan_exotic_expando_roots_mut`; this migrates the
/// owner *key*. Wired via `GcMoveHookKind::ExoticExpandoOwner`.
pub(crate) fn exotic_expando_owner_moved(old_addr: usize, new_addr: usize) {
if !expando_in_use() || old_addr == new_addr {
let tables = &crate::state::state().exotic_expando;
if !tables.in_use.get() || old_addr == new_addr {
return;
}
EXOTIC_EXPANDO.with(|m| {
let mut map = m.borrow_mut();
if let Some(entries) = map.remove(&old_addr) {
map.insert(new_addr, entries);
}
});
let mut map = tables.entries.borrow_mut();
if let Some(entries) = map.remove(&old_addr) {
map.insert(new_addr, entries);
}
}

/// `[[Set]]` on a Date/RegExp/Error instance. Honors accessor descriptors
Expand Down Expand Up @@ -613,12 +628,47 @@ pub(crate) fn exotic_put_value_set(
/// GC mutable-root scanner: keeps expando values alive (and rewrites them if
/// the collector relocates the referenced heap objects).
pub fn scan_exotic_expando_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) {
EXOTIC_EXPANDO.with(|m| {
let mut map = m.borrow_mut();
for (_, entries) in map.iter_mut() {
for (_, bits) in entries.iter_mut() {
visitor.visit_nanbox_u64_slot(bits);
}
let mut map = crate::state::state().exotic_expando.entries.borrow_mut();
for (_, entries) in map.iter_mut() {
for (_, bits) in entries.iter_mut() {
visitor.visit_nanbox_u64_slot(bits);
}
});
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn runtime_state_keeps_exotic_expandos_thread_local() {
let marker = 0u8;
let owner = &marker as *const u8 as usize;
let key = "runtime-state-isolation";

test_seed_exotic_expando_entry(owner, key, crate::value::TAG_UNDEFINED);
assert!(test_exotic_expando_entry_exists(owner));

std::thread::spawn(move || {
assert!(
!test_exotic_expando_entry_exists(owner),
"a worker observed the parent thread's expando table"
);
test_seed_exotic_expando_entry(owner, key, crate::value::TAG_TRUE);
assert_eq!(expando_lookup(owner, key), Some(crate::value::TAG_TRUE));
})
.join()
.expect("worker test thread panicked");

assert!(
test_exotic_expando_entry_exists(owner),
"the worker's RuntimeState disturbed the parent table"
);
assert_eq!(
expando_lookup(owner, key),
Some(crate::value::TAG_UNDEFINED),
"the worker overwrote the parent thread's expando value"
);
assert!(expando_remove(owner, key));
}
}
11 changes: 8 additions & 3 deletions crates/perry-runtime/src/object/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1551,9 +1551,14 @@ pub struct ObjectHeader {
/// garbage; every `meta` access must first establish a genuine shaped
/// object (`object_meta_slot_addr` centralizes that check).
///
/// Phase B lands incrementally: today the record holds the custom
/// `[[Prototype]]` plus the Phase C2 per-key descriptor summaries; the
/// exotic-kind tag migrates here next (#6759).
/// The shipped Phase B record holds the custom `[[Prototype]]`, the Phase C2
/// per-key descriptor summaries, object flags, and owned spill storage. The
/// RFC also sketched an exotic-kind tag here, but Date/RegExp/Error/Promise/
/// Map/Set/Temporal have distinct cell layouts rather than an `ObjectHeader`;
/// representing their kind here first requires header unification. Their
/// expando payloads therefore remain in the per-thread `RuntimeState` with GC
/// rekey/prune defenses instead of being described as the next incremental
/// `ObjectMeta` migration.
#[repr(C)]
pub struct ObjectMeta {
/// Custom `[[Prototype]]` recorded by `Object.setPrototypeOf` / object
Expand Down
5 changes: 5 additions & 0 deletions crates/perry-runtime/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ pub(crate) struct RuntimeState {
/// Property-lookup inline caches: the direct-mapped field cache
/// (previously a `thread_local!` in `object::field_get_set`).
pub(crate) field_lookup: crate::object::FieldLookupCaches,
/// User-defined properties attached to non-`ObjectHeader` cells such as
/// Date, RegExp, Promise, Map, and Set (previously two `thread_local!`s in
/// `object::exotic_expando`).
pub(crate) exotic_expando: crate::object::exotic_expando::ExoticExpandoTables,
/// #6759 Phase C1: first-class Shape records keyed on keys_array
/// identity (see `object::shapes` and docs/shape-tree-plan.md).
pub(crate) shapes: crate::object::ShapeTable,
Expand All @@ -51,6 +55,7 @@ impl RuntimeState {
descriptors: crate::object::DescriptorTables::new(),
object_hot: crate::object::ObjectHotTables::new(),
field_lookup: crate::object::FieldLookupCaches::new(),
exotic_expando: crate::object::exotic_expando::ExoticExpandoTables::new(),
shapes: crate::object::ShapeTable::new(),
})
}
Expand Down
Loading
Loading