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
5 changes: 4 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1206,7 +1206,10 @@ jobs:
# run zero copying minors (the #6942 / #7024 / #7025 failure mode).
# The detection property itself is a required-gate unit test:
# gc/tests/fromspace_protect.rs::quarantine_catches_a_planted_stale_from_space_deref.
# ~20s: the fixture is sized for ~1200 back-edge polls, not #7154's 240k.
# Arms 1-3/5 use a fixture sized for ~1200 back-edge polls (not #7154's
# 240k), pinned to every-poll zeal. Arm 6 (#7728) is the budgeted one:
# a realistic poll count at the SHIPPED default, which is the axis that
# a ~1200-poll fixture structurally cannot see.
- name: GC rooting-bug instruments (inert-when-off, live-when-on)
run: ./scripts/gc_instrument_smoke.sh target/release/perry

Expand Down
7 changes: 4 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

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.

**Current Version:** 0.5.1427
**Current Version:** 0.5.1428


## TypeScript Parity Status
Expand Down Expand Up @@ -140,10 +140,11 @@ A "GC value live but not rooted across a collection point" bug is invisible at c
|---|---|---|
| `PERRY_GC_PROTECT_FROMSPACE=1` (or `poison`) | the from-space reset performed by the **copying minor** (`arena::copying_reset_from_spaces_and_flip`). Retired Eden + active-survivor blocks are detached into a bounded quarantine, poison-filled (`0xDEADBEEFBAADF0DE`, `obj_type = 0xDE`) and, at `=1`, `mprotect(PROT_NONE)`d. A stale deref then SIGSEGVs at the faulting instruction; the installed reporter names the address, the retiring minor, and the last-known object's `obj_type`/size, then restores `SIG_DFL` and re-faults so a core/debugger still sees the real site. `poison` skips `mprotect`. | change the non-moving minor's `arena_reset_empty_blocks`, the full mark-sweep's reclaim, old-gen defrag, or the malloc sweep. **A run with zero copying minors protects nothing** — check that `PERRY_GC_DIAG=1` prints a `[gc-fromspace-protect] retired_set=#N` line. |
| `PERRY_GC_PROTECT_FROMSPACE_DEPTH=N` (default 4) | how many retired page-sets stay quarantined. Evicted sets are restored to RW and **recycled back into Eden**, never `dealloc`'d, so footprint is bounded at `N × from-space bytes`. `0` is clamped to 1 — a depth of 0 would read as ON and protect nothing. **Raise this when a suspected bug does not fault**: a value can cross hundreds of collections between its last valid observation and its stale use (one per back-edge poll under zeal). #7154's `new C(…)` reproducer needs `800` — its constructor crosses 600 polls, so the default 4 misses it silently. | — |
| `PERRY_GC_ZEAL=1` | forces an evacuating minor at every **GC safepoint**: `js_gc_loop_safepoint` (loop back-edge) and the outermost microtask-pump safepoint. It bypasses exactly two things — the `GC_SAFEPOINT_PENDING` requirement in `js_gc_loop_safepoint`, and the `gc_budgeted_due_trigger()` "is anything due?" test in `gc_safepoint_moving_minor`. Also makes `gc_force_evacuate_enabled()` true, so survivors actually MOVE. | bypass `gc_safepoint_moving_minor`'s **entry guards**: a safepoint reached mid-allocation (`GC_FLAG_IN_ALLOC`), suppressed (`GC_FLAG_SUPPRESSED`), inside an unsafe FFI zone, under a non-zero `GC_ROOT_LOCK_DEPTH`, or during a budgeted cycle still returns without collecting. Nor does it emit loop polls — those need the **compile-time** `PERRY_GC_MOVING_LOOP_POLLS=1` (default off since #7161), and even then codegen emits **no poll** for a provably alloc-free loop body (by design, `loop_purity::loop_may_allocate`) nor for the specialized `for` / `for-of` / `for-in` lowerings (by omission — see `emit_gc_loop_safepoint`'s COVERAGE note). Zeal on a poll-free binary only fires at event-loop boundaries; a compute-only loop never collects. **You no longer have to remember to check this**: since #7604 a zeal run prints `[gc-zeal] forced_collections=N copying_minors=M moved_objects=K` at exit and **exits 70** if N or M is zero, so a run that exercised nothing is a red run rather than a green one. (`process.exit()` and an uncaught throw bypass the exit boundary and get no verdict.) There is deliberately **no level 2**: the alloc-point arm forces a conservative stack scan, which makes the copying minor ineligible, so an "every allocation" zeal would run non-moving minors and move nothing. |
| `PERRY_GC_ZEAL=1` | forces an evacuating minor at **GC safepoints**: `js_gc_loop_safepoint` (loop back-edge) and the outermost microtask-pump safepoint. It bypasses exactly two things — the `GC_SAFEPOINT_PENDING` requirement in `js_gc_loop_safepoint`, and the `gc_budgeted_due_trigger()` "is anything due?" test in `gc_safepoint_moving_minor`. Also makes `gc_force_evacuate_enabled()` true, so survivors actually MOVE. **Allocation-PACED since #7728** — see the row below; it used to collect at every single poll, which cost 24 minutes on a 19 s program once #7721 made polls default-ON. | bypass `gc_safepoint_moving_minor`'s **entry guards**: a safepoint reached mid-allocation (`GC_FLAG_IN_ALLOC`), suppressed (`GC_FLAG_SUPPRESSED`), inside an unsafe FFI zone, under a non-zero `GC_ROOT_LOCK_DEPTH`, or during a budgeted cycle still returns without collecting. Nor does it emit loop polls — those come from the **compile-time** `PERRY_GC_MOVING_LOOP_POLLS` (**default ON since #7721**; it was off from #7161 until then, which is why zeal used to look free — it was collecting nothing), and even then codegen emits **no poll** for a provably alloc-free loop body (by design, `loop_purity::loop_may_allocate`) nor for the specialized `for` / `for-of` / `for-in` lowerings (by omission — see `emit_gc_loop_safepoint`'s COVERAGE note). Zeal on a poll-free binary only fires at event-loop boundaries; a compute-only loop never collects. **You no longer have to remember to check this**: since #7604 a zeal run prints `[gc-zeal] forced_collections=N copying_minors=M moved_objects=K loop_polls=P paced_polls=Q stride_bytes=S` at exit and **exits 70** if N or M is zero, so a run that exercised nothing is a red run rather than a green one. (`process.exit()` and an uncaught throw bypass the exit boundary and get no verdict.) There is deliberately **no level 2**: the alloc-point arm forces a conservative stack scan, which makes the copying minor ineligible, so an "every allocation" zeal would run non-moving minors and move nothing. |
| `PERRY_GC_ZEAL_ALLOC_KB=N` (default 4) | how much NEW nursery material must accumulate between zeal-forced collections. Zeal's cost is ~511 us of fixed root-scan per collection to relocate a mean of 5.9 objects, so unpaced "every back-edge poll" is one whole collection per loop iteration. The stride is a monotone high-water mark (rearmed to `from_space_after + N`), so total forced collections are bounded by `bytes_allocated / N` even when a collection reclaims nothing. **`=0` restores the literal every-poll mode** — use it for a small fixture, or for a bug window executed exactly once. | change WHICH safepoints are eligible, or weaken evacuation: a paced collection is the same collection, just less often. A recurring window is still caught, after N KB of allocation rather than on the first iteration. |
| `PERRY_GC_FROMSPACE_SCAN_ABORT=1` | now **implies** `PERRY_GC_FROMSPACE_SCAN=1`. It used to be inert alone (the scan never ran, so nothing aborted, and the run reported success). | — |

`PERRY_GC_ZEAL=1 PERRY_GC_PROTECT_FROMSPACE=1` together is the pairing that turns a #7154 bug into an immediate precise fault. Compile *and* run with `PERRY_GC_MOVING_LOOP_POLLS=1` for in-loop coverage.
`PERRY_GC_ZEAL=1 PERRY_GC_PROTECT_FROMSPACE=1` together is the pairing that turns a #7154 bug into an immediate precise fault. Loop polls are default-ON since #7721, so in-loop coverage no longer needs a flag — check `loop_polls=` in the exit verdict rather than assuming. If a hunt needs maximum sensitivity on a small program, add `PERRY_GC_ZEAL_ALLOC_KB=0`.
Comment on lines +143 to +147

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the CLAUDE.md documentation change.

This PR already contains a PR-keyed changelog fragment and dedicated documentation updates. Keep this file unchanged.

Based on learnings, contributors must not edit CLAUDE.md in external PRs; maintainers own release and version metadata updates.

🤖 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 `@CLAUDE.md` around lines 143 - 147, Remove the documentation changes shown in
CLAUDE.md and restore the file to its pre-PR state. Keep the PR-keyed changelog
fragment and other dedicated documentation updates unchanged, since CLAUDE.md
must remain maintainer-owned.

Source: Learnings


### GC knob kill-policy (binding)

Expand Down
Loading