diff --git a/crates/perry-codegen/src/expr/literals_vars.rs b/crates/perry-codegen/src/expr/literals_vars.rs index 497295de8..98444efed 100644 --- a/crates/perry-codegen/src/expr/literals_vars.rs +++ b/crates/perry-codegen/src/expr/literals_vars.rs @@ -724,10 +724,13 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { UpdateOp::Increment => blk.fadd(&old, "1.0"), UpdateOp::Decrement => blk.fsub(&old, "1.0"), }; - // GC_STORE_AUDIT(STACK/ROOT): update writes a local alloca or registered module-global root slot. if storage_is_root { + // Module globals are registered mutable GC roots and route + // through the root helper; the raw store below is stack-only. emit_root_nanbox_store_on_block(blk, &new, &storage); } else { + // GC_STORE_AUDIT(STACK): update writes a function-local alloca; + // module globals use the root helper. blk.store(DOUBLE, &new, &storage); } // Keep the parallel i32 counter slot in sync (if active). diff --git a/crates/perry-codegen/src/expr/property_set.rs b/crates/perry-codegen/src/expr/property_set.rs index b5b7c8e95..a190e6938 100644 --- a/crates/perry-codegen/src/expr/property_set.rs +++ b/crates/perry-codegen/src/expr/property_set.rs @@ -276,6 +276,8 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // Guarded raw-f64 slots are pointer-free by typed // shape descriptor; non-number writes miss the // guard and use the boxed setter fallback. + // GC_STORE_AUDIT(POINTER_FREE): typed raw-f64 class + // slots contain numbers only. blk.store(DOUBLE, &val_double, &field_ptr); } else { let field_addr = blk.ptrtoint(&field_ptr, I64); diff --git a/crates/perry-codegen/src/lower_call/new.rs b/crates/perry-codegen/src/lower_call/new.rs index 1a9486ac9..4410d5f73 100644 --- a/crates/perry-codegen/src/lower_call/new.rs +++ b/crates/perry-codegen/src/lower_call/new.rs @@ -315,6 +315,7 @@ pub(crate) fn lower_new(ctx: &mut FnCtx<'_>, class_name: &str, args: &[Expr]) -> // ---- Fast path: bump and return data + aligned ---- ctx.current_block = fast_idx; let blk = ctx.block(); + // GC_STORE_AUDIT(INIT): inline arena bump offset is allocator metadata, not a JS heap edge. blk.store(I64, &new_offset, &offset_field_ptr); // data ptr is at byte offset 0 in InlineArenaState let data_ptr = blk.load(PTR, &state_ptr); @@ -350,6 +351,7 @@ pub(crate) fn lower_new(ctx: &mut FnCtx<'_>, class_name: &str, args: &[Expr]) -> | (GC_FLAG_ARENA << 8) | (GC_LAYOUT_POINTER_FREE << 16) | ((total_size as u64) << 32); + // GC_STORE_AUDIT(INIT): inline headers initialize freshly allocated unpublished object storage. blk.store(I64, &gc_packed.to_string(), &raw); // Write ObjectHeader at raw + 8. @@ -367,6 +369,7 @@ pub(crate) fn lower_new(ctx: &mut FnCtx<'_>, class_name: &str, args: &[Expr]) -> // above is an i64 (carries the ArrayHeader address); store as // i64 since the underlying memory is 8 bytes either way. let oh_addr_3 = blk.gep(I8, &raw, &[(I64, "24")]); + // GC_STORE_AUDIT(INIT): keys_array edge is installed before publishing the new object. blk.store(I64, &keys_ptr, &oh_addr_3); // User pointer = raw + 8 (the ObjectHeader address — what the diff --git a/crates/perry-runtime/src/array/alloc.rs b/crates/perry-runtime/src/array/alloc.rs index fa452b567..c686d0e47 100644 --- a/crates/perry-runtime/src/array/alloc.rs +++ b/crates/perry-runtime/src/array/alloc.rs @@ -83,6 +83,7 @@ pub extern "C" fn js_array_alloc_with_length(capacity: u32) -> *mut ArrayHeader (*ptr).capacity = actual_capacity; let elements_ptr = (ptr as *mut u8).add(std::mem::size_of::()) as *mut u64; for i in 0..capacity as usize { + // GC_STORE_AUDIT(POINTER_FREE): TAG_HOLE is a non-pointer sentinel for fresh array slots. std::ptr::write(elements_ptr.add(i), crate::value::TAG_HOLE); } clear_array_numeric_layout(ptr); @@ -148,6 +149,7 @@ pub extern "C" fn js_array_from_f64(elements: *const f64, count: u32) -> *mut Ar unsafe { (*arr).length = count; let arr_elements = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + // GC_STORE_AUDIT(BARRIERED): bulk array initialization is followed by layout/barrier rebuild. ptr::copy_nonoverlapping(elements, arr_elements, count as usize); rebuild_array_layout(arr); } @@ -186,10 +188,12 @@ pub(crate) unsafe fn js_array_from_arraylike( for i in 0..len { // Pre-init to undefined in case the key lookup returns the // wrong type / produces a sentinel we want to coerce. + // GC_STORE_AUDIT(POINTER_FREE): undefined prefill is a non-pointer sentinel. *elements.add(i as usize) = undefined; let key_str = i.to_string(); let key = crate::string::js_string_from_bytes(key_str.as_ptr(), key_str.len() as u32); let v = crate::object::js_object_get_field_by_name_f64(obj, key); + // GC_STORE_AUDIT(BARRIERED): arraylike element write is immediately recorded via note_array_slot. *elements.add(i as usize) = v; note_array_slot(arr, i as usize, v.to_bits()); } @@ -229,6 +233,7 @@ pub(crate) unsafe fn js_array_from_string_codepoints( let s_ref = ch.encode_utf8(&mut buf); let s_ptr = crate::string::js_string_from_bytes(s_ref.as_ptr(), s_ref.len() as u32); let value = crate::value::js_nanbox_string(s_ptr as i64); + // GC_STORE_AUDIT(BARRIERED): string codepoint array slot is immediately recorded via note_array_slot. *elements.add(i) = value; note_array_slot(arr, i, value.to_bits()); } diff --git a/crates/perry-runtime/src/array/concat_reverse.rs b/crates/perry-runtime/src/array/concat_reverse.rs index 886a3aed8..de451189a 100644 --- a/crates/perry-runtime/src/array/concat_reverse.rs +++ b/crates/perry-runtime/src/array/concat_reverse.rs @@ -71,6 +71,7 @@ pub extern "C" fn js_array_concat( }; let dst_elements = (result as *mut u8).add(std::mem::size_of::()) as *mut f64; + // GC_STORE_AUDIT(BARRIERED): concat bulk copy is followed by exact layout/barrier rebuild. ptr::copy_nonoverlapping( src_elements, dst_elements.add(dest_len as usize), @@ -142,6 +143,7 @@ pub extern "C" fn js_array_reverse(arr: *mut ArrayHeader) -> *mut ArrayHeader { let mut j = len - 1; while i < j { let tmp = *elements.add(i); + // GC_STORE_AUDIT(BARRIERED): reverse slot swap is followed by layout/barrier rebuild. *elements.add(i) = *elements.add(j); *elements.add(j) = tmp; i += 1; @@ -167,6 +169,7 @@ pub extern "C" fn js_array_fill(arr: *mut ArrayHeader, value: f64) -> *mut Array } let elements = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; for i in 0..len { + // GC_STORE_AUDIT(BARRIERED): fill slot writes are followed by layout/barrier rebuild. *elements.add(i) = value; } rebuild_array_layout(arr); @@ -226,6 +229,7 @@ pub extern "C" fn js_array_fill_range( } let elements = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; for i in s..e { + // GC_STORE_AUDIT(BARRIERED): fill range writes are followed by layout/barrier rebuild. *elements.add(i as usize) = value; } rebuild_array_layout(arr); diff --git a/crates/perry-runtime/src/array/flat_clone.rs b/crates/perry-runtime/src/array/flat_clone.rs index f92275e97..a3a0c5ecb 100644 --- a/crates/perry-runtime/src/array/flat_clone.rs +++ b/crates/perry-runtime/src/array/flat_clone.rs @@ -383,6 +383,7 @@ pub extern "C" fn js_array_clone(src: *const ArrayHeader) -> *mut ArrayHeader { (src as *const u8).add(std::mem::size_of::()) as *const f64; let dst_elements = (result as *mut u8).add(std::mem::size_of::()) as *mut f64; + // GC_STORE_AUDIT(BARRIERED): clone bulk copy is followed by exact layout/barrier rebuild. ptr::copy_nonoverlapping(src_elements, dst_elements, len as usize); (*result).length = len; rebuild_array_layout_exact(result); @@ -444,12 +445,14 @@ pub extern "C" fn js_array_entries(arr: *const ArrayHeader) -> *mut ArrayHeader let pair = js_array_alloc(2); (*pair).length = 2; let pair_elems = (pair as *mut u8).add(std::mem::size_of::()) as *mut f64; + // GC_STORE_AUDIT(BARRIERED): entries pair slots are immediately recorded via note_array_slot. *pair_elems.add(0) = i as f64; *pair_elems.add(1) = *src_elements.add(i); note_array_slot(pair, 0, (i as f64).to_bits()); note_array_slot(pair, 1, (*src_elements.add(i)).to_bits()); // NaN-box the inner array pointer so the outer storage slot keeps tag info. let pair_value = crate::value::js_nanbox_pointer(pair as i64); + // GC_STORE_AUDIT(BARRIERED): outer entries slot is immediately recorded via note_array_slot. *dst_elements.add(i) = pair_value; note_array_slot(result, i, pair_value.to_bits()); } @@ -482,6 +485,7 @@ pub extern "C" fn js_array_keys(arr: *const ArrayHeader) -> *mut ArrayHeader { (*result).length = len; let dst_elements = (result as *mut u8).add(std::mem::size_of::()) as *mut f64; for i in 0..len as usize { + // GC_STORE_AUDIT(POINTER_FREE): keys array stores numeric indices only. *dst_elements.add(i) = i as f64; } result @@ -516,6 +520,7 @@ pub extern "C" fn js_array_values(arr: *const ArrayHeader) -> *mut ArrayHeader { (arr as *const u8).add(std::mem::size_of::()) as *const f64; let dst_elements = (result as *mut u8).add(std::mem::size_of::()) as *mut f64; + // GC_STORE_AUDIT(BARRIERED): values bulk copy is followed by layout/barrier rebuild. ptr::copy_nonoverlapping(src_elements, dst_elements, len as usize); (*result).length = len; rebuild_array_layout(result); diff --git a/crates/perry-runtime/src/array/header.rs b/crates/perry-runtime/src/array/header.rs index e7637d4a0..617798d25 100644 --- a/crates/perry-runtime/src/array/header.rs +++ b/crates/perry-runtime/src/array/header.rs @@ -418,6 +418,7 @@ unsafe fn rebuild_array_numeric_raw_f64(arr: *mut ArrayHeader) -> bool { clear_array_numeric_layout(arr); return false; }; + // GC_STORE_AUDIT(POINTER_FREE): raw-f64 layout rewrite stores numeric payloads only. std::ptr::write(elements.add(i) as *mut f64, number); } @@ -497,6 +498,7 @@ pub(crate) unsafe fn note_array_numeric_index_write( }; if array_has_raw_f64_layout_flag(arr) && index < (*arr).length as usize { let elements = array_elements_ptr(arr) as *mut f64; + // GC_STORE_AUDIT(POINTER_FREE): raw-f64 numeric slot update cannot contain a heap pointer. std::ptr::write(elements.add(index), number); return number.to_bits(); } @@ -554,6 +556,7 @@ pub(crate) unsafe fn array_numeric_raw_f64_set_inbounds( return false; } let elements_ptr = array_elements_ptr(arr) as *mut f64; + // GC_STORE_AUDIT(POINTER_FREE): raw-f64 numeric field store is layout-noted below. std::ptr::write(elements_ptr.add(index as usize), value); note_array_numeric_index_write(arr, index as usize, value_bits); crate::gc::layout_note_slot(arr as usize, index as usize, value_bits); @@ -580,6 +583,7 @@ pub(crate) unsafe fn array_numeric_raw_f64_push_inbounds( return false; }; let elements_ptr = array_elements_ptr(arr) as *mut f64; + // GC_STORE_AUDIT(POINTER_FREE): raw-f64 push stores numeric payloads only. std::ptr::write(elements_ptr.add(length as usize), number); crate::gc::layout_note_slot(arr as usize, length as usize, number.to_bits()); (*arr).length = length + 1; @@ -681,6 +685,7 @@ pub(crate) unsafe fn gc_element_slot_range( #[inline] pub(crate) unsafe fn note_array_slot(arr: *mut ArrayHeader, index: usize, value_bits: u64) { let value_bits = canonicalize_array_numeric_store_bits(arr, value_bits); + // GC_STORE_AUDIT(BARRIERED): shared helper notes layout and emits the array slot barrier below. std::ptr::write(array_elements_ptr(arr).add(index), value_bits); note_array_numeric_index_write(arr, index, value_bits); crate::gc::layout_note_slot(arr as usize, index, value_bits); @@ -695,6 +700,7 @@ pub(crate) unsafe fn note_array_slot_layout_only( value_bits: u64, ) { let value_bits = canonicalize_array_numeric_store_bits(arr, value_bits); + // GC_STORE_AUDIT(INIT): layout-only helper is restricted to fresh/suppressed caller sites. std::ptr::write(array_elements_ptr(arr).add(index), value_bits); note_array_numeric_index_write(arr, index, value_bits); crate::gc::layout_note_slot(arr as usize, index, value_bits); diff --git a/crates/perry-runtime/src/array/immutable.rs b/crates/perry-runtime/src/array/immutable.rs index c70484a62..dc6ad12fd 100644 --- a/crates/perry-runtime/src/array/immutable.rs +++ b/crates/perry-runtime/src/array/immutable.rs @@ -21,6 +21,7 @@ pub extern "C" fn js_array_to_reversed(arr: *const ArrayHeader) -> *mut ArrayHea let src = (arr as *const u8).add(std::mem::size_of::()) as *const f64; let dst = (new_arr as *mut u8).add(std::mem::size_of::()) as *mut f64; for i in 0..len { + // GC_STORE_AUDIT(BARRIERED): reversed copy initializes a fresh array rebuilt below. *dst.add(i) = *src.add(len - 1 - i); } rebuild_array_layout(new_arr); @@ -47,6 +48,7 @@ pub extern "C" fn js_array_to_sorted_default(arr: *const ArrayHeader) -> *mut Ar (*new_arr).length = len as u32; let src = (arr as *const u8).add(std::mem::size_of::()) as *const f64; let dst = (new_arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + // GC_STORE_AUDIT(BARRIERED): sorted clone copy initializes a fresh array rebuilt below. std::ptr::copy_nonoverlapping(src, dst, len); rebuild_array_layout(new_arr); // Sort the copy in-place using default sort @@ -78,6 +80,7 @@ pub extern "C" fn js_array_to_sorted_with_comparator( (*new_arr).length = len as u32; let src = (arr as *const u8).add(std::mem::size_of::()) as *const f64; let dst = (new_arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + // GC_STORE_AUDIT(BARRIERED): comparator sorted clone copy initializes a fresh array rebuilt below. std::ptr::copy_nonoverlapping(src, dst, len); rebuild_array_layout(new_arr); // Sort the copy in-place @@ -130,15 +133,18 @@ pub extern "C" fn js_array_to_spliced( let dst = (new_arr as *mut u8).add(std::mem::size_of::()) as *mut f64; // Copy elements before start + // GC_STORE_AUDIT(BARRIERED): toSpliced result writes are followed by layout/barrier rebuild. for i in 0..s as usize { *dst.add(i) = *src.add(i); } // Copy inserted items + // GC_STORE_AUDIT(BARRIERED): inserted toSpliced items are included in the rebuild below. for i in 0..items_count as usize { *dst.add(s as usize + i) = *items.add(i); } // Copy elements after deleted range let after_start = (s + dc) as usize; + // GC_STORE_AUDIT(BARRIERED): toSpliced tail copy is included in the rebuild below. for i in after_start..len as usize { *dst.add(s as usize + items_count as usize + i - after_start) = *src.add(i); } @@ -178,6 +184,7 @@ pub extern "C" fn js_array_with( (*new_arr).length = len as u32; let src = (arr as *const u8).add(std::mem::size_of::()) as *const f64; let dst = (new_arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + // GC_STORE_AUDIT(BARRIERED): with() unchanged copy is followed by layout/barrier rebuild. std::ptr::copy_nonoverlapping(src, dst, len as usize); rebuild_array_layout(new_arr); return new_arr; @@ -186,6 +193,7 @@ pub extern "C" fn js_array_with( let new_arr = js_array_alloc(len as u32); (*new_arr).length = len as u32; let dst = (new_arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + // GC_STORE_AUDIT(BARRIERED): with() clone and replacement are followed by layout/barrier rebuild. std::ptr::copy_nonoverlapping(src, dst, len as usize); *dst.add(idx as usize) = value; rebuild_array_layout(new_arr); @@ -246,6 +254,7 @@ pub extern "C" fn js_array_copy_within( } // Use memmove semantics (handles overlapping regions) + // GC_STORE_AUDIT(BARRIERED): copyWithin mutates array slots and rebuilds layout/barriers below. std::ptr::copy( elements.add(s as usize), elements.add(t as usize), diff --git a/crates/perry-runtime/src/array/indexing.rs b/crates/perry-runtime/src/array/indexing.rs index 1085629df..d00adab5d 100644 --- a/crates/perry-runtime/src/array/indexing.rs +++ b/crates/perry-runtime/src/array/indexing.rs @@ -232,6 +232,7 @@ pub extern "C" fn js_array_set_f64_unchecked(arr: *mut ArrayHeader, index: u32, let value = canonicalize_array_numeric_store_value(arr, value); let value_bits = value.to_bits(); let elements_ptr = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + // GC_STORE_AUDIT(BARRIERED): unchecked array set is immediately recorded via note_array_slot. ptr::write(elements_ptr.add(index as usize), value); note_array_slot(arr, index as usize, value_bits); } @@ -285,6 +286,7 @@ pub extern "C" fn js_array_set_f64(arr: *mut ArrayHeader, index: u32, value: f64 let value = canonicalize_array_numeric_store_value(arr, value); let value_bits = value.to_bits(); let elements_ptr = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + // GC_STORE_AUDIT(BARRIERED): array set is immediately recorded via note_array_slot. ptr::write(elements_ptr.add(index as usize), value); note_array_slot(arr, index as usize, value_bits); } @@ -332,6 +334,7 @@ pub extern "C" fn js_array_set_f64_extend( let value = canonicalize_array_numeric_store_value(arr, value); let value_bits = value.to_bits(); let elements_ptr = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + // GC_STORE_AUDIT(BARRIERED): in-bounds extending set is immediately recorded via note_array_slot. ptr::write(elements_ptr.add(index as usize), value); note_array_slot(arr, index as usize, value_bits); return arr; @@ -356,6 +359,7 @@ pub extern "C" fn js_array_set_f64_extend( let elements_ptr = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; let hole = f64::from_bits(crate::value::TAG_HOLE); for i in length..index { + // GC_STORE_AUDIT(BARRIERED): sparse gap sentinel is immediately recorded via note_array_slot. ptr::write(elements_ptr.add(i as usize), hole); note_array_slot(arr, i as usize, crate::value::TAG_HOLE); } @@ -363,6 +367,7 @@ pub extern "C" fn js_array_set_f64_extend( // Set the value let value = canonicalize_array_numeric_store_value(arr, value); let value_bits = value.to_bits(); + // GC_STORE_AUDIT(BARRIERED): extending set value is immediately recorded via note_array_slot. ptr::write(elements_ptr.add(index as usize), value); note_array_slot(arr, index as usize, value_bits); (*arr).length = new_length; diff --git a/crates/perry-runtime/src/array/iter_methods.rs b/crates/perry-runtime/src/array/iter_methods.rs index 87ef0f012..f49892b43 100644 --- a/crates/perry-runtime/src/array/iter_methods.rs +++ b/crates/perry-runtime/src/array/iter_methods.rs @@ -77,6 +77,7 @@ pub extern "C" fn js_array_map( } else { js_closure_call2(callback, element, i as f64) }; + // GC_STORE_AUDIT(INIT): map result is unpublished; slot layout is noted immediately below. ptr::write(result_elements.add(i), mapped); let mapped_bits = mapped.to_bits(); if length <= 64 { diff --git a/crates/perry-runtime/src/array/jsvalue_api.rs b/crates/perry-runtime/src/array/jsvalue_api.rs index 31c9968f5..741264afa 100644 --- a/crates/perry-runtime/src/array/jsvalue_api.rs +++ b/crates/perry-runtime/src/array/jsvalue_api.rs @@ -36,6 +36,7 @@ pub extern "C" fn js_array_from_jsvalue(elements: *const u64, count: u32) -> *mu // Each u64 contains NaN-boxed JSValue bits, store as f64 bits for i in 0..count as usize { let bits = *elements.add(i); + // GC_STORE_AUDIT(BARRIERED): JSValue array initialization is followed by layout/barrier rebuild. ptr::write(arr_elements.add(i), f64::from_bits(bits)); } rebuild_array_layout(arr); diff --git a/crates/perry-runtime/src/array/push_pop.rs b/crates/perry-runtime/src/array/push_pop.rs index 95948c37a..f5659c6f5 100644 --- a/crates/perry-runtime/src/array/push_pop.rs +++ b/crates/perry-runtime/src/array/push_pop.rs @@ -30,6 +30,7 @@ pub extern "C" fn js_array_grow(arr: *mut ArrayHeader, min_capacity: u32) -> *mu // Allocate new from arena and copy old data. let new_ptr = arena_alloc_gc(new_size, 8, crate::gc::GC_TYPE_ARRAY) as *mut ArrayHeader; let arr = arr_handle.get_raw_mut_ptr::(); + // GC_STORE_AUDIT(BARRIERED): array growth copy transfers layout and replays write barriers below. ptr::copy_nonoverlapping(arr as *const u8, new_ptr as *mut u8, old_size); (*new_ptr).capacity = new_capacity; @@ -112,6 +113,7 @@ pub extern "C" fn js_array_push_f64(arr: *mut ArrayHeader, value: f64) -> *mut A let value = canonicalize_array_numeric_store_value(arr, value); let value_bits = value.to_bits(); let elements_ptr = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + // GC_STORE_AUDIT(BARRIERED): push slot is immediately recorded via note_array_slot. ptr::write(elements_ptr.add(length as usize), value); note_array_slot(arr, length as usize, value_bits); (*arr).length = length + 1; @@ -151,6 +153,7 @@ unsafe fn js_array_push_f64_grow( let value_bits = value.to_bits(); let elements_ptr = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + // GC_STORE_AUDIT(BARRIERED): grown push slot is immediately recorded via note_array_slot. ptr::write(elements_ptr.add(length as usize), value); note_array_slot(arr, length as usize, value_bits); (*arr).length = length + 1; @@ -261,6 +264,7 @@ pub extern "C" fn js_array_set_length(arr: *mut ArrayHeader, new_length: f64) { const TAG_UNDEFINED_F64: f64 = f64::from_bits(0x7FFC_0000_0000_0001u64); let elements_ptr = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; for i in n..cur { + // GC_STORE_AUDIT(BARRIERED): length truncation sentinel is immediately recorded via note_array_slot. std::ptr::write(elements_ptr.add(i as usize), TAG_UNDEFINED_F64); note_array_slot(arr, i as usize, TAG_UNDEFINED_F64.to_bits()); } @@ -282,6 +286,7 @@ pub extern "C" fn js_array_set_length(arr: *mut ArrayHeader, new_length: f64) { let elements_ptr = (target as *mut u8).add(std::mem::size_of::()) as *mut f64; for i in cur..n { + // GC_STORE_AUDIT(BARRIERED): length extension sentinel is immediately recorded via note_array_slot. std::ptr::write(elements_ptr.add(i as usize), TAG_UNDEFINED_F64); note_array_slot(target, i as usize, TAG_UNDEFINED_F64.to_bits()); } @@ -309,6 +314,7 @@ pub extern "C" fn js_array_delete(arr: *mut ArrayHeader, index: u32) -> i32 { } const TAG_UNDEFINED_F64: f64 = f64::from_bits(0x7FFC_0000_0000_0001u64); let elements_ptr = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + // GC_STORE_AUDIT(BARRIERED): delete sentinel is immediately recorded via note_array_slot. std::ptr::write(elements_ptr.add(index as usize), TAG_UNDEFINED_F64); note_array_slot(arr, index as usize, TAG_UNDEFINED_F64.to_bits()); 1 @@ -338,6 +344,7 @@ pub extern "C" fn js_array_shift_f64(arr: *mut ArrayHeader) -> f64 { let value = *elements_ptr; // Shift all elements down + // GC_STORE_AUDIT(BARRIERED): shift memmove is followed by layout/barrier rebuild. ptr::copy(elements_ptr.add(1), elements_ptr, (length - 1) as usize); (*arr).length = length - 1; rebuild_array_layout(arr); @@ -370,6 +377,7 @@ pub extern "C" fn js_array_unshift_f64(arr: *mut ArrayHeader, value: f64) -> *mu let elements_ptr = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; // Shift all elements up + // GC_STORE_AUDIT(BARRIERED): unshift memmove and new slot are followed by layout/barrier rebuild. ptr::copy(elements_ptr, elements_ptr.add(1), length as usize); // Write new element at beginning ptr::write(elements_ptr, value); diff --git a/crates/perry-runtime/src/array/sort.rs b/crates/perry-runtime/src/array/sort.rs index 47ac00a23..8f0999341 100644 --- a/crates/perry-runtime/src/array/sort.rs +++ b/crates/perry-runtime/src/array/sort.rs @@ -56,6 +56,7 @@ pub extern "C" fn js_array_sort_default(arr: *mut ArrayHeader) -> *mut ArrayHead pairs.sort_by(|a, b| a.0.cmp(&b.0)); for (i, (_, val)) in pairs.into_iter().enumerate() { + // GC_STORE_AUDIT(BARRIERED): default sort writes are followed by layout/barrier rebuild. *elements_ptr.add(i) = val; } rebuild_array_layout(arr); @@ -129,6 +130,7 @@ pub extern "C" fn js_array_sort_with_comparator( while j >= 0 { let cmp = cmp_with(comparator, direct_call, *elements_ptr.add(j as usize), key); if cmp > 0.0 { + // GC_STORE_AUDIT(BARRIERED): insertion-sort shift is included in the rebuild below. ptr::write( elements_ptr.add((j + 1) as usize), *elements_ptr.add(j as usize), @@ -138,6 +140,7 @@ pub extern "C" fn js_array_sort_with_comparator( break; } } + // GC_STORE_AUDIT(BARRIERED): insertion-sort key write is included in the rebuild below. ptr::write(elements_ptr.add((j + 1) as usize), key); } } else { @@ -156,6 +159,7 @@ pub extern "C" fn js_array_sort_with_comparator( let cmp = cmp_with(comparator, direct_call, *elements_ptr.add(j as usize), key); if cmp > 0.0 { + // GC_STORE_AUDIT(BARRIERED): large-sort insertion shift is included in the rebuild below. ptr::write( elements_ptr.add((j + 1) as usize), *elements_ptr.add(j as usize), @@ -165,6 +169,7 @@ pub extern "C" fn js_array_sort_with_comparator( break; } } + // GC_STORE_AUDIT(BARRIERED): large-sort insertion key write is included in the rebuild below. ptr::write(elements_ptr.add((j + 1) as usize), key); } run_start = run_end; @@ -187,6 +192,7 @@ pub extern "C" fn js_array_sort_with_comparator( let mut l = left; let mut r = mid; let mut k = left; + // GC_STORE_AUDIT(STACK): merge destination is a function-local Vec buffer, not GC heap. while l < mid && r < right { let cmp = cmp_with(comparator, direct_call, *src.add(l), *src.add(r)); if cmp <= 0.0 { @@ -198,11 +204,13 @@ pub extern "C" fn js_array_sort_with_comparator( } k += 1; } + // GC_STORE_AUDIT(STACK): remaining left run copies into the temporary merge buffer. while l < mid { *dst.add(k) = *src.add(l); l += 1; k += 1; } + // GC_STORE_AUDIT(STACK): remaining right run copies into the temporary merge buffer. while r < right { *dst.add(k) = *src.add(r); r += 1; @@ -218,6 +226,7 @@ pub extern "C" fn js_array_sort_with_comparator( // If final result is in buf, copy back to elements if src != elements_ptr { + // GC_STORE_AUDIT(BARRIERED): merge buffer copyback is followed by layout/barrier rebuild. ptr::copy_nonoverlapping(src, elements_ptr, length); } } diff --git a/crates/perry-runtime/src/array/splice_slice.rs b/crates/perry-runtime/src/array/splice_slice.rs index a035890c1..108e7c2f0 100644 --- a/crates/perry-runtime/src/array/splice_slice.rs +++ b/crates/perry-runtime/src/array/splice_slice.rs @@ -53,6 +53,7 @@ pub extern "C" fn js_array_splice( // Copy deleted elements to return array for i in 0..actual_delete as usize { + // GC_STORE_AUDIT(BARRIERED): deleted-array initialization is followed by layout/barrier rebuild. ptr::write( deleted_elements.add(i), *elements_ptr.add(start_idx as usize + i), @@ -79,12 +80,14 @@ pub extern "C" fn js_array_splice( // Need to shift the tail let src = elements_ptr.add(tail_start as usize); let dst = elements_ptr.add((start_idx + items_count) as usize); + // GC_STORE_AUDIT(BARRIERED): splice tail memmove is followed by layout/barrier rebuild. ptr::copy(src, dst, tail_len as usize); } // Insert new items if items_count > 0 && !items.is_null() { for i in 0..items_count as usize { + // GC_STORE_AUDIT(BARRIERED): splice inserted item writes are followed by layout/barrier rebuild. ptr::write(elements_ptr.add(start_idx as usize + i), *items.add(i)); } } @@ -179,6 +182,7 @@ pub extern "C" fn js_array_slice( let dst_elements = (result as *mut u8).add(std::mem::size_of::()) as *mut f64; for i in 0..slice_len as usize { + // GC_STORE_AUDIT(BARRIERED): slice result initialization is followed by layout/barrier rebuild. ptr::write( dst_elements.add(i), ptr::read(src_elements.add(start_idx as usize + i)), diff --git a/crates/perry-runtime/src/closure/alloc.rs b/crates/perry-runtime/src/closure/alloc.rs index e1ad0179c..5e9ac8062 100644 --- a/crates/perry-runtime/src/closure/alloc.rs +++ b/crates/perry-runtime/src/closure/alloc.rs @@ -323,6 +323,7 @@ pub extern "C" fn js_closure_alloc_with_captures_singleton( .collect(); unsafe { let dest = closure_capture_slots_mut(allocated); + // GC_STORE_AUDIT(BARRIERED): copied captures are followed by closure layout/barrier rebuild. std::ptr::copy_nonoverlapping(rewritten_captures.as_ptr(), dest, n); rebuild_closure_layout_and_barriers(allocated, n); } @@ -376,6 +377,7 @@ pub extern "C" fn js_closure_alloc_with_captures_singleton( if n > 0 && !captures_ptr.is_null() { unsafe { let dest = closure_capture_slots_mut(allocated); + // GC_STORE_AUDIT(BARRIERED): cached closure captures are followed by layout/barrier rebuild. std::ptr::copy_nonoverlapping(rewritten_captures.as_ptr(), dest, n); rebuild_closure_layout_and_barriers(allocated, n); } @@ -434,6 +436,7 @@ pub extern "C" fn js_closure_set_capture_f64(closure: *mut ClosureHeader, index: } unsafe { let captures_ptr = closure_capture_slots_mut(closure) as *mut f64; + // GC_STORE_AUDIT(BARRIERED): closure f64 capture write is immediately recorded via note_closure_capture_slot. *captures_ptr.add(index as usize) = value; note_closure_capture_slot(closure, index as usize, value.to_bits()); } @@ -460,6 +463,7 @@ pub extern "C" fn js_closure_set_capture_ptr(closure: *mut ClosureHeader, index: } unsafe { let captures_ptr = closure_capture_slots_mut(closure) as *mut i64; + // GC_STORE_AUDIT(BARRIERED): closure pointer capture write is immediately recorded via note_closure_capture_slot. *captures_ptr.add(index as usize) = value; note_closure_capture_slot(closure, index as usize, value as u64); } diff --git a/crates/perry-runtime/src/closure/dispatch.rs b/crates/perry-runtime/src/closure/dispatch.rs index 78b28c2fa..4efb2bebc 100644 --- a/crates/perry-runtime/src/closure/dispatch.rs +++ b/crates/perry-runtime/src/closure/dispatch.rs @@ -1293,18 +1293,22 @@ pub unsafe extern "C" fn js_closure_call_apply_with_spread( let mut heap_buf: Vec; let buf_ptr: *const f64 = if total <= 16 { if !regular_args.is_null() && reg_n > 0 { + // GC_STORE_AUDIT(STACK): spread-call regular args copy into a temporary stack buffer. std::ptr::copy_nonoverlapping(regular_args, stack_buf.as_mut_ptr(), reg_n); } if !spread_data.is_null() && spread_n > 0 { + // GC_STORE_AUDIT(STACK): spread args copy into a temporary stack buffer. std::ptr::copy_nonoverlapping(spread_data, stack_buf.as_mut_ptr().add(reg_n), spread_n); } stack_buf.as_ptr() } else { heap_buf = vec![0.0; total]; if !regular_args.is_null() && reg_n > 0 { + // GC_STORE_AUDIT(STACK): regular args copy into a temporary native Vec buffer. std::ptr::copy_nonoverlapping(regular_args, heap_buf.as_mut_ptr(), reg_n); } if !spread_data.is_null() && spread_n > 0 { + // GC_STORE_AUDIT(STACK): spread args copy into a temporary native Vec buffer. std::ptr::copy_nonoverlapping(spread_data, heap_buf.as_mut_ptr().add(reg_n), spread_n); } heap_buf.as_ptr() diff --git a/crates/perry-runtime/src/closure/dynamic_props.rs b/crates/perry-runtime/src/closure/dynamic_props.rs index e3d15a107..4e1e3889d 100644 --- a/crates/perry-runtime/src/closure/dynamic_props.rs +++ b/crates/perry-runtime/src/closure/dynamic_props.rs @@ -368,6 +368,7 @@ pub extern "C" fn js_closure_unbind_this(val: f64) -> f64 { let src_captures = closure_capture_slots_mut(source_ptr as *mut ClosureHeader); let dst_captures = closure_capture_slots_mut(new_closure); // Set slot 0 to undefined + // GC_STORE_AUDIT(BARRIERED): cloned closure capture stores are followed by layout/barrier rebuild. *dst_captures = crate::value::TAG_UNDEFINED; // Copy remaining captures (slots 1..count) for i in 1..count { @@ -483,10 +484,12 @@ pub(crate) fn clone_closure_rebind_this(closure_bits: u64, recv_box: f64) -> u64 let src_captures = closure_capture_slots_mut(source_ptr as *mut ClosureHeader); let dst_captures = closure_capture_slots_mut(new_closure); // Copy every capture verbatim, then overwrite the `this` slot (last) with recv_box. + // GC_STORE_AUDIT(BARRIERED): rebound closure captures are followed by layout/barrier rebuild. for i in 0..count { *dst_captures.add(i) = *src_captures.add(i); } let this_slot = count - 1; + // GC_STORE_AUDIT(BARRIERED): rebound this capture is included in the layout/barrier rebuild. *dst_captures.add(this_slot) = recv_handle.get_nanbox_f64().to_bits(); rebuild_closure_layout_and_barriers(new_closure, count); let new_ptr = new_closure as u64; diff --git a/crates/perry-runtime/src/gc/mod.rs b/crates/perry-runtime/src/gc/mod.rs index 6233801d7..9a2929eee 100644 --- a/crates/perry-runtime/src/gc/mod.rs +++ b/crates/perry-runtime/src/gc/mod.rs @@ -273,6 +273,18 @@ pub fn gc_init() { gc_register_mutable_root_scanner(crate::typed_feedback::scan_typed_feedback_roots_mut); gc_register_mutable_root_scanner(transition_cache_mutable_root_scanner); gc_register_mutable_root_scanner(crate::object::scan_object_cache_roots_mut); + gc_register_budgeted_mutable_root_scanner_with_source( + crate::object::scan_class_side_table_roots_mut, + crate::object::scan_class_side_table_roots_mut_step, + crate::object::new_class_side_table_root_scan_state, + MutableRootScannerSource::RuntimeMutableScanner, + ); + gc_register_budgeted_mutable_root_scanner_with_source( + crate::symbol::scan_symbol_side_table_roots_mut, + crate::symbol::scan_symbol_side_table_roots_mut_step, + crate::symbol::new_symbol_side_table_root_scan_state, + MutableRootScannerSource::RuntimeMutableScanner, + ); // Issue #1813: the implicit-`this` cell holds the live receiver across a // dynamically-dispatched method body. A moving GC triggered from inside // that body (e.g. @perryts/mysql Pool.acquire → handshake → nativeScramble diff --git a/crates/perry-runtime/src/gc/tests/copying.rs b/crates/perry-runtime/src/gc/tests/copying.rs index 7dd49e17e..7857ed752 100644 --- a/crates/perry-runtime/src/gc/tests/copying.rs +++ b/crates/perry-runtime/src/gc/tests/copying.rs @@ -1699,6 +1699,148 @@ fn test_copying_minor_rewrites_overflow_owner_metadata_key() { assert!(crate::arena::pointer_in_nursery(mapped_value)); } +#[test] +fn test_copying_minor_rewrites_class_side_table_values_and_function_keys() { + let _guard = CopyingNurseryTestGuard::new(1); + crate::object::test_clear_class_side_table_roots(); + gc_register_mutable_root_scanner(crate::object::scan_class_side_table_roots_mut); + + let value = young_leaf(); + let prototype_object = crate::object::js_object_alloc(0, 0) as usize; + let parent_closure = crate::arena::arena_alloc_gc( + std::mem::size_of::(), + std::mem::align_of::(), + GC_TYPE_CLOSURE, + ) as usize; + let key = crate::arena::arena_alloc_gc( + std::mem::size_of::(), + std::mem::align_of::(), + GC_TYPE_CLOSURE, + ) as usize; + unsafe { + init_test_closure(parent_closure as *mut u8); + init_test_closure(key as *mut u8); + } + js_shadow_slot_set(0, ptr_bits(key)); + + crate::object::test_seed_class_dynamic_prop_root(0x5401, "dyn", string_bits(value)); + crate::object::test_seed_class_prototype_method_root(0x5401, "proto", string_bits(value)); + crate::object::test_seed_class_prototype_method_value_root(0x5401, "bound", string_bits(value)); + crate::object::test_seed_class_prototype_object_root(0x5401, prototype_object); + crate::object::test_seed_class_parent_closure_root(0x5401, parent_closure); + crate::object::test_seed_function_class_id_key(ptr_bits(key), 0x8200_5401); + + let _ = gc_collect_minor(); + + let dynamic_bits = crate::object::test_class_dynamic_prop_root_bits(0x5401, "dyn"); + let prototype_bits = crate::object::test_class_prototype_method_root_bits(0x5401, "proto"); + let cached_bits = crate::object::test_class_prototype_method_value_root_bits(0x5401, "bound"); + let prototype_object_after = crate::object::test_class_prototype_object_root_addr(0x5401); + let parent_closure_after = crate::object::test_class_parent_closure_root_addr(0x5401); + let value_after = (dynamic_bits & POINTER_MASK) as usize; + let key_after_bits = js_shadow_slot_get(0); + + assert_eq!(dynamic_bits & TAG_MASK, STRING_TAG); + assert_eq!(prototype_bits, dynamic_bits); + assert_eq!(cached_bits, dynamic_bits); + assert_ne!(value_after, value); + assert!(crate::arena::pointer_in_nursery(value_after)); + assert_ne!(prototype_object_after, prototype_object); + assert!(crate::arena::pointer_in_nursery(prototype_object_after)); + assert_ne!(parent_closure_after, parent_closure); + assert!(crate::arena::pointer_in_nursery(parent_closure_after)); + assert_ne!(key_after_bits, ptr_bits(key)); + assert_eq!( + crate::object::test_function_class_id_key_for_class(0x8200_5401), + key_after_bits + ); + assert_eq!( + crate::object::function_class_id(f64::from_bits(key_after_bits)), + 0x8200_5401 + ); +} + +#[test] +fn test_copying_minor_rewrites_symbol_side_table_roots_and_lookups() { + let _guard = CopyingNurseryTestGuard::new(1); + crate::symbol::test_clear_symbol_side_table_roots(); + gc_register_mutable_root_scanner(crate::symbol::scan_symbol_side_table_roots_mut); + + let owner = crate::object::js_object_alloc(0, 0) as usize; + let sym_key = unsafe { alloc_nursery_test_symbol() }; + let value = young_leaf(); + let static_sym_key = unsafe { alloc_nursery_test_symbol() }; + let static_value = young_leaf(); + js_shadow_slot_set(0, ptr_bits(owner)); + + crate::symbol::test_seed_symbol_pointer_root(sym_key); + crate::symbol::test_seed_symbol_pointer_root(static_sym_key); + unsafe { + crate::symbol::js_object_set_symbol_property( + f64::from_bits(ptr_bits(owner)), + f64::from_bits(ptr_bits(sym_key)), + f64::from_bits(string_bits(value)), + ); + crate::symbol::js_class_register_static_symbol( + 0x5402, + f64::from_bits(ptr_bits(static_sym_key)), + f64::from_bits(string_bits(static_value)), + ); + } + + let _ = gc_collect_minor(); + + let owner_after = (js_shadow_slot_get(0) & POINTER_MASK) as usize; + let entries = crate::symbol::test_symbol_property_roots(owner_after); + assert_eq!(entries.len(), 1); + let (sym_key_after, value_bits_after) = entries[0]; + let value_after = (value_bits_after & POINTER_MASK) as usize; + let static_entries = crate::symbol::test_class_static_symbol_roots_for_class(0x5402); + assert_eq!(static_entries.len(), 1); + let (static_sym_key_after, static_value_bits_after) = static_entries[0]; + let static_value_after = (static_value_bits_after & POINTER_MASK) as usize; + + assert_ne!(owner_after, owner); + assert_ne!(sym_key_after, sym_key); + assert_ne!(value_after, value); + assert_ne!(static_sym_key_after, static_sym_key); + assert_ne!(static_value_after, static_value); + assert!(crate::arena::pointer_in_nursery(owner_after)); + assert!(crate::arena::pointer_in_nursery(sym_key_after)); + assert!(crate::arena::pointer_in_nursery(value_after)); + assert!(crate::arena::pointer_in_nursery(static_sym_key_after)); + assert!(crate::arena::pointer_in_nursery(static_value_after)); + assert!( + !crate::symbol::test_symbol_property_owner_exists(owner), + "symbol side table should not keep the stale owner key after moving" + ); + assert!(crate::symbol::test_symbol_pointer_root_contains( + sym_key_after + )); + assert!(crate::symbol::test_symbol_pointer_root_contains( + static_sym_key_after + )); + assert!(!crate::symbol::test_symbol_pointer_root_contains(sym_key)); + assert!(!crate::symbol::test_symbol_pointer_root_contains( + static_sym_key + )); + + let moved_owner = f64::from_bits(ptr_bits(owner_after)); + let moved_sym = f64::from_bits(ptr_bits(sym_key_after)); + let moved_static_sym = f64::from_bits(ptr_bits(static_sym_key_after)); + let class_ref = f64::from_bits(crate::value::INT32_TAG | 0x5402); + unsafe { + assert_eq!( + crate::symbol::js_object_get_symbol_property(moved_owner, moved_sym).to_bits(), + value_bits_after + ); + assert_eq!( + crate::symbol::js_object_get_symbol_property(class_ref, moved_static_sym).to_bits(), + static_value_bits_after + ); + } +} + #[test] fn test_copying_minor_rewrites_old_overflow_object_child_without_reentrant_borrow() { struct OverflowFieldsRootGuard; diff --git a/crates/perry-runtime/src/gc/tests/cycle_state.rs b/crates/perry-runtime/src/gc/tests/cycle_state.rs index cdda7d51f..fbf71647a 100644 --- a/crates/perry-runtime/src/gc/tests/cycle_state.rs +++ b/crates/perry-runtime/src/gc/tests/cycle_state.rs @@ -87,6 +87,26 @@ fn alloc_tracked_test_closure() -> *mut u8 { child } +fn alloc_tracked_test_object() -> *mut crate::object::ObjectHeader { + let header_size = std::mem::size_of::(); + let fields_size = 8 * std::mem::size_of::(); + let child = + gc_malloc(header_size + fields_size, GC_TYPE_OBJECT) as *mut crate::object::ObjectHeader; + unsafe { + (*child).object_type = crate::error::OBJECT_TYPE_REGULAR; + (*child).class_id = 0; + (*child).parent_class_id = 0; + (*child).field_count = 0; + (*child).keys_array = std::ptr::null_mut(); + let fields_ptr = (child as *mut u8).add(header_size) as *mut crate::JSValue; + for i in 0..8 { + std::ptr::write(fields_ptr.add(i), crate::JSValue::undefined()); + } + crate::gc::layout_init_pointer_free(child as *mut u8); + } + child +} + const VALID_POINTER_TEST_OBJECT_FIELDS: u32 = 1000; fn alloc_large_nursery_objects(count: usize) -> Vec { @@ -452,6 +472,66 @@ fn root_scan_slices_many_registered_timer_roots_with_tiny_budget() { } } +#[test] +fn root_scan_slices_many_registered_class_side_table_roots_with_tiny_budget() { + let _guard = CopyingNurseryTestGuard::new(0); + let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + crate::object::test_clear_class_side_table_roots(); + gc_register_budgeted_mutable_root_scanner_with_source( + crate::object::scan_class_side_table_roots_mut, + crate::object::scan_class_side_table_roots_mut_step, + crate::object::new_class_side_table_root_scan_state, + MutableRootScannerSource::RuntimeMutableScanner, + ); + + const ROOTS: usize = 32; + let children = (0..ROOTS).map(|_| young_leaf()).collect::>(); + for (idx, &child) in children.iter().enumerate() { + crate::object::test_seed_class_dynamic_prop_root( + 0x5300 + idx as u32, + "root", + string_bits(child), + ); + } + let prototype_object = crate::object::js_object_alloc(0, 0) as usize; + let parent_closure = alloc_tracked_test_closure() as usize; + crate::object::test_seed_class_prototype_object_root(0x53f0, prototype_object); + crate::object::test_seed_class_parent_closure_root(0x53f1, parent_closure); + + let mut state = GcCycleState::new_full(trace_snapshot(GcTriggerKind::Manual)); + run_cycle_until_phase(&mut state, GcCyclePhase::RootScan); + + let mut root_steps = 0usize; + while state.phase() == GcCyclePhase::RootScan { + state.step(GcWorkBudget::bounded(1)); + root_steps += 1; + assert!(root_steps < 10_000, "root scan did not finish"); + } + assert!( + root_steps > ROOTS, + "class side-table roots should require multiple tiny root_scan steps" + ); + for &child in &children { + let header = unsafe { header_from_user_ptr(child as *const u8) }; + unsafe { + assert_ne!( + (*header).gc_flags & GC_FLAG_MARKED, + 0, + "class side-table value should be marked by the sliced scanner" + ); + } + } + assert_marked_user_ptr( + prototype_object, + "prototype-object side-table value in sliced scanner", + ); + assert_marked_user_ptr( + parent_closure, + "parent-closure side-table value in sliced scanner", + ); + crate::object::test_clear_class_side_table_roots(); +} + #[test] fn root_scan_slices_many_registered_tui_state_roots_with_tiny_budget() { let _guard = CopyingNurseryTestGuard::new(0); @@ -952,6 +1032,244 @@ fn full_cycle_global_root_store_after_root_scan_preserves_new_value() { ); } +#[test] +fn full_cycle_class_static_field_store_after_root_scan_preserves_new_value() { + let _guard = CopyingNurseryTestGuard::new(0); + let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + + let child = alloc_tracked_test_closure(); + let mut state = GcCycleState::new_full(trace_snapshot(GcTriggerKind::Manual)); + run_cycle_until_phase(&mut state, GcCyclePhase::BlockPersistence); + assert!( + incremental_mark_barrier_active(), + "full cycle should keep root barriers active after root scan" + ); + + let name = b"lateStatic"; + unsafe { + crate::object::js_class_register_static_field( + 0x5101, + name.as_ptr(), + name.len(), + f64::from_bits(ptr_bits(child as usize)), + ); + } + run_cycle_in_single_unit_steps(&mut state); + + assert!( + malloc_user_ptr_tracked(child), + "static class field stored after root scan should survive via the side-table root barrier" + ); +} + +#[test] +fn full_cycle_symbol_property_store_after_root_scan_preserves_new_value() { + let _guard = CopyingNurseryTestGuard::new(0); + let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + crate::symbol::test_clear_symbol_side_table_roots(); + + let owner = alloc_tracked_test_object(); + let sym = alloc_tracked_test_symbol(); + let child = alloc_tracked_test_closure(); + let mut state = GcCycleState::new_full(trace_snapshot(GcTriggerKind::Manual)); + run_cycle_until_phase(&mut state, GcCyclePhase::BlockPersistence); + assert!( + incremental_mark_barrier_active(), + "full cycle should keep root barriers active after root scan" + ); + + unsafe { + crate::symbol::js_object_set_symbol_property( + f64::from_bits(ptr_bits(owner as usize)), + f64::from_bits(ptr_bits(sym as usize)), + f64::from_bits(ptr_bits(child as usize)), + ); + } + run_cycle_in_single_unit_steps(&mut state); + + assert!( + malloc_user_ptr_tracked(sym as *mut u8), + "symbol property key stored after root scan should survive via the side-table root barrier" + ); + assert!( + malloc_user_ptr_tracked(child), + "symbol property value stored after root scan should survive via the side-table root barrier" + ); + crate::symbol::test_clear_symbol_side_table_roots(); +} + +#[test] +fn full_cycle_class_static_symbol_store_after_root_scan_preserves_new_value() { + let _guard = CopyingNurseryTestGuard::new(0); + let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + crate::symbol::test_clear_symbol_side_table_roots(); + + let sym = alloc_tracked_test_symbol(); + let child = alloc_tracked_test_closure(); + let mut state = GcCycleState::new_full(trace_snapshot(GcTriggerKind::Manual)); + run_cycle_until_phase(&mut state, GcCyclePhase::BlockPersistence); + assert!( + incremental_mark_barrier_active(), + "full cycle should keep root barriers active after root scan" + ); + + unsafe { + crate::symbol::js_class_register_static_symbol( + 0x5106, + f64::from_bits(ptr_bits(sym as usize)), + f64::from_bits(ptr_bits(child as usize)), + ); + } + run_cycle_in_single_unit_steps(&mut state); + + assert!( + malloc_user_ptr_tracked(sym as *mut u8), + "class static symbol key stored after root scan should survive via the side-table root barrier" + ); + assert!( + malloc_user_ptr_tracked(child), + "class static symbol value stored after root scan should survive via the side-table root barrier" + ); + crate::symbol::test_clear_symbol_side_table_roots(); +} + +#[test] +fn full_cycle_class_ref_dynamic_prop_store_after_root_scan_preserves_new_value() { + let _guard = CopyingNurseryTestGuard::new(0); + let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + + let child = alloc_tracked_test_closure(); + let key = crate::string::js_string_from_bytes(b"lateDynamic".as_ptr(), 11); + let class_ref_bits = 0x7FFE_0000_0000_0000u64 | 0x5102; + let mut state = GcCycleState::new_full(trace_snapshot(GcTriggerKind::Manual)); + run_cycle_until_phase(&mut state, GcCyclePhase::BlockPersistence); + assert!( + incremental_mark_barrier_active(), + "full cycle should keep root barriers active after root scan" + ); + + crate::object::js_object_set_field_by_name( + class_ref_bits as *mut crate::object::ObjectHeader, + key, + f64::from_bits(ptr_bits(child as usize)), + ); + run_cycle_in_single_unit_steps(&mut state); + + assert!( + malloc_user_ptr_tracked(child), + "dynamic class-ref property stored after root scan should survive via the side-table root barrier" + ); +} + +#[test] +fn full_cycle_prototype_method_store_after_root_scan_preserves_new_value() { + let _guard = CopyingNurseryTestGuard::new(0); + let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + + let child = alloc_tracked_test_closure(); + let name = b"lateMethod"; + let mut state = GcCycleState::new_full(trace_snapshot(GcTriggerKind::Manual)); + run_cycle_until_phase(&mut state, GcCyclePhase::BlockPersistence); + assert!( + incremental_mark_barrier_active(), + "full cycle should keep root barriers active after root scan" + ); + + unsafe { + crate::object::js_register_prototype_method( + 0x5103, + name.as_ptr(), + name.len(), + f64::from_bits(ptr_bits(child as usize)), + ); + } + run_cycle_in_single_unit_steps(&mut state); + + assert!( + malloc_user_ptr_tracked(child), + "prototype method stored after root scan should survive via the side-table root barrier" + ); +} + +#[test] +fn full_cycle_prototype_object_store_after_root_scan_preserves_new_value() { + let _guard = CopyingNurseryTestGuard::new(0); + let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + + let child = alloc_tracked_test_object(); + let mut state = GcCycleState::new_full(trace_snapshot(GcTriggerKind::Manual)); + run_cycle_until_phase(&mut state, GcCyclePhase::BlockPersistence); + assert!( + incremental_mark_barrier_active(), + "full cycle should keep root barriers active after root scan" + ); + + let _created = crate::object::js_object_create(f64::from_bits(ptr_bits(child as usize))); + run_cycle_in_single_unit_steps(&mut state); + + assert!( + malloc_user_ptr_tracked(child as *mut u8), + "prototype object stored after root scan should survive via the side-table root barrier" + ); +} + +#[test] +fn full_cycle_parent_closure_store_after_root_scan_preserves_new_value() { + let _guard = CopyingNurseryTestGuard::new(0); + let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + + let child = alloc_tracked_test_closure(); + let mut state = GcCycleState::new_full(trace_snapshot(GcTriggerKind::Manual)); + run_cycle_until_phase(&mut state, GcCyclePhase::BlockPersistence); + assert!( + incremental_mark_barrier_active(), + "full cycle should keep root barriers active after root scan" + ); + + crate::object::js_register_class_parent_dynamic( + 0x5105, + f64::from_bits(ptr_bits(child as usize)), + ); + run_cycle_in_single_unit_steps(&mut state); + + assert!( + malloc_user_ptr_tracked(child), + "parent closure stored after root scan should survive via the side-table root barrier" + ); +} + +#[test] +fn full_cycle_bound_prototype_method_cache_after_root_scan_marks_new_value() { + let _guard = CopyingNurseryTestGuard::new(0); + let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + + let mut state = GcCycleState::new_full(trace_snapshot(GcTriggerKind::Manual)); + run_cycle_until_phase(&mut state, GcCyclePhase::BlockPersistence); + assert!( + incremental_mark_barrier_active(), + "full cycle should keep root barriers active after root scan" + ); + + let value = crate::object::class_prototype_method_value_for_name(0x5104, "lateBound"); + let value_bits = value.to_bits(); + assert_eq!(value_bits & TAG_MASK, POINTER_TAG); + let value_ptr = (value_bits & POINTER_MASK) as usize; + let value_header = unsafe { header_from_user_ptr(value_ptr as *const u8) }; + unsafe { + assert_ne!( + (*value_header).gc_flags & GC_FLAG_MARKED, + 0, + "bound prototype-method cache creation after root scan should fire the root barrier" + ); + } + + run_cycle_in_single_unit_steps(&mut state); + assert_eq!( + crate::object::test_class_prototype_method_value_root_bits(0x5104, "lateBound"), + value_bits + ); +} + #[test] fn full_cycle_exception_root_store_after_root_scan_preserves_new_value() { let _guard = CopyingNurseryTestGuard::new(0); diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots.rs b/crates/perry-runtime/src/gc/tests/runtime_roots.rs index 662545593..d1fe9a693 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots.rs @@ -26,13 +26,26 @@ fn force_next_general_arena_alloc_slow() { let _ = crate::arena::arena_alloc(TEST_BLOCK_SIZE, 8); } -fn register_runtime_handle_root_scanner_for_tests() { - gc_register_budgeted_mutable_root_scanner_with_source( - scan_runtime_handle_roots_mut, - scan_runtime_handle_roots_mut_step, - new_runtime_handle_root_scan_state, - MutableRootScannerSource::RuntimeHandles, - ); +fn assert_marked_user_ptr(ptr: usize, label: &str) { + unsafe { + let header = header_from_user_ptr(ptr as *const u8); + assert_ne!( + (*header).gc_flags & GC_FLAG_MARKED, + 0, + "{label} should be marked" + ); + } +} + +fn assert_unmarked_user_ptr(ptr: usize, label: &str) { + unsafe { + let header = header_from_user_ptr(ptr as *const u8); + assert_eq!( + (*header).gc_flags & GC_FLAG_MARKED, + 0, + "{label} should not be marked" + ); + } } fn assert_automatic_minor_gc_progressed(before: u64, context: &str) -> bool { @@ -92,6 +105,15 @@ fn assert_moved_closure_ptr(bits: u64, original: usize) -> usize { rewritten } +fn register_runtime_handle_root_scanner_for_tests() { + gc_register_budgeted_mutable_root_scanner_with_source( + scan_runtime_handle_roots_mut, + scan_runtime_handle_roots_mut_step, + new_runtime_handle_root_scan_state, + MutableRootScannerSource::RuntimeHandles, + ); +} + #[test] fn test_scoped_root_scanner_registry_guard_restores_counts() { let before = root_scanner_registry_counts(); @@ -364,6 +386,255 @@ fn test_implicit_this_root_scanner_marks_and_rewrites() { clear_mark_seeds(); } +#[test] +fn test_class_side_table_scanner_marks_values_but_not_function_keys() { + let _guard = GcTestIsolationGuard::new(); + clear_marks(); + clear_mark_seeds(); + crate::object::test_clear_class_side_table_roots(); + + let dynamic_value = young_leaf(); + let prototype_value = young_leaf(); + let cached_value = young_leaf(); + let prototype_object = crate::object::js_object_alloc(0, 0) as usize; + let parent_closure = crate::arena::arena_alloc_gc( + std::mem::size_of::(), + std::mem::align_of::(), + GC_TYPE_CLOSURE, + ) as usize; + let function_key = crate::arena::arena_alloc_gc( + std::mem::size_of::(), + std::mem::align_of::(), + GC_TYPE_CLOSURE, + ) as usize; + unsafe { + init_test_closure(parent_closure as *mut u8); + init_test_closure(function_key as *mut u8); + } + + crate::object::test_seed_class_dynamic_prop_root(0x5201, "dyn", string_bits(dynamic_value)); + crate::object::test_seed_class_prototype_method_root( + 0x5201, + "proto", + string_bits(prototype_value), + ); + crate::object::test_seed_class_prototype_method_value_root( + 0x5201, + "bound", + string_bits(cached_value), + ); + crate::object::test_seed_class_prototype_object_root(0x5201, prototype_object); + crate::object::test_seed_class_parent_closure_root(0x5201, parent_closure); + crate::object::test_seed_function_class_id_key(ptr_bits(function_key), 0x8200_5201); + + let valid_ptrs = build_valid_pointer_set(); + crate::object::scan_class_side_table_roots_mut(&mut RuntimeRootVisitor::for_mark(&valid_ptrs)); + + assert_marked_user_ptr(dynamic_value, "dynamic class property value"); + assert_marked_user_ptr(prototype_value, "prototype method value"); + assert_marked_user_ptr(cached_value, "cached bound prototype method value"); + assert_marked_user_ptr(prototype_object, "prototype-object side-table value"); + assert_marked_user_ptr(parent_closure, "parent-closure side-table value"); + assert_unmarked_user_ptr(function_key, "function-to-class metadata key"); + + crate::object::test_clear_class_side_table_roots(); + clear_marks(); + clear_mark_seeds(); +} + +#[test] +fn test_registered_class_side_table_scanner_rewrites_values_and_function_keys() { + let _guard = GcTestIsolationGuard::new(); + crate::object::test_clear_class_side_table_roots(); + gc_register_mutable_root_scanner(crate::object::scan_class_side_table_roots_mut); + + let value_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); + let key_user = crate::arena::arena_alloc_gc( + std::mem::size_of::(), + std::mem::align_of::(), + GC_TYPE_CLOSURE, + ); + unsafe { + init_test_closure(key_user); + } + let valid_ptrs = build_valid_pointer_set(); + let value_old = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT); + let key_old = crate::arena::arena_alloc_gc_old( + std::mem::size_of::(), + std::mem::align_of::(), + GC_TYPE_CLOSURE, + ); + unsafe { + init_test_closure(key_old); + set_forwarding_address(header_from_user_ptr(value_user) as *mut GcHeader, value_old); + set_forwarding_address(header_from_user_ptr(key_user) as *mut GcHeader, key_old); + } + + let value_bits = ptr_bits(value_user as usize); + let value_old_bits = ptr_bits(value_old as usize); + let key_bits = ptr_bits(key_user as usize); + let key_old_bits = ptr_bits(key_old as usize); + crate::object::test_seed_class_dynamic_prop_root(0x5202, "dyn", value_bits); + crate::object::test_seed_class_prototype_method_root(0x5202, "proto", value_bits); + crate::object::test_seed_class_prototype_method_value_root(0x5202, "bound", value_bits); + crate::object::test_seed_class_prototype_object_root(0x5202, value_user as usize); + crate::object::test_seed_class_parent_closure_root(0x5202, key_user as usize); + crate::object::test_seed_function_class_id_key(key_bits, 0x8200_5202); + + rewrite_mutable_registered_roots(&valid_ptrs); + + assert_eq!( + crate::object::test_class_dynamic_prop_root_bits(0x5202, "dyn"), + value_old_bits + ); + assert_eq!( + crate::object::test_class_prototype_method_root_bits(0x5202, "proto"), + value_old_bits + ); + assert_eq!( + crate::object::test_class_prototype_method_value_root_bits(0x5202, "bound"), + value_old_bits + ); + assert_eq!( + crate::object::test_class_prototype_object_root_addr(0x5202), + value_old as usize + ); + assert_eq!( + crate::object::test_class_parent_closure_root_addr(0x5202), + key_old as usize + ); + assert_eq!( + crate::object::function_class_id(f64::from_bits(key_old_bits)), + 0x8200_5202 + ); + assert_eq!( + crate::object::test_function_class_id_key_for_class(0x8200_5202), + key_old_bits + ); + assert_eq!( + crate::object::function_class_id(f64::from_bits(key_bits)), + 0 + ); + + crate::object::test_clear_class_side_table_roots(); +} + +#[test] +fn test_symbol_side_table_scanner_marks_keys_and_values_without_marking_owner() { + let _guard = GcTestIsolationGuard::new(); + clear_marks(); + clear_mark_seeds(); + crate::symbol::test_clear_symbol_side_table_roots(); + + let owner = crate::object::js_object_alloc(0, 0) as usize; + let sym_key = unsafe { alloc_nursery_test_symbol() }; + let value = young_leaf(); + let static_sym_key = unsafe { alloc_nursery_test_symbol() }; + let static_value = young_leaf(); + + crate::symbol::test_seed_symbol_property_root(owner, sym_key, string_bits(value)); + crate::symbol::test_seed_class_static_symbol_root( + 0x5301, + static_sym_key, + string_bits(static_value), + ); + + let valid_ptrs = build_valid_pointer_set(); + crate::symbol::scan_symbol_side_table_roots_mut(&mut RuntimeRootVisitor::for_mark(&valid_ptrs)); + + assert_unmarked_user_ptr(owner, "symbol side-table owner metadata key"); + assert_marked_user_ptr(sym_key, "symbol property key"); + assert_marked_user_ptr(value, "symbol property value"); + assert_marked_user_ptr(static_sym_key, "class static symbol key"); + assert_marked_user_ptr(static_value, "class static symbol value"); + + crate::symbol::test_clear_symbol_side_table_roots(); + clear_marks(); + clear_mark_seeds(); +} + +#[test] +fn test_symbol_side_table_registered_scanner_rewrites_roots_and_metadata() { + let _guard = GcTestIsolationGuard::new(); + crate::symbol::test_clear_symbol_side_table_roots(); + gc_register_mutable_root_scanner(crate::symbol::scan_symbol_side_table_roots_mut); + + let owner = crate::object::js_object_alloc(0, 0) as usize; + let sym_key = unsafe { alloc_nursery_test_symbol() }; + let value = young_leaf(); + let static_sym_key = unsafe { alloc_nursery_test_symbol() }; + let static_value = young_leaf(); + + let valid_ptrs = build_valid_pointer_set(); + let owner_old = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT) as usize; + let sym_key_old = unsafe { alloc_old_test_symbol() }; + let value_old = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_STRING) as usize; + let static_sym_key_old = unsafe { alloc_old_test_symbol() }; + let static_value_old = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_STRING) as usize; + unsafe { + set_forwarding_address( + header_from_user_ptr(owner as *const u8) as *mut GcHeader, + owner_old as *mut u8, + ); + set_forwarding_address( + header_from_user_ptr(sym_key as *const u8) as *mut GcHeader, + sym_key_old as *mut u8, + ); + set_forwarding_address( + header_from_user_ptr(value as *const u8) as *mut GcHeader, + value_old as *mut u8, + ); + set_forwarding_address( + header_from_user_ptr(static_sym_key as *const u8) as *mut GcHeader, + static_sym_key_old as *mut u8, + ); + set_forwarding_address( + header_from_user_ptr(static_value as *const u8) as *mut GcHeader, + static_value_old as *mut u8, + ); + } + + crate::symbol::test_seed_symbol_pointer_root(sym_key); + crate::symbol::test_seed_symbol_pointer_root(static_sym_key); + crate::symbol::test_seed_symbol_property_root(owner, sym_key, string_bits(value)); + crate::symbol::test_seed_class_static_symbol_root( + 0x5302, + static_sym_key, + string_bits(static_value), + ); + + rewrite_mutable_registered_roots(&valid_ptrs); + + assert!( + !crate::symbol::test_symbol_property_owner_exists(owner), + "symbol side table should remove the stale owner key" + ); + assert_eq!( + crate::symbol::test_symbol_property_root_bits(owner_old, sym_key_old), + Some(string_bits(value_old)) + ); + assert_eq!( + crate::symbol::test_class_static_symbol_root_bits(0x5302, static_sym_key_old), + Some(string_bits(static_value_old)) + ); + assert_eq!( + crate::symbol::test_class_static_symbol_root_bits(0x5302, static_sym_key), + None + ); + assert!(crate::symbol::test_symbol_pointer_root_contains( + sym_key_old + )); + assert!(crate::symbol::test_symbol_pointer_root_contains( + static_sym_key_old + )); + assert!(!crate::symbol::test_symbol_pointer_root_contains(sym_key)); + assert!(!crate::symbol::test_symbol_pointer_root_contains( + static_sym_key + )); + + crate::symbol::test_clear_symbol_side_table_roots(); +} + #[test] fn test_runtime_root_visitor_rewrites_raw_pointer_slots() { let nursery_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/callback_scanners.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/callback_scanners.rs index 44e3eb96f..bc207990f 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/callback_scanners.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/callback_scanners.rs @@ -931,6 +931,14 @@ fn test_gc_init_mutable_scanner_families_rewrite_runtime_slots() { ); crate::object::test_seed_transition_cache_root(fixture.nursery_addr()); crate::object::test_seed_object_cache_roots([fixture.nursery_bits; 7], fixture.nursery_i64()); + crate::object::test_seed_class_dynamic_prop_root(0x5501, "dyn", fixture.nursery_bits); + crate::object::test_seed_class_prototype_method_root(0x5501, "proto", fixture.nursery_bits); + crate::object::test_seed_class_prototype_method_value_root( + 0x5501, + "bound", + fixture.nursery_bits, + ); + crate::object::test_seed_function_class_id_key(fixture.nursery_bits, 0x8200_5501); crate::json::test_seed_parse_roots( fixture.nursery_value(), fixture.nursery_user as *const crate::string::StringHeader, @@ -988,6 +996,7 @@ fn test_gc_init_mutable_scanner_families_rewrite_runtime_slots() { crate::array::scan_template_raw_roots_mut(&mut visitor); transition_cache_mutable_root_scanner(&mut visitor); crate::object::scan_object_cache_roots_mut(&mut visitor); + crate::object::scan_class_side_table_roots_mut(&mut visitor); json_parse_mutable_root_scanner(&mut visitor); intern_table_mutable_root_scanner(&mut visitor); small_int_cache_mutable_root_scanner(&mut visitor); @@ -1075,6 +1084,22 @@ fn test_gc_init_mutable_scanner_families_rewrite_runtime_slots() { crate::object::test_object_cache_roots(), ([fixture.old_bits; 7], fixture.old_addr() as i64) ); + assert_eq!( + crate::object::test_class_dynamic_prop_root_bits(0x5501, "dyn"), + fixture.old_bits + ); + assert_eq!( + crate::object::test_class_prototype_method_root_bits(0x5501, "proto"), + fixture.old_bits + ); + assert_eq!( + crate::object::test_class_prototype_method_value_root_bits(0x5501, "bound"), + fixture.old_bits + ); + assert_eq!( + crate::object::test_function_class_id_key_for_class(0x8200_5501), + fixture.old_bits + ); assert_eq!( crate::json::test_parse_roots_snapshot(), (fixture.old_bits, fixture.old_addr()) diff --git a/crates/perry-runtime/src/gc/tests/support.rs b/crates/perry-runtime/src/gc/tests/support.rs index 0b700fab6..434c6a683 100644 --- a/crates/perry-runtime/src/gc/tests/support.rs +++ b/crates/perry-runtime/src/gc/tests/support.rs @@ -40,6 +40,17 @@ pub(super) fn test_heap_child_slot_count(user_ptr: *mut u8) -> usize { } } +pub(super) fn assert_marked_user_ptr(ptr: usize, label: &str) { + unsafe { + let header = header_from_user_ptr(ptr as *const u8); + assert_ne!( + (*header).gc_flags & GC_FLAG_MARKED, + 0, + "{label} should be marked" + ); + } +} + pub(super) fn malloc_user_ptr_tracked(ptr: *mut u8) -> bool { let header = unsafe { header_from_user_ptr(ptr) }; MALLOC_STATE.with(|s| s.borrow().objects.iter().any(|&tracked| tracked == header)) @@ -257,6 +268,9 @@ fn reset_copying_nursery_runtime_test_state() { crate::object::test_clear_overflow_fields_root(); crate::object::test_clear_transition_cache_root(); crate::object::test_clear_object_cache_roots(); + crate::object::test_clear_class_side_table_roots(); + crate::symbol::test_clear_symbol_side_table_roots(); + crate::json::test_clear_parse_roots(); crate::set::test_clear_set_roots(); crate::os::test_clear_process_event_listeners(); crate::promise::test_clear_promise_scanner_roots(); @@ -677,6 +691,46 @@ pub(super) unsafe fn alloc_nursery_test_object( (obj, fields) } +pub(super) unsafe fn init_test_symbol(ptr: *mut u8) { + let id = YOUNG_LEAF_COUNTER.fetch_add(1, Ordering::Relaxed) as u64; + let sym = ptr as *mut crate::symbol::SymbolHeader; + (*sym).magic = crate::symbol::SYMBOL_MAGIC; + (*sym).registered = 0; + (*sym).description = std::ptr::null_mut(); + (*sym).id = 0x5A00_0000 | id; +} + +pub(super) unsafe fn alloc_nursery_test_symbol() -> usize { + let ptr = crate::arena::arena_alloc_gc( + std::mem::size_of::(), + std::mem::align_of::(), + GC_TYPE_STRING, + ); + init_test_symbol(ptr); + ptr as usize +} + +pub(super) unsafe fn alloc_old_test_symbol() -> usize { + let ptr = crate::arena::arena_alloc_gc_old( + std::mem::size_of::(), + std::mem::align_of::(), + GC_TYPE_STRING, + ); + init_test_symbol(ptr); + ptr as usize +} + +pub(super) fn alloc_tracked_test_symbol() -> *mut crate::symbol::SymbolHeader { + let ptr = gc_malloc( + std::mem::size_of::(), + GC_TYPE_STRING, + ); + unsafe { + init_test_symbol(ptr); + } + ptr as *mut crate::symbol::SymbolHeader +} + pub(super) unsafe fn alloc_old_test_array( length: u32, ) -> (*mut crate::array::ArrayHeader, *mut u64) { diff --git a/crates/perry-runtime/src/json/mod.rs b/crates/perry-runtime/src/json/mod.rs index 12b076c3a..7d49a2f6d 100644 --- a/crates/perry-runtime/src/json/mod.rs +++ b/crates/perry-runtime/src/json/mod.rs @@ -334,6 +334,7 @@ pub(crate) unsafe fn parse_shape_keys_array( (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; for (i, &key_ptr) in keys.iter().enumerate() { let bits = crate::value::STRING_TAG | (key_ptr as u64 & crate::value::POINTER_MASK); + // GC_STORE_AUDIT(INIT): parse shape keys array is filled before cache publication. *elements_ptr.add(i) = f64::from_bits(bits); crate::array::note_array_slot_layout_only(arr, i, bits); } @@ -450,6 +451,14 @@ pub(crate) fn test_seed_parse_roots(value: f64, key_ptr: *const StringHeader) { }); } +#[cfg(test)] +pub(crate) fn test_clear_parse_roots() { + PARSE_ROOTS.with(|r| r.borrow_mut().clear()); + PARSE_KEY_CACHE.with(|c| c.borrow_mut().clear()); + PARSE_KEY_RING.with(|ring| ring.borrow_mut().clear()); + PARSE_SHAPE_CACHE.with(|cache| cache.borrow_mut().clear()); +} + #[cfg(test)] pub(crate) fn test_parse_roots_snapshot() -> (u64, usize) { let value_bits = diff --git a/crates/perry-runtime/src/json/parser.rs b/crates/perry-runtime/src/json/parser.rs index 53c9085e6..60dc2ea15 100644 --- a/crates/perry-runtime/src/json/parser.rs +++ b/crates/perry-runtime/src/json/parser.rs @@ -133,6 +133,7 @@ impl<'a> DirectParser<'a> { let elements_ptr = (arr as *mut u8).add(std::mem::size_of::()) as *mut u64; let value_bits = value.bits(); let slot = elements_ptr.add(length as usize); + // GC_STORE_AUDIT(INIT): JSON.parse suppresses GC and notes layout for same-parse arrays below. std::ptr::write(slot, value_bits); // JSON.parse suppresses GC and writes only into arrays allocated // by the same parse, so a generational write barrier is redundant. @@ -361,6 +362,7 @@ impl<'a> DirectParser<'a> { for i in 0..alloc_field_count { let fields_ptr = (js_obj as *mut u8).add(std::mem::size_of::()) as *mut JSValue; + // GC_STORE_AUDIT(INIT): shaped JSON object fields are initialized before parse publication. std::ptr::write(fields_ptr.add(i), JSValue::undefined()); } let obj_slot = parse_root_push(JSValue::object_ptr(js_obj as *mut u8)); @@ -572,6 +574,7 @@ impl<'a> DirectParser<'a> { let fields_ptr = (js_obj as *mut u8).add(std::mem::size_of::()) as *mut JSValue; for i in 0..8 { + // GC_STORE_AUDIT(INIT): empty JSON object fields are initialized before parse publication. std::ptr::write(fields_ptr.add(i), JSValue::undefined()); } parse_root_restore(saved_roots); diff --git a/crates/perry-runtime/src/json/reviver.rs b/crates/perry-runtime/src/json/reviver.rs index c803a4ee5..43d36d04c 100644 --- a/crates/perry-runtime/src/json/reviver.rs +++ b/crates/perry-runtime/src/json/reviver.rs @@ -82,11 +82,8 @@ pub(crate) unsafe fn apply_reviver( ); // Write back the revived value let obj = - (value_handle.get_nanbox_u64() & POINTER_MASK) as *const crate::ObjectHeader; - let fields_ptr = - (obj as *const u8).add(std::mem::size_of::()) as *mut f64; - *fields_ptr.add(f as usize) = f64::from_bits(revived_child.bits()); - crate::gc::layout_note_slot(obj as usize, f as usize, revived_child.bits()); + (value_handle.get_nanbox_u64() & POINTER_MASK) as *mut crate::ObjectHeader; + crate::object::store_object_field_slot(obj, f as usize, revived_child.bits()); } } else if obj_type == crate::gc::GC_TYPE_ARRAY { let arr = (value_handle.get_nanbox_u64() & POINTER_MASK) as *const crate::ArrayHeader; @@ -112,10 +109,6 @@ pub(crate) unsafe fn apply_reviver( ); let arr = (value_handle.get_nanbox_u64() & POINTER_MASK) as *mut crate::ArrayHeader; - let elements = (arr as *const u8) - .add(std::mem::size_of::()) - as *mut f64; - *elements.add(i as usize) = f64::from_bits(revived_child.bits()); crate::array::note_array_slot(arr, i as usize, revived_child.bits()); } } diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs index bea4ca093..517c5d671 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -21,6 +21,29 @@ pub use super::class_handles::{ }; use super::*; +pub(crate) fn class_dynamic_prop_root_store(class_id: u32, name: String, value: f64) { + CLASS_DYNAMIC_PROPS.with(|m| { + m.borrow_mut() + .entry(class_id) + .or_insert_with(std::collections::HashMap::new) + .insert(name, value); + }); + crate::gc::runtime_write_barrier_root_nanbox(value.to_bits()); +} + +pub(crate) fn class_prototype_method_value_cache_root_store( + class_id: u32, + method_name: String, + value_bits: u64, +) { + CLASS_PROTOTYPE_METHOD_VALUES.with(|cache| { + cache + .borrow_mut() + .insert((class_id, method_name), value_bits); + }); + crate::gc::runtime_write_barrier_root_nanbox(value_bits); +} + // ============================================================================ // Class method vtable registry — enables runtime dispatch for interface-typed // and dynamically-typed method calls. Each class registers its methods and @@ -98,6 +121,30 @@ pub static CLASS_PROTOTYPE_OBJECTS: RwLock>> = RwLock /// Stored as `usize` (raw address) for Send + Sync; converted back at use. pub static CLASS_PARENT_CLOSURES: RwLock>> = RwLock::new(None); +pub(crate) fn class_prototype_object_root_store(class_id: u32, proto_ptr: *mut ObjectHeader) { + if class_id == 0 || proto_ptr.is_null() { + return; + } + let mut guard = CLASS_PROTOTYPE_OBJECTS.write().unwrap(); + if guard.is_none() { + *guard = Some(HashMap::new()); + } + guard.as_mut().unwrap().insert(class_id, proto_ptr as usize); + crate::gc::runtime_write_barrier_root_raw_ptr(proto_ptr); +} + +pub(crate) fn class_parent_closure_root_store(class_id: u32, closure_addr: usize) { + if class_id == 0 || closure_addr == 0 { + return; + } + let mut guard = CLASS_PARENT_CLOSURES.write().unwrap(); + if guard.is_none() { + *guard = Some(HashMap::new()); + } + guard.as_mut().unwrap().insert(class_id, closure_addr); + crate::gc::runtime_write_barrier_root_raw_ptr(closure_addr as *const u8); +} + /// Look up the parent-closure address recorded for a child class_id, if any. pub(crate) fn class_parent_closure(class_id: u32) -> Option { CLASS_PARENT_CLOSURES @@ -171,14 +218,7 @@ pub extern "C" fn js_set_function_prototype(func: f64, proto: f64) -> u32 { if let Some(&existing) = map.get(&func_bits) { // Update the prototype object (allow re-pointing) // without changing the class_id. - let mut proto_write = CLASS_PROTOTYPE_OBJECTS.write().unwrap(); - if proto_write.is_none() { - *proto_write = Some(HashMap::new()); - } - proto_write - .as_mut() - .unwrap() - .insert(existing, proto_ptr as usize); + class_prototype_object_root_store(existing, proto_ptr); crate::typed_feedback::invalidate_method_change(existing); return existing; } @@ -192,13 +232,7 @@ pub extern "C" fn js_set_function_prototype(func: f64, proto: f64) -> u32 { } write.as_mut().unwrap().insert(func_bits, new_cid); } - { - let mut write = CLASS_PROTOTYPE_OBJECTS.write().unwrap(); - if write.is_none() { - *write = Some(HashMap::new()); - } - write.as_mut().unwrap().insert(new_cid, proto_ptr as usize); - } + class_prototype_object_root_store(new_cid, proto_ptr); // Register the synthetic id so REGISTERED_CLASS_IDS-gated paths // (e.g., the #687 ClassRef-as-receiver short-circuit) recognize it. unsafe { js_register_class_id(new_cid) }; @@ -575,12 +609,7 @@ pub unsafe extern "C" fn js_class_register_static_field( Ok(s) => s.to_string(), Err(_) => return, }; - CLASS_DYNAMIC_PROPS.with(|m| { - m.borrow_mut() - .entry(class_id) - .or_insert_with(std::collections::HashMap::new) - .insert(name, value); - }); + class_dynamic_prop_root_store(class_id, name, value); } /// Issue #838: JS-classic prototype method assignment. @@ -609,6 +638,20 @@ pub unsafe extern "C" fn js_class_register_static_field( pub static CLASS_PROTOTYPE_METHODS: RwLock>>> = RwLock::new(None); +pub(crate) fn class_prototype_method_root_store(class_id: u32, name: String, value_bits: u64) { + let mut guard = CLASS_PROTOTYPE_METHODS.write().unwrap(); + if guard.is_none() { + *guard = Some(HashMap::new()); + } + guard + .as_mut() + .unwrap() + .entry(class_id) + .or_insert_with(HashMap::new) + .insert(name, value_bits); + crate::gc::runtime_write_barrier_root_nanbox(value_bits); +} + /// Register a JS-classic prototype-method assignment on a class. /// Called by codegen-emitted init code for each `Class.prototype. /// = ` (or aliased form) that the HIR recognises. `value` is the @@ -628,16 +671,7 @@ pub unsafe extern "C" fn js_register_prototype_method( Ok(s) => s.to_string(), Err(_) => return, }; - let mut guard = CLASS_PROTOTYPE_METHODS.write().unwrap(); - if guard.is_none() { - *guard = Some(HashMap::new()); - } - guard - .as_mut() - .unwrap() - .entry(class_id) - .or_insert_with(HashMap::new) - .insert(name, value.to_bits()); + class_prototype_method_root_store(class_id, name, value.to_bits()); // Ensure the receiver class can be `typeof`-detected. Method-less // classes that only get extended via `Class.prototype.m = fn` // wouldn't otherwise reach js_register_class_id. @@ -719,16 +753,7 @@ pub unsafe extern "C" fn js_register_function_prototype_method( Ok(s) => s.to_string(), Err(_) => return cid, }; - let mut guard = CLASS_PROTOTYPE_METHODS.write().unwrap(); - if guard.is_none() { - *guard = Some(HashMap::new()); - } - guard - .as_mut() - .unwrap() - .entry(cid) - .or_insert_with(HashMap::new) - .insert(name, value.to_bits()); + class_prototype_method_root_store(cid, name, value.to_bits()); js_register_class_id(cid); crate::typed_feedback::invalidate_method_change(cid); cid @@ -1027,6 +1052,403 @@ pub(crate) fn lookup_prototype_method(class_id: u32, name: &str) -> Option None } +#[derive(Clone)] +enum ClassSideTableRootSlot { + DynamicProp { class_id: u32, name: String }, + PrototypeMethod { class_id: u32, name: String }, + PrototypeMethodValue { class_id: u32, name: String }, + PrototypeObject { class_id: u32 }, + ParentClosure { class_id: u32 }, + FunctionClassIdKey { bits: u64 }, +} + +pub(crate) struct ClassSideTableRootScanState { + slots: Vec, + cursor: usize, +} + +pub(crate) fn new_class_side_table_root_scan_state() -> Box { + Box::new(ClassSideTableRootScanState { + slots: class_side_table_root_snapshot(), + cursor: 0, + }) +} + +pub(crate) fn scan_class_side_table_roots_mut_step( + visitor: &mut crate::gc::RuntimeRootVisitor<'_>, + state: &mut dyn std::any::Any, + remaining: &mut usize, +) -> bool { + let state = state + .downcast_mut::() + .expect("class side-table root scanner state type"); + while *remaining > 0 && state.cursor < state.slots.len() { + scan_class_side_table_root_slot(visitor, &state.slots[state.cursor]); + state.cursor += 1; + *remaining -= 1; + } + state.cursor >= state.slots.len() +} + +pub fn scan_class_side_table_roots(mark: &mut dyn FnMut(f64)) { + let mut visitor = crate::gc::RuntimeRootVisitor::for_copy(mark); + scan_class_side_table_roots_mut(&mut visitor); +} + +pub fn scan_class_side_table_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { + CLASS_DYNAMIC_PROPS.with(|m| { + let mut m = m.borrow_mut(); + for props in m.values_mut() { + for value in props.values_mut() { + visitor.visit_nanbox_f64_slot(value); + } + } + }); + + if let Ok(mut guard) = CLASS_PROTOTYPE_METHODS.write() { + if let Some(map) = guard.as_mut() { + for methods in map.values_mut() { + for value_bits in methods.values_mut() { + visitor.visit_nanbox_u64_slot(value_bits); + } + } + } + } + + CLASS_PROTOTYPE_METHOD_VALUES.with(|cache| { + let mut cache = cache.borrow_mut(); + for value_bits in cache.values_mut() { + visitor.visit_nanbox_u64_slot(value_bits); + } + }); + + if let Ok(mut guard) = CLASS_PROTOTYPE_OBJECTS.write() { + if let Some(map) = guard.as_mut() { + for proto_addr in map.values_mut() { + visitor.visit_usize_slot(proto_addr); + } + } + } + + if let Ok(mut guard) = CLASS_PARENT_CLOSURES.write() { + if let Some(map) = guard.as_mut() { + for closure_addr in map.values_mut() { + visitor.visit_usize_slot(closure_addr); + } + } + } + + scan_function_class_id_keys_mut(visitor); +} + +fn class_side_table_root_snapshot() -> Vec { + let mut slots = Vec::new(); + + CLASS_DYNAMIC_PROPS.with(|m| { + let m = m.borrow(); + for (&class_id, props) in m.iter() { + for name in props.keys() { + slots.push(ClassSideTableRootSlot::DynamicProp { + class_id, + name: name.clone(), + }); + } + } + }); + + if let Ok(guard) = CLASS_PROTOTYPE_METHODS.read() { + if let Some(map) = guard.as_ref() { + for (&class_id, methods) in map.iter() { + for name in methods.keys() { + slots.push(ClassSideTableRootSlot::PrototypeMethod { + class_id, + name: name.clone(), + }); + } + } + } + } + + CLASS_PROTOTYPE_METHOD_VALUES.with(|cache| { + let cache = cache.borrow(); + for ((class_id, name), _) in cache.iter() { + slots.push(ClassSideTableRootSlot::PrototypeMethodValue { + class_id: *class_id, + name: name.clone(), + }); + } + }); + + if let Ok(guard) = CLASS_PROTOTYPE_OBJECTS.read() { + if let Some(map) = guard.as_ref() { + for &class_id in map.keys() { + slots.push(ClassSideTableRootSlot::PrototypeObject { class_id }); + } + } + } + + if let Ok(guard) = CLASS_PARENT_CLOSURES.read() { + if let Some(map) = guard.as_ref() { + for &class_id in map.keys() { + slots.push(ClassSideTableRootSlot::ParentClosure { class_id }); + } + } + } + + if let Ok(guard) = FUNCTION_CLASS_IDS.read() { + if let Some(map) = guard.as_ref() { + for &bits in map.keys() { + slots.push(ClassSideTableRootSlot::FunctionClassIdKey { bits }); + } + } + } + + slots +} + +fn scan_class_side_table_root_slot( + visitor: &mut crate::gc::RuntimeRootVisitor<'_>, + slot: &ClassSideTableRootSlot, +) { + match slot { + ClassSideTableRootSlot::DynamicProp { class_id, name } => { + CLASS_DYNAMIC_PROPS.with(|m| { + if let Some(value) = m + .borrow_mut() + .get_mut(class_id) + .and_then(|props| props.get_mut(name)) + { + visitor.visit_nanbox_f64_slot(value); + } + }); + } + ClassSideTableRootSlot::PrototypeMethod { class_id, name } => { + if let Ok(mut guard) = CLASS_PROTOTYPE_METHODS.write() { + if let Some(value_bits) = guard + .as_mut() + .and_then(|map| map.get_mut(class_id)) + .and_then(|methods| methods.get_mut(name)) + { + visitor.visit_nanbox_u64_slot(value_bits); + } + } + } + ClassSideTableRootSlot::PrototypeMethodValue { class_id, name } => { + CLASS_PROTOTYPE_METHOD_VALUES.with(|cache| { + if let Some(value_bits) = cache.borrow_mut().get_mut(&(*class_id, name.clone())) { + visitor.visit_nanbox_u64_slot(value_bits); + } + }); + } + ClassSideTableRootSlot::PrototypeObject { class_id } => { + if let Ok(mut guard) = CLASS_PROTOTYPE_OBJECTS.write() { + if let Some(proto_addr) = guard.as_mut().and_then(|map| map.get_mut(class_id)) { + visitor.visit_usize_slot(proto_addr); + } + } + } + ClassSideTableRootSlot::ParentClosure { class_id } => { + if let Ok(mut guard) = CLASS_PARENT_CLOSURES.write() { + if let Some(closure_addr) = guard.as_mut().and_then(|map| map.get_mut(class_id)) { + visitor.visit_usize_slot(closure_addr); + } + } + } + ClassSideTableRootSlot::FunctionClassIdKey { bits } => { + rewrite_function_class_id_key_if_forwarded(visitor, *bits); + } + } +} + +fn scan_function_class_id_keys_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { + if !visitor.is_metadata_rewrite_phase() { + return; + } + let mut rewrites = Vec::new(); + if let Ok(mut guard) = FUNCTION_CLASS_IDS.write() { + let Some(map) = guard.as_mut() else { + return; + }; + for old_bits in map.keys().copied().collect::>() { + let mut new_bits = old_bits; + if visit_metadata_nanbox_key(visitor, &mut new_bits) && new_bits != old_bits { + rewrites.push((old_bits, new_bits)); + } + } + for (old_bits, new_bits) in rewrites { + if let Some(class_id) = map.remove(&old_bits) { + map.insert(new_bits, class_id); + } + } + } +} + +fn rewrite_function_class_id_key_if_forwarded( + visitor: &mut crate::gc::RuntimeRootVisitor<'_>, + old_bits: u64, +) { + if !visitor.is_metadata_rewrite_phase() { + return; + } + let mut new_bits = old_bits; + if !visit_metadata_nanbox_key(visitor, &mut new_bits) || new_bits == old_bits { + return; + } + if let Ok(mut guard) = FUNCTION_CLASS_IDS.write() { + if let Some(map) = guard.as_mut() { + if let Some(class_id) = map.remove(&old_bits) { + map.insert(new_bits, class_id); + } + } + } +} + +fn visit_metadata_nanbox_key( + visitor: &mut crate::gc::RuntimeRootVisitor<'_>, + bits: &mut u64, +) -> bool { + let tag = *bits & crate::value::TAG_MASK; + if tag != crate::value::POINTER_TAG + && tag != crate::value::STRING_TAG + && tag != crate::value::BIGINT_TAG + { + return false; + } + let mut addr = (*bits & crate::value::POINTER_MASK) as usize; + if visitor.visit_metadata_usize_slot(&mut addr) { + *bits = tag | (addr as u64 & crate::value::POINTER_MASK); + true + } else { + false + } +} + +#[cfg(test)] +pub(crate) fn test_clear_class_side_table_roots() { + CLASS_DYNAMIC_PROPS.with(|m| m.borrow_mut().clear()); + CLASS_PROTOTYPE_METHOD_VALUES.with(|cache| cache.borrow_mut().clear()); + if let Ok(mut guard) = CLASS_PROTOTYPE_METHODS.write() { + *guard = None; + } + if let Ok(mut guard) = FUNCTION_CLASS_IDS.write() { + *guard = None; + } + if let Ok(mut guard) = CLASS_PROTOTYPE_OBJECTS.write() { + *guard = None; + } + if let Ok(mut guard) = CLASS_PARENT_CLOSURES.write() { + *guard = None; + } + NEXT_SYNTHETIC_CLASS_ID.store(0x8000_0000, std::sync::atomic::Ordering::Relaxed); +} + +#[cfg(test)] +pub(crate) fn test_seed_class_dynamic_prop_root(class_id: u32, name: &str, value_bits: u64) { + class_dynamic_prop_root_store(class_id, name.to_string(), f64::from_bits(value_bits)); +} + +#[cfg(test)] +pub(crate) fn test_class_dynamic_prop_root_bits(class_id: u32, name: &str) -> u64 { + CLASS_DYNAMIC_PROPS.with(|m| { + m.borrow() + .get(&class_id) + .and_then(|props| props.get(name)) + .map(|value| value.to_bits()) + .unwrap_or(0) + }) +} + +#[cfg(test)] +pub(crate) fn test_seed_class_prototype_method_root(class_id: u32, name: &str, value_bits: u64) { + class_prototype_method_root_store(class_id, name.to_string(), value_bits); +} + +#[cfg(test)] +pub(crate) fn test_class_prototype_method_root_bits(class_id: u32, name: &str) -> u64 { + CLASS_PROTOTYPE_METHODS + .read() + .ok() + .and_then(|guard| { + guard + .as_ref() + .and_then(|map| map.get(&class_id)) + .and_then(|methods| methods.get(name)) + .copied() + }) + .unwrap_or(0) +} + +#[cfg(test)] +pub(crate) fn test_seed_class_prototype_method_value_root( + class_id: u32, + name: &str, + value_bits: u64, +) { + class_prototype_method_value_cache_root_store(class_id, name.to_string(), value_bits); +} + +#[cfg(test)] +pub(crate) fn test_class_prototype_method_value_root_bits(class_id: u32, name: &str) -> u64 { + CLASS_PROTOTYPE_METHOD_VALUES.with(|cache| { + cache + .borrow() + .get(&(class_id, name.to_string())) + .copied() + .unwrap_or(0) + }) +} + +#[cfg(test)] +pub(crate) fn test_seed_class_prototype_object_root(class_id: u32, addr: usize) { + class_prototype_object_root_store(class_id, addr as *mut ObjectHeader); +} + +#[cfg(test)] +pub(crate) fn test_class_prototype_object_root_addr(class_id: u32) -> usize { + CLASS_PROTOTYPE_OBJECTS + .read() + .ok() + .and_then(|guard| guard.as_ref().and_then(|map| map.get(&class_id).copied())) + .unwrap_or(0) +} + +#[cfg(test)] +pub(crate) fn test_seed_class_parent_closure_root(class_id: u32, addr: usize) { + class_parent_closure_root_store(class_id, addr); +} + +#[cfg(test)] +pub(crate) fn test_class_parent_closure_root_addr(class_id: u32) -> usize { + CLASS_PARENT_CLOSURES + .read() + .ok() + .and_then(|guard| guard.as_ref().and_then(|map| map.get(&class_id).copied())) + .unwrap_or(0) +} + +#[cfg(test)] +pub(crate) fn test_seed_function_class_id_key(func_bits: u64, class_id: u32) { + let mut guard = FUNCTION_CLASS_IDS.write().unwrap(); + if guard.is_none() { + *guard = Some(HashMap::new()); + } + guard.as_mut().unwrap().insert(func_bits, class_id); +} + +#[cfg(test)] +pub(crate) fn test_function_class_id_key_for_class(class_id: u32) -> u64 { + FUNCTION_CLASS_IDS + .read() + .ok() + .and_then(|guard| { + guard.as_ref().and_then(|map| { + map.iter() + .find_map(|(&bits, &cid)| (cid == class_id).then_some(bits)) + }) + }) + .unwrap_or(0) +} + /// Returns true if `class_id` corresponds to a registered class. Used by /// `js_value_typeof` (refs #618 / #420 followup) to distinguish a class /// reference (NaN-boxed INT32 with class_id payload) from a regular int32 @@ -1561,11 +1983,7 @@ pub extern "C" fn js_register_class_parent_dynamic(class_id: u32, parent_value: if tag == POINTER_TAG { let ptr = crate::value::js_nanbox_get_pointer(parent_value) as *mut ObjectHeader; if !ptr.is_null() && js_object_get_class_id(ptr as *const ObjectHeader) != 0 { - let mut write = CLASS_PROTOTYPE_OBJECTS.write().unwrap(); - if write.is_none() { - *write = Some(HashMap::new()); - } - write.as_mut().unwrap().insert(class_id, ptr as usize); + class_prototype_object_root_store(class_id, ptr); } else if !ptr.is_null() && crate::closure::is_closure_ptr(ptr as usize) { // #36 / #321: the parent is a plain FUNCTION value (closure), e.g. // effect's `class Svc extends Context.Tag("Svc")<...>() {}`. Record @@ -1574,11 +1992,7 @@ pub extern "C" fn js_register_class_parent_dynamic(class_id: u32, parent_value: // function's own props + ITS static prototype. The parent class_id // edge isn't wired (a closure carries no class_id), so this is the // only inheritance link for a function-valued superclass. - let mut write = CLASS_PARENT_CLOSURES.write().unwrap(); - if write.is_none() { - *write = Some(HashMap::new()); - } - write.as_mut().unwrap().insert(class_id, ptr as usize); + class_parent_closure_root_store(class_id, ptr as usize); } } } diff --git a/crates/perry-runtime/src/object/field_set_by_name.rs b/crates/perry-runtime/src/object/field_set_by_name.rs index 351d02901..6f228b000 100644 --- a/crates/perry-runtime/src/object/field_set_by_name.rs +++ b/crates/perry-runtime/src/object/field_set_by_name.rs @@ -185,12 +185,7 @@ pub extern "C" fn js_object_set_field_by_name( .unwrap_or("") .to_string(); if !name.is_empty() { - CLASS_DYNAMIC_PROPS.with(|m| { - m.borrow_mut() - .entry(class_id) - .or_insert_with(std::collections::HashMap::new) - .insert(name, value); - }); + class_dynamic_prop_root_store(class_id, name, value); } } return; diff --git a/crates/perry-runtime/src/object/native_module.rs b/crates/perry-runtime/src/object/native_module.rs index db1245379..2e46299ce 100644 --- a/crates/perry-runtime/src/object/native_module.rs +++ b/crates/perry-runtime/src/object/native_module.rs @@ -1577,25 +1577,32 @@ pub(crate) fn class_has_own_method(class_id: u32, method_name: &str) -> bool { } pub fn class_prototype_method_value_for_name(class_id: u32, method_name: &str) -> f64 { - CLASS_PROTOTYPE_METHOD_VALUES.with(|cache| { - let mut cache = cache.borrow_mut(); + if let Some(bits) = CLASS_PROTOTYPE_METHOD_VALUES.with(|cache| { + let cache = cache.borrow(); if let Some(bits) = cache.get(&(class_id, method_name.to_string())).copied() { - return f64::from_bits(bits); + return Some(bits); } + None + }) { + return f64::from_bits(bits); + } - // Bounded leak: `js_class_method_bind` keeps the byte pointer for the - // lifetime of the bound closure (it's stashed inside the closure's - // capture frame). We leak one allocation per unique - // `(class_id, method_name)` pair the program ever asks for, so the - // total leak is bounded by the static set of decorated method - // descriptors. The cache below short-circuits repeat queries. - let leaked: &'static [u8] = method_name.as_bytes().to_vec().leak(); - let class_bits = 0x7FFE_0000_0000_0000u64 | (class_id as u64 & 0xFFFF_FFFF); - let class_ref = f64::from_bits(class_bits); - let value = js_class_method_bind(class_ref, leaked.as_ptr(), leaked.len()); - cache.insert((class_id, method_name.to_string()), value.to_bits()); - value - }) + // Bounded leak: `js_class_method_bind` keeps the byte pointer for the + // lifetime of the bound closure (it's stashed inside the closure's + // capture frame). We leak one allocation per unique + // `(class_id, method_name)` pair the program ever asks for, so the + // total leak is bounded by the static set of decorated method + // descriptors. The cache below short-circuits repeat queries. + let leaked: &'static [u8] = method_name.as_bytes().to_vec().leak(); + let class_bits = 0x7FFE_0000_0000_0000u64 | (class_id as u64 & 0xFFFF_FFFF); + let class_ref = f64::from_bits(class_bits); + let value = js_class_method_bind(class_ref, leaked.as_ptr(), leaked.len()); + class_prototype_method_value_cache_root_store( + class_id, + method_name.to_string(), + value.to_bits(), + ); + value } #[no_mangle] diff --git a/crates/perry-runtime/src/object/object_ops.rs b/crates/perry-runtime/src/object/object_ops.rs index 2f6eba44a..c84be6683 100644 --- a/crates/perry-runtime/src/object/object_ops.rs +++ b/crates/perry-runtime/src/object/object_ops.rs @@ -1400,13 +1400,7 @@ pub extern "C" fn js_object_create(proto_value: f64) -> f64 { if valid { let cid = NEXT_SYNTHETIC_CLASS_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed); - { - let mut write = CLASS_PROTOTYPE_OBJECTS.write().unwrap(); - if write.is_none() { - *write = Some(HashMap::new()); - } - write.as_mut().unwrap().insert(cid, proto_ptr as usize); - } + class_prototype_object_root_store(cid, proto_ptr); unsafe { js_register_class_id(cid) }; // #1805: link the synthetic class_id into the original class's // inheritance chain. `Object.getPrototypeOf(instance)` returns diff --git a/crates/perry-runtime/src/string/char_ops.rs b/crates/perry-runtime/src/string/char_ops.rs index c98cba2bf..a459cb588 100644 --- a/crates/perry-runtime/src/string/char_ops.rs +++ b/crates/perry-runtime/src/string/char_ops.rs @@ -102,6 +102,7 @@ pub extern "C" fn js_string_to_char_array(s: i64) -> i64 { let nanboxed = f64::from_bits(crate::value::STRING_TAG | (ch_ptr as u64 & crate::value::POINTER_MASK)); unsafe { + // GC_STORE_AUDIT(BARRIERED): char array slot is immediately recorded via note_array_slot. *elements.add(i) = nanboxed; crate::array::note_array_slot(arr, i, nanboxed.to_bits()); } diff --git a/crates/perry-runtime/src/string/split.rs b/crates/perry-runtime/src/string/split.rs index 1d04f17b3..bff78606d 100644 --- a/crates/perry-runtime/src/string/split.rs +++ b/crates/perry-runtime/src/string/split.rs @@ -75,6 +75,7 @@ pub extern "C" fn js_string_split_n( let elements_ptr = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; for (i, p) in parts.iter().enumerate() { let nanboxed = STRING_TAG | (*p as u64 & POINTER_MASK); + // GC_STORE_AUDIT(BARRIERED): split char slot is immediately recorded via note_array_slot. std::ptr::write(elements_ptr.add(i), f64::from_bits(nanboxed)); crate::array::note_array_slot(arr, i, nanboxed); } @@ -108,6 +109,7 @@ pub extern "C" fn js_string_split_n( ptr::copy_nonoverlapping(part.as_ptr(), data_ptr, byte_len as usize); } let nanboxed = STRING_TAG | (sh as u64 & POINTER_MASK); + // GC_STORE_AUDIT(BARRIERED): split part slot is immediately recorded via note_array_slot. std::ptr::write(elements_ptr.add(i), f64::from_bits(nanboxed)); crate::array::note_array_slot(arr, i, nanboxed); } diff --git a/crates/perry-runtime/src/symbol.rs b/crates/perry-runtime/src/symbol.rs index 6af6bc099..c2af192e3 100644 --- a/crates/perry-runtime/src/symbol.rs +++ b/crates/perry-runtime/src/symbol.rs @@ -164,7 +164,7 @@ pub fn is_well_known_symbol(ptr: usize) -> bool { } fn register_symbol_pointer(ptr: usize) { - let mut guard = SYMBOL_POINTERS.lock().unwrap(); + let mut guard = crate::gc::lock_gc_root_registry(&SYMBOL_POINTERS); if guard.is_none() { *guard = Some(HashSet::new()); } @@ -199,13 +199,14 @@ pub(crate) fn is_global_registered_symbol(ptr: usize) -> bool { // Storage is intentionally simple (linear scan per lookup) — symbol-keyed // properties on a single object are rare. // -// NOTE: this side table holds raw pointers and is GC-blind. Stored values -// (symbol pointers and any pointer-shaped JSValues) won't be traced as roots. -// For the test scenarios this matters: symbols allocated through `Symbol(desc)` -// hit `gc_malloc` and would be reclaimed if a GC ran while the user code only -// kept a reference via `obj[sym]`. In practice the test doesn't trigger GC -// between the `obj[sym] = v` write and the `getOwnPropertySymbols(obj)` read, -// so this is acceptable for now. +// GC invariant: +// - SYMBOL_PROPERTIES outer object keys are metadata-only raw pointers. They +// are rewritten when an owner moves but do not keep that owner alive. +// - SYMBOL_PROPERTIES inner symbol keys are raw-pointer roots. +// - SYMBOL_PROPERTIES values and CLASS_STATIC_SYMBOLS values are NaN-box roots. +// - CLASS_STATIC_SYMBOLS symbol keys are raw-pointer roots. +// - SYMBOL_POINTERS is metadata-only: moved symbol addresses are rewritten, but +// tracking a pointer there does not by itself keep the symbol alive. static SYMBOL_PROPERTIES: Mutex>>> = Mutex::new(None); // Monotonic id counter for fresh symbols. Not thread-safe per-thread but @@ -480,7 +481,7 @@ pub(crate) fn clone_symbol_entries_for_obj_ptr(src_obj_ptr: usize) -> Vec<(usize if src_obj_ptr == 0 { return Vec::new(); } - let guard = SYMBOL_PROPERTIES.lock().unwrap(); + let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); guard .as_ref() .and_then(|m| m.get(&src_obj_ptr)) @@ -544,27 +545,54 @@ unsafe fn infer_symbol_function_name(sym_key: usize, val_bits: u64) { crate::builtins::register_function_name_if_absent(func_ptr as usize, &inferred); } +fn publish_symbol_side_table_root_edges(sym_key: usize, value_bits: u64) { + crate::gc::runtime_write_barrier_root_raw_ptr(sym_key as *const SymbolHeader); + crate::gc::runtime_write_barrier_root_nanbox(value_bits); +} + +fn store_object_symbol_property_root(obj_key: usize, sym_key: usize, value_bits: u64) -> bool { + { + let mut guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); + if guard.is_none() { + *guard = Some(HashMap::new()); + } + let map = guard.as_mut().unwrap(); + let entries = map.entry(obj_key).or_default(); + for entry in entries.iter_mut() { + if entry.0 == sym_key { + entry.1 = value_bits; + drop(guard); + publish_symbol_side_table_root_edges(sym_key, value_bits); + return false; + } + } + entries.push((sym_key, value_bits)); + } + publish_symbol_side_table_root_edges(sym_key, value_bits); + true +} + +fn store_class_static_symbol_root(class_id: u32, sym_key: usize, value_bits: u64) { + { + let mut guard = crate::gc::lock_gc_root_registry(&CLASS_STATIC_SYMBOLS); + if guard.is_none() { + *guard = Some(HashMap::new()); + } + guard + .as_mut() + .unwrap() + .insert((class_id, sym_key), value_bits); + } + publish_symbol_side_table_root_edges(sym_key, value_bits); +} + unsafe fn set_symbol_property(obj_f64: f64, sym_f64: f64, value_f64: f64) -> f64 { let obj_key = obj_key_from_f64(obj_f64); let sym_key = sym_key_from_f64(sym_f64); if obj_key == 0 || sym_key == 0 { return value_f64; } - let val_bits = value_f64.to_bits(); - let mut guard = SYMBOL_PROPERTIES.lock().unwrap(); - if guard.is_none() { - *guard = Some(HashMap::new()); - } - let map = guard.as_mut().unwrap(); - let entries = map.entry(obj_key).or_default(); - // Update existing entry if the symbol is already present. - for entry in entries.iter_mut() { - if entry.0 == sym_key { - entry.1 = val_bits; - return value_f64; - } - } - entries.push((sym_key, val_bits)); + store_object_symbol_property_root(obj_key, sym_key, value_f64.to_bits()); value_f64 } @@ -620,14 +648,7 @@ pub unsafe extern "C" fn js_class_register_static_symbol(class_id: u32, sym: f64 if class_id == 0 || sym_key == 0 { return; } - let mut guard = CLASS_STATIC_SYMBOLS.lock().unwrap(); - if guard.is_none() { - *guard = Some(HashMap::new()); - } - guard - .as_mut() - .unwrap() - .insert((class_id, sym_key), value.to_bits()); + store_class_static_symbol_root(class_id, sym_key, value.to_bits()); } /// Look up a static Symbol-keyed property on a class by class_id. @@ -638,13 +659,378 @@ pub fn class_static_symbol_lookup(class_id: u32, sym_f64: f64) -> Option { if class_id == 0 || sym_key == 0 { return None; } - let guard = CLASS_STATIC_SYMBOLS.lock().unwrap(); + let guard = crate::gc::lock_gc_root_registry(&CLASS_STATIC_SYMBOLS); guard .as_ref() .and_then(|m| m.get(&(class_id, sym_key)).copied()) } } +fn merge_symbol_property_entries(dst: &mut Vec<(usize, u64)>, src: Vec<(usize, u64)>) { + for (sym_key, value_bits) in src { + if let Some(existing) = dst.iter_mut().find(|entry| entry.0 == sym_key) { + existing.1 = value_bits; + } else { + dst.push((sym_key, value_bits)); + } + } +} + +pub fn scan_symbol_side_table_roots(mark: &mut dyn FnMut(f64)) { + let mut visitor = crate::gc::RuntimeRootVisitor::for_copy(mark); + scan_symbol_side_table_roots_mut(&mut visitor); +} + +pub fn scan_symbol_side_table_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { + scan_symbol_property_roots_mut(visitor); + scan_class_static_symbol_roots_mut(visitor); + scan_symbol_pointer_metadata_roots_mut(visitor); +} + +fn scan_symbol_property_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { + let mut owner_rewrites = Vec::new(); + let mut guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); + let Some(map) = guard.as_mut() else { + return; + }; + + for (&owner, entries) in map.iter_mut() { + let mut new_owner = owner; + if visitor.visit_metadata_usize_slot(&mut new_owner) && new_owner != owner { + owner_rewrites.push((owner, new_owner)); + } + for (sym_key, value_bits) in entries.iter_mut() { + visitor.visit_usize_slot(sym_key); + visitor.visit_nanbox_u64_slot(value_bits); + } + } + + for (old_owner, new_owner) in owner_rewrites { + let Some(entries) = map.remove(&old_owner) else { + continue; + }; + match map.entry(new_owner) { + std::collections::hash_map::Entry::Occupied(mut entry) => { + merge_symbol_property_entries(entry.get_mut(), entries); + } + std::collections::hash_map::Entry::Vacant(entry) => { + entry.insert(entries); + } + } + } +} + +fn scan_class_static_symbol_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { + let mut key_rewrites = Vec::new(); + let mut guard = crate::gc::lock_gc_root_registry(&CLASS_STATIC_SYMBOLS); + let Some(map) = guard.as_mut() else { + return; + }; + + for (class_id, old_sym_key) in map.keys().copied().collect::>() { + let Some(value_bits) = map.get_mut(&(class_id, old_sym_key)) else { + continue; + }; + let mut new_sym_key = old_sym_key; + if visitor.visit_usize_slot(&mut new_sym_key) && new_sym_key != old_sym_key { + key_rewrites.push(((class_id, old_sym_key), (class_id, new_sym_key))); + } + visitor.visit_nanbox_u64_slot(value_bits); + } + + for (old_key, new_key) in key_rewrites { + if let Some(value_bits) = map.remove(&old_key) { + map.insert(new_key, value_bits); + } + } +} + +fn scan_symbol_pointer_metadata_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { + let mut rewrites = Vec::new(); + let mut guard = crate::gc::lock_gc_root_registry(&SYMBOL_POINTERS); + let Some(set) = guard.as_mut() else { + return; + }; + for old_ptr in set.iter().copied().collect::>() { + let mut new_ptr = old_ptr; + if visitor.visit_metadata_usize_slot(&mut new_ptr) && new_ptr != old_ptr { + rewrites.push((old_ptr, new_ptr)); + } + } + for (old_ptr, new_ptr) in rewrites { + set.remove(&old_ptr); + if new_ptr != 0 { + set.insert(new_ptr); + } + } +} + +#[derive(Clone, Copy)] +enum SymbolSideTableRootSlot { + SymbolPropertyOwner { owner: usize }, + SymbolPropertyEntry { owner: usize, sym_key: usize }, + ClassStaticSymbol { class_id: u32, sym_key: usize }, + SymbolPointer { ptr: usize }, +} + +pub(crate) struct SymbolSideTableRootScanState { + slots: Vec, + cursor: usize, +} + +pub(crate) fn new_symbol_side_table_root_scan_state() -> Box { + Box::new(SymbolSideTableRootScanState { + slots: symbol_side_table_root_snapshot(), + cursor: 0, + }) +} + +pub(crate) fn scan_symbol_side_table_roots_mut_step( + visitor: &mut crate::gc::RuntimeRootVisitor<'_>, + state: &mut dyn std::any::Any, + remaining: &mut usize, +) -> bool { + let state = state + .downcast_mut::() + .expect("symbol side-table root scanner state type"); + while *remaining > 0 && state.cursor < state.slots.len() { + scan_symbol_side_table_root_slot(visitor, state.slots[state.cursor]); + state.cursor += 1; + *remaining -= 1; + } + state.cursor >= state.slots.len() +} + +fn symbol_side_table_root_snapshot() -> Vec { + let mut slots = Vec::new(); + + { + let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); + if let Some(map) = guard.as_ref() { + for (&owner, entries) in map.iter() { + slots.push(SymbolSideTableRootSlot::SymbolPropertyOwner { owner }); + for &(sym_key, _) in entries.iter() { + slots.push(SymbolSideTableRootSlot::SymbolPropertyEntry { owner, sym_key }); + } + } + } + } + + { + let guard = crate::gc::lock_gc_root_registry(&CLASS_STATIC_SYMBOLS); + if let Some(map) = guard.as_ref() { + for &(class_id, sym_key) in map.keys() { + slots.push(SymbolSideTableRootSlot::ClassStaticSymbol { class_id, sym_key }); + } + } + } + + { + let guard = crate::gc::lock_gc_root_registry(&SYMBOL_POINTERS); + if let Some(set) = guard.as_ref() { + for &ptr in set.iter() { + slots.push(SymbolSideTableRootSlot::SymbolPointer { ptr }); + } + } + } + + slots +} + +fn scan_symbol_side_table_root_slot( + visitor: &mut crate::gc::RuntimeRootVisitor<'_>, + slot: SymbolSideTableRootSlot, +) { + match slot { + SymbolSideTableRootSlot::SymbolPropertyOwner { owner } => { + rewrite_symbol_property_owner_if_forwarded(visitor, owner); + } + SymbolSideTableRootSlot::SymbolPropertyEntry { owner, sym_key } => { + let mut guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); + let Some((entry_sym, value_bits)) = guard + .as_mut() + .and_then(|map| map.get_mut(&owner)) + .and_then(|entries| entries.iter_mut().find(|entry| entry.0 == sym_key)) + else { + return; + }; + visitor.visit_usize_slot(entry_sym); + visitor.visit_nanbox_u64_slot(value_bits); + } + SymbolSideTableRootSlot::ClassStaticSymbol { class_id, sym_key } => { + rewrite_class_static_symbol_entry_if_forwarded(visitor, class_id, sym_key); + } + SymbolSideTableRootSlot::SymbolPointer { ptr } => { + rewrite_symbol_pointer_metadata_if_forwarded(visitor, ptr); + } + } +} + +fn rewrite_symbol_property_owner_if_forwarded( + visitor: &mut crate::gc::RuntimeRootVisitor<'_>, + owner: usize, +) { + let mut new_owner = owner; + if !visitor.visit_metadata_usize_slot(&mut new_owner) || new_owner == owner { + return; + } + let mut guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); + if let Some(map) = guard.as_mut() { + if let Some(entries) = map.remove(&owner) { + match map.entry(new_owner) { + std::collections::hash_map::Entry::Occupied(mut entry) => { + merge_symbol_property_entries(entry.get_mut(), entries); + } + std::collections::hash_map::Entry::Vacant(entry) => { + entry.insert(entries); + } + } + } + } +} + +fn rewrite_class_static_symbol_entry_if_forwarded( + visitor: &mut crate::gc::RuntimeRootVisitor<'_>, + class_id: u32, + sym_key: usize, +) { + let mut guard = crate::gc::lock_gc_root_registry(&CLASS_STATIC_SYMBOLS); + let Some(map) = guard.as_mut() else { + return; + }; + let Some(value_bits) = map.get_mut(&(class_id, sym_key)) else { + return; + }; + let mut new_sym_key = sym_key; + let moved = visitor.visit_usize_slot(&mut new_sym_key); + visitor.visit_nanbox_u64_slot(value_bits); + if moved && new_sym_key != sym_key { + if let Some(value_bits) = map.remove(&(class_id, sym_key)) { + map.insert((class_id, new_sym_key), value_bits); + } + } +} + +fn rewrite_symbol_pointer_metadata_if_forwarded( + visitor: &mut crate::gc::RuntimeRootVisitor<'_>, + ptr: usize, +) { + let mut new_ptr = ptr; + if !visitor.visit_metadata_usize_slot(&mut new_ptr) || new_ptr == ptr { + return; + } + let mut guard = crate::gc::lock_gc_root_registry(&SYMBOL_POINTERS); + if let Some(set) = guard.as_mut() { + set.remove(&ptr); + if new_ptr != 0 { + set.insert(new_ptr); + } + } +} + +#[cfg(test)] +pub(crate) fn test_clear_symbol_side_table_roots() { + *crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES) = None; + *crate::gc::lock_gc_root_registry(&CLASS_STATIC_SYMBOLS) = None; + + let mut persistent = Vec::new(); + { + let guard = SYMBOL_REGISTRY.lock().unwrap(); + if let Some(map) = guard.as_ref() { + persistent.extend(map.values().copied()); + } + } + { + let guard = WELL_KNOWN_SYMBOLS.lock().unwrap(); + if let Some(map) = guard.as_ref() { + persistent.extend(map.values().copied()); + } + } + + let mut guard = crate::gc::lock_gc_root_registry(&SYMBOL_POINTERS); + if persistent.is_empty() { + *guard = None; + } else { + *guard = Some(persistent.into_iter().collect()); + } +} + +#[cfg(test)] +pub(crate) fn test_seed_symbol_property_root(owner: usize, sym_key: usize, value_bits: u64) { + if owner != 0 && sym_key != 0 { + store_object_symbol_property_root(owner, sym_key, value_bits); + } +} + +#[cfg(test)] +pub(crate) fn test_symbol_property_roots(owner: usize) -> Vec<(usize, u64)> { + let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); + guard + .as_ref() + .and_then(|map| map.get(&owner)) + .cloned() + .unwrap_or_default() +} + +#[cfg(test)] +pub(crate) fn test_symbol_property_root_bits(owner: usize, sym_key: usize) -> Option { + let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); + guard.as_ref().and_then(|map| { + map.get(&owner) + .and_then(|entries| entries.iter().find(|entry| entry.0 == sym_key)) + .map(|entry| entry.1) + }) +} + +#[cfg(test)] +pub(crate) fn test_symbol_property_owner_exists(owner: usize) -> bool { + let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); + guard.as_ref().is_some_and(|map| map.contains_key(&owner)) +} + +#[cfg(test)] +pub(crate) fn test_seed_class_static_symbol_root(class_id: u32, sym_key: usize, value_bits: u64) { + if class_id != 0 && sym_key != 0 { + store_class_static_symbol_root(class_id, sym_key, value_bits); + } +} + +#[cfg(test)] +pub(crate) fn test_class_static_symbol_root_bits(class_id: u32, sym_key: usize) -> Option { + let guard = crate::gc::lock_gc_root_registry(&CLASS_STATIC_SYMBOLS); + guard + .as_ref() + .and_then(|map| map.get(&(class_id, sym_key)).copied()) +} + +#[cfg(test)] +pub(crate) fn test_class_static_symbol_roots_for_class(class_id: u32) -> Vec<(usize, u64)> { + let guard = crate::gc::lock_gc_root_registry(&CLASS_STATIC_SYMBOLS); + guard + .as_ref() + .map(|map| { + map.iter() + .filter_map(|(&(cid, sym_key), &value_bits)| { + (cid == class_id).then_some((sym_key, value_bits)) + }) + .collect() + }) + .unwrap_or_default() +} + +#[cfg(test)] +pub(crate) fn test_seed_symbol_pointer_root(ptr: usize) { + if ptr != 0 { + register_symbol_pointer(ptr); + } +} + +#[cfg(test)] +pub(crate) fn test_symbol_pointer_root_contains(ptr: usize) -> bool { + let guard = crate::gc::lock_gc_root_registry(&SYMBOL_POINTERS); + guard.as_ref().is_some_and(|set| set.contains(&ptr)) +} + /// `Object.prototype.hasOwnProperty.call(obj, sym)` for Symbol keys. /// Refs #420 — drizzle's `is(value, type)` checks entityKind which is a Symbol. /// @@ -662,7 +1048,7 @@ pub unsafe extern "C" fn js_object_has_own_symbol(obj_f64: f64, sym_f64: f64) -> if obj_key == 0 || sym_key == 0 { return false; } - let guard = SYMBOL_PROPERTIES.lock().unwrap(); + let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); if let Some(map) = guard.as_ref() { if let Some(entries) = map.get(&obj_key) { for &(sk, _) in entries.iter() { @@ -695,7 +1081,7 @@ pub(crate) unsafe fn own_symbol_property(obj_f64: f64, sym_f64: f64) -> Option i64 if obj_key == 0 { return crate::array::js_array_alloc(0) as i64; } - let guard = SYMBOL_PROPERTIES.lock().unwrap(); + let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); let entries = match guard.as_ref().and_then(|m| m.get(&obj_key)) { Some(v) if !v.is_empty() => v.clone(), _ => return crate::array::js_array_alloc(0) as i64, diff --git a/scripts/gc_store_site_inventory.py b/scripts/gc_store_site_inventory.py index 600d232b7..16c7e005c 100644 --- a/scripts/gc_store_site_inventory.py +++ b/scripts/gc_store_site_inventory.py @@ -45,7 +45,7 @@ r"\(\*[^)\n]+\)\.(?Pon_fulfilled|on_rejected|next)\s*=" ) RUST_POINTER_FIELD_STORE_RE = re.compile( - r"\b(?P[A-Za-z_][A-Za-z0-9_]*)\.(?Pstring_ptr)\s*=" + r"\b(?P[A-Za-z_][A-Za-z0-9_]*)\.(?Pstring_ptr)\s*=(?!=)" ) RUST_GLOBAL_INDEX_STORE_RE = re.compile( r"\b(?P[A-Z][A-Z0-9_]*)\s*\[[^\]]+\]\s*=" @@ -68,20 +68,20 @@ SCAN_PATHS = [ - Path("crates/perry-codegen/src/expr"), - Path("crates/perry-runtime/src/array.rs"), + Path("crates/perry-codegen/src"), + Path("crates/perry-runtime/src/array"), Path("crates/perry-runtime/src/object"), - Path("crates/perry-runtime/src/closure.rs"), - Path("crates/perry-runtime/src/json.rs"), + Path("crates/perry-runtime/src/closure"), + Path("crates/perry-runtime/src/json"), Path("crates/perry-runtime/src/regex.rs"), Path("crates/perry-runtime/src/plugin.rs"), Path("crates/perry-runtime/src/thread.rs"), Path("crates/perry-runtime/src/promise"), Path("crates/perry-runtime/src/map.rs"), Path("crates/perry-runtime/src/set.rs"), - Path("crates/perry-runtime/src/string.rs"), + Path("crates/perry-runtime/src/string"), Path("crates/perry-runtime/src/typedarray.rs"), - Path("crates/perry-runtime/src/buffer.rs"), + Path("crates/perry-runtime/src/buffer"), Path("crates/perry-stdlib/src"), ] @@ -223,6 +223,24 @@ def iter_scan_roots() -> Iterable[Path]: yield from sorted(src.rglob("*.rs")) +def repo_rel(path: Path) -> str: + try: + return path.relative_to(REPO_ROOT).as_posix() + except ValueError: + return path.as_posix() + + +def is_codegen_path(path: Path) -> bool: + return repo_rel(path).startswith("crates/perry-codegen/src/") + + +def is_runtime_module(path: Path, module: str) -> bool: + rel = repo_rel(path) + flat = f"crates/perry-runtime/src/{module}.rs" + directory = f"crates/perry-runtime/src/{module}/" + return rel == flat or rel.startswith(directory) + + def is_comment_or_blank(line: str) -> bool: stripped = line.strip() return not stripped or stripped.startswith("//") or stripped.startswith("///") @@ -274,7 +292,7 @@ def classify_rust_store(path: Path, lines: list[str], index: int) -> str | None: if RUST_TLS_INDEX_STORE_RE.search(line) and is_risky_tls_index_store(window): return "raw TLS cache pointer table store" - if "crates/perry-runtime/src/promise/" in path.as_posix(): + if is_runtime_module(path, "promise"): if RUST_PROMISE_FIELD_STORE_RE.search(line): return "raw Promise heap pointer field store" @@ -284,7 +302,7 @@ def classify_rust_store(path: Path, lines: list[str], index: int) -> str | None: deref = RUST_DEREF_ASSIGN_RE.search(line) if deref and any(hint in deref.group("target") for hint in RUST_DEREF_RISK_TARGETS): - if path.name in {"buffer.rs", "typedarray.rs"}: + if is_runtime_module(path, "buffer") or path.name == "typedarray.rs": return None return "raw direct slot assignment" @@ -299,13 +317,17 @@ def classify_rust_store(path: Path, lines: list[str], index: int) -> str | None: if RUST_COPY_RE.search(line): if any(hint in window for hint in STACK_COPY_HINTS): return "raw stack/temporary argument copy" - if path.name in {"string.rs", "buffer.rs", "typedarray.rs"}: + if ( + is_runtime_module(path, "string") + or is_runtime_module(path, "buffer") + or path.name == "typedarray.rs" + ): return None if any(hint in window for hint in RUST_POINTER_FREE_COPY_HINTS): return None if any(hint in window for hint in RUST_COPY_RISK_HINTS): return "raw slot copy" - if path.name == "array.rs": + if is_runtime_module(path, "array"): return "raw array slot copy" return None @@ -349,7 +371,7 @@ def scan_file(path: Path) -> list[Finding]: continue reason: str | None = None - if "crates/perry-codegen/src/expr" in path.as_posix(): + if is_codegen_path(path): if is_risky_codegen_store(line): reason = "raw generated heap/global store" else: @@ -428,6 +450,11 @@ def check(rel_path: str, lines: list[str], expected: str | None) -> None: ["entry.string_ptr = key as usize;"], "raw cache/global pointer field store", ) + check( + "crates/perry-runtime/src/string/intern.rs", + ["if entry.string_ptr == 0 {"], + None, + ) check( "crates/perry-runtime/src/string.rs", [ @@ -520,6 +547,15 @@ def check(rel_path: str, lines: list[str], expected: str | None) -> None: "raw Promise heap pointer field store", ) + scanned_paths = {repo_rel(path) for path in iter_scan_roots()} + for expected_path in ( + "crates/perry-runtime/src/array/alloc.rs", + "crates/perry-runtime/src/buffer/from.rs", + "crates/perry-codegen/src/lower_call/new.rs", + ): + if expected_path not in scanned_paths: + failures.append(f"scanner roots: missing {expected_path}") + if failures: print("GC store-site inventory self-test failed:") for failure in failures: