Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2b35cd3
fix: preserve forward closure initializers and TDZ names
Sep 5, 2026
a1a39e9
docs: key changelog fragment to PR 9762
Sep 5, 2026
7aa3b0d
perf(shapes): append a freshly allocated descriptor id without the fa…
Sep 5, 2026
21b5d25
perf(intl): give Intl.Segmenter records one shared shape instead of o…
Sep 5, 2026
d2a5eb3
test(macos): guard source-free node:http release links
Sep 5, 2026
6e4f75c
docs: key HTTP regression changeset to PR 9770
Sep 5, 2026
2b7f896
feat(runtime): PERRY_ALLOC_CENSUS — attribute native-heap bytes to ca…
Sep 5, 2026
0c2cabf
feat(codegen): replay typed feedback with freshness checks and guarde…
Sep 5, 2026
51e2952
fix(codegen): fingerprint concat cache setting for profile freshness
Sep 5, 2026
3e92806
docs: key replay changelog fragment to PR 9773
Sep 5, 2026
b997349
fix(compile): key the build cache on PERRY_CONCAT_SITE_CACHE
Sep 5, 2026
08cfcec
docs(changelog): add PR 9777 fragment
Sep 5, 2026
5a55d13
fix(gc): idle compaction selects whole blocks, so its prediction is a…
Sep 5, 2026
6b68a5f
fix(gc): correct the move-budget rationale against the confirming mea…
Sep 5, 2026
7a4aeb2
ci: add an opt-in Claude Code parity gate
Sep 5, 2026
4d36135
docs: number the Claude Code parity changeset
Sep 5, 2026
5c2b68f
fix(runtime): honor inherited descriptors when filling array holes
Sep 5, 2026
250ed9a
docs: number array-hole changelog for PR 9797
Sep 5, 2026
9122363
test(codegen): assert the TDZ property, not the box-helper spelling
Sep 5, 2026
f74beee
fix(gates): hot TLS for hot_diag and alloc_census, re-pin the census …
Sep 5, 2026
4a239b6
fix(gates): classify alloc-census and idle-defrag counters
Sep 5, 2026
cdcf889
fix(gates): resolve train125 gate failures at the source
Sep 5, 2026
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
105 changes: 105 additions & 0 deletions .github/workflows/cc-parity.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Opt-in bundle-scale parity (#9346). Non-required until maintainers promote it.
name: cc-parity

on:
pull_request:
types: [opened, synchronize, reopened, labeled]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: cc-parity-${{ github.event_name }}-${{ github.event_name == 'pull_request' && github.ref || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
changes:
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-cc-parity')
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
pull-requests: read
outputs:
relevant: ${{ steps.filter.outputs.relevant }}
steps:
- id: filter
env:
GH_TOKEN: ${{ github.token }}
EVENT_NAME: ${{ github.event_name }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail
if [ "$EVENT_NAME" != pull_request ]; then
echo 'relevant=true' >> "$GITHUB_OUTPUT"
exit 0
fi
cc_files=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" --paginate --jq '.[].filename')
# An empty listing must not quietly turn a requested gate green.
if [ -z "$cc_files" ] || grep -E '^(crates/|Cargo\.(toml|lock)$|rust-toolchain|\.cargo/|\.github/workflows/cc-parity\.yml$|scripts/cc_parity_gate\.py$|tests/(test_cc_parity_gate\.py$|cc-parity/))' <<< "$cc_files" > /dev/null; then
echo 'relevant=true' >> "$GITHUB_OUTPUT"
else
echo 'relevant=false' >> "$GITHUB_OUTPUT"
fi

cc-parity:
needs: changes
if: needs.changes.outputs.relevant == 'true'
# The bundle's IR construction exceeds the ARM runner's 7 GB RAM.
runs-on: macos-15-intel
timeout-minutes: 90
env:
CARGO_BUILD_JOBS: '4'
CARGO_INCREMENTAL: '0'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false

- name: Test the gate's failure paths
run: python3 -m unittest discover -s tests -p test_cc_parity_gate.py -v

- name: Set scratch work directory
run: echo "CC_PARITY_WORK=$RUNNER_TEMP/cc-parity" >> "$GITHUB_ENV"

# This job uses the macOS SDK, not the preinstalled simulator images.
# simctl unmounts runtime images before deleting their backing storage.
- name: Free simulator runtime disk space
run: |
sudo xcrun simctl runtime delete all
df -h /

- name: Fetch and verify the pinned bundle
run: python3 scripts/cc_parity_gate.py prepare --work-dir "$CC_PARITY_WORK"

- name: Install LLVM 22
run: |
set -euo pipefail
brew install llvm@22 2>/dev/null || brew install llvm
cc_llvm_prefix="$(brew --prefix llvm@22 2>/dev/null || brew --prefix llvm)"
"$cc_llvm_prefix/bin/llvm-config" --version | grep -q '^22\.'
echo "LLVM_SYS_221_PREFIX=$cc_llvm_prefix" >> "$GITHUB_ENV"

- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
shared-key: cc-parity-wasm-host
save-if: ${{ github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' }}

- name: Build compiler, then all runtime archives together
run: python3 scripts/cc_parity_gate.py build --work-dir "$CC_PARITY_WORK"

- name: Compile the pinned bundle natively
run: python3 scripts/cc_parity_gate.py compile --timeout 4500 --work-dir "$CC_PARITY_WORK" --perry "$GITHUB_WORKSPACE/target/perry-dev/perry"

- name: Check help and version offline against golden bytes
run: python3 scripts/cc_parity_gate.py check --work-dir "$CC_PARITY_WORK"

- name: Upload compiler logs and parity results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: cc-parity-results
path: ${{ runner.temp }}/cc-parity/logs/
if-no-files-found: warn
retention-days: 7
2 changes: 2 additions & 0 deletions changelog.d/9762-forward-const-tdz.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
### Fixes
- Preserve closure initializers that earlier closures capture, fixing false temporal-dead-zone errors in mutually recursive `const` functions. Genuine TDZ errors now name the source binding, including captured reads and updates. Fixes #9721.
5 changes: 5 additions & 0 deletions changelog.d/9770-macos-http-link.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Add a source-free installation regression for #8907, the macOS arm64
v0.5.1220 `node:http` link failure previously fixed by #5983. Exercise the
full prebuilt runtime, stdlib, and HTTP wrapper in both default and
`PERRY_NO_AUTO_OPTIMIZE=1` modes, checking that the minimal server links,
listens on an ephemeral port, closes, and exits successfully.
1 change: 1 addition & 0 deletions changelog.d/9772-idle-compaction-block-selection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fix(gc): the idle old-generation compaction now selects whole BLOCKS, so the bytes it predicts are the bytes it can return (#9772). Old-gen memory is released a block at a time (`old_arena_reclaim_selected_dead_blocks`), but selection ranked individual 4 KB pages by fragmentation, and the emptied pages were scattered across blocks that kept other live occupants. Measured on the compiled claude-code TUI: the pass chose 10,740 pages, predicted 44 MB of "releasable block bytes", ran for 228 ms and released **nothing**; in a controlled two-arm run the same selection predicted 44.4 MB, spent 516 ms and released 0, because a page-granular prediction is not achievable by a block-granular reclaim. Selection now groups pages by their containing block (`arena::old_arena_block_ranges`), skips blocks holding pinned bytes, ranks the rest cheapest-to-empty and takes whole blocks, so every selected block ends the pass with no live occupant. Same workload, same binary, one env var apart: **released 46.6 MB of 52.4 MB predicted (`kept_promise=true`, 50 of 50 targeted blocks, `has_live=0`), old-gen occupancy 120.4 MB -> 73.8 MB**, against **0 MB released of 44.4 MB predicted** for the old selection. Two counters make a barren pass visible instead of silent: `[gc-old-block-reclaim]` reports targeted/released/kept-by-reason, and `[gc-idle-compact] done` now carries `predicted=` and `kept_promise=`, summarised as `broken_promises=` in the exit line. Two defects that only became visible once the pass could be judged against its own prediction are fixed with it: a pass that declines to evacuate no longer drops the excluded pages' holes first (it was destroying 40.7 MB of reusable free list and returning nothing), and `IDLE_COMPACT_MOVE_BUDGET_BYTES` is 8 MiB -> 1 MiB, which moves ~15 blocks per pass instead of ~50. Three interleaved pairs show the pause is unchanged by that (1,070 ms mean against the old selection's 1,044 ms, spread 515-1,375 ms tracking machine load), so the pass is dominated by fixed per-pass cost rather than by moving: the measured claim is that this returns ~15 MB per pass for the same pause the barren pass already spent, not that it made the pass cheaper. Kill switch `PERRY_GC_IDLE_COMPACT_BLOCKS=0`.
1 change: 1 addition & 0 deletions changelog.d/9773-typed-feedback-profile-replay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add opt-in typed-feedback profile replay for guarded numeric array reads, with versioned capture catalogs, exact freshness checks, deterministic selection and rejection diagnostics, native-region verifier checks, and explain-lowering evidence. Profiles remain advisory and retain the runtime guard and boxed fallback.
10 changes: 10 additions & 0 deletions changelog.d/9777-build-cache-concat-site-cache.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
**`PERRY_CONCAT_SITE_CACHE` is now a build-cache input.** #9514's per-site
concat cache reads the variable as a build-time kill switch — setting it to
`0` removes the lowering lane entirely — but it was registered neither in
`BUILD_CACHE_ENV_VARS` nor as a justified exclusion. A build with the switch
flipped could therefore be served a cached object produced with it in the
other state.

`codegen_env_vars_are_build_cache_inputs` caught this by scanning
`crates/perry-codegen/src` for every `env::var("PERRY_…")`, which is why the
check scans the source instead of trusting a hand-maintained list.
1 change: 1 addition & 0 deletions changelog.d/9793-cc-parity-gate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add an opt-in `run-cc-parity` CI gate that compiles pinned Claude Code 2.1.112 and checks native help/version output against offline Node goldens.
1 change: 1 addition & 0 deletions changelog.d/9797-array-hole-inherited-setters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix numeric writes into array holes bypassing inherited setters and read-only properties on `Array.prototype` and `Object.prototype`. Existing own elements and arrays with unmodified prototype chains keep their fast path.
28 changes: 28 additions & 0 deletions changelog.d/9799-train125-gate-followups.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
**Gate follow-ups for train125.** Two of these are gates pinning a *spelling*
where the guarantee is a *property*; both were sabotage-checked after widening,
so the guarantee is unchanged.

- `tdz_numeric_const_read_is_not_constant_folded` pinned
`call i64 @js_box_get_bits(...)`. #9721 routes the read through
`js_box_get_bits_named`, which additionally passes the binding name so the
thrown `ReferenceError` can identify it — a strictly better error, which the
test read as a lost guard. It now accepts either helper and additionally
asserts the read is NOT folded to the later value, which it never checked.
- `shape_descriptor_census` required `family_push_back` after
`slab_mut().insert`. #9768 added `family_append_fresh` — the same append minus
a membership scan that is dead work for an id `alloc_shape_id` just minted and
never reuses. The census now accepts either append and still enforces the
ordering: the by-id descriptor must exist before the reverse accelerator
points at it.
- `hot_diag.rs` and `alloc_census.rs` moved to `perry_thread_local!`, which also
made their holders visible to `gc_runtime_root_holders` (the #9740 design);
`CREDIT` and `LAST_IDLE_PREDICTED_RELEASE` are classified as counters.
- `intl/segmenter.rs`'s shared keys array is built inside `with_mut_ptr`, with
every use — including publication into `SEGMENT_RECORD_KEYS`, which #9769
registers a scanner for — inside the scope.
- `PASS1_MARKED`'s window re-pinned after #9769 and #9771 touched pinned files.
Decisive: `census_take_if_armed_at_full_sweep_start` takes the snapshot out of
the thread-local BEFORE calling `take_census`, so #9771's feature-gated
Rust-heap dump inside it runs after the window has closed.
- Two `page_meta.rs` band literals are allowlisted as what they are: synthetic
block ranges inside a `#[test]`, not runtime classification.
1 change: 1 addition & 0 deletions changelog.d/alloc-census-rust-heap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
feat(runtime): `PERRY_ALLOC_CENSUS` — a sampling profiler for the Rust heap, behind the off-by-default `alloc-census` feature. The GC census accounts for the arena and the side tables; on the compiled claude-code TUI those two explain ~115 MB of a 300 MB idle footprint and ~430 MB of a 2 GB peak, and nothing in the runtime could say where the rest came from. The census wraps the `#[global_allocator]` and reports exact totals plus a power-of-two size-class histogram, and samples one call site per MiB allocated (raw `backtrace(3)` frames, symbolised offline with `atos`), subtracting a sampled pointer again when it is freed — so it reports *live* native bytes per call site, not just churn. Measured on a 400-character reply: **22.5 GB allocated in 31.7 M calls, peak live 1.70 GB**, of which the largest single owners are the GC's own side-table scanners rebuilding their hash maps inside every copying minor (`descriptor_state::scan_descriptor_roots_mut` 222 MB, `shapes::scan_shape_table_rekey_mut` 75 MB, `restore_surviving_dirty_coverage` 29 MB) and regex program construction (~127 MB). Dumped with the heap census on `SIGUSR2`, alongside `mi_stats_print`.
15 changes: 15 additions & 0 deletions changelog.d/keystroke-segmenter-shared-shape.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
### Fixed

- `Intl.Segmenter` segment records now share one shape. They were built
property-by-property with `set_field`, which allocates a fresh key string
per call and clones the object's key list before each write, so every record
got its own keys array — and, because the shape table is keyed on that
array's address, its own ShapeId. That made every read of `.segment` /
`.index` / `.input` a guaranteed inline-cache miss and added one descriptor
to the shape table per record. Grapheme-aware text measurement segments
every string a terminal UI renders: one 400-character reply in the compiled
claude-code TUI produces 175,797 segment records, and `PERRY_IC_DIAG`
attributes 175,797 of that turn's 2,589,696 IC misses to the `.segment` read
site alone. The two record shapes (with and without `isWordLike`) now share
one `GC_FLAG_SHAPE_SHARED` keys array each, built at most twice per thread —
the same construction #7564 used for `{ value, done }` iterator results.
14 changes: 14 additions & 0 deletions changelog.d/keystroke-shape-family-append.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
### Fixed

- Interning a shape descriptor no longer scans the keys array's whole
descriptor history. `ShapeTableInner::family_push_back` / `facts_push_back`
answer "is this id already here?" with a linear scan of the family, and a
family accumulates every descriptor ever created for one keys array — so
interning the *n*-th descriptor for a keys array cost O(n) and a render that
keeps bumping a shape's semantic generation paid quadratic time. The two
interning sites append ids that `alloc_shape_id` has just handed out, and
that allocator never reuses a value, so the scan was provably dead work:
they now use `IdList::append_unchecked`. On the compiled claude-code TUI
`IdList::contains` was 6.2 % of main-thread leaf samples during a streamed
reply and 5.9 % in the window after it, 95 % of it under
`family_push_back`.
4 changes: 4 additions & 0 deletions crates/perry-codegen/src/codegen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ mod hoisted_callback_method_tests;
mod index_method_clone_tests;
mod indexed_method_artifacts;
mod ordinary_method_artifacts;
mod tdz_names;
// `pub(crate)` so `crate::linker` can read the inline-hot-small policy
// (`inline_hot_small_enabled` / `inline_hot_small_hint_threshold`).
pub(crate) mod helpers;
Expand Down Expand Up @@ -475,6 +476,7 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
// becomes part of every emitted global so multi-module programs
// don't collide on `.str.0.handle`.
let mut strings = StringPool::with_prefix(module_prefix.clone());
strings.tdz_binding_names = tdz_names::collect(hir);
// #5247: install per-module source-location context for the dynamic
// call-dispatch throw path, but only under `--debug-symbols` (which sets
// `opts.debug_locations` + `opts.module_source`). Off by default — no
Expand Down Expand Up @@ -3662,6 +3664,8 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
progress.phase(1, "lowering complete; finalizing generated IR");
crate::root_reload::apply_to_module(&mut llmod);

crate::typed_feedback_profile::finish_module(&mut llmod.native_rep_records);

let verify_native_regions = opts.verify_native_regions
|| std::env::var("PERRY_VERIFY_NATIVE_REGIONS").ok().as_deref() == Some("1");
if verify_native_regions {
Expand Down
Loading
Loading