fix(runtime): convert #7811/#7815's new bare raw-handle reads to across_* (#7838) - #7840
Conversation
…ss_* (#7838) The raw-handle debt ratchet went red on main: 1,013 bare reads against a baseline of 998, with four per-module violations. The +15 all arrived with the two #6949 rooting fixes in the 2026-08-11 batch, and #7825 -- same batch -- closed the hole that had been letting a PR's own checkout carry the comparison baseline, so the ratchet only started seeing them once it landed. All fifteen convert. None needed a ceiling raise and the baseline is untouched: the total lands back on 998 exactly, and regex/replace_fn.rs returns to its recorded ceiling of 3. #7815 (5 sites, three modules, all previously unlisted -> all now zero) disposable.rs, messaging.rs, builtins/formatting/boxed_primitives.rs. Each is the exact shape across_* exists for: allocate the receiver, run one allocating coercion, then write through a re-read pointer. #7811 (10 sites in regex/replace_fn.rs) NOT the sanctioned "loop across a user-visible trap" shape that raw_handle_debt_files.txt permits joining the list for -- that shape is the three PRE-EXISTING reads (`cur_str` in the two string-replacer loops, plus the subject re-read in call_string_replace_callback), which is what the ceiling of 3 was recorded for. The ten new ones are plain root -> one allocating js_string_coerce -> read-for-the-call, which across_* expresses directly. Two small local combinators carry the four two-receiver call sites; two receivers compose by NESTING across_const, the same way path::value_args::with_two_headers already does it. Two of the conversions are not purely mechanical, because writing the ordering out made a latent stale-address use visible in disposable.rs: - set_nonenum ran crate::object::set_property_attrs(obj as usize, ..) AFTER js_object_set_field_by_name, which allocates when the object grows. That side table is keyed on the address, so a pre-call copy does not fault -- it files the attributes under an address nothing looks up, and `error` / `suppressed` / `message` silently become enumerable on a SuppressedError that grew during the set. - js_nanbox_pointer(obj) was built BEFORE the SuppressedError.prototype lookup and returned afterwards. A NaN-box is a frozen address the collector cannot rewrite, so the returned value named from-space if the lookup collected. The box is now built last, after every allocating call. Both are the #7192 shape (the store is in-frame but after a call that allocates), and neither is reachable without evacuation moving the receiver, so this is ordering hygiene rather than an observed crash. Verified: scripts/raw_handle_debt.py exits 0 at 998/998, "110 module(s) within ceilings; every other runtime module is locked at zero".
📝 WalkthroughWalkthroughThe runtime now uses ChangesGC pointer safety
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/perry-runtime/src/disposable.rs (1)
485-512: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick winRoot all NaN-boxed input values across allocations.
obj_handleroots onlyobj. It does not rooterror,suppressed, ormessage.
set_nonenumreceivesvalueas an unrootedf64, thenjs_string_from_bytescan collect beforejs_object_set_field_by_nameconsumes it. The earlierset_nonenumcalls can also collect before Line 504 readsmessage. If an input contains a movable heap value, these paths can use a stale pointer after evacuation.Root
error,suppressed, andmessageinscopebefore the first allocation. Root each value passed toset_nonenumbefore key creation, then reload it withget_nanbox_f64()after that allocation. Apply the same rule to the coerced message value.Based on learnings: NaN-boxed
f64values must be rooted across allocating operations and reloaded before reuse.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/perry-runtime/src/disposable.rs` around lines 485 - 512, Root error, suppressed, and message in the RuntimeHandleScope before any allocation, and update set_nonenum to root each value before js_string_from_bytes, then reload it with get_nanbox_f64() before js_object_set_field_by_name. Apply the same rooting and reload behavior to the coerced message value, preserving the existing undefined and string-coercion logic.Source: Learnings
🧹 Nitpick comments (1)
changelog.d/7840-raw-handle-debt-across.md (1)
1-48: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRewrite the fragment as a final release note.
This fragment mixes final runtime behavior with CI baseline history, issue chronology, a
git showcommand, and an abandoned ceiling proposal. These details describe development work, not the shipped behavior. Replace them with one concise entry that states theRuntimeHandle::across_*conversions and theSuppressedErrorordering fixes. Keep### Fixedand do not add version metadata.Based on learnings: changelog fragments should describe final shipped behavior as one coherent release-note entry and should not include development-slice narratives.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@changelog.d/7840-raw-handle-debt-across.md` around lines 1 - 48, Rewrite the fragment under ### Fixed as one concise release-note entry describing the shipped RuntimeHandle::across_mut/across_const conversions and the SuppressedError ordering fixes. Remove CI baseline counts, issue chronology, git commands, ceiling proposals, and other development-history details; do not add version metadata.Source: Learnings
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs`:
- Around line 311-324: Preserve GC handle roots across all allocations: in
crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs:311-324, update
install_string_wrapper_indices and install_string_wrapper_length to root and
reload both the object and string handles; in
crates/perry-runtime/src/messaging.rs:616-617, root name_ptr and root/reload the
object in set_field across key(name) and in install_method across closure_value.
- Around line 311-324: Update the boxed String installation within the
obj_handle.across_mut closure to use refreshed GC handles throughout
allocations. Root the result of js_string_coerce, retain obj_handle across each
allocation, and reload object and character pointers after js_string_from_bytes,
js_string_coerce, js_string_char_at, and key-array growth. Root each
intermediate character string until its field store completes, and pass handles
rather than raw pointer snapshots to these helpers.
In `@crates/perry-runtime/src/messaging.rs`:
- Around line 616-617: In the allocation flow around js_object_alloc and
set_field, create RuntimeHandleScope and obj_handle immediately after allocating
the object, then refresh obj from obj_handle.get_raw_mut_ptr() after every
allocation before accessing it. Root the coerced name_ptr before subsequent
allocations, including install_method, refresh it afterward, and use the
refreshed pointer when setting the "name" field.
- Around line 616-619: Update the name-coercion path around obj_handle and
set_field to root the coerced name_value with scope.root_nanbox_f64, passing
get_nanbox_f64() after any helper allocation. Preserve obj_handle as the
receiver handle and pass it into set_field so the write uses the current
receiver; apply the same rooting and current-receiver handling to closure_value
and install_method.
---
Outside diff comments:
In `@crates/perry-runtime/src/disposable.rs`:
- Around line 485-512: Root error, suppressed, and message in the
RuntimeHandleScope before any allocation, and update set_nonenum to root each
value before js_string_from_bytes, then reload it with get_nanbox_f64() before
js_object_set_field_by_name. Apply the same rooting and reload behavior to the
coerced message value, preserving the existing undefined and string-coercion
logic.
---
Nitpick comments:
In `@changelog.d/7840-raw-handle-debt-across.md`:
- Around line 1-48: Rewrite the fragment under ### Fixed as one concise
release-note entry describing the shipped RuntimeHandle::across_mut/across_const
conversions and the SuppressedError ordering fixes. Remove CI baseline counts,
issue chronology, git commands, ceiling proposals, and other development-history
details; do not add version metadata.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 7778c1e7-3196-4718-b166-d29385ca1be0
📒 Files selected for processing (5)
changelog.d/7840-raw-handle-debt-across.mdcrates/perry-runtime/src/builtins/formatting/boxed_primitives.rscrates/perry-runtime/src/disposable.rscrates/perry-runtime/src/messaging.rscrates/perry-runtime/src/regex/replace_fn.rs
| let (ptr, obj) = obj_handle.across_mut::<crate::object::ObjectHeader, _>(|| { | ||
| // `new String()` (no args) is spec'd to box "", not "undefined". | ||
| if has_arg == 0 { | ||
| crate::string::js_string_from_bytes(std::ptr::null(), 0) | ||
| } else { | ||
| // ECMA-262 §22.1.1 step 2b: ToString(value) — throws TypeError for Symbol. | ||
| if unsafe { crate::symbol::js_is_symbol(value) } != 0 { | ||
| crate::collection_iter::throw_type_error( | ||
| "Cannot convert a Symbol value to a string", | ||
| ); | ||
| } | ||
| js_string_coerce(value) | ||
| } | ||
| js_string_coerce(value) | ||
| }; | ||
| let obj = obj_handle.get_raw_mut_ptr::<crate::object::ObjectHeader>(); | ||
| }); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 6 \
'across_mut|let \(.*obj\)|obj_handle|get_raw_mut_ptr|set_field|install_method|boxed_object' \
crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs \
crates/perry-runtime/src/messaging.rsRepository: PerryTS/perry
Length of output: 20264
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- boxed_primitives.rs ---'
sed -n '280,380p' crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs
printf '%s\n' '--- messaging.rs ---'
sed -n '590,635p' crates/perry-runtime/src/messaging.rs
printf '%s\n' '--- RuntimeHandle and across_mut definitions ---'
rg -n -C 12 \
'fn across_mut|struct RuntimeHandle|root_raw_mut_ptr|get_raw_mut_ptr|RuntimeHandleScope' \
crates/perry-runtime/src
printf '%s\n' '--- allocation-capable callees used in the affected regions ---'
rg -n -C 5 \
'fn (register_boxed_primitive_payload|install_string_wrapper|attach_boxed_primitive_prototype|js_object_set_field_by_name|js_object_define_accessor|js_string_coerce|js_string_from_bytes)' \
crates/perry-runtime/srcRepository: PerryTS/perry
Length of output: 50370
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re
checks = {
"crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs": (309, 370),
"crates/perry-runtime/src/messaging.rs": (614, 627),
}
for filename, (start, end) in checks.items():
lines = Path(filename).read_text().splitlines()
segment = lines[start-1:end]
print(f"--- {filename}:{start}-{end} ---")
across = next((i for i, line in enumerate(segment, start) if "across_mut" in line), None)
if across is None:
print("ERROR: no across_mut call found")
continue
post_across = [(i, line.strip()) for i, line in enumerate(segment, start)
if i > across and re.search(r'\bobj\b', line)]
print(f"across_mut line: {across}")
for i, line in post_across:
print(f"{i}: {line}")
unsafe_snapshot_uses = [
(i, line) for i, line in post_across
if "obj_handle.get_raw_mut_ptr()" not in line and
not line.lstrip().startswith("//")
]
print(f"post-across raw obj uses requiring review: {len(unsafe_snapshot_uses)}")
PYRepository: PerryTS/perry
Length of output: 1796
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- affected boxed-string constructor ---'
sed -n '300,334p' crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs
printf '%s\n' '--- affected broadcast-channel constructor ---'
sed -n '602,630p' crates/perry-runtime/src/messaging.rs
printf '%s\n' '--- handle API ---'
rg -n -l 'pub struct RuntimeHandle|fn across_mut|fn root_raw_mut_ptr|fn get_raw_mut_ptr' crates/perry-runtime/src/gc
for file in $(rg -l 'pub struct RuntimeHandle|fn across_mut|fn root_raw_mut_ptr|fn get_raw_mut_ptr' crates/perry-runtime/src/gc); do
echo "--- $file ---"
rg -n -A 35 -B 8 'pub struct RuntimeHandle|fn across_mut|fn root_raw_mut_ptr|fn get_raw_mut_ptr' "$file"
done
printf '%s\n' '--- relevant callee definitions ---'
rg -n -l \
'fn register_boxed_primitive_payload|fn install_string_wrapper_indices|fn install_string_wrapper_length|fn attach_boxed_primitive_prototype|fn js_object_set_field_by_name|fn js_string_coerce' \
crates/perry-runtime/src
for file in $(rg -l \
'fn register_boxed_primitive_payload|fn install_string_wrapper_indices|fn install_string_wrapper_length|fn attach_boxed_primitive_prototype|fn js_object_set_field_by_name|fn js_string_coerce' \
crates/perry-runtime/src); do
echo "--- $file ---"
rg -n -A 45 -B 5 \
'fn register_boxed_primitive_payload|fn install_string_wrapper_indices|fn install_string_wrapper_length|fn attach_boxed_primitive_prototype|fn js_object_set_field_by_name|fn js_string_coerce' \
"$file"
doneRepository: PerryTS/perry
Length of output: 32578
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
regions = {
"crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs": (311, 330),
"crates/perry-runtime/src/messaging.rs": (617, 626),
}
for filename, (start, end) in regions.items():
lines = Path(filename).read_text().splitlines()[start-1:end]
print(f"--- {filename}:{start}-{end} ---")
for lineno, line in enumerate(lines, start):
if "across_mut" in line or "obj" in line or "name_ptr" in line:
print(f"{lineno}: {line}")
PYRepository: PerryTS/perry
Length of output: 1257
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- messaging helper chain ---'
sed -n '45,75p' crates/perry-runtime/src/messaging.rs
sed -n '110,126p' crates/perry-runtime/src/messaging.rs
printf '%s\n' '--- closure_value definition ---'
rg -n -l 'fn closure_value' crates/perry-runtime/src
for file in $(rg -l 'fn closure_value' crates/perry-runtime/src); do
rg -n -A 30 -B 5 'fn closure_value' "$file"
done
printf '%s\n' '--- string-wrapper helper bodies ---'
sed -n '90,180p' crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs
printf '%s\n' '--- allocation and collection contract for string creation ---'
rg -n -A 22 -B 8 \
'pub extern "C" fn js_string_from_bytes|fn js_string_from_bytes|js_string_from_bytes_with_capacity|gc_malloc|collect_minor|EVACUATE' \
crates/perry-runtime/src/string crates/perry-runtime/src/gc \
| head -n 260Repository: PerryTS/perry
Length of output: 40579
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re
cases = {
"crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs": (311, 330),
"crates/perry-runtime/src/messaging.rs": (617, 626),
}
allocation_markers = (
"js_string_from_bytes", "js_string_coerce", "closure_value",
"js_object_set_field_by_name", "js_object_define_accessor",
)
for filename, (start, end) in cases.items():
lines = Path(filename).read_text().splitlines()
print(f"--- {filename} ---")
for number in range(start, end + 1):
text = lines[number - 1].strip()
if not text:
continue
flags = []
if re.search(r"\bobj\b", text) and "get_raw_mut_ptr" not in text:
flags.append("raw_obj")
if "ptr" in text and "get_raw" not in text:
flags.append("raw_result")
if any(marker in text for marker in allocation_markers):
flags.append("allocation_capable")
if flags:
print(f"{number}: [{', '.join(flags)}] {text}")
PYRepository: PerryTS/perry
Length of output: 1621
Preserve handle-based roots across every allocation.
across_mut returns one post-collection address. Later allocations can evacuate that object and the returned string pointer.
- In
boxed_primitives.rs, root and reload both the object and string handles. Apply this insideinstall_string_wrapper_indicesandinstall_string_wrapper_length. - In
messaging.rs, rootname_ptr. Root and reload the object insideset_fieldacrosskey(name)and insideinstall_methodacrossclosure_value.
📍 Affects 2 files
crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs#L311-L324(this comment)crates/perry-runtime/src/messaging.rs#L616-L617
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs` around
lines 311 - 324, Preserve GC handle roots across all allocations: in
crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs:311-324, update
install_string_wrapper_indices and install_string_wrapper_length to root and
reload both the object and string handles; in
crates/perry-runtime/src/messaging.rs:616-617, root name_ptr and root/reload the
object in set_field across key(name) and in install_method across closure_value.
Source: Learnings
🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 8 \
'install_string_wrapper_(indices|length)|js_string_(from_bytes|char_at|length)|js_object_set_field_by_name' \
crates/perry-runtime/src/builtins/formatting/boxed_primitives.rsRepository: PerryTS/perry
Length of output: 6258
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- RuntimeHandleScope and root APIs ---'
rg -n -C 12 \
'struct RuntimeHandleScope|impl RuntimeHandleScope|root_string_ptr|root_raw_mut_ptr|across_mut|gc_register_mutable_root_scanner' \
crates/perry-runtime/src
printf '%s\n' '--- String allocation and movement APIs ---'
rg -n -C 10 \
'pub .*fn js_string_(from_bytes|coerce|char_at|length)|struct StringHeader|StringHeader' \
crates/perry-runtime/src/string* crates/perry-runtime/src
printf '%s\n' '--- Relevant callers and registration order ---'
rg -n -C 8 \
'install_string_wrapper_(indices|length)|register_boxed_primitive_payload|js_boxed_string_new' \
crates/perry-runtime/srcRepository: PerryTS/perry
Length of output: 50370
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- Definitions ---'
rg -l \
'struct RuntimeHandleScope|root_string_ptr|root_raw_mut_ptr|fn across_mut' \
crates/perry-runtime/src | sort
printf '%s\n' '--- String definitions ---'
rg -l \
'fn js_string_from_bytes|fn js_string_char_at|fn js_string_length|struct StringHeader' \
crates/perry-runtime/src | sort
printf '%s\n' '--- Targeted call sites ---'
rg -n -C 6 \
'install_string_wrapper_(indices|length)|register_boxed_primitive_payload|js_boxed_string_new' \
crates/perry-runtime/src/builtins/formatting/boxed_primitives.rsRepository: PerryTS/perry
Length of output: 11912
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- Runtime handle implementation ---'
file=$(rg -l 'pub struct RuntimeHandleScope|struct RuntimeHandleScope' crates/perry-runtime/src/gc)
cat -n "$file" | sed -n '1,280p'
printf '%s\n' '--- String allocation and access implementation ---'
for file in $(rg -l 'fn js_string_from_bytes|fn js_string_char_at|fn js_string_length|struct StringHeader' crates/perry-runtime/src/string); do
printf '\n--- %s ---\n' "$file"
rg -n -C 18 \
'struct StringHeader|fn js_string_from_bytes|fn js_string_char_at|fn js_string_length|GC_TYPE_STRING|gc_alloc' \
"$file"
done
printf '%s\n' '--- Object allocation and field insertion ---'
rg -n -C 16 \
'pub .*fn js_object_alloc|fn js_object_set_field_by_name|fn register_boxed_primitive_payload|thread_local!|BOXED_PRIMITIVE_PAYLOADS' \
crates/perry-runtime/src/object/alloc.rs \
crates/perry-runtime/src/object/field_set_by_name \
crates/perry-runtime/src/builtins/formatting/boxed_primitives.rsRepository: PerryTS/perry
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- Handle reread methods ---'
file=crates/perry-runtime/src/gc/roots/runtime_handles.rs
rg -n -C 14 \
'get_raw_mut_ptr|get_raw_const_ptr|set_raw|RuntimeHandleSlot|STRING_TAG' \
"$file"
printf '%s\n' '--- Object allocation and movement-sensitive writes ---'
rg -n -C 12 \
'arena_alloc_gc|js_object_set_field_by_name|set_builtin_property_attrs|object_set_static_prototype' \
crates/perry-runtime/src/object/alloc.rs \
crates/perry-runtime/src/object/field_set_by_name \
crates/perry-runtime/src/object \
crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs | head -n 500
printf '%s\n' '--- Exact string helper bodies ---'
cat -n crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs | sed -n '90,185p;294,333p'
cat -n crates/perry-runtime/src/string/char_ops.rs | sed -n '242,290p'
cat -n crates/perry-runtime/src/string/alloc.rs | sed -n '1,22p;124,145p;203,212p'Repository: PerryTS/perry
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- Exact wrapper helpers and constructor ---'
cat -n crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs | sed -n '90,185p;294,333p'
printf '%s\n' '--- Exact field setter implementation ---'
rg -l \
'pub .*fn js_object_set_field_by_name|fn js_object_set_field_by_name' \
crates/perry-runtime/src/object/field_set_by_name crates/perry-runtime/src/object
for file in $(rg -l \
'pub .*fn js_object_set_field_by_name|fn js_object_set_field_by_name' \
crates/perry-runtime/src/object/field_set_by_name crates/perry-runtime/src/object); do
printf '\n--- %s ---\n' "$file"
rg -n -C 20 \
'js_object_set_field_by_name|js_array_alloc|js_array_push|arena_alloc_gc|js_string_from_bytes|keys_array' \
"$file"
done
printf '%s\n' '--- Pointer-returning string coercion ---'
rg -n -C 24 \
'fn js_string_coerce|js_string_coerce|js_get_string_pointer_unified|js_string_char_at' \
crates/perry-runtime/src/value crates/perry-runtime/src/stringRepository: PerryTS/perry
Length of output: 50370
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- Field setter entry and allocation points ---'
cat -n crates/perry-runtime/src/object/field_set_by_name.rs | sed -n '33,230p'
rg -n -C 10 \
'fn js_object_set_field_by_name|js_array_alloc|js_array_push|arena_alloc_gc|js_string_intern|RuntimeHandleScope' \
crates/perry-runtime/src/object/field_set_by_name/tail.rs \
crates/perry-runtime/src/object/field_set_by_name/write_helpers.rs
printf '%s\n' '--- js_string_coerce definition ---'
rg -n -l 'fn js_string_coerce|pub.*js_string_coerce' crates/perry-runtime/src
for file in $(rg -l 'fn js_string_coerce|pub.*js_string_coerce' crates/perry-runtime/src); do
printf '\n--- %s ---\n' "$file"
rg -n -C 30 'fn js_string_coerce|js_string_coerce' "$file"
doneRepository: PerryTS/perry
Length of output: 49006
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
boxed = Path("crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs").read_text()
numbers = Path("crates/perry-runtime/src/builtins/numbers.rs").read_text()
setter = Path("crates/perry-runtime/src/object/field_set_by_name/tail.rs").read_text()
def section(text, start, end):
a = text.index(start)
b = text.index(end, a)
return text[a:b]
length = section(
boxed,
"fn install_string_wrapper_length(",
"/// String exotic objects",
)
indices = section(
boxed,
"fn install_string_wrapper_indices(",
"pub fn scan_boxed_primitive_payload_roots_mut",
)
constructor = section(
boxed,
"pub extern \"C\" fn js_boxed_string_new(",
"#[no_mangle]\npub extern \"C\" fn js_boxed_boolean_new",
)
coerce = section(
numbers,
"pub extern \"C\" fn js_string_coerce(",
"/// True when [`js_string_coerce`]",
)
checks = {
"object raw pointer is reused after the rooting callback":
"let (ptr, obj) = obj_handle.across_mut" in constructor
and "install_string_wrapper_indices(obj, ptr)" in constructor,
"length reads string after key allocation":
length.index("js_string_from_bytes") < length.index("js_string_length"),
"indices reads character before key allocation":
indices.index("js_string_char_at") < indices.index("js_string_from_bytes"),
"indices uses character pointer after key allocation":
indices.index("js_string_from_bytes") < indices.index("JSValue::string_ptr(ch)"),
"field setter allocates while updating a new key":
"js_array_alloc" in setter and "refresh_roots_after_alloc!()" in setter,
"string coercion can allocate":
"js_string_from_bytes(result.as_ptr()" in coerce
and "js_string_materialize_to_heap" in coerce,
}
for name, result in checks.items():
print(f"{'PASS' if result else 'FAIL'}: {name}")
assert all(checks.values())
PYRepository: PerryTS/perry
Length of output: 459
Use refreshed GC handles throughout boxed String installation.
obj, ptr, and ch are raw pointers held across allocations. js_string_coerce, js_string_char_at, js_string_from_bytes, and key-array growth can evacuate them. Root the coerced string, keep the object handle active, reload both pointers after each allocation, and root each intermediate character string until its field store completes. Do not pass raw pointer snapshots through the helpers.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs` around
lines 311 - 324, Update the boxed String installation within the
obj_handle.across_mut closure to use refreshed GC handles throughout
allocations. Root the result of js_string_coerce, retain obj_handle across each
allocation, and reload object and character pointers after js_string_from_bytes,
js_string_coerce, js_string_char_at, and key-array growth. Root each
intermediate character string until its field store completes, and pass handles
rather than raw pointer snapshots to these helpers.
Source: Learnings
| let (name_ptr, obj) = obj_handle | ||
| .across_mut::<object::ObjectHeader, _>(|| crate::builtins::js_string_coerce(name)); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 8 \
'across_mut|obj_handle|get_raw_mut_ptr|set_field|install_method|boxed_object' \
crates/perry-runtime/src/messaging.rsRepository: PerryTS/perry
Length of output: 9067
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- RuntimeHandleScope and across_mut definitions ---'
rg -n -C 12 \
'struct RuntimeHandleScope|impl RuntimeHandleScope|fn root_raw_mut_ptr|fn across_mut|get_raw_mut_ptr' \
crates/perry-runtime crates
printf '%s\n' '--- Relevant object mutation APIs ---'
rg -n -C 10 \
'fn js_object_set_field_by_name|js_object_define_accessor|fn set_object_prototype|fn js_object_alloc' \
crates/perry-runtime
printf '%s\n' '--- GC/rooting APIs and mutable-root scanner registration ---'
rg -n -C 8 \
'gc_register_mutable_root_scanner|root_raw_mut_ptr|root_nanbox_f64|across_mut' \
crates/perry-runtime/srcRepository: PerryTS/perry
Length of output: 50370
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- Definition files ---'
rg -l \
'struct RuntimeHandleScope|pub struct RuntimeHandleScope|fn js_object_set_field_by_name|pub fn js_object_set_field_by_name' \
crates/perry-runtime/src crates/perry-runtime/tests 2>/dev/null | sort -u
printf '%s\n' '--- Focused handle API matches ---'
rg -n \
'struct RuntimeHandleScope|impl RuntimeHandleScope|root_raw_mut_ptr|across_mut|get_raw_mut_ptr' \
crates/perry-runtime/src/gc crates/perry-runtime/src --glob '*.rs' \
| grep -E '(^|/)(handles?|mod|runtime|scope|gc)(\.rs:|/)' | head -n 240
printf '%s\n' '--- Focused object API matches ---'
rg -n \
'fn js_object_set_field_by_name|fn js_object_define_accessor|fn set_object_prototype|fn js_object_alloc' \
crates/perry-runtime/src --glob '*.rs' | head -n 120Repository: PerryTS/perry
Length of output: 413
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- Runtime handle implementation ---'
sed -n '1,280p' crates/perry-runtime/src/gc/roots/runtime_handles.rs
printf '%s\n' '--- Field setter implementation ---'
sed -n '1,260p' crates/perry-runtime/src/object/field_set_by_name.rs
printf '%s\n' '--- BroadcastChannel call site and nearby helpers ---'
sed -n '598,632p' crates/perry-runtime/src/messaging.rs
printf '%s\n' '--- Allocation/coercion definitions ---'
rg -n -C 8 \
'pub extern "C" fn js_string_coerce|fn js_string_coerce|pub fn js_string_coerce|pub extern "C" fn js_object_alloc|fn js_object_alloc|pub fn js_object_alloc' \
crates/perry-runtime/src --glob '*.rs'Repository: PerryTS/perry
Length of output: 40820
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- Messaging helper definitions ---'
sed -n '1,135p' crates/perry-runtime/src/messaging.rs
printf '%s\n' '--- String coercion body ---'
sed -n '625,705p' crates/perry-runtime/src/builtins/numbers.rs
printf '%s\n' '--- Object allocation body ---'
sed -n '119,163p' crates/perry-runtime/src/object/alloc.rs
printf '%s\n' '--- Setter tail and handle-related logic ---'
rg -n -C 12 \
'RuntimeHandleScope|root_|js_object_set_field_by_name\(|arena_alloc|key\(' \
crates/perry-runtime/src/object/field_set_by_name crates/perry-runtime/src/object/field_set_by_name.rs \
| head -n 360
printf '%s\n' '--- Closure and string allocation helpers ---'
rg -n -C 10 \
'fn closure_value|fn js_string_from_bytes|pub.*js_string_from_bytes|js_closure_alloc' \
crates/perry-runtime/src/messaging.rs crates/perry-runtime/src --glob '*.rs' \
| head -n 300Repository: PerryTS/perry
Length of output: 50369
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- String allocation implementation ---'
rg -n -C 18 \
'pub extern "C" fn js_string_from_bytes|pub fn js_string_from_bytes|fn js_string_from_bytes' \
crates/perry-runtime/src/string crates/perry-runtime/src --glob '*.rs' | head -n 180
printf '%s\n' '--- Closure allocation implementation ---'
rg -n -C 20 \
'pub extern "C" fn js_closure_alloc|pub fn js_closure_alloc|fn js_closure_alloc' \
crates/perry-runtime/src/closure crates/perry-runtime/src --glob '*.rs' | head -n 180
printf '%s\n' '--- Root refresh macro and allocation points in setter tail ---'
rg -n -C 10 \
'macro_rules! refresh_roots_after_alloc|refresh_roots_after_alloc!|js_string_intern|js_array_push|arena_alloc_gc' \
crates/perry-runtime/src/object/field_set_by_name/tail.rs | head -n 320
printf '%s\n' '--- Existing messaging GC-rooting tests or references ---'
rg -n -C 12 \
'broadcast_channel|js_broadcast_channel_new|6949|root_nanbox|root_string_ptr' \
crates/perry-runtime/src/gc crates/perry-runtime/src/messaging.rs --glob '*.rs' | head -n 260Repository: PerryTS/perry
Length of output: 50369
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
messaging = Path("crates/perry-runtime/src/messaging.rs").read_text()
coerce = Path("crates/perry-runtime/src/builtins/numbers.rs").read_text()
string_alloc = Path("crates/perry-runtime/src/string/alloc.rs").read_text()
string_mod = Path("crates/perry-runtime/src/string/mod.rs").read_text()
tail = Path("crates/perry-runtime/src/object/field_set_by_name/tail.rs").read_text()
checks = {
"scope starts after constructor field write":
messaging.index('set_field(\n obj,\n "constructor",\n get_global_constructor("BroadcastChannel"),\n );')
< messaging.index('let scope = crate::gc::RuntimeHandleScope::new();', messaging.index('js_broadcast_channel_new')),
"set_field allocates its key before calling setter":
'object::js_object_set_field_by_name(obj, key(name), value);' in messaging,
"ordinary string allocation reaches storage allocator":
'js_string_from_bytes_with_capacity(data, len, len)' in string_alloc
and 'let (ptr, data_ptr) = string_storage_alloc(capacity);' in string_alloc,
"non-string coercion returns a newly allocated string":
'js_string_from_bytes(result.as_ptr(), result.len() as u32)' in coerce,
"setter roots only after entry":
'let obj_handle = scope.root_raw_mut_ptr(obj);' in tail
and 'let key_handle = scope.root_string_ptr(key);' in tail,
"setter refreshes its own local object after internal allocations":
'obj = obj_handle.get_raw_mut_ptr::<ObjectHeader>();' in tail,
"messaging keeps coerced name as an unrooted raw-pointer NaN-box":
'let name_value = f64::from_bits(JSValue::string_ptr(name_ptr).bits());' in messaging
and 'root_string_ptr(name_ptr)' not in messaging
and 'root_nanbox_f64(name_value)' not in messaging,
}
for label, result in checks.items():
print(f"{'PASS' if result else 'FAIL'}: {label}")
assert all(checks.values())
PY
printf '%s\n' '--- Storage allocator and exact coercion allocation path ---'
rg -n -C 16 \
'fn string_storage_alloc|pub.*string_storage_alloc|js_string_from_bytes_with_capacity' \
crates/perry-runtime/src/string --glob '*.rs' | head -n 180Repository: PerryTS/perry
Length of output: 16489
Root obj and the coerced name across all allocations.
Create RuntimeHandleScope and obj_handle immediately after js_object_alloc. set_field allocates its key before receiving obj, and install_method allocates a closure. Reload obj_handle.get_raw_mut_ptr() after each allocation before using the object. Root name_ptr before the next allocation and use its refreshed value for the "name" field.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/perry-runtime/src/messaging.rs` around lines 616 - 617, In the
allocation flow around js_object_alloc and set_field, create RuntimeHandleScope
and obj_handle immediately after allocating the object, then refresh obj from
obj_handle.get_raw_mut_ptr() after every allocation before accessing it. Root
the coerced name_ptr before subsequent allocations, including install_method,
refresh it afterward, and use the refreshed pointer when setting the "name"
field.
Source: Learnings
| let (name_ptr, obj) = obj_handle | ||
| .across_mut::<object::ObjectHeader, _>(|| crate::builtins::js_string_coerce(name)); | ||
| let name_value = f64::from_bits(JSValue::string_ptr(name_ptr).bits()); | ||
| set_field(obj, "name", name_value); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 8 \
'name_ptr|name_value|root_nanbox_f64|set_field|js_object_set_field_by_name' \
crates/perry-runtime/src/messaging.rs crates/perry-runtime/src/atomics.rsRepository: PerryTS/perry
Length of output: 19219
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- messaging helpers and target ---'
sed -n '1,80p;598,628p' crates/perry-runtime/src/messaging.rs
printf '%s\n' '--- handle and GC APIs ---'
rg -n -C 12 \
'struct RuntimeHandleScope|fn root_nanbox_f64|fn root_raw_mut_ptr|fn across_mut|struct .*Handle|root_string_ptr' \
crates/perry-runtime/src
printf '%s\n' '--- coercion and field-write implementations ---'
rg -n -C 16 \
'fn js_string_coerce|js_string_coerce|js_object_set_field_by_name|fn key\\(' \
crates/perry-runtime/srcRepository: PerryTS/perry
Length of output: 50370
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- messaging helpers and target ---'
sed -n '1,80p;598,628p' crates/perry-runtime/src/messaging.rs
printf '%s\n' '--- handle and GC APIs ---'
rg -n -C 12 \
'struct RuntimeHandleScope|fn root_nanbox_f64|fn root_raw_mut_ptr|fn across_mut|struct .*Handle|root_string_ptr' \
crates/perry-runtime/src
printf '%s\n' '--- coercion and field-write implementations ---'
rg -n -C 16 \
'fn js_string_coerce|js_string_coerce|js_object_set_field_by_name|fn key\(' \
crates/perry-runtime/srcRepository: PerryTS/perry
Length of output: 50370
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- candidate definitions ---'
rg -l \
'pub struct RuntimeHandleScope|impl RuntimeHandleScope|fn js_string_coerce|pub extern "C" fn js_object_set_field_by_name|pub fn js_object_set_field_by_name' \
crates/perry-runtime/src | sort
printf '%s\n' '--- exact target references ---'
rg -n -C 6 \
'pub struct RuntimeHandleScope|root_nanbox_f64|root_raw_mut_ptr|across_mut' \
crates/perry-runtime/src/gc.rs crates/perry-runtime/src/gc 2>/dev/null || true
printf '%s\n' '--- exact coercion references ---'
rg -n -C 10 \
'fn js_string_coerce|pub extern "C" fn js_string_coerce|js_string_coerce' \
crates/perry-runtime/src/builtins.rs crates/perry-runtime/src/builtins 2>/dev/null | head -n 180
printf '%s\n' '--- exact field setter references ---'
rg -n -C 12 \
'js_object_set_field_by_name' \
crates/perry-runtime/src/object.rs crates/perry-runtime/src/object 2>/dev/null | head -n 240Repository: PerryTS/perry
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- runtime handle implementation ---'
sed -n '1,115p;180,245p' crates/perry-runtime/src/gc/roots/runtime_handles.rs
printf '%s\n' '--- string coercion implementation ---'
rg -n 'js_string_coerce' crates/perry-runtime/src/builtins/numbers.rs
line="$(rg -n -m1 'js_string_coerce' crates/perry-runtime/src/builtins/numbers.rs | cut -d: -f1)"
start=$((line > 25 ? line - 25 : 1))
end=$((line + 65))
sed -n "${start},${end}p" crates/perry-runtime/src/builtins/numbers.rs
printf '%s\n' '--- field setter implementation ---'
rg -n 'js_object_set_field_by_name' crates/perry-runtime/src/object/field_set_by_name.rs
line="$(rg -n -m1 'js_object_set_field_by_name' crates/perry-runtime/src/object/field_set_by_name.rs | cut -d: -f1)"
start=$((line > 25 ? line - 25 : 1))
end=$((line + 180))
sed -n "${start},${end}p" crates/perry-runtime/src/object/field_set_by_name.rsRepository: PerryTS/perry
Length of output: 21376
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- messaging helper ---'
sed -n '55,72p;604,626p' crates/perry-runtime/src/messaging.rs
printf '%s\n' '--- key-string allocation path ---'
rg -n -m 5 'pub extern "C" fn js_string_from_bytes|pub fn js_string_from_bytes|fn js_string_from_bytes' \
crates/perry-runtime/src
file="$(rg -l -m 1 'pub extern "C" fn js_string_from_bytes|pub fn js_string_from_bytes|fn js_string_from_bytes' \
crates/perry-runtime/src | head -n 1)"
line="$(rg -n -m 1 'pub extern "C" fn js_string_from_bytes|pub fn js_string_from_bytes|fn js_string_from_bytes' "$file" | cut -d: -f1)"
start=$((line > 15 ? line - 15 : 1))
end=$((line + 90))
sed -n "${start},${end}p" "$file"
printf '%s\n' '--- read-only structural verifier ---'
python3 - <<'PY'
from pathlib import Path
p = Path("crates/perry-runtime/src/messaging.rs")
s = p.read_text()
helper = s[s.index("fn set_field("):s.index("fn get_global_constructor(")]
target = s[s.index("pub extern \"C\" fn js_broadcast_channel_new"):s.index("pub(crate) extern \"C\" fn js_broadcast_channel_constructor_call_error")]
checks = {
"set_field passes raw obj before constructing key": "js_object_set_field_by_name(obj, key(name), value)" in helper,
"key constructs a runtime string": "js_string_from_bytes(name.as_ptr(), name.len() as u32)" in s[s.index("fn key("):s.index("fn set_field(")],
"target roots object across coercion": "let obj_handle = scope.root_raw_mut_ptr(obj);" in target,
"target does not root coerced name value": "root_nanbox_f64(name_value)" not in target,
"target uses raw name value for field write": 'set_field(obj, "name", name_value);' in target,
}
for name, ok in checks.items():
print(f"{name}: {'yes' if ok else 'no'}")
raise SystemExit(0 if all(checks.values()) else 1)
PYRepository: PerryTS/perry
Length of output: 8005
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- js_string_from_bytes allocator ---'
sed -n '1,95p' crates/perry-runtime/src/string/alloc.rs
printf '%s\n' '--- setter rooting and allocation-sensitive sections ---'
rg -n 'RuntimeHandleScope|root_raw|root_nanbox|js_string_from_bytes|js_object_alloc|gc_malloc|arena_alloc|set_object_keys_array|transition_cache_lookup' \
crates/perry-runtime/src/object/field_set_by_name.rs \
crates/perry-runtime/src/object/field_set_by_name/tail.rs \
crates/perry-runtime/src/object/field_set_by_name/write_helpers.rs
printf '%s\n' '--- setter tail start ---'
sed -n '1,220p' crates/perry-runtime/src/object/field_set_by_name/tail.rsRepository: PerryTS/perry
Length of output: 19029
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- install_method and closure_value ---'
rg -n -C 18 \
'fn closure_value|fn install_method|closure_value\\(' \
crates/perry-runtime/src/messaging.rs
printf '%s\n' '--- complete string allocation helper ---'
rg -n -m 3 'js_string_from_bytes_with_capacity' crates/perry-runtime/src/string/alloc.rs
line="$(rg -n -m1 'pub extern "C" fn js_string_from_bytes_with_capacity|fn js_string_from_bytes_with_capacity' crates/perry-runtime/src/string/alloc.rs | cut -d: -f1)"
start=$((line > 5 ? line - 5 : 1))
end=$((line + 100))
sed -n "${start},${end}p" crates/perry-runtime/src/string/alloc.rsRepository: PerryTS/perry
Length of output: 303
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- install_method and closure_value ---'
rg -n -C 18 -e 'fn closure_value' -e 'fn install_method' -e 'closure_value\(' \
crates/perry-runtime/src/messaging.rs
printf '%s\n' '--- complete string allocation helper ---'
rg -n -m 3 'js_string_from_bytes_with_capacity' crates/perry-runtime/src/string/alloc.rs
line="$(rg -n -m1 'pub extern "C" fn js_string_from_bytes_with_capacity|fn js_string_from_bytes_with_capacity' crates/perry-runtime/src/string/alloc.rs | cut -d: -f1)"
start=$((line > 5 ? line - 5 : 1))
end=$((line + 100))
sed -n "${start},${end}p" crates/perry-runtime/src/string/alloc.rsRepository: PerryTS/perry
Length of output: 8138
Keep the coerced value and receiver rooted across helper allocations.
key(name) allocates before js_object_set_field_by_name roots its arguments. This can evacuate both name_value and obj. closure_value can create the same gap in install_method. Root name_value with scope.root_nanbox_f64, pass get_nanbox_f64(), and pass obj_handle into set_field/install_method so each write uses a current receiver.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/perry-runtime/src/messaging.rs` around lines 616 - 619, Update the
name-coercion path around obj_handle and set_field to root the coerced
name_value with scope.root_nanbox_f64, passing get_nanbox_f64() after any helper
allocation. Preserve obj_handle as the receiver handle and pass it into
set_field so the write uses the current receiver; apply the same rooting and
current-receiver handling to closure_value and install_method.
Source: Learnings
|
Test result promised in the PR body, posting for the record now that this has merged: Green, single-threaded per #7791. Also verified the gate holds on |
Closes #7838.
The raw-handle debt ratchet is red on
main: 1,013 bare reads against a baseline of 998, four per-module violations. The +15 arrived with the two #6949 rooting fixes in the 2026-08-11 batch (#7811, #7815), and #7825 — same batch — closed the hole that had been letting a PR's own checkout carry the comparison baseline, so the ratchet only started seeing them once it landed.All fifteen convert to
RuntimeHandle::across_{mut,const}. No ceiling was raised andraw_handle_debt_baseline.txtis untouched — the total lands back on 998 exactly, andregex/replace_fn.rsreturns to its recorded ceiling of 3.disposable.rsbuiltins/formatting/boxed_primitives.rsmessaging.rsregex/replace_fn.rsWhy
replace_fn.rsdid not need a ceiling raise#7838 proposed raising it 3 → 13 on the grounds that the thirteen are the one shape
raw_handle_debt_files.txtsanctions joining the list for — a loop whose collection window is a user-visible callback, wherecur_str = || string_as_str(handle.get_raw_const_ptr(..))re-derives at every access andacross_*(one call ↔ one re-read) cannot express it.That is true of three of them, not thirteen — and those three are pre-existing, which is exactly what the ceiling of 3 was recorded for.
git show 6af7e5840^:crates/perry-runtime/src/regex/replace_fn.rs | grep -c get_raw_is 3; at6af7e5840it is 13. So #7811 added 10, and all ten are the plainacross_*shape: root → one allocatingjs_string_coerce→ read-for-the-call. They convert mechanically.Four of the ten are two-receiver call sites (
s+pattern,s+re). Two receivers compose by nestingacross_const— the inner call runs the coercion and re-readsb, the outer then re-readsa— which is howpath::value_args::with_two_headersalready does it. Two small private combinators (with_two_receivers_across,with_receiver_across) carry them so the nesting is written once.Two conversions are not purely mechanical
Writing the ordering out made a latent stale-address use visible in
disposable.rs(js_suppressed_error_new). Both are the #7192 shape — the store is in-frame but after a call that allocates — and neither is reachable without evacuation actually moving the receiver, so this is ordering hygiene rather than an observed crash:set_nonenumfiled attributes under a possibly-stale address. It calledcrate::object::set_property_attrs(obj as usize, ..)afterjs_object_set_field_by_name, which allocates when the object grows. That side table is keyed on the address, so a pre-call copy does not fault — it files the attributes where nothing will look them up, anderror/suppressed/messagesilently become enumerable on aSuppressedErrorthat grew during the set. Now re-read across the field-set.js_nanbox_pointer(obj)was computed, thenbuiltin_prototype_value("SuppressedError")ran, then the box was returned. A NaN-box is a frozen address the collector cannot rewrite, so the returned value named from-space if that lookup collected. The box is now built last, after every allocating call.Validation
(the script's own exit code, not a pipeline's).
--no-raise-vs origin/main, the mode CI runs, also exits 0:baseline 998 -> 998, 110 -> 110 module ceiling(s), none raised. Release build of-p perry -p perry-runtime-static -p perry-stdlib-staticis clean; theperry-runtimetest suite is still running and I will post the result.Summary by CodeRabbit