Skip to content

Merge train 204: dependency group, store-plan cache, builder fold, GC shape borrow, ambient declare (v0.5.1582) - #10393

Merged
proggeramlug merged 9 commits into
mainfrom
train204r
Sep 16, 2026
Merged

proggeramlug merged 9 commits into
mainfrom
train204r

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

This train lands six PRs as v0.5.1582. They touch no file in common and all six cherry-picked clean.

Eight source commits, each verified to preserve its authorship. No train repairs were needed — all four crates/-touching PRs shipped their own changelog.d/ fragment, and every gate passed as assembled.

One patch-id differs and the reason is recorded in train204r-source-proof.json: #10314's commit touches only Cargo.lock, its added and removed lines are byte-identical to the source, and only the surrounding context moved because main advanced the workspace version line inside the same region. git patch-id hashes context alongside the hunk body, so an unmodified edit replayed onto a moved base yields a different id. No content change.

Validation

Validated head 06c5249840, on main 99363beb1a. Five-package release build pinned and hash-verified; artifacts_match_pin true after the suites and again after the gap phase.

  • All nine preflight gates pass, including both the raw-handle and unrooted-local-shape ratchets against main.
  • Crate suites: codegen 1,563 · runtime 3,970 · standard library 139 · HIR 419 · transform 137 · CLI 1,139. The runtime suite's only failure is main's known release-only gc::tests::heap_generation::a_free_or_move_outside_every_scope_is_caught_in_debug_builds.
  • The three integration suites the diff names: builder_fold_gap, builder_fold_gap_semantics, descriptor_store_fast_paths — all pass.
  • Lint 82 of 83; the only failure is Public benchmark evidence freshness.

Gap: filters object, store, builder, declare, shape, iterator, descriptor. Seven fixtures diverge. Three were newly checked here by compiling and running each under both the main and train compilers, with Node as a third reference; all three behave identically on the two arms:

Fixture Node main train
test_issue_3580_arguments_object_semantics rc=0 rc=0 rc=0, identical
test_issue_4034_object_literal_semantics rc=1 rc=0 rc=0, identical
test_issue_927_jwt_verify_returns_object rc=1 rc=0 rc=0, identical

Those three were checked hardest because #10346 changes the store-plan cache and jwt_verify sits downstream of #10314's crypto bumps — an object fixture failing on this train is exactly where a real regression would hide.

The A/B asserts build provenance from the binaries themselves: each compiled arm must carry exactly one git:<sha> stamp and it must be that arm's pin (d0e99ea92b for main, 06c5249840 for the train). A PERRY_RUNTIME_DIR that does not resolve falls through to real archives silently, so without this an arm can measure a build that was never made and report a confident, wrong green.

The remaining four are previously attributed: test_gap_iterator_prototype_next_patch (failing on main since the commit that added it — see below), test_issue_7981_thread_shape_stamp_parent (perry-only; Node cannot resolve perry/thread), test_three_like_native_class_descriptors (Node exits 1), and test_issue58_object_string (prints Date.now() elapsed; the same binary varies run to run).

A note on main's long-standing gap failures

Three fixtures the snapshot expects to PASS have been failing on main for over a week and are not caused by this train or any recent one. They were bisected today, per fixture rather than per shard:

  • test_gap_iterator_prototype_next_patchnever passed. Added already failing by 8a73d9f80a, the commit that introduced prototype_next_is_canonical. Perry returns raw values where Node returns patched ones, so the fast path misreads a patched prototype. A correctness claim with no passing evidence behind it.
  • test_gap_disposablestack_2875 — last passed 87dc33492, first failed 43200e9d4 (34 commits).
  • test_gap_2899_2779_2777_static_helpers — last passed 43200e9d4, first failed 7651a36c3 (107 commits); grouped values come back null.

Before merging, the pushed head and unchanged main are checked again. After merging, the rewritten commits are checked for preserved authorship and the main tree must match the validated train exactly.

Summary by CodeRabbit

  • New Features

    • Ambient declare const, let, and var declarations no longer create runtime bindings; references resolve to global values as expected.
    • Special platform constants continue to behave as compile-time values.
  • Bug Fixes

    • Object-building code now preserves correct execution order while avoiding unnecessary performance slowdowns.
    • Property stores continue to invoke own accessors and enforce descriptor restrictions correctly.
  • Performance

    • Improved property-store performance for objects with descriptors.
    • Reduced garbage-collection overhead during shape and slot processing.

dependabot Bot and others added 9 commits September 16, 2026 18:09
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.87.3 to 2.87.8.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](taiki-e/install-action@0758d23...d438492)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.87.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
…ith 12 updates

Bumps the cargo-minor-and-patch group with 12 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [aho-corasick](https://github.com/BurntSushi/aho-corasick) | `1.1.4` | `1.1.5` |
| [toml](https://github.com/toml-rs/toml) | `1.1.4+spec-1.1.0` | `1.1.5+spec-1.1.0` |
| [tokio-rustls](https://github.com/rustls/tokio-rustls) | `0.26.4` | `0.26.5` |
| [rustls](https://github.com/rustls/rustls) | `0.23.43` | `0.23.44` |
| [hickory-proto](https://github.com/hickory-dns/hickory-dns) | `0.26.1` | `0.26.2` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.4.4` | `1.4.5` |
| [encoding_rs](https://github.com/hsivonen/encoding_rs) | `0.8.35` | `0.8.41` |
| [lru](https://github.com/jeromefroe/lru-rs) | `0.18.3` | `0.18.4` |
| [redis](https://github.com/redis-rs/redis-rs) | `1.6.0` | `1.7.0` |
| [mongodb](https://github.com/mongodb/mongo-rust-driver) | `3.8.2` | `3.9.0` |
| [der](https://github.com/RustCrypto/formats) | `0.8.1` | `0.8.2` |
| [indexmap](https://github.com/indexmap-rs/indexmap) | `2.14.1` | `2.14.2` |



Updates `aho-corasick` from 1.1.4 to 1.1.5
- [Commits](BurntSushi/aho-corasick@1.1.4...1.1.5)

Updates `toml` from 1.1.4+spec-1.1.0 to 1.1.5+spec-1.1.0
- [Commits](toml-rs/toml@toml-v1.1.4...toml-v1.1.5)

Updates `tokio-rustls` from 0.26.4 to 0.26.5
- [Release notes](https://github.com/rustls/tokio-rustls/releases)
- [Commits](rustls/tokio-rustls@v/0.26.4...v/0.26.5)

Updates `rustls` from 0.23.43 to 0.23.44
- [Release notes](https://github.com/rustls/rustls/releases)
- [Changelog](https://github.com/rustls/rustls/blob/main/CHANGELOG.md)
- [Commits](rustls/rustls@v/0.23.43...v/0.23.44)

Updates `hickory-proto` from 0.26.1 to 0.26.2
- [Release notes](https://github.com/hickory-dns/hickory-dns/releases)
- [Changelog](https://github.com/hickory-dns/hickory-dns/blob/main/CHANGELOG.md)
- [Commits](hickory-dns/hickory-dns@v0.26.1...v0.26.2)

Updates `cc` from 1.4.4 to 1.4.5
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](rust-lang/cc-rs@cc-v1.4.4...cc-v1.4.5)

Updates `encoding_rs` from 0.8.35 to 0.8.41
- [Commits](hsivonen/encoding_rs@v0.8.35...v0.8.41)

Updates `lru` from 0.18.3 to 0.18.4
- [Changelog](https://github.com/jeromefroe/lru-rs/blob/master/CHANGELOG.md)
- [Commits](jeromefroe/lru-rs@0.18.3...0.18.4)

Updates `redis` from 1.6.0 to 1.7.0
- [Release notes](https://github.com/redis-rs/redis-rs/releases)
- [Commits](redis-rs/redis-rs@redis-1.6.0...redis-1.7.0)

Updates `mongodb` from 3.8.2 to 3.9.0
- [Release notes](https://github.com/mongodb/mongo-rust-driver/releases)
- [Commits](mongodb/mongo-rust-driver@v3.8.2...v3.9.0)

Updates `der` from 0.8.1 to 0.8.2
- [Commits](RustCrypto/formats@der/v0.8.1...der/v0.8.2)

Updates `indexmap` from 2.14.1 to 2.14.2
- [Changelog](https://github.com/indexmap-rs/indexmap/blob/main/RELEASES.md)
- [Commits](indexmap-rs/indexmap@2.14.1...2.14.2)

---
updated-dependencies:
- dependency-name: aho-corasick
  dependency-version: 1.1.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-and-patch
- dependency-name: cc
  dependency-version: 1.4.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-and-patch
- dependency-name: der
  dependency-version: 0.8.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-and-patch
- dependency-name: encoding_rs
  dependency-version: 0.8.40
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-and-patch
- dependency-name: hickory-proto
  dependency-version: 0.26.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-and-patch
- dependency-name: indexmap
  dependency-version: 2.14.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-and-patch
- dependency-name: lru
  dependency-version: 0.18.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-and-patch
- dependency-name: mongodb
  dependency-version: 3.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: cargo-minor-and-patch
- dependency-name: redis
  dependency-version: 1.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: cargo-minor-and-patch
- dependency-name: rustls
  dependency-version: 0.23.44
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-and-patch
- dependency-name: tokio-rustls
  dependency-version: 0.26.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-and-patch
- dependency-name: toml
  dependency-version: 1.1.5+spec-1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
The store-plan cache refused any receiver carrying OBJ_FLAG_HAS_DESCRIPTORS.
Every zod schema object carries `_zod`, so none ever held a plan and each of
its stores re-ran the whole interception vet.

The flag was there for a real reason - a plan hit skips the own-accessor
short-circuit - but that is a per-KEY fact, and `desc_gate_ok` on the same
path already proves the key uncovered. Vetting per key is worth -7.3% on a
300-schema zod workload and -19.7% on the 2,000-receiver fixture.

The new native test warms the plan for a class across 300 receivers, then
proves an own accessor on a different key still dispatches its setter and a
non-writable data descriptor is still respected; it fails if a plan hit ever
skips that dispatch.
…0353)

`fold_builder_sequences` (#6812) only matched when the `o.k = v`
assignments followed the `const o = {}` binding immediately, so a single
ordinary declaration in between — the usual way initialisation code names
its constants — dropped the whole sequence. The unfolded `{}` lowers to a
0-field `__AnonShape_…`, which denies `Ptr<Shape>` containment (every key
really is undeclared on a shape that declares nothing) and sends every
store down `js_put_value_set`, re-interning and re-coercing the key per
execution: 108,444,840 instructions against 1,400,471 for the same program
with the constants written inline.

The scan now skips up to 64 statements between an EMPTY literal and its
first assignment, sinking the allocation below them. A statement qualifies
only when moving the allocation past it is unobservable, which is the pair
of conditions the value side already carries: it must not name the binding,
and it must not be able to execute user code (a call can reach a hoisted
`function peek() { return o; }` that names the binding without naming it in
the statement, turning a successful read into a TDZ ReferenceError).
Destructuring patterns, `enum`/`namespace` and populated literals are
excluded; `type`/`interface` are erased and are skipped. Skipped statements
keep their relative order and still run before every folded value.
…e iterator (#10362)

Base: 33690c5 (main, includes the #10348 fix from #10352).

gc_child_slots built a 152-byte HeapChildSlotIterator for every traced
object, 40 bytes of it a lifted ShapeDescriptor (#8122). Two by-value moves
of that iterator compiled to out-of-line 152-byte memcpy calls inside
visit_gc_layout_slot_descriptors:

  * the Array and Closure arms' `Option::map(..).unwrap_or_else(..)`
    temporary, copied into the caller's slot (62% of the calls);
  * `for .. in child_slots` in the Masked arm, which moved the iterator
    into the loop (35%).

On the #10362 retained-graph fixture that was 6,181,945 memcpy calls of
exactly 152 bytes (counted with an LD_PRELOAD shim). After this change: 6.

  * object/shapes.rs: ShapeRecordRef, a Copy handle to one live slab
    record (live bound, keys word, keys slot). shape_record_by_id and
    object_shape_record make the same slab probe as the descriptor lookups,
    without lifting a copy. note_old_generation_carrier and
    note_full_trace_carrier take the handle. ShapeDescriptor::record_ref()
    adapts the one caller that holds a descriptor. keys_slot() is now used
    only by tests, so it is cfg(test).
  * object/gc_slots.rs: gc_shape_keys_edge_slot and gc_field_slot_range
    take the handle.
  * gc/layout.rs: the iterator carries Option<ShapeRecordRef> (8 bytes; the
    iterator is now 120). The `_from` mask helpers take the handle.
    gc_child_slots' Array and Closure arms build the iterator directly in
    the return slot with let-else, as the ObjectFields arm already did.
  * gc/layout_slot_visit.rs: the Masked arm iterates `&mut child_slots`.

Still one shape-table probe per receiver (#8122). The handle is read at the
same points the lifted copy was: the carrier notes and the keys edge, before
any visit. It relies on the same record-address validity the carrier notes
already write through (#9706). The #8112 old_carrier/ephemeron gate and the
#9726 full-trace note are unchanged.

instructions:u, min of 5, same host, base (main 33690c5) vs this:
  gc3       12,572,092,967 -> 12,275,653,370  -2.36%
  w1000      1,090,107,336 ->  1,081,645,123  -0.78%
  w5000      2,049,860,743 ->  2,018,013,960  -1.55%
  w20000     5,094,775,666 ->  4,994,573,033  -1.97%
  oldyoung   1,525,484,973 ->  1,503,383,712  -1.45%
  alloc-only   320,266,130 ->    320,266,017   0.00%

All outputs match node byte for byte. PERRY_GC_FROMSPACE_SCAN_ABORT=1 is
clean on all six fixtures, and it aborts on the same gc3 built at
fcd108b. PERRY_GC_VERIFY_EVACUATION=1 on gc3 exits 0.
`declare const x: T` describes a binding the host supplies. TypeScript
erases it, but perry lowered it as a real binding initialized to
`undefined`. That shadowed the global it describes: reads never reached a
globalThis data property or accessor, `typeof` said "undefined", and a
`declare var` was reflected as a non-configurable global property, so a
later `Object.defineProperty(globalThis, "x", ...)` threw.

Every pass that models bindings now asks one predicate,
`lower::ambient::declarator_binds_nothing`: source-position lowering
(module, namespace, function body), `export declare const` (erased like
`export declare function`), the module forward pre-registration and
Script-var reflection, function/closure `var` hoisting and forward-capture
boxing, block TDZ names, the Annex B forbidden set, and the `new Function`
constant-argument scan (which recorded `declare var src` as a never-written
`var` and folded `new Function(src)` to `new Function("undefined")`).
Ambient names also suppress the unknown-identifier warning, like platform
globals.

`declare const __platform__` / `__plugins__` keep their binding: they are
the documented compile-time constants the backends fold from a
`Stmt::Let` with no initializer. #6871 gave every uninitialized lexical
binding an explicit `undefined`, which hid them from that fold, so
`__platform__` read `undefined` on every target. The ambient path restores
the `init: None` shape.

An `export declare const` inside a namespace is left as it was: TypeScript
reads it as `N.x`, not as the global.
@coderabbitai

coderabbitai Bot commented Sep 16, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 4f978384-96ac-4c3b-80ce-91af541baa64

📥 Commits

Reviewing files that changed from the base of the PR and between 99363be and 06c5249.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (30)
  • .github/workflows/coverage.yml
  • CLAUDE.md
  • Cargo.toml
  • changelog.d/10287-store-plan-per-key.md
  • changelog.d/10355-builder-fold-gap.md
  • changelog.d/10371-gc-slot-iterator-memcpy.md
  • changelog.d/10374-ambient-declare-binds-nothing.md
  • crates/perry-hir/src/lower/ambient.rs
  • crates/perry-hir/src/lower/builder_fold.rs
  • crates/perry-hir/src/lower/expr_function.rs
  • crates/perry-hir/src/lower/fn_ctor_env.rs
  • crates/perry-hir/src/lower/lower_module_fn.rs
  • crates/perry-hir/src/lower/lowering_context.rs
  • crates/perry-hir/src/lower/mod.rs
  • crates/perry-hir/src/lower/module_decl.rs
  • crates/perry-hir/src/lower/module_decl/namespace.rs
  • crates/perry-hir/src/lower/stmt.rs
  • crates/perry-hir/src/lower/tests.rs
  • crates/perry-hir/src/lower/tests/ambient_declare.rs
  • crates/perry-hir/src/lower_decl/block.rs
  • crates/perry-hir/src/lower_decl/block/var_names.rs
  • crates/perry-hir/src/lower_decl/body_stmt.rs
  • crates/perry-hir/tests/builder_fold_gap.rs
  • crates/perry-runtime/src/gc/layout.rs
  • crates/perry-runtime/src/gc/layout_slot_visit.rs
  • crates/perry-runtime/src/object/field_set_by_name/tail.rs
  • crates/perry-runtime/src/object/gc_slots.rs
  • crates/perry-runtime/src/object/shapes.rs
  • crates/perry/tests/builder_fold_gap_semantics.rs
  • crates/perry/tests/descriptor_store_fast_paths.rs

📝 Walkthrough

Walkthrough

The pull request changes TypeScript ambient declaration lowering, object builder folding, GC shape handling, and descriptor-aware store plans. It adds focused HIR, runtime, and integration tests, updates changelog entries, bumps the workspace version, and updates the coverage action pin.

Changes

Ambient declaration lowering

Layer / File(s) Summary
Ambient declaration contract and lowering
crates/perry-hir/src/lower/ambient.rs, crates/perry-hir/src/lower/stmt.rs, crates/perry-hir/src/lower/lower_module_fn.rs, crates/perry-hir/src/lower/lowering_context.rs
Ordinary ambient declarations no longer create runtime bindings. Their names are recorded for global lookup. __platform__ and __plugins__ retain compile-time constant shapes.
Ambient scope and export registration
crates/perry-hir/src/lower/expr_function.rs, crates/perry-hir/src/lower/fn_ctor_env.rs, crates/perry-hir/src/lower/module_decl.rs, crates/perry-hir/src/lower/module_decl/namespace.rs, crates/perry-hir/src/lower_decl/*
Hoisting, capture, namespace, export, block, and function-constructor scans skip ambient bindings.
Ambient declaration validation
crates/perry-hir/src/lower/tests/ambient_declare.rs, crates/perry-hir/src/lower/tests.rs
Tests cover global resolution, exports, namespaces, closures, Annex B behavior, TDZ handling, and compile-time constants.

Builder fold gap handling

Layer / File(s) Summary
Gap detection and safety checks
crates/perry-hir/src/lower/builder_fold.rs, changelog.d/10355-builder-fold-gap.md
The builder fold scans up to 64 statements and accepts only inert, hoistable gaps for empty object literals.
Gap-aware fold application
crates/perry-hir/src/lower/builder_fold.rs
The fold moves the allocation below skipped statements while preserving their order and value evaluation order.
Builder fold validation
crates/perry-hir/tests/builder_fold_gap.rs, crates/perry/tests/builder_fold_gap_semantics.rs
Tests cover foldable and blocked gaps, populated literals, TDZ behavior, evaluation order, and object semantics.

GC shape-record handling

Layer / File(s) Summary
Shape-record handle and lookup
crates/perry-runtime/src/object/shapes.rs, changelog.d/10371-gc-slot-iterator-memcpy.md
GC carrier paths can retain borrowed ShapeRecordRef handles instead of copied ShapeDescriptor values.
GC layout propagation
crates/perry-runtime/src/gc/layout.rs, crates/perry-runtime/src/object/gc_slots.rs
GC layout and slot-range helpers pass borrowed shape records through shared-mask, object-field, array, and closure paths.
GC iterator copy reduction
crates/perry-runtime/src/gc/layout_slot_visit.rs
The masked payload scan iterates by reference, avoiding an iterator copy per traced object.

Per-key store-plan eligibility

Layer / File(s) Summary
Per-key store-plan gate
crates/perry-runtime/src/object/field_set_by_name/tail.rs, changelog.d/10287-store-plan-per-key.md
Own descriptors no longer reject store plans for every key. The gate and plan-recording path check whether the specific key is covered.
Descriptor dispatch validation
crates/perry/tests/descriptor_store_fast_paths.rs
A warmed plan is tested against own accessors, non-writable descriptors, and key ordering.

Release metadata

Layer / File(s) Summary
Version and workflow updates
Cargo.toml, CLAUDE.md, .github/workflows/coverage.yml
The workspace version changes to 0.5.1582, and the coverage workflow pin changes to taiki-e/install-action v2.87.8.

Estimated code review effort: 4 (Complex) | ~60 minutes

Change: Other · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant Source
  participant HIRLowerer
  participant LoweringContext
  participant GlobalLookup
  Source->>HIRLowerer: declare const/let/var
  HIRLowerer->>LoweringContext: record erased ambient name
  HIRLowerer-->>Source: emit no runtime binding
  Source->>GlobalLookup: reference ambient name
  GlobalLookup-->>Source: resolve by global lookup
Loading
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch train204r

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant