Summary
test_gap_gc_regexp_receiver_rooting still exits 139 on every allocation-point arm, 10/10 deterministic, after #7217's fix landed. #7217 turned out to be the lazy globalThis bootstrap — a different site — and closing it fixed test_gap_gc_spread_accessor_rooting and test_gap_gc_static_block_this_rooting but left this one untouched (base and fixed both exit=139, 10/10 each). So the ten regexp_receiver entries in test-parity/gc_repsel_triage.txt are retargeted here, not deleted.
Reproduction
export PERRY_RUNTIME_DIR=<build>/perry-dev PERRY_NO_AUTO_OPTIMIZE=1
perry test-files/test_gap_gc_regexp_receiver_rooting.ts -o /tmp/rx # no compile-time env
PERRY_GC_HEAP_LIMIT=8 PERRY_GC_INCREMENTAL=0 PERRY_CONSERVATIVE_STACK_SCAN=off /tmp/rx
# exit=139, no output, 10/10
Clean on the safepoint route (loop_polls) and on the shipped default, exactly as #7227 claimed.
The site, named by #7196's quarantine
PERRY_GC_PROTECT_FROMSPACE=1 faults at the stale dereference (build with PERRY_DEBUG_SYMBOLS=1 for the frames):
[gc-fromspace-protect] FAULT: signal 10 at 0x27609a90064
block=0x27609a90000 +100 retired_bytes=640 retired_by_minor=#0
last-known object: user_ptr=0x27609a90050 obj_type=3 size=40
2 std::thread::local::LocalKey<RefCell<HashMap<(String,String), Arc<fancy_regex::Regex>>>>::with::{closure}
(perry_runtime::regex::js_regexp_new)
3 js_regexp_new + 1396
4 main + 228
obj_type=3 is a string, and the frame is the REGEX_CACHE.with(...) closure inside js_regexp_new.
Why
crates/perry-runtime/src/regex.rs:616 opens with
let pattern_str = if is_valid_ptr(pattern) { string_as_str(pattern) } else { "" };
let raw_flags_str = if is_valid_ptr(flags) { string_as_str(flags) } else { "" };
Both are &str borrows into the heap StringHeader's payload region, and they are held for the whole function — through validate_and_canonicalize_flags, the REGEX_CACHE probe (pattern_str.to_string(), flags_str.to_string()), the validation block, get_or_compile_regex, and the RegExpHeader allocation. Every one of those is an allocation point on this route, and an evacuating minor moves the pattern string out from under the borrow.
This is the #7215 borrow shape, and the same one #7216 fixed for object_assign_string_source by taking one owned copy up front (let owned: String = s.to_string();). The &str-into-a-movable-StringHeader pattern is the family, not the individual site: string_as_str / str_bytes_from_jsvalue / js_string_key_bytes all hand out a pointer into a relocatable payload, and their own safety notes say so.
Suggested shape
Take owned copies of pattern_str / raw_flags_str before the first allocation (js_regexp_new is already O(pattern length) — it does pattern.chars().collect::<Vec<char>>() on a cache miss), or root the two StringHeaders in a RuntimeHandleScope and re-derive the slices after each allocating step. The owned copy is the one #7216 chose for the sibling and it removes the dependence on where the collection lands.
Refs #7217, #7227, #7216, #7215, #7196.
Summary
test_gap_gc_regexp_receiver_rootingstill exits 139 on every allocation-point arm, 10/10 deterministic, after #7217's fix landed. #7217 turned out to be the lazyglobalThisbootstrap — a different site — and closing it fixedtest_gap_gc_spread_accessor_rootingandtest_gap_gc_static_block_this_rootingbut left this one untouched (base and fixed bothexit=139, 10/10 each). So the tenregexp_receiverentries intest-parity/gc_repsel_triage.txtare retargeted here, not deleted.Reproduction
Clean on the safepoint route (
loop_polls) and on the shipped default, exactly as #7227 claimed.The site, named by #7196's quarantine
PERRY_GC_PROTECT_FROMSPACE=1faults at the stale dereference (build withPERRY_DEBUG_SYMBOLS=1for the frames):obj_type=3is a string, and the frame is theREGEX_CACHE.with(...)closure insidejs_regexp_new.Why
crates/perry-runtime/src/regex.rs:616opens withBoth are
&strborrows into the heapStringHeader's payload region, and they are held for the whole function — throughvalidate_and_canonicalize_flags, theREGEX_CACHEprobe (pattern_str.to_string(),flags_str.to_string()), the validation block,get_or_compile_regex, and theRegExpHeaderallocation. Every one of those is an allocation point on this route, and an evacuating minor moves the pattern string out from under the borrow.This is the #7215 borrow shape, and the same one #7216 fixed for
object_assign_string_sourceby taking one owned copy up front (let owned: String = s.to_string();). The&str-into-a-movable-StringHeaderpattern is the family, not the individual site:string_as_str/str_bytes_from_jsvalue/js_string_key_bytesall hand out a pointer into a relocatable payload, and their own safety notes say so.Suggested shape
Take owned copies of
pattern_str/raw_flags_strbefore the first allocation (js_regexp_newis alreadyO(pattern length)— it doespattern.chars().collect::<Vec<char>>()on a cache miss), or root the twoStringHeaders in aRuntimeHandleScopeand re-derive the slices after each allocating step. The owned copy is the one #7216 chose for the sibling and it removes the dependence on where the collection lands.Refs #7217, #7227, #7216, #7215, #7196.