Skip to content

fix(codegen): sloppy-mode class-field number stores keep the inline raw store - #7423

Merged
proggeramlug merged 4 commits into
mainfrom
fix/7288-path-dependent-codegen
Aug 5, 2026
Merged

fix(codegen): sloppy-mode class-field number stores keep the inline raw store#7423
proggeramlug merged 4 commits into
mainfrom
fix/7288-path-dependent-codegen

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #7288. 3762ms → 81ms outside a Perry checkout, matching the in-checkout number exactly.

Root cause: strict mode, from an upward package.json walk

perry_parser::file_is_in_esm_package_context walks up for the nearest package.json. "type": "module" ⇒ ES module ⇒ strict. Perry's root package.json is "type": "module", so everything inside the checkout compiles strict and everything outside compiles sloppy.

Three if !strict { return None; } bails in expr/proxy_reflect.rs (added by #6542) then discard the fast class-field-store path. The HIR diff between arms is one bit:

PutValueSet { … strict: true  }   ← in-checkout → class_field_set.fast → store double
PutValueSet { … strict: false }   ← outside     → put.dynic.* + js_put_value_set_dyn_ic per iteration

benchmarks/results/public-node-bun-v1.json reports the fast arm because compare.sh does cd benchmarks/suite. A user compiling the same file in their own project got the slow one.

6/6 controls, including the decisive one: inside the checkout with a nested {"name":"x"} → slow, because Node stops the walk at the nearest package scope. A bare {"name":"x"} with no perry key reproduces the flip exactly.

The fix

The bail discarded the fast arm in order to fix the fallback arm — but only the fallback needed strict-awareness, and js_put_value_set(…, strict=0) already existed.

The #5093 precheck already rejects OBJ_FLAG_FROZEN, OBJ_FLAG_HAS_DESCRIPTORS, mismatched class-id/keys, cleared layout-intact, and every non-plain-finite value — so a store reaching the raw slot could not have been rejected in either mode. No runtime change. Scope: raw-f64 fields, receiver == target.

Verification

  • Independently re-measured after rebuild: outside 81–83ms, inside 81ms, fresh PERRY_CACHE_DIR on both, at load 25 (the 46× gap is far outside any noise band)
  • 20-case sloppy differential — inheritance, shadowed fields, accessors, non-number stores, boundary numbers, preventExtensions, delete+re-add, aliasing, 100k megamorphic loop, Object.freeze mid-loop, enumeration order — byte-identical to Node 26.5.1 and to the pre-change compiler, with 150 class_field_sloppy_set blocks proving the arm is live
  • 24/24 test_gap_class*/test_gap_object* match Node; --lib 633 pass; native_proof_regressions identical 4 pre-existing failures before and after
  • New test verified non-vacuous (fails on origin/main)

Two things worth knowing

Today's two perf wins are unaffected — verified by IR diff, not timing: the only difference between arms for bench_bitwise and bench_array_ops is one startup-time js_register_closure_strict_function call; hot code byte-identical. #7416 and #7421 stand.

The gap suite structurally cannot exercise the sloppy arm. Every test-files/*.ts sits under the root "type": "module", so the entire corpus compiles strict. run_parity_tests.sh already acknowledges this for Node (retrying globals fixtures as .cts) but there is no sloppy arm on the compiler side — so any codegen predicate keyed on strict is tested in one state only. Worth its own issue.

Summary by CodeRabbit

  • Performance

    • Improved performance for sloppy-mode assignments to eligible numeric class fields.
    • Optimized behavior is now consistent inside and outside ESM package scopes.
  • Bug Fixes

    • Preserved correct silent-failure behavior when class-field assignment checks do not match.
    • Retained existing handling for strict mode and unsupported field types.
  • Tests

    • Added regression coverage for optimized class-field writes and fallback behavior.

Ralph Küpper added 4 commits August 5, 2026 09:12
…aw store (#7288)

A byte-identical .ts file compiled to a 46x slower object depending on where
on disk it lived: 83 ms inside the Perry checkout, 3,762 ms anywhere else. The
discriminator is strict mode, resolved by an upward walk for the nearest
package.json -- Perry's root is "type": "module", so every file inside the
checkout is strict and every file outside it is sloppy.

put_value_static_property_fast_path barred sloppy code from the whole
class-field store route (#6542) because that route's terminal fallback throws
on a non-writable slot, which is right for strict PutValue and wrong for
sloppy. That discarded the fast arm to fix the fallback arm. The #5093 inline
precheck already rejects frozen and descriptor-bearing receivers and every
non-plain-finite value, so a store that reaches the raw slot could not have
been rejected in either mode -- the fast arm is mode-independent.

Sloppy obj.f = <number> on a declared number field now emits that same
precheck and raw slot store, sending every miss to js_put_value_set(..., 0),
the sloppy-correct runtime the surrounding lowering already used. No runtime
change.

09_method_calls outside a checkout: 3,762 ms -> 81 ms, matching the in-checkout
arm exactly.
@proggeramlug
proggeramlug merged commit fe51069 into main Aug 5, 2026
@proggeramlug
proggeramlug deleted the fix/7288-path-dependent-codegen branch August 5, 2026 07:43
@coderabbitai

coderabbitai Bot commented Aug 5, 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: Pro Plus

Run ID: 3d4c16ee-349f-4a85-9c0f-5a4ae1c6ca9b

📥 Commits

Reviewing files that changed from the base of the PR and between 4143c2a and 8365ded.

📒 Files selected for processing (4)
  • changelog.d/7288-path-dependent-class-field-store.md
  • crates/perry-codegen/src/expr/property_set.rs
  • crates/perry-codegen/src/expr/proxy_reflect.rs
  • crates/perry-codegen/tests/native_proof_regressions.rs

📝 Walkthrough

Walkthrough

The compiler adds sloppy-mode lowering for eligible numeric class-field stores. It emits an inline guard and direct raw-slot write on hits, and calls js_put_value_set with strict = 0 on misses. Integration and regression coverage validate the new path.

Changes

Sloppy class-field store

Layer / File(s) Summary
Raw-store eligibility and helper
crates/perry-codegen/src/expr/property_set.rs
Adds try_lower_sloppy_class_field_raw_store with checks for module shape, computed properties, setters, class metadata, and raw-f64 fields.
Guarded lowering and integration
crates/perry-codegen/src/expr/property_set.rs, crates/perry-codegen/src/expr/proxy_reflect.rs
Evaluates assignment operands in order, emits the class-field precheck and direct canonicalized slot store, and routes misses through js_put_value_set with strict = 0.
Regression coverage and changelog
crates/perry-codegen/tests/native_proof_regressions.rs, changelog.d/7288-path-dependent-class-field-store.md
Tests sloppy hit and miss lowering and confirms strict-mode behavior remains unchanged. Documents scope and validation results.

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

Sequence Diagram(s)

sequenceDiagram
  participant PutValueSet
  participant ClassFieldLowering
  participant RawFieldSlot
  participant RuntimeSetter
  PutValueSet->>ClassFieldLowering: attempt eligible sloppy class-field store
  ClassFieldLowering->>RawFieldSlot: run inline guard and store canonicalized f64
  ClassFieldLowering->>RuntimeSetter: call js_put_value_set with strict = 0 on guard miss
Loading

Possibly related PRs

  • PerryTS/perry#6811: Uses related PutValueSet and guarded raw/static object-write lowering paths.
✨ 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 fix/7288-path-dependent-codegen

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.

proggeramlug added a commit that referenced this pull request Aug 5, 2026
…2 counters (#7425)

* perf(codegen): revive the #5093 class-field versioned loop for canonical-i32 counters

Repsel Phase 1 made the canonical i32 slot the ONLY storage for a proven-integer
local, so such a local has no `ctx.locals` entry. The #5093 matcher gated its
counter and its bound on `ctx.locals`, so it matched nothing.

Also teach the sloppy class-field store (#7423) about the loop fact, so the fast
clone stays call-free.

* test(codegen): assert the class-field versioned loop is actually reached (#7287)

* docs(changelog): #7287 class-field loop guard hoist

* docs: name the fragment for its real PR (#7425)

---------

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.

build: byte-identical source compiles to a 44x-slower object depending on where the .ts file lives; the published baseline measures the fast arm

1 participant