Skip to content

Merge train: #9808, #9818, #9819, #9820, #9822, #9823, #9826 - #9866

Merged
proggeramlug merged 18 commits into
mainfrom
train129
Sep 6, 2026
Merged

Merge train: #9808, #9818, #9819, #9820, #9822, #9823, #9826#9866
proggeramlug merged 18 commits into
mainfrom
train129

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Merge train: #9808, #9818, #9819, #9820, #9822, #9823, #9826 — seven PRs, several of which conflicted against work that landed earlier today.

Conflict resolutions

Gate work

Validation

64/64 lint gates; perry-runtime, perry-codegen, perry-hir, perry-stdlib, perry-transform — all green, run serially (RUST_TEST_THREADS=1), 0 failures.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed computed property access on primitive strings, including prototype methods, inherited properties, symbols, and coerced keys.
    • Improved garbage-collection verification for retained array-growth references, preventing false failures while preserving stale-reference detection.
  • Performance

    • Reduced allocations when creating regular expressions with canonical flags.
    • Reduced unnecessary work and allocations during for-in enumeration.
  • Documentation

    • Expanded documentation for example testing requirements and compiler code-generation mechanisms.
    • Restored additional Fastify examples to cross-platform documentation-test coverage.

Ralph Küpper added 18 commits September 6, 2026 10:30
… advertises a proof

`transfer_element_shape` runs for every relocated array and already decides
`had_bit` from header words it has read anyway — then took the side table's
`RefCell` and hashed both addresses regardless, for two removes that remove
nothing whenever the source proved nothing and the destination advertises
nothing. It now returns before the table in that case.

The gate cannot be `!had_bit` alone: a destination still advertising a proof
describes storage the move has just replaced, so that case keeps the full
fail-closed path. Both halves are pinned by a new test, which fails on its
named assertion if the gate is widened.

Leaving a record behind at an address whose bit is clear is not a new state:
the bit is the sole authority for a read, `establish` draws identities from
`ELEMENT_SHAPE_PROOF_SEQ` rather than from the record at the address, and
`prune_dead_element_shape_owners` drops it on the next collection — the same
guarantees a fail-closed transfer already depended on.

Claude-Session: https://claude.ai/code/session_014UZWia6L37DpA93VLtNK9m
…has a key to filter

`js_for_in_keys_value` maintained a `HashSet<String>` of every own name at
every prototype level so that a name owned closer to the receiver hides the
same name further along the chain (ECMA-262 14.7.5, 12.6.4-2). It built that
set unconditionally: at every level it materialised a SECOND key array (all
own names, including non-enumerable ones) on top of the enumerable one, and
turned every name at every level into a heap `String` so it could be hashed
into the set.

The set can only ever filter a level >= 1, and a level that contributes no
enumerable keys of its own never consults it. So the set is now built on
demand, at the moment a level >= 1 actually has an enumerable key, from
exactly the levels already walked — which is the same content the eager
version held at that point, so the emitted key sequence is unchanged.

Measured with the new `PERRY_ENUM_DIAG`, one 400-character reply through the
compiled claude-code TUI, one binary and one environment variable apart:

    eager (today)                   deferred
    for-in calls        17,281      17,266
    key arrays        69,124        34,532      4.00 -> 2.00 per call
    String allocs    159,947             0
    seen.insert      159,947             0      (SipHash of the whole key)
    keys emitted      11,342        11,246
    emitted at proto level >=1         0             0
    shadow set built                   -             0 times

**Not one key in 17,281 `for-in` loops came from a prototype level**, so the
159,947 `String` allocations and 159,947 hash inserts filtered nothing at all.
Half the key arrays go with them: the all-own-names array is materialised only
once the set is live.

Those `String`s are 1.91 MB in total, which is why no allocation-byte ranking
found this — the cost is 160k mallocs, memcpys, hashes and frees, not the
bytes. Collection schedule is unchanged as predicted for a category this small
(41 vs 43 copying minors, 46 vs 48 budgeted full-cycle steps).

`VisitedLevels` keeps the walked levels inline (8 against a measured 2.00 per
call) so the rebuild's bookkeeping does not reintroduce one allocation per
`for-in` in place of the ones removed.

`PERRY_FORIN_LAZY_SHADOW=0` restores the eager path, so both live in one
binary and the A/B above is one environment variable.

Three tests, each verified to fail under sabotage: deleting the deferred build
fails two of them by name, and dropping the spill fails the third. The third
had to be rewritten to do so — its first version put the shadowing property on
every level, so the leaf still shadowed the name and deleting the spill changed
nothing.

Claude-Session: https://claude.ai/code/session_014UZWia6L37DpA93VLtNK9m
The first version of `only_a_spilled_level_shadows_the_root...` gave every
level the shadowing property, including the leaf. Deleting the spill arm left
it passing, because the leaf's own copy shadowed the root's on its own: the
assertion was true regardless of what the spill did. The doc comment now
carries that reasoning, and the general rule behind it, so the test cannot be
'simplified' back into one that cannot fail.

Claude-Session: https://claude.ai/code/session_014UZWia6L37DpA93VLtNK9m
`js_regexp_new` materialized the canonical flags twice on every construction —
a Rust `String` from `validate_and_canonicalize_flags`, and a fresh GC
`StringHeader` for `flags_ptr` — and a JS regex literal constructs a fresh
object every time it is evaluated. `PERRY_REGEX_DIAG` counts 161,897
constructions per 400-character claude-code reply: ~5.2 MB of identical one-
and two-byte GC strings, ~44 MB on a 3300-character reply, ~1.4 M allocations.

There are eight legal flags and each may appear once, so the canonical form is
at most eight ASCII bytes and now lives inline in `CanonicalFlags`. JS strings
are immutable and have no identity semantics, so when the caller's flags text
already IS the canonical text — a literal, whose flags the author wrote in spec
order — the header shares the caller's string instead of duplicating it.
Nothing downstream depends on the pointer being fresh: `flags_ptr`-keyed
lookups read it through `string_as_str` and compare content.

GC safety: the comparison and the root are taken BEFORE the validation block,
because `raw_flags_str` borrows the caller's GC string and that block can
allocate — the same hazard the ★ note on `pattern_root` describes, and the same
one #7341 fixed for the freshly-allocated flags string. The existing re-read
from `flags_root` after `gc_malloc` covers both arms unchanged.

Below the campaign's ~10 % line at ~2-3 % of arena traffic per turn, so the cc
rig is expected to read flat; the counter is the proof, not the benchmark.

Claude-Session: https://claude.ai/code/session_014UZWia6L37DpA93VLtNK9m
Rename only — the fragment was written before the PR number was known.

Claude-Session: https://claude.ai/code/session_014UZWia6L37DpA93VLtNK9m
@proggeramlug
proggeramlug merged commit 74dc68f into main Sep 6, 2026
16 of 22 checks passed
@proggeramlug
proggeramlug deleted the train129 branch September 6, 2026 09:07
@coderabbitai

coderabbitai Bot commented Sep 6, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 6f0e6b41-37db-4635-976a-33d74dc4d8bb

📥 Commits

Reviewing files that changed from the base of the PR and between 07e3774 and 1ec615a.

📒 Files selected for processing (47)
  • .github/workflows/test.yml
  • changelog.d/9808-element-shape-transfer-gate.md
  • changelog.d/9818-primitive-string-property-reads.md
  • changelog.d/9819-regex-flags-no-alloc.md
  • changelog.d/9820-fastify-doc-test-coverage.md
  • changelog.d/9822-retained-growth-verifier.md
  • changelog.d/9823-for-in-deferred-shadow-set.md
  • crates/perry-doc-tests/src/main.rs
  • crates/perry-doc-tests/src/tests.rs
  • crates/perry-doc-tests/tests/compiler_environment.rs
  • crates/perry-runtime/src/array/element_shape.rs
  • crates/perry-runtime/src/array/element_shape_tests.rs
  • crates/perry-runtime/src/gc/copying.rs
  • crates/perry-runtime/src/gc/cycle.rs
  • crates/perry-runtime/src/gc/diag_sites.rs
  • crates/perry-runtime/src/gc/roots.rs
  • crates/perry-runtime/src/gc/tests/forwarding_verification.rs
  • crates/perry-runtime/src/gc/tests/mod.rs
  • crates/perry-runtime/src/gc/tests/runtime_roots/callback_scanners.rs
  • crates/perry-runtime/src/gc/tests/runtime_roots/side_table_scanners.rs
  • crates/perry-runtime/src/gc/verify.rs
  • crates/perry-runtime/src/hot_diag.rs
  • crates/perry-runtime/src/object/field_get_set.rs
  • crates/perry-runtime/src/object/field_get_set/entries_shape.rs
  • crates/perry-runtime/src/object/field_get_set/enumeration.rs
  • crates/perry-runtime/src/object/field_get_set/enumeration_tests.rs
  • crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs
  • crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs
  • crates/perry-runtime/src/object/polymorphic_index.rs
  • crates/perry-runtime/src/regex.rs
  • crates/perry-runtime/src/regex/flags.rs
  • crates/perry-runtime/src/string/char_ops.rs
  • crates/perry-runtime/src/string/char_ops/computed_property_tests.rs
  • crates/perry-runtime/src/string/concat.rs
  • crates/perry-runtime/src/string/concat_site.rs
  • crates/perry-runtime/src/value/dyn_index.rs
  • docs/examples/README.md
  • docs/examples/getting-started/npm_packages.ts
  • docs/examples/stdlib/http/fastify_json.ts
  • docs/examples/stdlib/overview/snippets.ts
  • docs/src/SUMMARY.md
  • docs/src/internals/codegen-mechanisms.md
  • scripts/addr_class_ratchet_baseline.txt
  • scripts/codegen_mechanisms.json
  • scripts/gc_runtime_root_holders.json
  • scripts/string_payload_access_baseline.txt
  • test-files/test_gap_9815_primitive_computed_properties.ts

📝 Walkthrough

Walkthrough

The pull request updates documentation-test requirements, GC evacuation verification, primitive string property access, enumeration behavior, regex flag allocation, diagnostics, and codegen evidence records. It adds targeted runtime, harness, and regression tests.

Changes

Documentation test requirements

Layer / File(s) Summary
Auto-optimize requirement handling
crates/perry-doc-tests/src/*, crates/perry-doc-tests/tests/*
The harness parses requires: auto-optimize, configures compiler environments, rejects unknown requirements, and tests compile success and failure reporting.
Fastify CI coverage and documentation
.github/workflows/test.yml, docs/examples/*, docs/examples/README.md, changelog.d/9820-fastify-doc-test-coverage.md
Three Fastify examples now run in macOS and Windows doc-test jobs and document their compiler and execution requirements.

Garbage-collection verification

Layer / File(s) Summary
Element-shape transfer gate
crates/perry-runtime/src/array/*, changelog.d/9808-element-shape-transfer-gate.md
Element-shape transfer skips side-table work only when neither address advertises a proof. Tests cover stale records and destination cleanup.
Mode-aware evacuation verifier
crates/perry-runtime/src/gc/verify.rs
Forwarding verification now supports explicit all_forwarded and copying_minor policies with retained growth-array aliases.
Evacuation verifier wiring and tests
crates/perry-runtime/src/gc/{copying.rs,cycle.rs,roots.rs}, crates/perry-runtime/src/gc/tests/*, scripts/gc_runtime_root_holders.json
GC phases and root scanners use the new verifier. Tests accept retained aliases and reject stale from-space hops.

Primitive string property reads

Layer / File(s) Summary
Boxed string property lookup
crates/perry-runtime/src/string/char_ops.rs, crates/perry-runtime/src/value/dyn_index.rs, crates/perry-runtime/src/object/field_get_set/*, crates/perry-runtime/src/object/polymorphic_index.rs
Computed string reads now resolve own indices, length, and String-prototype properties through a common boxed path.
String lookup regression coverage
crates/perry-runtime/src/string/char_ops/computed_property_tests.rs, test-files/test_gap_9815_primitive_computed_properties.ts, changelog.d/9818-primitive-string-property-reads.md
Tests cover methods, keys, symbols, coercion, prototype changes, accessors, boxed strings, and receiver behavior.

Enumeration and diagnostics

Layer / File(s) Summary
Lazy for-in shadowing and Object.entries
crates/perry-runtime/src/object/field_get_set/*
for-in defers shadow-set construction. Object.entries handling moves into a dedicated module and snapshots keys before invoking getters.
Enumeration tests and diagnostics
crates/perry-runtime/src/object/field_get_set/enumeration_tests.rs, crates/perry-runtime/src/hot_diag.rs, crates/perry-runtime/src/string/*, changelog.d/9823-for-in-deferred-shadow-set.md
Tests compare lazy and eager traversal. Diagnostics count enumeration and concatenation activity. Baselines and GC holder metadata are updated.

Regular-expression flag allocation

Layer / File(s) Summary
Canonical flags representation and reuse
crates/perry-runtime/src/regex/*, crates/perry-runtime/src/hot_diag.rs, changelog.d/9819-regex-flags-no-alloc.md
Canonical flags use fixed inline storage. RegExp construction reuses canonical caller strings and counts fresh allocations.

Codegen mechanism evidence

Layer / File(s) Summary
Concat-site cache evidence index
scripts/codegen_mechanisms.json
The index records concat-site cache criteria, source locations, workload observations, kill-switch behavior, and regression tests.
Concat-site cache documentation
docs/src/SUMMARY.md, docs/src/internals/codegen-mechanisms.md
The documentation describes admission rules, generated artifacts, workload evidence, inspection commands, and evidence levels.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DocExample
  participant DocTests
  participant Compiler
  DocExample->>DocTests: provide requires: auto-optimize
  DocTests->>Compiler: compile with auto-optimize enabled
  Compiler-->>DocTests: pass or compile_fail
  DocTests-->>DocExample: include result in CI report
Loading
sequenceDiagram
  participant GCPhase
  participant EvacuationVerifier
  participant RootScanner
  GCPhase->>EvacuationVerifier: select verification mode
  RootScanner->>EvacuationVerifier: inspect forwarded value
  EvacuationVerifier-->>RootScanner: accept retained alias or reject stale hop
Loading
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch train129

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