Skip to content

perf(repsel): open the array-element escape for Ptr<Shape> (#7034 §3) - #7149

Merged
proggeramlug merged 1 commit into
mainfrom
perf/7148-ptr-shape-element-escape
Aug 1, 2026
Merged

perf(repsel): open the array-element escape for Ptr<Shape> (#7034 §3)#7149
proggeramlug merged 1 commit into
mainfrom
perf/7148-ptr-shape-element-escape

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Ptr<Shape> rule 2 disqualifies a local at any escape. #7034 §4 opened
return; this opens array/object element for the array half —
rows.push(row) and for (const r of rows) r.field.

return was easy because a return is a terminator: no use of the local can
follow it, so every access the pass licensed had already run while the object
was unaliased. An element escape is not. The object stays reachable through the
array for the rest of its life, so the containment region had to widen from
one local to one local array and everything derived from it, and the
array's own uses had to be bounded exactly as an object local's are.

The rule (collectors/ptr_shape_elements.rs)

A region-local A is an element-shape-proven array of class C when all
of:

  • E1 provenance — exactly one Let { mutable: false, init: Array([]) },
    not boxed, not a module global. The literal must be empty: a non-empty
    one can carry elisions, whose slots read back as undefined.
  • E2 element provenance — every write is ArrayPush of new C(...),
    inline or via a local bound by one Let { init: New { C } } and pushed
    exactly once. No other mutator at all — no pop/shift/splice/
    unshift/copyWithin, no IndexSet, no length write. That is what makes
    A dense and monomorphic for its whole lifetime.
  • E3 array containment — every other use is an in-bounds element read, a
    .length read, or return A (repsel row 1: scoping Ptr<Shape> beyond locals — measured, promotion is 0 on the motivating workload; build --opt-report (#6952) first #7034 §4's terminator exemption, unchanged).
    Call argument, closure capture, reassignment, container element, unrecognised
    array method: all still disqualify.
  • E4 admissibilityC passes the same chain_admissible gate rule 1
    applies to a new C(...) local, and the rule-5 module barrier is clear.
  • E5 in-bounds readsA[i] is licensed only where i is the induction
    variable of an enclosing for (let i = 0; i < A.length; i++), written
    nowhere else in the region. This conjunct is the whole difference between
    this pass and a wrong one.
    Without it A[i] can be undefined, and a
    guard-free fixed-offset load masks a NaN-boxed undefined into a wild
    pointer. E3 admits no mutator that can shrink A and E2 makes it dense, so
    0 <= i < A.length at the read means A[i] is an own element of class C.

for (const r of A) desugars to exactly the E5 shape
(lower/stmt_loops.rs::lazy_or_index_elem), so the iterator form is covered by
the indexed proof rather than by a second one.

Then two halves in ptr_shape.rs: A.push(row) stops disqualifying row, and
const r = A[i] at a licensed site is rule-1 provenance of new-strength.

Group integrity. Every member of an element group — the pushed producers
plus the element-read locals — references objects the other members also
reach. One member failing rule 2 (r.extra = 1, a closure capture, an opaque
call) can transition the shape the others read guard-free, so the group is
all-or-nothing: collect_shape_proven_ptr_locals drops every member when any
one fails. No fixpoint is needed — dropping never admits a member.

numeric_fields is not claimed for group members. The numeric proof is an
exhaustive-reachable-store proof that containment makes possible because no
alias exists
; a group has aliases by construction, and a sibling's
r.score = "s" (a declared field, so rule 2 permits it) downgrades the slot's
raw-f64 layout. Same stand-down as proven_this.rs and ptr_shape_returns.rs.
The shape proof alone still retires the whole guard diamond.

What it buys, measured

On a build-then-consume kernel (40 000 records, produced with a local, then
read back both ways) the promoted function goes 0 → 3 selected, 3 consumed,
and its emitted IR loses:

symbol base after
js_typed_feedback_class_field_get_guard 7 0
js_typed_feedback_record_fallback_call 7 0
js_object_get_field_by_name_f64 7 0
all js_* calls 72 51
IR lines / blocks 1426 / 127 1076 / 113

Every other js_* call count is identical, including all of
js_shadow_slot_bind (5), js_write_barrier_root_nanbox (5),
js_write_barrier_slot (1), js_array_push_f64 (2), js_gc_loop_safepoint
(4) and the inline incremental-mark barrier sites (5). Nothing but guard
machinery went away.

GC contract, verified in emitted IR

  • The element locals get js_shadow_slot_bind in the entry block (slots 3 and
    4 of the probe). TaPtr's callee-side no-bind shortcut is not copied —
    GC_TYPE_OBJECT is movable (fix(gc): typed-array constructor sources are precise roots (#6981) #6990, gc: default-on evacuating young-gen scavenge (moving minor at precise safepoints) #7019).
  • Every access re-derives the raw pointer from that alloca:
    load double, ptr %rNand POINTER_MASKinttoptrgep +header
    gep indexload. The for…of local is reloaded 3× (3 field reads) and
    the indexed local 4× (4 access sites); nothing is cached across a safepoint.
  • The store of the element into the bound slot is followed by the incremental
    mark-barrier check and js_write_barrier_root_nanbox.
  • Write barriers on element stores are untouched: this pass changes no store
    lowering, and js_array_push_f64 counts are identical between arms.
  • The read side uses the ptr_shape_get_number.plain / .coerce pair — the
    2-instruction plain-finite check with a cold arm — because the group claims
    no numeric fields.

What this does NOT reach, and the measurements that say so

batch.ts is unchanged: 2 selected / 1 consumed, identical to main. Both
of its element denials fail for reasons outside this rule:

Dependency JS gets essentially nothing. #7139 reported that ~103 candidates
its CJS barrier exemption freed were "immediately re-denied by rule 2", and
this position was picked on the assumption that those were element escapes.
They are not. Over 180 real __esModule CJS modules from
real-apps/scriptc/node_modules, compiled by a #7139-only arm and a
#7139+this-change arm (both 180/180), the 746 Ptr<Shape> candidates deny as:

bucket count
rule 1 — allocation never bound to a local 506
rule 5 — module barrier still armed 99
rule 2 — bare reference 130
rule 2 — call argument 5
rule 2 — array element 1
rule 2 — closure capture / undeclared property 1 / 1

Both arms are identical on every line. The rule-2 bare references are all
Perry's own __cjs_module wrapper local, and the 506 rule-1 denials break down
as constructor argument 182, statement 162, call argument 84, return 64, array
element 8, initializer 6. The wall in dependency JS is rule 1 (allocations
never bound to a local), not containment.

Validation

  • cargo test -p perry-codegen --lib: 429 passed (18 new).
  • Sabotage matrix, 15 conjuncts, each with a disjoint red set — every guard
    deleted in turn, the suite re-run, and the failing tests recorded: push
    exemption, in-bounds read, both GC rooting obligations, group integrity,
    single-push, empty-literal seed, const array binding, shrinking mutators,
    indexed store, class agreement, index write count, bare array reference,
    closure capture, .length receiver. Control green in all 16 runs. Two
    weaknesses it caught and fixed: a_local_pushed_into_two_arrays_is_not_exempt
    passed on HashMap iteration order (now asserts the facts directly), and the
    closure-capture test denied through the body walk rather than the capture
    list.
  • Census: new liveness fixture fixture_ptr_shape_elements with floors
    ptr-shape 3 / ptr-shape-consumed 3; no existing floor moved; the
    PERRY_PTR_SHAPE_LOCALS=0 sabotage step in repsel-census now asserts the
    element fixture goes to zero too — without that, the whole analysis could
    stop issuing facts and every counter in the job would be unchanged.
  • New test_gap_repsel_ptr_shape_elements.ts, registered in
    test-parity/gc_repsel_corpus.txt, byte-exact against Node 26.5.1: both read
    forms, 200 allocations between two field reads of the same element local,
    NaN/±Infinity/-0 written through one group member and read through another,
    and four arrays that must not be proven.

Follow-ups filed from this work

  • Deforestation hides the producer shape from this analysis (buildRows).
  • Direct A[i].field reads are admitted by E3 but not promoted (consumption
    keys on Expr::LocalGet).
  • Element reads bound to a let, non-empty array literals, and
    sort/reverse/slice on a proven array are all conservative refusals.
  • Dependency JS's real wall is rule 1: 506 of 746 candidates are allocations
    never bound to a local.

Summary by CodeRabbit

  • New Features

    • Improved handling of objects stored in arrays, including indexed access and iteration.
    • Supports safe optimization for consistent object types and supported usage patterns.
    • Preserves correct behavior during garbage-collection movement and object mutations.
    • Added configurable, isolated Zig compiler caches for benchmark builds.
  • Bug Fixes

    • Prevents unsafe optimizations for escaping arrays, mixed types, unsupported mutations, and out-of-bounds access.
  • Documentation

    • Documented supported array-element behavior and limitations.
  • Tests

    • Added regression, parity, and validation coverage for array access, iteration, GC movement, and edge cases.

@proggeramlug
proggeramlug force-pushed the perf/7148-ptr-shape-element-escape branch from 9d4647b to ecd4b6c Compare July 31, 2026 21:48
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a088d3f5-fd07-4d4a-b932-90a166b5721d

📥 Commits

Reviewing files that changed from the base of the PR and between 5a7e2d3 and 6a3b5bf.

📒 Files selected for processing (20)
  • .github/workflows/test.yml
  • benchmarks/honest_bench/workloads/1_json_pipeline/zig/build.sh
  • benchmarks/honest_bench/workloads/3_image_convolution/zig/build.sh
  • benchmarks/repsel_census/baseline.json
  • benchmarks/repsel_census/fixtures/fixture_ptr_shape_elements.ts
  • changelog.d/7149-ptr-shape-array-element-escape.md
  • changelog.d/7160-zig-cache-isolation.md
  • crates/perry-codegen/src/collectors/cjs_scaffolding.rs
  • crates/perry-codegen/src/collectors/hir_facts.rs
  • crates/perry-codegen/src/collectors/mod.rs
  • crates/perry-codegen/src/collectors/ptr_shape.rs
  • crates/perry-codegen/src/collectors/ptr_shape_elements.rs
  • crates/perry-codegen/src/collectors/ptr_shape_elements_tests.rs
  • crates/perry-codegen/src/collectors/ptr_shape_opt_report_tests.rs
  • crates/perry-codegen/src/collectors/ptr_shape_report.rs
  • crates/perry-codegen/src/collectors/ptr_shape_returns.rs
  • crates/perry-codegen/src/collectors/ptr_shape_returns_tests.rs
  • scripts/compiler_output_harness/repsel_census.py
  • test-files/test_gap_repsel_ptr_shape_elements.ts
  • test-parity/gc_repsel_corpus.txt
🚧 Files skipped from review as they are similar to previous changes (14)
  • scripts/compiler_output_harness/repsel_census.py
  • .github/workflows/test.yml
  • benchmarks/repsel_census/baseline.json
  • crates/perry-codegen/src/collectors/ptr_shape_opt_report_tests.rs
  • test-parity/gc_repsel_corpus.txt
  • benchmarks/repsel_census/fixtures/fixture_ptr_shape_elements.ts
  • crates/perry-codegen/src/collectors/mod.rs
  • crates/perry-codegen/src/collectors/ptr_shape_returns_tests.rs
  • crates/perry-codegen/src/collectors/ptr_shape_returns.rs
  • test-files/test_gap_repsel_ptr_shape_elements.ts
  • crates/perry-codegen/src/collectors/hir_facts.rs
  • crates/perry-codegen/src/collectors/ptr_shape_report.rs
  • crates/perry-codegen/src/collectors/ptr_shape.rs
  • crates/perry-codegen/src/collectors/ptr_shape_elements.rs

📝 Walkthrough

Walkthrough

Adds Phase 3b analysis for pointer-shaped objects stored in proven arrays. Integrates element provenance with pointer-shape promotion, return validation, reporting, census checks, regression coverage, and Zig benchmark cache configuration.

Changes

Ptr Shape Array-Element Analysis

Layer / File(s) Summary
Element-shape fact collection
crates/perry-codegen/src/collectors/ptr_shape_elements.rs, crates/perry-codegen/src/collectors/mod.rs
Collects proven arrays, aliases, homogeneous constructors, contained pushes, bounded reads, element groups, and GC-rooting facts.
Promotion and escape integration
crates/perry-codegen/src/collectors/hir_facts.rs, crates/perry-codegen/src/collectors/ptr_shape.rs, crates/perry-codegen/src/collectors/ptr_shape_returns.rs, crates/perry-codegen/src/collectors/ptr_shape_report.rs, crates/perry-codegen/src/collectors/*_tests.rs
Uses element facts for pointer-shape candidates, contained pushes, group integrity, return validation, numeric-field exclusions, and escape reporting.
Element analysis validation
crates/perry-codegen/src/collectors/ptr_shape_elements_tests.rs
Tests producer containment, bounded reads, invalid mutations and escapes, group invalidation, GC rooting, module barriers, class admissibility, and feature gating.
Runtime representation-selection coverage
test-files/test_gap_repsel_ptr_shape_elements.ts, test-parity/gc_repsel_corpus.txt
Adds indexed and iterator reads, GC churn, non-finite values, escaping arrays, mixed classes, mutation, and out-of-bounds scenarios.
Liveness and change documentation
benchmarks/repsel_census/fixtures/fixture_ptr_shape_elements.ts, scripts/compiler_output_harness/repsel_census.py, benchmarks/repsel_census/baseline.json, .github/workflows/test.yml, changelog.d/7149-ptr-shape-array-element-escape.md
Adds the census workload, promotion floors, sabotage assertion, baseline entry, and documented analysis and validation results.

Zig Cache Isolation

Layer / File(s) Summary
Benchmark cache configuration
benchmarks/honest_bench/workloads/*/zig/build.sh, changelog.d/7160-zig-cache-isolation.md
Configures separate Zig global and local caches under a temporary-directory default with PERRY_ZIG_CACHE_DIR override support.

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

Possibly related issues

  • PerryTS/perry issue 7034 — Extends Ptr<Shape> representation selection from local values to escaping array elements.
  • PerryTS/perry issue 7150 — Documents deforestation cases that can hide the array provenance used by this implementation.
  • PerryTS/perry issue 7151 — Covers the indexed and for...of element-read forms implemented by this change.

Possibly related PRs

  • PerryTS/perry#6911 — Adds the preceding Ptr<Shape> analysis extended here with array-element provenance.
  • PerryTS/perry#7107 — Extends related provenance and return-shape handling for function-return escape sources.
  • PerryTS/perry#7160 — Contains the same Zig benchmark cache-isolation changes.

Suggested labels: performance

Sequence Diagram(s)

sequenceDiagram
  participant HIRFacts
  participant ElementShapeFacts
  participant PtrShape
  participant ReturnShape
  participant RepselCensus
  HIRFacts->>ElementShapeFacts: collect proven array-element facts
  HIRFacts->>PtrShape: pass ElementShapeFacts
  PtrShape->>PtrShape: promote indexed-read locals and validate contained pushes
  PtrShape->>ReturnShape: validate promoted return locals
  RepselCensus->>PtrShape: measure selected and consumed promotions
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes opening Ptr promotion for array-element escapes.
Description check ✅ Passed The description provides detailed summary, changes, related issue references, validation results, and follow-ups for the PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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 perf/7148-ptr-shape-element-escape

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (3)
crates/perry-codegen/src/collectors/ptr_shape_elements_tests.rs (2)

190-200: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider passing the test's own class list into facts_for.

facts_for always builds the module from a pristine class_c(). Two tests then analyze a different class set: mixed_element_classes_deny_the_array adds D only to the classes map, and an_inadmissible_element_class_denies_the_array adds the getter only to the classes map. The assertions still hold, because both denials come from the classes map. The module dispatch facts, however, are computed over a class set that does not exist in the fixture. If a future barrier or admissibility rule reads the module instead of the map, these tests would pass for the wrong reason.

A small change keeps the two views consistent: let promote and elements forward their classes values into the HIR module.

🤖 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 `@crates/perry-codegen/src/collectors/ptr_shape_elements_tests.rs` around lines
190 - 200, Update the test helpers facts_for, promote, and elements so facts_for
accepts the test’s class list and constructs its HIR from that list, while
promote and elements forward their classes values when computing
ModuleDispatchFacts. Keep the existing baseline class_c() and assertions intact
while ensuring the classes map and module use the same fixture classes.

683-697: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for the array alias path.

The collector resolves array aliases through collect_alias_edges and closes over them in a fixpoint loop. The module comment in ptr_shape_elements.rs states that this exists because the for…of desugar binds const __arr_N = <the iterable> before the loop. No test in this file reads or pushes through an alias, so the alias fixpoint and the alias poisoning conditions (boxed_vars, module_globals, let_counts != 1) have no red set. The for…of read form named at line 470 is therefore only covered in its non-alias shape.

Add two cases: one positive, where const b = a then a bounded read of b[i] promotes; one negative, where the alias is re-declared and the read is denied.

🤖 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 `@crates/perry-codegen/src/collectors/ptr_shape_elements_tests.rs` around lines
683 - 697, Extend the tests around promote and the existing alias helpers with
two array-alias cases: verify that a single declaration assigning alias b from
array a, followed by a bounded read of b[i], promotes the relevant
producer/reader; then verify that re-declaring b causes the aliased read to
remain unpromoted. Cover the alias-resolution fixpoint and poisoning condition
through the existing boxed_vars, module_globals, or let_counts behavior without
changing collector implementation.
crates/perry-codegen/src/collectors/ptr_shape_elements.rs (1)

890-953: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse the existing walk_stmts helper instead of a second traversal.

The inner walk function repeats the traversal that the free walk_stmts at lines 839-882 already implements. Two traversals must stay identical, because one issues the facts and the other issues the seeds. If one gains a statement arm and the other does not, ptr_shape.rs seeds a candidate set that disagrees with element_reads.

♻️ Proposed refactor
 pub(super) fn element_read_seeds(
     stmts: &[Stmt],
     element_facts: &ElementShapeFacts,
 ) -> Vec<(u32, String)> {
     let mut out = Vec::new();
     if element_facts.is_empty() {
         return out;
     }
-    fn walk(stmts: &[Stmt], facts: &ElementShapeFacts, out: &mut Vec<(u32, String)>) {
-        // … 50 lines of duplicated statement traversal …
-    }
-    walk(stmts, element_facts, &mut out);
+    walk_stmts(stmts, &mut |s| {
+        if let Stmt::Let {
+            id,
+            init: Some(Expr::IndexGet { .. }),
+            ..
+        } = s
+        {
+            if let Some(class_name) = element_facts.element_read_class(*id) {
+                out.push((*id, class_name.to_string()));
+            }
+        }
+    });
     out
 }
🤖 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 `@crates/perry-codegen/src/collectors/ptr_shape_elements.rs` around lines 890 -
953, Update element_read_seeds to reuse the existing walk_stmts traversal helper
instead of defining its local walk function. Preserve the existing IndexGet
filtering and seed collection by supplying a callback or equivalent visitor
logic to walk_stmts, ensuring traversal coverage remains identical to
element_reads.
🤖 Prompt for all review comments with 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.

Inline comments:
In @.github/workflows/test.yml:
- Around line 1275-1286: Update the repository branch-protection configuration
to mark the repsel-census status check as required. Ensure the existing
array-element liveness assertion in the repsel-census workflow cannot be
bypassed when it fails.

In `@changelog.d/7149-ptr-shape-array-element-escape.md`:
- Line 116: Fix the Markdown syntax in the changelog sentence beginning with
“#7139+this-change” so it is treated as paragraph text rather than a heading;
reflow the sentence or escape the hash while preserving its meaning.

In `@crates/perry-codegen/src/collectors/ptr_shape_elements.rs`:
- Around line 727-742: Update the Expr::ArrayPush handling to disqualify the
pushed local when an Expr::LocalGet value resolves to a tracked root, while
preserving PushValue::Local for producer locals. Ensure this disqualification
occurs before the existing early return, since LocalGet values are intentionally
not passed to walk_expr.
- Around line 692-725: Track property names written through element accesses in
ArrayWalk for each array root instead of discarding property in PropertySet and
ignoring it in PropertyUpdate. After class_name is resolved in
collect_element_shape_facts, reject the root when any recorded property is not a
declared chain field for that class; preserve existing root disqualification for
direct array writes and admissibility for valid element accesses.

In `@crates/perry-codegen/src/collectors/ptr_shape.rs`:
- Around line 540-562: Update the group-integrity removal logic around
element_facts.group_members so that when any member is dropped, it also removes
every tracked alias whose root resolves to that dropped member, not just the
reported member IDs. Apply report::deny_local to each removed tracked ID,
preserving the existing all-or-nothing behavior for the group.

---

Nitpick comments:
In `@crates/perry-codegen/src/collectors/ptr_shape_elements_tests.rs`:
- Around line 190-200: Update the test helpers facts_for, promote, and elements
so facts_for accepts the test’s class list and constructs its HIR from that
list, while promote and elements forward their classes values when computing
ModuleDispatchFacts. Keep the existing baseline class_c() and assertions intact
while ensuring the classes map and module use the same fixture classes.
- Around line 683-697: Extend the tests around promote and the existing alias
helpers with two array-alias cases: verify that a single declaration assigning
alias b from array a, followed by a bounded read of b[i], promotes the relevant
producer/reader; then verify that re-declaring b causes the aliased read to
remain unpromoted. Cover the alias-resolution fixpoint and poisoning condition
through the existing boxed_vars, module_globals, or let_counts behavior without
changing collector implementation.

In `@crates/perry-codegen/src/collectors/ptr_shape_elements.rs`:
- Around line 890-953: Update element_read_seeds to reuse the existing
walk_stmts traversal helper instead of defining its local walk function.
Preserve the existing IndexGet filtering and seed collection by supplying a
callback or equivalent visitor logic to walk_stmts, ensuring traversal coverage
remains identical to element_reads.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c984e34e-d07a-491f-a45b-c3eacdf21238

📥 Commits

Reviewing files that changed from the base of the PR and between a3b31c0 and 9cbfe9f.

📒 Files selected for processing (16)
  • .github/workflows/test.yml
  • benchmarks/repsel_census/baseline.json
  • benchmarks/repsel_census/fixtures/fixture_ptr_shape_elements.ts
  • changelog.d/7149-ptr-shape-array-element-escape.md
  • crates/perry-codegen/src/collectors/hir_facts.rs
  • crates/perry-codegen/src/collectors/mod.rs
  • crates/perry-codegen/src/collectors/ptr_shape.rs
  • crates/perry-codegen/src/collectors/ptr_shape_elements.rs
  • crates/perry-codegen/src/collectors/ptr_shape_elements_tests.rs
  • crates/perry-codegen/src/collectors/ptr_shape_opt_report_tests.rs
  • crates/perry-codegen/src/collectors/ptr_shape_report.rs
  • crates/perry-codegen/src/collectors/ptr_shape_returns.rs
  • crates/perry-codegen/src/collectors/ptr_shape_returns_tests.rs
  • scripts/compiler_output_harness/repsel_census.py
  • test-files/test_gap_repsel_ptr_shape_elements.ts
  • test-parity/gc_repsel_corpus.txt

Comment thread .github/workflows/test.yml
Comment thread changelog.d/7149-ptr-shape-array-element-escape.md Outdated
Comment thread crates/perry-codegen/src/collectors/ptr_shape_elements.rs
Comment thread crates/perry-codegen/src/collectors/ptr_shape_elements.rs
Comment thread crates/perry-codegen/src/collectors/ptr_shape.rs
@proggeramlug
proggeramlug force-pushed the perf/7148-ptr-shape-element-escape branch 2 times, most recently from 397e09b to c1e4ce7 Compare July 31, 2026 22:07

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
crates/perry-codegen/src/collectors/ptr_shape_elements_tests.rs (2)

891-910: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename the getter so it does not shadow a declared field.

class_c() declares x as a field at line 39. This test then adds a getter also named x. The resulting class shape is self-contradictory and does not match any real Perry class. The guard under test is chain_admissible, which rejects the class for having any getter, so the test still isolates the intended guard. A distinct getter name removes the ambiguity.

♻️ Optional: use a getter name that is not a declared field
     let mut c = class_c();
     c.getters = vec![(
-        "x".to_string(),
+        "computed".to_string(),
         perry_hir::Function {
             id: 50,
-            name: "x".to_string(),
+            name: "computed".to_string(),
🤖 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 `@crates/perry-codegen/src/collectors/ptr_shape_elements_tests.rs` around lines
891 - 910, Rename the getter entry in the test setup from "x" to a distinct
property name not declared by class_c(), while preserving the getter’s
implementation and the chain_admissible assertion that rejects any getter.

190-223: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider passing the test's own classes into facts_for.

facts_for always pushes a pristine class_c() into the HIR module. promote and elements then call facts_for(Vec::new(), Vec::new()). So the module used for collect_module_dispatch_facts can differ from the classes map used by the collector.

Two tests rely on this: mixed_element_classes_deny_the_array registers D only in the classes map, and an_inadmissible_element_class_denies_the_array adds a getter to C only in the classes map. Both tests assert denial today, so the divergence does not create a false pass now. If a future change makes ModuleDispatchFacts depend on getters or on the full class set, the fixtures will stop modelling the intended input.

A small change keeps the two views in sync.

♻️ Optional: derive dispatch facts from the same class set
-fn elements(stmts: &[Stmt], classes: &HashMap<String, &Class>) -> ElementShapeFacts {
-    elements_with(stmts, classes, &facts_for(Vec::new(), Vec::new()))
+fn facts_of(classes: &HashMap<String, &Class>) -> ModuleDispatchFacts {
+    let mut hir = Module::new("t");
+    hir.classes = classes.values().map(|c| (*c).clone()).collect();
+    super::super::collect_module_dispatch_facts(&hir)
+}
+
+fn elements(stmts: &[Stmt], classes: &HashMap<String, &Class>) -> ElementShapeFacts {
+    elements_with(stmts, classes, &facts_of(classes))
 }
🤖 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 `@crates/perry-codegen/src/collectors/ptr_shape_elements_tests.rs` around lines
190 - 223, Update the test helpers facts_for, elements, and promote so dispatch
facts are built from the same classes map used by the collectors, rather than
always using a standalone class_c() fixture. Preserve the existing function
inputs and ensure both mixed_element_classes_deny_the_array and
an_inadmissible_element_class_denies_the_array model identical class sets across
ModuleDispatchFacts and collector inputs.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@crates/perry-codegen/src/collectors/ptr_shape_elements_tests.rs`:
- Line 882: Update ElementShapeFacts::is_empty to check arrays, array_roots,
pushed, and element_reads, returning true only when all fact collections are
empty; preserve its use as the gate for fact consumers.

---

Nitpick comments:
In `@crates/perry-codegen/src/collectors/ptr_shape_elements_tests.rs`:
- Around line 891-910: Rename the getter entry in the test setup from "x" to a
distinct property name not declared by class_c(), while preserving the getter’s
implementation and the chain_admissible assertion that rejects any getter.
- Around line 190-223: Update the test helpers facts_for, elements, and promote
so dispatch facts are built from the same classes map used by the collectors,
rather than always using a standalone class_c() fixture. Preserve the existing
function inputs and ensure both mixed_element_classes_deny_the_array and
an_inadmissible_element_class_denies_the_array model identical class sets across
ModuleDispatchFacts and collector inputs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 42d43d44-f3f4-4f43-a00f-580122e3d7a8

📥 Commits

Reviewing files that changed from the base of the PR and between 9cbfe9f and 397e09b.

📒 Files selected for processing (17)
  • .github/workflows/test.yml
  • benchmarks/repsel_census/baseline.json
  • benchmarks/repsel_census/fixtures/fixture_ptr_shape_elements.ts
  • changelog.d/7149-ptr-shape-array-element-escape.md
  • crates/perry-codegen/src/collectors/hir_facts.rs
  • crates/perry-codegen/src/collectors/mod.rs
  • crates/perry-codegen/src/collectors/ptr_shape.rs
  • crates/perry-codegen/src/collectors/ptr_shape_elements.rs
  • crates/perry-codegen/src/collectors/ptr_shape_elements_tests.rs
  • crates/perry-codegen/src/collectors/ptr_shape_opt_report_tests.rs
  • crates/perry-codegen/src/collectors/ptr_shape_report.rs
  • crates/perry-codegen/src/collectors/ptr_shape_returns.rs
  • crates/perry-codegen/src/collectors/ptr_shape_returns_tests.rs
  • crates/perry-codegen/src/linker.rs
  • scripts/compiler_output_harness/repsel_census.py
  • test-files/test_gap_repsel_ptr_shape_elements.ts
  • test-parity/gc_repsel_corpus.txt
🚧 Files skipped from review as they are similar to previous changes (14)
  • crates/perry-codegen/src/collectors/hir_facts.rs
  • crates/perry-codegen/src/collectors/ptr_shape_returns.rs
  • crates/perry-codegen/src/collectors/ptr_shape_opt_report_tests.rs
  • crates/perry-codegen/src/collectors/ptr_shape_returns_tests.rs
  • scripts/compiler_output_harness/repsel_census.py
  • crates/perry-codegen/src/collectors/mod.rs
  • test-parity/gc_repsel_corpus.txt
  • .github/workflows/test.yml
  • benchmarks/repsel_census/baseline.json
  • test-files/test_gap_repsel_ptr_shape_elements.ts
  • crates/perry-codegen/src/collectors/ptr_shape_report.rs
  • crates/perry-codegen/src/collectors/ptr_shape.rs
  • benchmarks/repsel_census/fixtures/fixture_ptr_shape_elements.ts
  • crates/perry-codegen/src/collectors/ptr_shape_elements.rs

Comment thread crates/perry-codegen/src/collectors/ptr_shape_elements_tests.rs
@proggeramlug
proggeramlug force-pushed the perf/7148-ptr-shape-element-escape branch 2 times, most recently from 816a5a3 to 5a7e2d3 Compare July 31, 2026 22:15
`Ptr<Shape>` rule 2 disqualifies a local at any escape. #7034 §4 opened
`return`; this opens `array/object element` for the array half —
`rows.push(row)` and `for (const r of rows) r.field`.

A `return` is a terminator, so no use of the local can follow it. An element
escape is not: the object stays reachable through the array for the rest of
its life. The containment region therefore widens from one local to one local
array and everything derived from it, with the array's own uses bounded
exactly as an object local's are (`collectors/ptr_shape_elements.rs`, five
conjuncts E1-E5, each with its own red set).

E5 — an element read is licensed only under `for (let i = 0; i < A.length;
i++)` — is the whole difference between this pass and a wrong one: without it
`A[i]` can be `undefined`, and a guard-free fixed-offset load masks a
NaN-boxed `undefined` into a wild pointer.

Measured on a build-then-consume kernel: 0 -> 3 promotions, all consumed, and
the promoted body loses all 7 `js_typed_feedback_class_field_get_guard`, all 7
`js_typed_feedback_record_fallback_call` and all 7
`js_object_get_field_by_name_f64` calls (72 -> 51 `js_*`, 1426 -> 1076 IR
lines) with every GC-related call count identical.

`batch.ts` is unchanged, and dependency JS gains ~nothing: over 180 real CJS
modules the element position accounts for 1 of 746 denials. Both findings, and
the rule-1 wall they point at instead, are in the changelog fragment.
@proggeramlug
proggeramlug force-pushed the perf/7148-ptr-shape-element-escape branch from 5a7e2d3 to 6a3b5bf Compare August 1, 2026 05:05
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Review round addressed — all 6 inline findings + all 4 nitpicks

Every reproducer was added as a test before any fix, so each finding had to prove itself red first. Result on the unfixed HEAD:

finding verdict
🔴 group integrity drops no aliases RED — genuine, fixed
🟠 nested array push keeps inner facts RED — genuine, fixed with your patch verbatim
🔴 property store through A[i] admitted GREEN — hazard real, mechanism already deleted in 816a5a3; both reproducers kept as regression tests
🟡 is_empty covers only arrays GREEN — invariant holds; now asserted in both directions
🟠 make repsel-census required declined with rationale on the thread (admin-only + documented deliberate deferral)
🟡 MD018 fixed

Nitpicks — all four taken, because three are the "test passes for the wrong reason" shape and one is the "two traversals drift" shape, both of which have bitten this campaign:

  • facts_for now receives the test's own classes. A test that mutates its class and hands the mutation only to chain_admissible, while dispatch facts come from a pristine copy, can go green on a rule other than the one under test.
  • The accessor fixture's getter was named x — also a declared field — so it could have passed on FIELD_METHOD_AMBIGUITY rather than the accessor rule. Renamed to derived.
  • element_read_seeds reuses this module's one walk_stmts instead of a second copy of the traversal (−45 lines). Two traversals that must agree, drifting by one Stmt variant, silently withholds a seed on one side and a disqualification on the other. I also corrected walk_stmts' doc, which claimed it descends into closure bodies — it does not, and both callers are correct precisely because it doesn't.
  • Added a direct unit test for the array alias path. for (const r of A) binds const __arr_N = A and indexes that, so the entire iterator read form runs through the alias edge, and only the end-to-end gap test covered it.

Re-validation (rebased onto 8d3bc0e7c, which includes #7139/#7140/#7141)

  • cargo test -p perry-codegen --lib: 453 passed (26 new).
  • Sabotage matrix: 21 conjuncts, each with a disjoint red set, control green in all 22 runs. New cases 18_nested_array_push, 19_element_prop_store, ALIAS_CLOSURE.
  • Census gate green, no floor moved; PERRY_PTR_SHAPE_LOCALS=0 still takes the element fixture to 0.
  • Oracle byte-exact vs Node 26.5.1 on the gap test, the census fixture, the kernel, and batch.ts.
  • IR delta unchanged: 72 → 51 js_*, 1426 → 1076 lines, all 21 guard/feedback/by-name calls gone, every GC-related count identical (js_shadow_slot_bind 5/5, js_write_barrier_root_nanbox 5/5, js_write_barrier_slot 1/1, js_array_push_f64 2/2, js_gc_loop_safepoint 4/4).
  • gc_repsel_matrix --arms all --pressure 8 re-running against the fixed compiler; the previous run was PASS=447 UNVER=119 XFAIL=1 FAIL=0 with the new gap file PASS in all 21 arm columns and zero UNVER. Will post the new figure.

One note for the record: I also had to add the missing 7th argument to collect_shape_proven_ptr_locals in cjs_scaffolding.rs, which arrived with #7139 during the rebase.

@proggeramlug
proggeramlug merged commit f005a2b into main Aug 1, 2026
1 of 8 checks passed
@proggeramlug
proggeramlug deleted the perf/7148-ptr-shape-element-escape branch August 1, 2026 05:13
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Matrix re-run against the fixed compiler is in: gc_repsel_matrix.sh --arms all --pressure 8 → PASS=447 UNVER=119 XFAIL=1 FAIL=0, and test_gap_repsel_ptr_shape_elements is PASS in all 21 arm columns with zero UNVER — every arm, evacuating ones included, was live on it, so those greens are not the inert-arm kind (#6942/#6946/#6950). Identical to the pre-fix run, as expected: both review fixes are strictly narrowing.

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