Skip to content

fix(codegen): read-PIC honors descriptors installed after priming (#6080) - #6254

Merged
proggeramlug merged 1 commit into
mainfrom
fix/6080-pic-descriptor-guard
Jul 10, 2026
Merged

fix(codegen): read-PIC honors descriptors installed after priming (#6080)#6254
proggeramlug merged 1 commit into
mainfrom
fix/6080-pic-descriptor-guard

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes sub-bug (b) of #6080: the read inline-cache (Read-PIC) bypassed a property/accessor descriptor installed after the call site was primed.

The generic property-get IC hit predicate (expr/property_get/generic_dispatch.rs) validated the receiver's keys_array pointer, GC type, object_type, non-null keys and closure-magic — but never checked OBJ_FLAG_HAS_DESCRIPTORS. Since Object.defineProperty does not change keys_array, a site primed on a plain data property kept serving a raw slot load after that key became a getter (or was redefined), silently returning the stale value instead of invoking the accessor.

Fix

Add a descriptor-flag guard to the hit predicate, mirroring the existing one in class_field_inline_guard.rs: load the GcHeader _reserved i16 at offset -6 and force a PIC miss whenever OBJ_FLAG_HAS_DESCRIPTORS (0x800) is set. The read then routes through js_object_get_field_ic_missjs_object_get_field_by_name, which honors descriptors. Cost is a single load+and+cmp folded into the existing hit cond_br; descriptor-free objects keep hitting the cache.

Verification

function f(o){ return o.x; }
const a = { x: 1 };
f(a);                                              // prime -> 1
Object.defineProperty(a, "x", { get(){ return 42; } });
f(a);                                              // before: 1 (stale)  after: 42 ✓
  • Deterministic repro now matches Node (42, and data-redefine 99).
  • Sanity: hot property-read loops still hit the cache (unchanged sums), getters-from-start and untouched sibling keys read correctly.
  • Added test-files/test_gap_6080_defineproperty_after_prime.ts — byte-for-byte parity with node --experimental-strip-types.

Scope / follow-up

Leaves the separate ABA-staleness sub-bug (a) of #6080 as a follow-up: the cache slot stores a raw, unrooted keys_array address that can be recycled by a different shape after GC. That fix needs GC-root registration or hot-path content validation and is not deterministically reproducible; it's the higher-risk half and deserves its own PR.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed property reads after Object.defineProperty changes a property into a getter or redefined data property.
    • Ensured property accesses no longer return stale cached values after descriptor changes.
    • Preserved correct reads for unrelated properties on the same object.

)

The generic property-get inline cache hit predicate checked only the
receiver's keys_array pointer, GC type, object_type, non-null keys and
closure-magic. It did not consult OBJ_FLAG_HAS_DESCRIPTORS, so a call
site primed on a plain data property kept serving a raw slot load after
Object.defineProperty converted that key to an accessor (or redefined
its descriptor) — keys_array is unchanged by defineProperty, so the
stale hit path bypassed the getter entirely.

Add a descriptor-flag guard to the hit predicate (mirroring the one in
class_field_inline_guard.rs): load the GcHeader `_reserved` i16 at
offset -6 and force a PIC miss whenever OBJ_FLAG_HAS_DESCRIPTORS (0x800)
is set, routing the read through js_object_get_field_ic_miss ->
js_object_get_field_by_name, which honors descriptors. The guard is a
single load+and+cmp folded into the existing `hit` cond_br; normal
descriptor-free objects keep hitting the cache.

Scope: this closes sub-bug (b) of #6080 (defineProperty-after-prime
bypass), which has a deterministic repro. The separate ABA-staleness
sub-bug (a) — the cache slot is a raw, unrooted keys_array address that
can be recycled after GC — is left as a follow-up; it needs GC root
registration / content validation on the hot path and is not
deterministically reproducible.

Regression test: test-files/test_gap_6080_defineproperty_after_prime.ts
@coderabbitai

coderabbitai Bot commented Jul 10, 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: 6d934ce4-3132-421e-9d1e-1c4db3a019dd

📥 Commits

Reviewing files that changed from the base of the PR and between 7ed3e5c and 1f7c8e2.

📒 Files selected for processing (2)
  • crates/perry-codegen/src/expr/property_get/generic_dispatch.rs
  • test-files/test_gap_6080_defineproperty_after_prime.ts

📝 Walkthrough

Walkthrough

The generic property GET PIC fast path now rejects receivers marked as having descriptors, routing them through descriptor-aware miss handling. A regression test primes property reads, applies Object.defineProperty, and verifies getter, redefined data-property, and unrelated-key behavior.

Changes

Descriptor-aware property reads

Layer / File(s) Summary
PIC descriptor guard and regression coverage
crates/perry-codegen/src/expr/property_get/generic_dispatch.rs, test-files/test_gap_6080_defineproperty_after_prime.ts
The PIC fast-hit predicate checks OBJ_FLAG_HAS_DESCRIPTORS; tests verify that post-priming descriptor changes return updated values while unrelated own keys remain readable.

Estimated code review effort: 3 (Moderate) | ~15–30 minutes

Possibly related issues

Possibly related PRs

  • PerryTS/perry#5675 — Modifies the same generic property-GET dispatch logic at related fast-path decision points.
  • PerryTS/perry#6057 — Adds a related OBJ_FLAG_HAS_DESCRIPTORS receiver check for disabling stale inline fast paths.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: read-PIC now honors descriptors installed after priming.
Description check ✅ Passed The description covers the fix, verification, and scope, though some template sections are not filled verbatim.
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 fix/6080-pic-descriptor-guard

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
proggeramlug merged commit 44675d0 into main Jul 10, 2026
23 of 25 checks passed
@proggeramlug
proggeramlug deleted the fix/6080-pic-descriptor-guard branch July 10, 2026 19:52
proggeramlug added a commit that referenced this pull request Aug 5, 2026
…s recycling (#6080) (#7434)

* fix(gc/codegen): epoch-gate read-PIC pointer tokens against GC address recycling (#6080)

#6080(a) residual: class instances (and any receiver without a #6804
ShapeId stamp) still prime the RAW keys-array pointer into their
per-site @perry_ic_N cache. Those globals are invisible to every GC
scanner, and GC_FLAG_SHAPE_SHARED keeps a keys array LIVE but not
address-STABLE - the copying minor moves it like anything else and the
vacated from-space address is recycled, so a stale prime can
pointer-match a different-shape keys array and the inline hit path
loads the wrong slot, silently.

Fix (the epoch design from the #6080 thread, narrowed to pointer
tokens):

- runtime: process-global PERRY_IC_EPOCH (starts at 1 so a
  zeroinitializer cache can never match), bumped in
  GcStats::record_collection - the single per-collection funnel - and
  again at budgeted-sweep ENTRY, because budgeted sweep slices
  interleave with the mutator before the end-of-cycle funnel runs.
- js_object_get_field_ic_miss snapshots the live epoch into cache[2]
  at prime time (cache widens [i64;2] -> [i64;3]; the emitted global
  was already [8 x i64], so no layout change).
- codegen: the inline monomorphic hit predicate requires
  cache[2] == @PERRY_IC_EPOCH before trusting a pointer token.
  Shape-ID tokens (bit 62) skip the check - ids are never reused - so
  the hot stamped-plain-object population never re-primes after GC.

Coverage: IR contract test asserts the emitted guard (epoch-slot gep +
@PERRY_IC_EPOCH load); runtime tests assert the funnel bumps the epoch
and that a pointer-token prime goes stale on bump (the guard's exact
inputs, so the instrument provably CAN fail). Probe run on Windows:
200k class-instance reads through one monomorphic site under copying
minors (copied_objects>0 confirmed via PERRY_GC_DIAG) and under
PERRY_GEN_GC=0, all values correct; defineProperty-after-prime (#6254
half) still honored.

Sibling not covered here: the #6812 3-way dynamic-key WRITE IC compares
the same discriminated shape token and packs 4 ways into its 8-slot
global (no free epoch slot); noted on #6080.

* docs: changelog fragment for #7434

---------

Co-authored-by: Ralph Kuepper <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