Skip to content

perf(object): #6812 w16 — per-site builder identity, learned width, and compile-time width hints beat node on overflow-slot writes#6833

Merged
proggeramlug merged 5 commits into
mainfrom
perf/6812-w16-builder-identity
Jul 25, 2026
Merged

perf(object): #6812 w16 — per-site builder identity, learned width, and compile-time width hints beat node on overflow-slot writes#6833
proggeramlug merged 5 commits into
mainfrom
perf/6812-w16-builder-identity

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes the last catastrophic row of the #6812 object-write matrix: w16 overflow-slot writes go from 173× slower than node to 10× faster (3 ms vs 29 ms median; was 4163 ms). Two stacked mechanisms, both broad rather than benchmark-shaped:

1. Builder-pattern objects get a learnable per-site identity (13053ae4a)

  • Empty object literals ({} — the seed of every dynamic builder: API responses, parser output, ORM rows) previously lowered to the legacy class-0 empty object. Class 0 is excluded from the static-key write PIC and from the runtime's learned inline sizing, so a {}-built object with more than INLINE_SLOT_FLOOR (4) fields keeps the rest in the thread-local overflow side-table forever — every write paying TLS + RefCell + PtrHashMap.
  • {} now lowers to new __AnonShape_<site-hash>(), a unique 0-field shape-only anon class per source site (content-addressed on file + byte offset, same naming scheme the shape-addressed anon classes already use, so all __AnonShape_ runtime introspection parity applies).
  • js_object_alloc_class_inline_keys (the compiled-new allocation path) now consumes learned_inline_field_count, mirroring the dynamic-construct path's capacity semantics: the first instance of a site that overflows teaches the runtime the site's true width; every later instance allocates all fields inline.

2. Compile-time width hint for const-bounded dynamic-key builders (83b1f5c48)

  • Learned sizing can't fix the FIRST instance — and as element 0 of the array built at the site, that one under-sized instance vetoes the whole-loop numeric clone guard ("first receiver target slot is out of bounds") for every hot loop over the array.
  • const o = {}; immediately followed by for (let k = C0; k < C1; k++) o[key] = v (1–4 writes per iteration, plain = member assigns to o only) proves the final width statically. The scan (builder_fold::empty_builder_width_hints) records it per site; the site class carries it as Class::alloc_width_hint (included in stable hashing per the build: the object cache doesn't key on codegen env vars — PERRY_WRITE_BARRIERS=0 silently does nothing with a warm cache #6394 rule); compiled new allocates max(fields, hint) inline slots. Pure capacity: keys/enumeration are unchanged, over-counting can only waste slots, and the value side of the assignments is deliberately unconstrained (it never affects width).
  • With instance Support custom menu bar items #1 right-sized, arrays built at the site are uniform → the perf: beat Node on object-write micro (#6809) #6811 whole-loop clone runs at full speed.

Measured (release, default pipeline, node v26.3.0 arm64, medians of 3 alternated runs, identical checksums)

cell before after node ratio
w16_overflow_slot 4163 ms 3 ms 29 ms 0.10 — beats node

Full 18-cell matrix: zero regressions; 10 of 18 rows now beat node (w0–w5, w7, w11, w16, w18).

Semantics

  • New empty_site_sanity.ts (14 cases): Object.keys/entries/values/getOwnPropertyNames, constructor/instanceof/prototype, delete + re-add, Object.assign/spread, for-in order, defineProperty, freeze/seal, symbol keys, String(o), JSON round-trip, two same-site instances (pre/post learning) — byte-identical vs node.
  • builder_fold_sanity.ts (11 cases, s3 = pre-existing Dynamic __proto__ assignment creates an own property instead of invoking the prototype setter #6828) and mul_index_sanity.ts — byte-identical, no regressions.
  • perry-hir / perry-codegen / perry-runtime suites green; gap gate clean (results in comment below).

Notes

  • An empty literal that never grows allocates exactly as before (hint 0, learned 0 → floor minimum); the only cost for non-builder {} is a per-site class registry entry.
  • Imported-class stubs don't carry hints (fall back to runtime learning).
  • Follow-up filed for the general case where the first K instances predate learning and no static width exists (prefix-tolerant guard with post-prologue revalidation).

Refs #6812.

https://claude.ai/code/session_01QJ5mwMDPc63tNLAFPdthAG

Summary by CodeRabbit

  • Performance
    • Improved allocation for object builders and empty object literals ({}) by using per-site builder identities and learned sizing to better match expected inline field capacity.
    • Updated allocation width behavior so early allocations are sized more accurately, reducing later widening and improving clone eligibility.
  • Bug Fixes
    • Ensured per-class keys globals are correctly registered as GC roots during module initialization.
  • Documentation
    • Refreshed the object-write performance matrix with updated w16_overflow_slot benchmark results and explanations.
  • Tests
    • Added/strengthened regression assertions and updated fixtures to reflect the new allocation-width hint behavior.

Ralph Küpper added 3 commits July 25, 2026 13:40
…h in compiled new

Empty object literals lower to a unique 0-field anon-shape class per
source site (content-addressed on source path + byte offset), giving
builder-pattern objects a non-zero class_id: the runtime's learned
inline sizing can attribute overflow growth to the site and right-size
later allocations, and the static-key write PIC gate admits the objects.
js_object_alloc_class_inline_keys (the compiled-new allocation path) now
consumes learned_inline_field_count and stores the widened field_count,
matching the dynamic-construct path's capacity semantics.

Claude-Session: https://claude.ai/code/session_01QJ5mwMDPc63tNLAFPdthAG
…ded dynamic-key builders

A '{} then for (let k = C0; k < C1; k++) o[key] = v' builder has a
statically provable final width even though its keys are computed. Learned
sizing alone right-sizes only instances AFTER the first overflow, leaving
instance #1 permanently under-sized — and as element 0 of the array built
at the site it vetoes the whole-loop clone guard for every hot loop over
that array. The scan (builder_fold::empty_builder_width_hints) records the
proven width per empty-literal site; the per-site anon-shape class carries
it as Class::alloc_width_hint (stable-hashed), and compiled new allocates
max(fields, hint) inline slots. Capacity only — keys/enumeration unchanged.

Claude-Session: https://claude.ai/code/session_01QJ5mwMDPc63tNLAFPdthAG
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

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: 5532e6f3-37a6-4299-8774-450b37b3eb98

📥 Commits

Reviewing files that changed from the base of the PR and between bb292cb and 9af1e26.

📒 Files selected for processing (3)
  • crates/perry-hir/src/lower/builder_fold.rs
  • crates/perry/src/commands/compile/helpers.rs
  • docs/object-write-matrix.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/object-write-matrix.md
  • crates/perry-hir/src/lower/builder_fold.rs

📝 Walkthrough

Walkthrough

Empty object literals now receive unique source-site shape classes with optional compile-time builder width hints. The hint propagates through HIR and codegen into runtime allocation, which combines it with learned widths. Supporting fixtures, IR regression coverage, documentation, and changelog entries were updated.

Changes

Builder identity and width-aware allocation

Layer / File(s) Summary
HIR width-hint contract and propagation
crates/perry-hir/src/ir/decl.rs, crates/perry-hir/src/lower/..., crates/perry-hir/src/monomorph/specialize.rs, crates/perry-hir/src/stable_hash/..., crates/perry-codegen/src/codegen/mod.rs, crates/perry-transform/**, crates/perry/src/commands/compile/helpers.rs
Adds Class::alloc_width_hint and preserves or initializes it across synthesized, specialized, hashed, namespace, class, imported-class, and test metadata.
Compile-time builder width analysis
crates/perry-hir/src/lower/builder_fold.rs, crates/perry-hir/src/lower/lower_module_fn.rs
Scans supported constant-bounded build loops, validates assignments, computes bounded widths, and records per-site hints for object lowering.
Per-site lowering, allocation, and validation
crates/perry-hir/src/lower/expr_object.rs, crates/perry-codegen/src/lower_call/new.rs, crates/perry-runtime/src/object/alloc.rs, crates/perry-codegen/tests/*, docs/object-write-matrix.md, changelog.d/6812-w16-builder-identity.md
Lowers empty literals to unique hinted shapes, applies hints and learned widths during allocation, and updates IR regression coverage, fallback expectations, benchmark documentation, and the changelog.

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

Sequence Diagram(s)

sequenceDiagram
  participant ModuleLowering
  participant WidthScanner
  participant ObjectLowering
  participant Codegen
  participant RuntimeAllocator
  ModuleLowering->>WidthScanner: compute builder width hints
  WidthScanner-->>ModuleLowering: return per-source-site widths
  ModuleLowering->>ObjectLowering: lower empty object literal
  ObjectLowering->>Codegen: emit unique hinted anon-shape class
  Codegen->>RuntimeAllocator: allocate inline fields
  RuntimeAllocator->>RuntimeAllocator: apply learned inline width
Loading

Possibly related PRs

  • PerryTS/perry#6712: Both changes modify inline field-slot allocation in crates/perry-runtime/src/object/alloc.rs.
  • PerryTS/perry#6829: Both changes extend the HIR builder-folding and module-lowering pipeline.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the PR’s main performance work on per-site builder identities and width hints.
Description check ✅ Passed The description is detailed and covers the change, issue reference, and validation results, though it doesn’t follow the template exactly.
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/6812-w16-builder-identity

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: 2

🧹 Nitpick comments (3)
crates/perry-hir/src/lower/builder_fold.rs (1)

909-913: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

hint_walk_expr's Call arm never visits the callee, unlike scan_expr's.

An IIFE builder ((function(){ const o = {}; for(...){} return o; })()) is invisible to this scanner since the callee Expr::Fn/Expr::Paren is never walked. scan_expr's existing E::Call arm already walks the callee for the same reason (matches!(&c.callee, ast::Callee::Expr(e) if scan_expr(e))).

♻️ Proposed fix: walk the callee too
         E::Call(c) => {
+            if let ast::Callee::Expr(callee) = &c.callee {
+                hint_walk_expr(callee, hints);
+            }
             for arg in &c.args {
                 hint_walk_expr(&arg.expr, hints);
             }
         }
🤖 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-hir/src/lower/builder_fold.rs` around lines 909 - 913, Update
the E::Call arm of hint_walk_expr to also visit the call’s callee expression
before walking its arguments, matching scan_expr’s callee traversal; preserve
the existing argument traversal and handle expression callees without attempting
to walk non-expression callees.
crates/perry-hir/src/stable_hash/tests.rs (1)

284-284: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a non-zero width-hint hash regression test.

alloc_width_hint: 0 only updates the fixture to compile; it does not verify that Class::hash includes the new field. Add a variant with a different non-zero hint and assert that its module hash differs.

🤖 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-hir/src/stable_hash/tests.rs` at line 284, Add a regression-test
variant in the existing stable-hash tests using a distinct non-zero
alloc_width_hint, then compute its hash through Class::hash and assert it
differs from the zero-hint fixture’s module hash. Keep the existing zero-hint
case and test structure unchanged.
crates/perry-codegen/tests/class_keys_gc_root.rs (1)

156-196: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep this regression in a PR-visible test target.

This new test lives under crates/perry-codegen/tests, but integration suites there do not run on every PR. Prefer cargo-test-visible unit coverage or wire this target into every PR so the GC-root regression cannot be skipped.
As per coding guidelines, acceptance coverage under crates/*/tests/*.rs should preferably be covered by unit tests that run on every PR.

🤖 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/tests/class_keys_gc_root.rs` around lines 156 - 196,
Move class_keys_global_is_registered_as_gc_root from the integration-test
location into cargo-test-visible unit coverage, or wire its current target into
every PR’s test workflow. Preserve the existing assertions verifying the
per-class keys global address is passed to js_gc_register_global_root.

Source: Coding guidelines

🤖 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-hir/src/lower/builder_fold.rs`:
- Around line 759-774: Update empty_builder_width_hints to handle
ast::ModuleItem::ModuleDecl entries in both top-level scans, including exported
declarations and default exports. Reuse the existing note_hint_pair and
hint_walk_stmt traversal logic so exported function/class bodies and relevant
declaration statements receive the same width hints as non-exported statements.

In `@docs/object-write-matrix.md`:
- Line 52: Reconcile the w16 benchmark figures using one recorded benchmark run:
in docs/object-write-matrix.md:52-52, correct the final ratio or Node
measurement so they agree arithmetically; in
changelog.d/6812-w16-builder-identity.md:1-1, replace the 173× claim with the
matching pre-fix multiplier. Ensure both files report the same baseline and
measurements.

---

Nitpick comments:
In `@crates/perry-codegen/tests/class_keys_gc_root.rs`:
- Around line 156-196: Move class_keys_global_is_registered_as_gc_root from the
integration-test location into cargo-test-visible unit coverage, or wire its
current target into every PR’s test workflow. Preserve the existing assertions
verifying the per-class keys global address is passed to
js_gc_register_global_root.

In `@crates/perry-hir/src/lower/builder_fold.rs`:
- Around line 909-913: Update the E::Call arm of hint_walk_expr to also visit
the call’s callee expression before walking its arguments, matching scan_expr’s
callee traversal; preserve the existing argument traversal and handle expression
callees without attempting to walk non-expression callees.

In `@crates/perry-hir/src/stable_hash/tests.rs`:
- Line 284: Add a regression-test variant in the existing stable-hash tests
using a distinct non-zero alloc_width_hint, then compute its hash through
Class::hash and assert it differs from the zero-hint fixture’s module hash. Keep
the existing zero-hint case and test structure unchanged.
🪄 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: d821ad84-e404-4eb8-ad2b-c7f331ca15f4

📥 Commits

Reviewing files that changed from the base of the PR and between 8b2c736 and 31fb81f.

📒 Files selected for processing (34)
  • changelog.d/6812-w16-builder-identity.md
  • crates/perry-codegen/src/codegen/mod.rs
  • crates/perry-codegen/src/collectors/class_accessors.rs
  • crates/perry-codegen/src/collectors/scalar_method_dispatch.rs
  • crates/perry-codegen/src/collectors/this_as_value.rs
  • crates/perry-codegen/src/lower_call/buffer_intrinsic.rs
  • crates/perry-codegen/src/lower_call/new.rs
  • crates/perry-codegen/src/type_analysis_tests.rs
  • crates/perry-codegen/tests/class_keys_gc_root.rs
  • crates/perry-codegen/tests/constructor_recursion.rs
  • crates/perry-codegen/tests/native_proof_buffer_views.rs
  • crates/perry-codegen/tests/native_proof_regressions.rs
  • crates/perry-codegen/tests/private_guard_declaring_class.rs
  • crates/perry-codegen/tests/static_symbol_hygiene.rs
  • crates/perry-codegen/tests/typed_feedback.rs
  • crates/perry-codegen/tests/typed_shape_descriptor.rs
  • crates/perry-hir/src/analysis/value_types_tests.rs
  • crates/perry-hir/src/ir/decl.rs
  • crates/perry-hir/src/lower/builder_fold.rs
  • crates/perry-hir/src/lower/context.rs
  • crates/perry-hir/src/lower/expr_object.rs
  • crates/perry-hir/src/lower/lower_module_fn.rs
  • crates/perry-hir/src/lower/lowering_context.rs
  • crates/perry-hir/src/lower/module_decl/namespace.rs
  • crates/perry-hir/src/lower_decl/class_decl.rs
  • crates/perry-hir/src/monomorph/specialize.rs
  • crates/perry-hir/src/stable_hash/decls.rs
  • crates/perry-hir/src/stable_hash/tests.rs
  • crates/perry-runtime/src/object/alloc.rs
  • crates/perry-transform/src/async_to_generator.rs
  • crates/perry-transform/src/deforest/tests.rs
  • crates/perry-transform/src/generator/id_scan.rs
  • crates/perry-transform/src/inline/mod.rs
  • docs/object-write-matrix.md

Comment thread crates/perry-hir/src/lower/builder_fold.rs
Comment thread docs/object-write-matrix.md Outdated
Both reproduce at origin/main and at f6fb9a4~1 — broken before the
whole #6812 line, surfaced now because this PR touches the test files and
e2e-scoped selects suites by changed paths.

- native_owned_uint8array_get_fallback: codegen migrated the disposed-view
  fallback to js_uint8array_index_get_value, which validates against the
  typed-array kind registry before dereferencing (the memory-safety
  property the test pins); assert the new helper.
- typed_feedback_trace_dump_runs_before_entry_return: the entry epilogue
  gained a second exit path returning the dynamic
  js_process_pending_exit_code result, so rfind("ret i32 0") no longer
  finds the last return. Assert the stronger real invariant: every entry
  return is immediately preceded by the trace dump.

Claude-Session: https://claude.ai/code/session_01QJ5mwMDPc63tNLAFPdthAG

@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

🤖 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/tests/typed_feedback.rs`:
- Around line 221-241: Update the return validation loop in the typed-feedback
test to verify that each `ret i32` is immediately preceded by the trace-dump
call, ignoring only whitespace or empty lines. Remove the fixed 200-byte
`preceding.contains` window so an earlier exit path’s dump cannot satisfy a
later return; preserve the existing requirement that the entry contains at least
one return.
🪄 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: 28d216b0-5ded-47a0-8090-90525dc26316

📥 Commits

Reviewing files that changed from the base of the PR and between 31fb81f and bb292cb.

📒 Files selected for processing (2)
  • crates/perry-codegen/tests/native_proof_buffer_views.rs
  • crates/perry-codegen/tests/typed_feedback.rs

Comment on lines +221 to +241
// The entry epilogue now has TWO exit paths: the host-return path
// (`ret i32 0`) and the event-loop exit path, which returns the dynamic
// `js_process_pending_exit_code` result (`ret i32 %rN`). The old
// rfind(dump) < rfind("ret i32 0") comparison broke the day the second
// path appeared — its dump call sits after the literal `ret i32 0`. The
// real invariant is stronger: EVERY entry return must be immediately
// preceded by the trace dump, so no exit path can skip the dump.
let mut ret_count = 0;
let mut search_from = 0;
while let Some(rel) = ir[search_from..].find("ret i32") {
let ret_pos = search_from + rel;
ret_count += 1;
let preceding = &ir[ret_pos.saturating_sub(200)..ret_pos];
assert!(
preceding.contains("call void @js_typed_feedback_maybe_dump_trace()"),
"entry return at byte {ret_pos} is not preceded by the typed-feedback \
trace dump:\n...{preceding}"
);
search_from = ret_pos + 1;
}
assert!(ret_count >= 1, "entry should return i32");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert adjacency instead of searching a 200-byte window.

preceding.contains(...) can match a dump belonging to an earlier exit path, allowing a later ret i32 without its own dump to pass. It also makes the test depend on unrelated IR size. Check the immediately preceding non-empty instruction, or parse the entry-function control flow, for each return.

Proposed assertion tightening
-    let mut search_from = 0;
-    while let Some(rel) = ir[search_from..].find("ret i32") {
-        let ret_pos = search_from + rel;
+    let mut previous_nonempty = None;
+    for line in ir.lines() {
+        if line.trim_start().starts_with("ret i32") {
         ret_count += 1;
-        let preceding = &ir[ret_pos.saturating_sub(200)..ret_pos];
-        assert!(
-            preceding.contains("call void `@js_typed_feedback_maybe_dump_trace`()"),
-            "entry return at byte {ret_pos} is not preceded by the typed-feedback \
-             trace dump:\n...{preceding}"
-        );
-        search_from = ret_pos + 1;
+            assert_eq!(
+                previous_nonempty,
+                Some("call void `@js_typed_feedback_maybe_dump_trace`()"),
+                "entry return is not immediately preceded by the typed-feedback trace dump"
+            );
+        }
+        if !line.trim().is_empty() {
+            previous_nonempty = Some(line.trim());
+        }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// The entry epilogue now has TWO exit paths: the host-return path
// (`ret i32 0`) and the event-loop exit path, which returns the dynamic
// `js_process_pending_exit_code` result (`ret i32 %rN`). The old
// rfind(dump) < rfind("ret i32 0") comparison broke the day the second
// path appeared — its dump call sits after the literal `ret i32 0`. The
// real invariant is stronger: EVERY entry return must be immediately
// preceded by the trace dump, so no exit path can skip the dump.
let mut ret_count = 0;
let mut search_from = 0;
while let Some(rel) = ir[search_from..].find("ret i32") {
let ret_pos = search_from + rel;
ret_count += 1;
let preceding = &ir[ret_pos.saturating_sub(200)..ret_pos];
assert!(
preceding.contains("call void @js_typed_feedback_maybe_dump_trace()"),
"entry return at byte {ret_pos} is not preceded by the typed-feedback \
trace dump:\n...{preceding}"
);
search_from = ret_pos + 1;
}
assert!(ret_count >= 1, "entry should return i32");
// The entry epilogue now has TWO exit paths: the host-return path
// (`ret i32 0`) and the event-loop exit path, which returns the dynamic
// `js_process_pending_exit_code` result (`ret i32 %rN`). The old
// rfind(dump) < rfind("ret i32 0") comparison broke the day the second
// path appeared — its dump call sits after the literal `ret i32 0`. The
// real invariant is stronger: EVERY entry return must be immediately
// preceded by the trace dump, so no exit path can skip the dump.
let mut ret_count = 0;
let mut previous_nonempty = None;
for line in ir.lines() {
if line.trim_start().starts_with("ret i32") {
ret_count += 1;
assert_eq!(
previous_nonempty,
Some("call void `@js_typed_feedback_maybe_dump_trace`()"),
"entry return is not immediately preceded by the typed-feedback trace dump"
);
}
if !line.trim().is_empty() {
previous_nonempty = Some(line.trim());
}
}
assert!(ret_count >= 1, "entry should return i32");
🤖 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/tests/typed_feedback.rs` around lines 221 - 241, Update
the return validation loop in the typed-feedback test to verify that each `ret
i32` is immediately preceded by the trace-dump call, ignoring only whitespace or
empty lines. Remove the fixed 200-byte `preceding.contains` window so an earlier
exit path’s dump cannot satisfy a later return; preserve the existing
requirement that the entry contains at least one return.

…ing Class field in perry test cfg; one-source w16 numbers

- empty_builder_width_hints now descends ModuleDecl items (export fn/class/
  const, export default) and pairs 'export const o = {}' with its build
  loop — exported builder helpers are the most common real-world shape.
- perry crate test-cfg Class literals gained alloc_width_hint (CI cargo-test
  E0063; local checks missed the CLI crate's tests).
- w16 doc row and changelog now cite one measured lineage (pre-fix 4163/24
  = 173x, post-fix 3/29 = 0.10, 2026-07-25 sweeps).

Claude-Session: https://claude.ai/code/session_01QJ5mwMDPc63tNLAFPdthAG
@proggeramlug
proggeramlug merged commit 60e4e2c into main Jul 25, 2026
30 of 31 checks passed
@proggeramlug
proggeramlug deleted the perf/6812-w16-builder-identity branch July 25, 2026 19:07
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