Skip to content

perf(codegen): shape-cache path for object literals with captures_this methods - #9122

Merged
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:perf/object-literal-shape-cache-split
Aug 30, 2026
Merged

perf(codegen): shape-cache path for object literals with captures_this methods#9122
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:perf/object-literal-shape-cache-split

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

What

A captures_this method used to force the WHOLE object literal onto the by-name path (all-or-nothing gate at the shape-cache fast path): this_patches needs each method closure value after every later initializer has run, and the by-name arm keeps those values alive through nested rooted accumulators. But the shape allocator pre-populates keys_array, so by-INDEX stores are equally valid for method props — and the patch loop can simply re-read each method closure from its own field slot at patch time (js_object_get_field, declared for IR here): the rooted object keeps the closure alive, and the re-read observes any evacuation that moved it, with no per-closure rooting at all.

Kill switch: PERRY_OBJECT_LITERAL_SHAPE_METHODS=0 restores the old routing (read per literal, uncached, so the tests below can pin either arm).

Measurements

Mac mini kill-switch pairs (7 runs, medians; node for scale):

shape by-name (off) shape path (on) node Δ
{a, b, inc()} literal build+call 853.6 ns 426.2 ns 3.5 −50.1%
5-prop 2-method literal 1585.5 765.6 5.9 −51.7%
plain 3-prop literal 1.0 1.0 0.8 +0.0%

The residual (~257 ns build-only, ~63 ns per method call) is per-instance closure allocation and the dynamic method-call family — separate levers, sized in the follow-up investigation (the fn_param receiver finding is filed with the team).

Correctness

  • Differential vs node — methods + this-binding, alias preservation, methods-only literals, method-first literals, nested literal methods, JSON.stringify round-trip, and a 200k-literal churn loop under allocation pressure (evacuating minors between build and patch): byte-identical, both switch states.
  • The five by_name_method_closure_tests (the gc-root-dominance: 7/3 gated (8/4 unfiltered) NEW root-dominance regressions landed 2026-08-15..25 while the gate was red #8809 rooting-hazard pins) now pin their arm explicitly via the kill switch under a module-local mutex, and three new shape_method_literal_tests twins pin the shape path's invariants: patches below every store, each patch re-reads its closure from its field slot (a kept register is stale after an evacuating initializer), and source-order patching.
  • Gates: -D warnings 0, codegen 1833/0, full runtime suite 2819/0 (runtime untouched by this change; suite run on the same tree), lints clean, integration 8655 2/2 / 8690 3/3 / 8897 3/3.

https://claude.ai/code/session_01F1dt1jfzK2cheMZyus6y6p

Summary by CodeRabbit

  • Bug Fixes

    • Improved object literal handling for methods that capture this, enabling safer and more consistent property initialization.
    • Added reliable field-based access during object creation to ensure captured method closures receive the correct this context.
  • Tests

    • Added coverage for shape-based object literals and expanded validation of alternate routing behavior.

@coderabbitai

coderabbitai Bot commented Aug 30, 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: 1edd45d3-fd36-49c8-8f21-0c5909df5e5e

📥 Commits

Reviewing files that changed from the base of the PR and between 35447e7 and 796f50c.

📒 Files selected for processing (2)
  • crates/perry-codegen/src/expr/object_literal.rs
  • crates/perry-codegen/src/runtime_decls/objects.rs

Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

Changes

Capturing method closures now use the object-literal shape path by default. The compiler stores properties by index, rereads method closures, and patches their reserved this captures. An environment variable preserves the by-name path for tests and fallback use.

Object literal shape lowering

Layer / File(s) Summary
Shape routing and closure patching
crates/perry-codegen/src/expr/object_literal.rs, crates/perry-codegen/src/runtime_decls/objects.rs
captures_this method literals use shape storage unless PERRY_OBJECT_LITERAL_SHAPE_METHODS disables it. The lowering code rereads closures with js_object_get_field and applies js_closure_set_capture_bits after all property stores.
Routing controls and validation
crates/perry-codegen/src/expr/object_literal.rs
Tests serialize environment-variable changes with RoutingPin. New tests validate shape allocation, indexed stores, closure rereads, patch ordering, and the retained by-name path.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: 🟡 Moderate · up to 796f5

Object literals with this-capturing methods now use indexed shape construction by default, but duplicate or empty-string property keys can be represented differently than before, causing incorrect lookup or enumeration behavior. The PR is not merge-ready until this key-identity handling is fixed or explicitly accepted.

Sequence Diagram(s)

sequenceDiagram
  participant lower_object_literal
  participant js_object_set_field
  participant js_object_get_field
  participant js_closure_set_capture_bits
  lower_object_literal->>js_object_set_field: store object properties by index
  lower_object_literal->>js_object_get_field: reread each method closure
  lower_object_literal->>js_closure_set_capture_bits: patch reserved this capture
Loading

Suggested reviewers: thehypnoo

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description provides detailed technical context, measurements, correctness claims, and test results, but it does not follow the required template. It omits the required ## Summary, ## Changes,… Rewrite the description using the repository template. Add the required headings, summarize the change under ## Summary, list concrete modifications under ## Changes, state an issue reference or n/a under ## Related issue, list exac…
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding a shape-cache path for object literals with methods that capture this.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 15 functions across 2 files.
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.
Full details: Description check

Explanation

The description provides detailed technical context, measurements, correctness claims, and test results, but it does not follow the required template. It omits the required ## Summary, ## Changes, ## Related issue, ## Test plan, and ## Checklist sections.

Resolution

Rewrite the description using the repository template. Add the required headings, summarize the change under ## Summary, list concrete modifications under ## Changes, state an issue reference or n/a under ## Related issue, list exact verification commands and mark applicable test-plan items under ## Test plan, and complete the checklist.

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

…s methods

A captures_this method used to force the WHOLE literal onto the by-name
path (all-or-nothing gate): this_patches needs each method closure value
after every later initializer has run, and the by-name arm keeps those
values alive through nested rooted accumulators. But the shape allocator
pre-populates keys_array, so by-INDEX stores are equally valid for method
props — and the patch loop can simply RE-READ each method closure from its
own field slot at patch time (js_object_get_field, declared for IR here):
the rooted object keeps the closure alive, and the re-read observes any
evacuation that moved it, with no per-closure rooting at all.

Kill switch: PERRY_OBJECT_LITERAL_SHAPE_METHODS=0 restores the old routing.

Mac mini kill-switch pairs: {a, b, inc()} literal 853.6 -> 426.2 ns/op
(-50.1%), 5-prop 2-method literal 1585.5 -> 765.6 (-51.7%), plain literal
flat. Differential vs node (methods + this-binding, 200k-literal churn
under allocation pressure, methods-only literals, nested literal methods,
JSON round-trip): byte-identical.

Claude-Session: https://claude.ai/code/session_01F1dt1jfzK2cheMZyus6y6p
@proggeramlug
proggeramlug force-pushed the perf/object-literal-shape-cache-split branch from 796f50c to c40c321 Compare August 30, 2026 07:27
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Merged as part of a six-PR merge train: all six were cherry-picked onto one branch and validated together in a single build rather than six separate ones, at the maintainer's request to speed up a backlog. Combined validation: hir 361 passed, codegen 1352, runtime 2822 (exit 0, 0 abort markers), perry --bins 1066, and run_lint_gates.sh all 60 gates passed. git diff origin/main --diff-filter=D empty across the whole train.

I added one commit: PERRY_OBJECT_LITERAL_SHAPE_METHODS was not registered in BUILD_CACHE_ENV_VARS, so codegen_env_vars_are_build_cache_inputs would have gone red. It gates two different literal-birth emission sequences, so it is a cache key. That gate has now caught five PRs in this series — worth adding the registration to the checklist when introducing a codegen knob.

@proggeramlug
proggeramlug merged commit c2f5bc9 into PerryTS:main Aug 30, 2026
17 of 20 checks passed
proggeramlug added a commit that referenced this pull request Aug 30, 2026
…che input (#9144)

#9122 introduced the knob; its registration was prepared but did not make
it into the merged commit, leaving codegen_env_vars_are_build_cache_inputs
red on main. The knob selects between two literal-birth emission
sequences, so it is a cache key.

Co-authored-by: Ralph Küpper <ralph@skelpo.com>
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