Skip to content

Commit 2ddbc7b

Browse files
proggeramlugRalph Küpper
andauthored
gc: close the strhandle, derived-mask and new.target unrooted hazards in the native lowering (#7664) (#7667)
* gc: close the strhandle, derived-mask and new.target unrooted hazards (#7664) `gc-root-dominance-statepoints`' `--max-unrooted` ratchet goes 21 -> 7. #7663 pointed the root-dominance rule at the NATIVE root lowering -- the one that ships since #7370 -- and reported 21 `unrooted` hazards. Fourteen were shapes `root_reload.rs` looked straight through, because its rule is stated over the load's own register and in both shapes the value at risk lives somewhere else. 1. The root is a GLOBAL, not an alloca (10 hits). A string literal lowers to `load double, ptr @<mod>_.str.N.handle`; the handle global is a registered root, so the string is never swept, and an evacuating cycle REWRITES the global while a register loaded beforehand keeps the pre-move address. #7240's shape, whose fix covered call operands only. 2. The stale register is DERIVED from the load (3 of 7 unmasked receivers). `this.count++` holds the unmasked receiver across the property GET; the load's only use is the bitcast ABOVE the collecting call, so the window was empty and the function took zero reloads. 3. `new.target`'s saved previous value (1 hit). `new.rs` saved `js_new_target_get()` in a bare register across the whole constructor body; the cell is a registered mutable root, so the restore publishes a pre-move address back INTO a root the collector scans. #7226's `prev_this` bug for `new.target`. The window is anchored at the ROOT LOAD, not at the derived value. Anchoring at the derivation looks more precise and is wrong: `main`'s class-object read has the scope-end shadow-slot clear landing between the load and the mask, so a walk starting at the mask never sees it and re-read a slot the program had just nulled -- `(makeAnon(77) as any).v` became `undefined`. Caught by an A/B against the branch point on `test_gap_class_expr_identity`, not by the dominance checker, which cannot see a value-correctness bug. The reload rule is restated over the value's derivation rather than its register: for a value read out of a collector-rewritten location -- a shadow slot or a string-handle global -- and any value derived from it by pure bit ops, every use a collection point can reach re-materialises the whole derivation. A recipe is extended only through ops that are pure functions of their operands and whose every register operand is already in the same single root's recipe, which makes it self-contained and materialisable anywhere. Grouping by root load also puts the cost back at O(blocks x loads). `new.target` gets `new_target_save`/`new_target_restore` in `crate::rooting`, structurally `implicit_this_save`/`implicit_this_restore`. Re-reading the cell would be the wrong repair: `js_new_target_set` has already overwritten it. Measured on `Counter__increment`: before, all three statepoints carried an EMPTY live set, so the receiver was marked by nothing; after, each carries a "gc-live" bundle and a `gc.relocate`, and the SET reads a mask re-derived from the relocated pointer plus a fresh load of the handle global. Remaining 7, each its own slice: 4 unmasked are phi-mediated (the reload has to go in the predecessor, on the edge); 2 `@perry_global_*` are module-level variables the program assigns, so they need rooting rather than reloading (pinned by `a_module_global_is_not_a_reload_source`); 1 capture read. #7664 stays open as the budget's referent. Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix * chore: bump version to 0.5.1382 Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix --------- Co-authored-by: Ralph Küpper <ralph@skelpo.com>
1 parent db4a2d0 commit 2ddbc7b

9 files changed

Lines changed: 953 additions & 177 deletions

File tree

.github/workflows/gc-root-dominance.yml

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -464,17 +464,35 @@ jobs:
464464
#
465465
# BUDGETS. `unrooted` (the object is in NO live bundle at all, so
466466
# nothing marks or rewrites it) is the serious class, and it is a
467-
# RATCHET at its measured value rather than a calibrated zero: this
468-
# is a new instrument pointed at a lowering nothing has ever checked
469-
# statically, and 21 is a population under triage, not a number
470-
# anyone has adjudicated. It is a budget rather than allowlist
471-
# entries for the reason `--stale-registers` records — 21 tombstones
472-
# with no issue numbers would be worse documentation than one number
473-
# that can only go down. Lower it as sites are fixed.
467+
# RATCHET rather than a calibrated zero: this is a young instrument
468+
# pointed at a lowering nothing had ever checked statically, and the
469+
# remainder is a population under triage rather than a number anyone
470+
# has adjudicated. It is a budget rather than allowlist entries for
471+
# the reason `--stale-registers` records — tombstones with no issue
472+
# numbers would be worse documentation than one number that can only
473+
# go down. Lower it as sites are fixed.
474474
#
475-
# The 21 are enumerated by shape in #7664, which is this budget's
476-
# referent: a number with nothing behind it is the thing CLAUDE.md
477-
# warns a threshold decays into.
475+
# 21 at #7663. #7664 fixed 14 of them in `root_reload.rs` and
476+
# `lower_call/new.rs` — the whole `strhandle` population (10), the
477+
# `js_new_target_get` save/restore (1), and 3 of the 7 unmasked
478+
# receivers — so the budget is 7. What remains, and why each is its
479+
# own slice rather than a widening of the same fix:
480+
#
481+
# 4 unmasked, all PHI-MEDIATED. The stale value reaches its use
482+
# through a `phi`, and `root_reload` cannot insert above a phi;
483+
# the reload has to go in the PREDECESSOR, on the edge, which is
484+
# a different insertion model.
485+
# 2 global (`@perry_global_*`). NOT reloadable: a module-level
486+
# variable is one the program assigns, so a re-read can observe
487+
# a later assignment instead of the value the call was given
488+
# (`operand_needs_root`). That population needs ROOTING, and
489+
# `a_module_global_is_not_a_reload_source` in root_reload.rs
490+
# pins the distinction so it cannot be widened away by accident.
491+
# 1 capture, a `js_closure_get_capture_bits` read held across
492+
# `js_number_coerce`.
493+
#
494+
# #7664 stays open as this budget's referent: a number with nothing
495+
# behind it is the thing CLAUDE.md warns a threshold decays into.
478496
#
479497
# `stale` (the object survives and is relocated, but a raw copy of
480498
# its pre-move address is used below) reads 0 today and is held
@@ -494,7 +512,7 @@ jobs:
494512
--min-statepoints 15000 \
495513
--min-live-bundles 8000 \
496514
--min-relocates 20000 \
497-
--max-unrooted 21 \
515+
--max-unrooted 7 \
498516
--max-stale 0 \
499517
--allowlist scripts/gc_root_dominance_allowlist.json \
500518
--seeded-violations 40 \

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
88

99
Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation.
1010

11-
**Current Version:** 0.5.1381
11+
**Current Version:** 0.5.1382
1212

1313

1414
## TypeScript Parity Status

0 commit comments

Comments
 (0)