You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
raw-handle ratchet red on main: #7811/#7815 landed 15 bare reads the same batch's #7825 now counts #7838
Found by the post-landing audit of the 2026-08-11 batch. The Raw-handle debt ratchet lint step fails on current main (82f0e96): 1,013 bare reads vs baseline 998, four per-module violations. lint will go red on main's next CI run.
The +15 all come from the two #6949 rooting fixes that merged in the batch:
The code is not unsound — both PRs root their subjects in RuntimeHandleScope and re-derive raw pointers from the rooted handles after every allocating call; #7811's fragment records a clean run under the seeded schedule + quarantine. The debt is stylistic: the sites use manual get_raw_*_ptr re-reads where the ratchet's discipline wants RuntimeHandle::across_{mut,const,nanbox}. The ratchet only sees this now because #7825 (same batch) closed the hole that let a PR's own checkout carry the comparison baseline.
The fix, per scripts/raw_handle_debt_files.txt's own header
fix(runtime): root string/regex receivers across the ToString argument coercion (#6949 shape a) #7811's thirteen fit the header's ONE sanctioned join-the-list shape — a loop whose collection window is a user-visible callback (the replacer fn), with cur_str = || string_as_str(handle.get_raw_const_ptr(...)) re-deriving at every access across interleaved callback calls. across_* pairs one call with one re-read and cannot express this. Raise replace_fn.rs's ceiling 3 → 13 naming the loop and the trap, per the header.
The header's condition for (2) is that the global baseline must not rise. After (1) converts 5, the total sits at 1,008 vs the 998 baseline — so either convert ~10 more pairs elsewhere in the same PR (the header's "honest move"), or the maintainer explicitly re-records the baseline at the new floor in the same commit that documents (2). The first is preferred; the second is at least a decision on the record rather than a silent raise.
Until this lands, every PR runs lint against a red step on main — the exact "required + red ⇒ every merge bypasses" gate-theatre shape CLAUDE.md warns about, so this is urgent for the ratchet's credibility, not just tidiness.
Fix is up as #7840. One correction to the proposed path, because it changes the outcome from "raise a ceiling and re-record the baseline" to "no ceiling raised, baseline untouched".
Step 2 of the plan rests on a miscount. It reads #7811 as having added thirteen bare reads to regex/replace_fn.rs, all of them the sanctioned loop-across-a-user-visible-trap shape, and concludes the ceiling must go 3 → 13. Thirteen is the module's total, not the delta:
$ git show 6af7e5840^:crates/perry-runtime/src/regex/replace_fn.rs | grep -c 'get_raw_\(mut\|const\)_ptr'
3
$ git show 6af7e5840:crates/perry-runtime/src/regex/replace_fn.rs | grep -c 'get_raw_\(mut\|const\)_ptr'
13
So #7811 added ten, and the pre-existing three are precisely the trap-loop shape — cur_str in js_string_replace_string_fn and js_string_replace_all_string_fn, plus the subject re-read in call_string_replace_callback, whose collection window is the replacer callback. That is what the ceiling of 3 was recorded for in the first place.
The ten new ones are not that shape at all. Every one is root → one allocating js_string_coerce → read-for-the-call, in the six *_dyn dispatchers — the textbook across_* case. Four of them have two receivers (s + pattern, s + re), which is the only reason they look awkward; two receivers compose by nestingacross_const (inner runs the coercion and re-reads b, outer re-reads a), exactly as path::value_args::with_two_headers already does.
Consequence for step 3. With all ten converted plus #7815's five, the total lands back on 998 — the baseline exactly. So the dilemma in step 3 does not arise: nothing needs to be re-recorded, no ceiling moves, and replace_fn.rs returns to its existing 3.
$ python3 scripts/raw_handle_debt.py ; echo $?
bare raw-handle reads: 998 (baseline 998)
per-module: 110 module(s) within ceilings; every other runtime module is locked at zero
0
Writing the ordering out also surfaced two latent stale-address uses in disposable.rs that the mechanical read-and-reuse shape had been hiding — set_property_attrs keyed on a pre-call address after an allocating field set, and the returned NaN-box built before the prototype lookup. Details in #7840.
Activity
proggeramlug commentedon Aug 11, 2026
Fix is up as #7840. One correction to the proposed path, because it changes the outcome from "raise a ceiling and re-record the baseline" to "no ceiling raised, baseline untouched".
Step 2 of the plan rests on a miscount. It reads #7811 as having added thirteen bare reads to
regex/replace_fn.rs, all of them the sanctioned loop-across-a-user-visible-trap shape, and concludes the ceiling must go 3 → 13. Thirteen is the module's total, not the delta:So #7811 added ten, and the pre-existing three are precisely the trap-loop shape —
cur_strinjs_string_replace_string_fnandjs_string_replace_all_string_fn, plus the subject re-read incall_string_replace_callback, whose collection window is the replacer callback. That is what the ceiling of 3 was recorded for in the first place.The ten new ones are not that shape at all. Every one is
root → one allocating js_string_coerce → read-for-the-call, in the six*_dyndispatchers — the textbookacross_*case. Four of them have two receivers (s+pattern,s+re), which is the only reason they look awkward; two receivers compose by nestingacross_const(inner runs the coercion and re-readsb, outer re-readsa), exactly aspath::value_args::with_two_headersalready does.Consequence for step 3. With all ten converted plus #7815's five, the total lands back on 998 — the baseline exactly. So the dilemma in step 3 does not arise: nothing needs to be re-recorded, no ceiling moves, and
replace_fn.rsreturns to its existing 3.Writing the ordering out also surfaced two latent stale-address uses in
disposable.rsthat the mechanical read-and-reuse shape had been hiding —set_property_attrskeyed on a pre-call address after an allocating field set, and the returned NaN-box built before the prototype lookup. Details in #7840.fix(runtime): convert #7811/#7815's new bare raw-handle reads to acro…