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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions changelog.d/7840-raw-handle-debt-across.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
### Fixed

- **The raw-handle debt ratchet is green again on `main` (#7838).** It went red at
1,013 bare `get_raw_{mut,const}_ptr` 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 (#7811, #7815); #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. While a required gate is red on `main`, no agent
can tell its own breakage from inherited breakage, which is the condition a real
regression merges unnoticed in.

**All fifteen convert to `RuntimeHandle::across_{mut,const}`. No ceiling was raised
and `raw_handle_debt_baseline.txt` is untouched** — the total lands back on 998
exactly, and `regex/replace_fn.rs` returns to its recorded ceiling of 3.
`disposable.rs`, `messaging.rs` and `builtins/formatting/boxed_primitives.rs` reach
zero and stay unlisted.

**`replace_fn.rs` did not need the 3 → 13 raise #7838 proposed.** The proposal rested
on all thirteen being the one shape `raw_handle_debt_files.txt` sanctions joining the
list for — a loop whose collection window is a user-visible callback, where a
`cur_str` helper re-derives at every access and `across_*` (one call ↔ one re-read)
cannot express it. That describes **three** of them, and those three are
*pre-existing*: `git show 6af7e5840^:…/replace_fn.rs | grep -c get_raw_` is 3, which
is what the ceiling of 3 was recorded for. #7811 added **ten**, all of them plain
root → one allocating `js_string_coerce` → read-for-the-call. Four are two-receiver
sites; two receivers compose by **nesting** `across_const`, the same way
`path::value_args::with_two_headers` already does it, and two small private
combinators carry them.

- **`SuppressedError` filed its property attributes under a possibly-stale address.**
`set_nonenum` called `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
where nothing will look them up, and `error` / `suppressed` / `message` silently
become **enumerable** on a `SuppressedError` that grew during the set.

- **`js_suppressed_error_new` returned a NaN-box built before its last allocating
call.** `js_nanbox_pointer(obj)` was computed, then the `SuppressedError.prototype`
lookup 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.

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. They are recorded because writing the
`across_*` ordering out is what made them visible, which is the argument for the
discipline the ratchet enforces.
23 changes: 13 additions & 10 deletions crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,17 +308,20 @@ pub extern "C" fn js_boxed_string_new(value: f64, has_arg: i32) -> f64 {
// dereferences or keys on it.
let scope = crate::gc::RuntimeHandleScope::new();
let obj_handle = scope.root_raw_mut_ptr(obj);
// `new String()` (no args) is spec'd to box "", not "undefined".
let ptr = 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");
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>();
});
Comment on lines +311 to +324

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.rs

Repository: 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/src

Repository: 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)}")
PY

Repository: 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"
done

Repository: 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}")
PY

Repository: 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 260

Repository: 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}")
PY

Repository: 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 inside install_string_wrapper_indices and install_string_wrapper_length.
  • In messaging.rs, root name_ptr. Root and reload the object inside set_field across key(name) and inside install_method across closure_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.rs

Repository: 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/src

Repository: 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.rs

Repository: 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.rs

Repository: 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/string

Repository: 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"
done

Repository: 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())
PY

Repository: 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 boxed = f64::from_bits(crate::value::JSValue::string_ptr(ptr).bits());
register_boxed_primitive_payload(obj, boxed);
install_string_wrapper_indices(obj, ptr);
Expand Down
44 changes: 32 additions & 12 deletions crates/perry-runtime/src/disposable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -474,13 +474,23 @@ pub extern "C" fn js_suppressed_error_new(error: f64, suppressed: f64, message:
// prototype under an address nothing will look up, and `instanceof
// SuppressedError` quietly stops resolving.
//
// So root once and re-read at every use, which is what the handle gives.
// So root once and re-read at every use, which is what the handle gives —
// through `across_mut`, so the pre-call address is never *nameable* (#7341).
// Note the second `across_mut` below is not cosmetic: `set_property_attrs`
// keys the attribute side table on `obj as usize`, and it runs AFTER
// `js_object_set_field_by_name`, which allocates when the object grows. A
// pre-call address there does not fault — it files the attributes under an
// address nothing will look up, and the property silently becomes
// enumerable.
let scope = crate::gc::RuntimeHandleScope::new();
let obj_handle = scope.root_raw_mut_ptr(obj);
let set_nonenum = |key: &str, value: f64| {
let key_ptr = js_string_from_bytes(key.as_ptr(), key.len() as u32);
let obj = obj_handle.get_raw_mut_ptr::<crate::object::ObjectHeader>();
js_object_set_field_by_name(obj, key_ptr, value);
let (key_ptr, obj) = obj_handle.across_mut::<crate::object::ObjectHeader, _>(|| {
js_string_from_bytes(key.as_ptr(), key.len() as u32)
});
let ((), obj) = obj_handle.across_mut::<crate::object::ObjectHeader, _>(|| {
js_object_set_field_by_name(obj, key_ptr, value)
});
crate::object::set_property_attrs(
obj as usize,
key.to_string(),
Expand All @@ -501,16 +511,26 @@ pub extern "C" fn js_suppressed_error_new(error: f64, suppressed: f64, message:
};
set_nonenum("message", message_val);
}
let obj = obj_handle.get_raw_mut_ptr::<crate::object::ObjectHeader>();
let result = js_nanbox_pointer(obj as i64);
// Link the instance to `SuppressedError.prototype` so `name`/`message`
// defaults and `instanceof SuppressedError` resolve through the chain.
let proto = crate::object::builtin_prototype_value("SuppressedError");
if proto.to_bits() != TAG_UNDEFINED && js_nanbox_get_pointer(proto) != 0 {
let obj = obj_handle.get_raw_mut_ptr::<crate::object::ObjectHeader>();
crate::object::prototype_chain::object_set_static_prototype(obj as usize, proto.to_bits());
}
result
//
// The NaN-box of the result is built LAST, after every allocating call. The
// pre-#7341 shape built it before the prototype lookup and returned that
// copy, which is a stale address by the same argument as everything above —
// `js_nanbox_pointer` freezes an address into a return value the collector
// cannot rewrite.
let (proto, obj) = obj_handle.across_mut::<crate::object::ObjectHeader, _>(|| {
crate::object::builtin_prototype_value("SuppressedError")
});
let ((), obj) = obj_handle.across_mut::<crate::object::ObjectHeader, _>(|| {
if proto.to_bits() != TAG_UNDEFINED && js_nanbox_get_pointer(proto) != 0 {
crate::object::prototype_chain::object_set_static_prototype(
obj as usize,
proto.to_bits(),
);
}
});
js_nanbox_pointer(obj as i64)
}

// ---------------------------------------------------------------------------
Expand Down
4 changes: 2 additions & 2 deletions crates/perry-runtime/src/messaging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -613,8 +613,8 @@ pub extern "C" fn js_broadcast_channel_new(name: f64) -> f64 {
// through it. Root it across the coercion and re-read.
let scope = crate::gc::RuntimeHandleScope::new();
let obj_handle = scope.root_raw_mut_ptr(obj);
let name_ptr = crate::builtins::js_string_coerce(name);
let obj = obj_handle.get_raw_mut_ptr::<object::ObjectHeader>();
let (name_ptr, obj) = obj_handle
.across_mut::<object::ObjectHeader, _>(|| crate::builtins::js_string_coerce(name));
Comment on lines +616 to +617

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.rs

Repository: 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/src

Repository: 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 120

Repository: 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 300

Repository: 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 260

Repository: 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 180

Repository: 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_value = f64::from_bits(JSValue::string_ptr(name_ptr).bits());
set_field(obj, "name", name_value);
Comment on lines +616 to 619

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.rs

Repository: 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/src

Repository: 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/src

Repository: 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 240

Repository: 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.rs

Repository: 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)
PY

Repository: 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.rs

Repository: 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.rs

Repository: 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.rs

Repository: 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

install_method(obj, "close", noop0 as *const u8, 0);
Expand Down
112 changes: 66 additions & 46 deletions crates/perry-runtime/src/regex/replace_fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,44 @@ fn replacement_is_callable(value: f64) -> bool {
crate::closure::is_closure_ptr((bits & crate::value::POINTER_MASK) as usize)
}

/// Root TWO raw receivers across one allocating coercion and hand the callee
/// the POST-collection addresses.
///
/// `RuntimeHandle::across_const` pairs exactly one allocating call with one
/// re-read, so two receivers compose by NESTING: the inner call runs `coerce`
/// and re-reads `b`, the outer then re-reads `a`. Neither pre-call address is
/// ever bound, which is the property `across_*` exists to provide (#7341) and
/// the one `scripts/raw_handle_debt.py` counts. `path::value_args`'
/// `with_two_headers` uses the same nesting for the same reason.
///
/// Both receivers are rooted BEFORE `coerce` runs, so a collection inside it
/// marks and rewrites both slots; `f` then sees two addresses that are current
/// as of the same collection.
fn with_two_receivers_across<A, B, C, R>(
a: *const A,
b: *const B,
coerce: impl FnOnce() -> C,
f: impl FnOnce(*const A, *const B, C) -> R,
) -> R {
let scope = crate::gc::RuntimeHandleScope::new();
let a_handle = scope.root_raw_const_ptr(a);
let b_handle = scope.root_raw_const_ptr(b);
let ((coerced, b), a) = a_handle.across_const::<A, _>(|| b_handle.across_const::<B, _>(coerce));
f(a, b, coerced)
}

/// One-receiver twin of [`with_two_receivers_across`].
fn with_receiver_across<A, C, R>(
a: *const A,
coerce: impl FnOnce() -> C,
f: impl FnOnce(*const A, C) -> R,
) -> R {
let scope = crate::gc::RuntimeHandleScope::new();
let a_handle = scope.root_raw_const_ptr(a);
let (coerced, a) = a_handle.across_const::<A, _>(coerce);
f(a, coerced)
}

#[no_mangle]
pub extern "C" fn js_string_replace_string_dyn(
s: *const StringHeader,
Expand All @@ -370,14 +408,11 @@ pub extern "C" fn js_string_replace_string_dyn(
if replacement_is_callable(replacement) {
return js_string_replace_string_fn(s, pattern, replacement);
}
let scope = crate::gc::RuntimeHandleScope::new();
let s_handle = scope.root_raw_const_ptr(s);
let pattern_handle = scope.root_raw_const_ptr(pattern);
let coerced = crate::builtins::js_string_coerce(replacement);
js_string_replace_string(
s_handle.get_raw_const_ptr::<StringHeader>(),
pattern_handle.get_raw_const_ptr::<StringHeader>(),
coerced,
with_two_receivers_across(
s,
pattern,
|| crate::builtins::js_string_coerce(replacement),
|s, pattern, coerced| js_string_replace_string(s, pattern, coerced),
)
}

Expand All @@ -390,14 +425,11 @@ pub extern "C" fn js_string_replace_all_string_dyn(
if replacement_is_callable(replacement) {
return js_string_replace_all_string_fn(s, pattern, replacement);
}
let scope = crate::gc::RuntimeHandleScope::new();
let s_handle = scope.root_raw_const_ptr(s);
let pattern_handle = scope.root_raw_const_ptr(pattern);
let coerced = crate::builtins::js_string_coerce(replacement);
js_string_replace_all_string(
s_handle.get_raw_const_ptr::<StringHeader>(),
pattern_handle.get_raw_const_ptr::<StringHeader>(),
coerced,
with_two_receivers_across(
s,
pattern,
|| crate::builtins::js_string_coerce(replacement),
|s, pattern, coerced| js_string_replace_all_string(s, pattern, coerced),
)
}

Expand Down Expand Up @@ -440,13 +472,10 @@ pub extern "C" fn js_string_replace_search_dyn(
if let Some(re) = needle_regex_ptr(needle) {
return js_string_replace_regex_dyn(s, re, replacement);
}
let scope = crate::gc::RuntimeHandleScope::new();
let s_handle = scope.root_raw_const_ptr(s);
let needle = crate::builtins::js_string_coerce(needle);
js_string_replace_string_dyn(
s_handle.get_raw_const_ptr::<StringHeader>(),
needle,
replacement,
with_receiver_across(
s,
|| crate::builtins::js_string_coerce(needle),
|s, needle| js_string_replace_string_dyn(s, needle, replacement),
)
}

Expand All @@ -461,13 +490,10 @@ pub extern "C" fn js_string_replace_all_search_dyn(
if let Some(re) = needle_regex_ptr(needle) {
return js_string_replace_all_regex_dyn(s, re, replacement);
}
let scope = crate::gc::RuntimeHandleScope::new();
let s_handle = scope.root_raw_const_ptr(s);
let needle = crate::builtins::js_string_coerce(needle);
js_string_replace_all_string_dyn(
s_handle.get_raw_const_ptr::<StringHeader>(),
needle,
replacement,
with_receiver_across(
s,
|| crate::builtins::js_string_coerce(needle),
|s, needle| js_string_replace_all_string_dyn(s, needle, replacement),
)
}

Expand All @@ -485,14 +511,11 @@ pub extern "C" fn js_string_replace_regex_dyn(
// #6949(a): both raw params span the coercion — and `re` is a
// `RegExpHeader`, not a string, so a stale one is read for its compiled
// pattern rather than merely for bytes.
let scope = crate::gc::RuntimeHandleScope::new();
let s_handle = scope.root_raw_const_ptr(s);
let re_handle = scope.root_raw_const_ptr(re);
let coerced = crate::builtins::js_string_coerce(replacement);
crate::regex::js_string_replace_regex_named(
s_handle.get_raw_const_ptr::<StringHeader>(),
re_handle.get_raw_const_ptr::<crate::regex::RegExpHeader>(),
coerced,
with_two_receivers_across(
s,
re,
|| crate::builtins::js_string_coerce(replacement),
|s, re, coerced| crate::regex::js_string_replace_regex_named(s, re, coerced),
)
}

Expand All @@ -509,14 +532,11 @@ pub extern "C" fn js_string_replace_all_regex_dyn(
// #6949(a): both raw params span the coercion — and `re` is a
// `RegExpHeader`, not a string, so a stale one is read for its compiled
// pattern rather than merely for bytes.
let scope = crate::gc::RuntimeHandleScope::new();
let s_handle = scope.root_raw_const_ptr(s);
let re_handle = scope.root_raw_const_ptr(re);
let coerced = crate::builtins::js_string_coerce(replacement);
crate::regex::js_string_replace_all_regex_named(
s_handle.get_raw_const_ptr::<StringHeader>(),
re_handle.get_raw_const_ptr::<crate::regex::RegExpHeader>(),
coerced,
with_two_receivers_across(
s,
re,
|| crate::builtins::js_string_coerce(replacement),
|s, re, coerced| crate::regex::js_string_replace_all_regex_named(s, re, coerced),
)
}

Expand Down
Loading