Skip to content

majit: stop resolving a forced instance's w_class header in the optimizer (#920 review) - #922

Merged
youknowone merged 2 commits into
mainfrom
rewrite-tracer
Jul 31, 2026
Merged

majit: stop resolving a forced instance's w_class header in the optimizer (#920 review)#922
youknowone merged 2 commits into
mainfrom
rewrite-tracer

Conversation

@youknowone

@youknowone youknowone commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Acts on the two P1s and the CodeRabbit finding on #920, which merged before they could be addressed.

All three land on the same conclusion: the w_class fold cannot be made sound while reads and writes of that header carry different descr spellings. Removing it is the fix; re-earning the ops needs the split closed first.

Why the heap-side version was still wrong

#920 moved the resolution into optimize_getfield, after the cache consult, gated on no store being pending. Two paths still answer with the layout's canonical class while a retag to a user subclass is outstanding:

  • A store IS pending under the other spelling. Suppressing the fold only declines the constant. Execution falls through and emits the GETFIELD_GC while the materializing SETFIELD_GC is still sitting unflushed in CachedField::lazy_set, so at runtime the load reads the pre-retag word and the store lands after it. Upstream's UNKNOWN_ALIAS path calls force_lazy_set before returning a miss precisely to prevent this; the cross-spelling case never reaches that call.
  • A store has already been flushed. put_field_back_to_info slots it by field_slot_index(write_descr), which for the parentless walker spelling is Descr::index() — not the index_in_parent() the lookup reads. The value is invisible to the read, and lazy_set is empty by then, so the gate passes and the canonical class wins anyway.

Why not patch it again

Upstream has no counterpart to repair. jtransform.py:1004-1009 handle_getfield_typeptr deletes this read at codewriter time — no typeptr getfield reaches the optimizer at all — and descr.py:236-238 cannot mint the parentless FieldDescr the walker uses here. Every optimizer-side repair so far has been a special case stacked on a split that upstream does not have, and each one has needed another. AGENTS.md's parity rule points the other way.

Cost

Append loop steady body 31 → 40 ops. That is 9, not the 6 the original 43 → 37 measurement implied — the fold was also carrying part of the _immutable_fields_ win. Correctness first; the ops come back when the read and the write name the same descr.

Also reverted

majit: exit the short-preamble force loop on a non-growing arg list (==<=). unroll.py:425 uses equality and the list can only grow at that point, so it covered an unreachable shrink at the cost of a divergence in a line-by-line port — #556 left it literal for that reason. Reverted.

Verification

  • check.py --backend dynasm,cranelift: dynasm 349/349, cranelift 349/349
  • retag_force.py (class read before AND after the forcing store) matches the PYRE_NO_JIT oracle on both backends: 25000 50000 1249975000 tuple MyTuple
  • w_subclass2.py unchanged: 799600000 50000 MyInt int

Follow-up

The blocker named here is the same one blocking the LOAD_ATTR method-cache pair: the walker mints ad-hoc field descrs with no parent, so its reads share neither the OptHeap cache key nor the PtrInfo slot with the layout's own fielddescrs, and AbstractStructPtrInfo.produce_short_preamble_ops (info.py:255-271) has no _fields to export. Unifying the walker's field-descr minting with the layout's is the prerequisite for both.

authored by Claude

Summary by CodeRabbit

  • Bug Fixes

    • Improved optimization stability by ensuring processing completes only after all jump arguments are fully accounted for.
    • Updated field access handling to follow the standard loading path, improving consistency and reducing the risk of incorrect value substitutions.
  • Documentation

    • Clarified optimization behavior and limitations for specialized object field reads.

…g list"

This reverts commit 54e8fc9.

`unroll.py:425` uses equality, and the list at this point can only grow, so
`<=` covered an unreachable shrink at the cost of a divergence from the
line-by-line port. #556 left the condition literal for that reason.

Assisted-by: Claude
…izer

The fold cannot be made sound while reads and writes of this header carry
different descr spellings.

`optimize_getfield` resolved it after the cache consult, gated on no store
being pending. Two paths still answer with the layout's canonical class while a
retag to a user subclass is outstanding:

- when a store IS pending under the other spelling, suppressing the fold only
  declines the constant — the GETFIELD_GC is then emitted ahead of the still
  unflushed SETFIELD_GC, so the load reads the pre-retag word;
- when a store has already been flushed, `put_field_back_to_info` slots it by
  `field_slot_index(write_descr)`, which for the parentless walker spelling is
  `Descr::index()`, not the `index_in_parent()` the lookup reads, so the value
  is invisible and `lazy_set` is empty by then.

Upstream has no counterpart to repair: `jtransform.py:1004-1009
handle_getfield_typeptr` deletes the read at codewriter time, so no typeptr
getfield reaches the optimizer at all, and `descr.py:236-238` cannot mint the
parentless FieldDescr the walker uses here.

Remove both arms. Append loop steady body 31 -> 40 ops; re-earning that needs
the descr split closed first, not another special case above it.

Assisted-by: Claude
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d9f6918c-736f-404b-8649-e94404d27b51

📥 Commits

Reviewing files that changed from the base of the PR and between 93a7239 and 4dcad94.

📒 Files selected for processing (3)
  • majit/majit-metainterp/src/optimizeopt/heap.rs
  • majit/majit-metainterp/src/optimizeopt/unroll.rs
  • majit/majit-metainterp/src/optimizeopt/virtualize.rs
💤 Files with no reviewable changes (1)
  • majit/majit-metainterp/src/optimizeopt/heap.rs

Walkthrough

The optimizer stops folding w_class reads from heap metadata and pending stores. Comments document the deferred handling of forced-object reads. The short-preamble fixpoint loop now requires stable jump-argument list length.

Changes

Optimizer updates

Layer / File(s) Summary
w_class read handling
majit/majit-metainterp/src/optimizeopt/heap.rs, majit/majit-metainterp/src/optimizeopt/virtualize.rs
Removed pending-store detection and special w_class GETFIELD folding. Expanded comments explain why forced-object reads remain unresolved.
Short-preamble fixpoint termination
majit/majit-metainterp/src/optimizeopt/unroll.rs
The loop exits only when the current and previous jump-argument lists have equal lengths.

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

Possibly related PRs

Suggested reviewers: lifthrasiir

Poem

A rabbit hops through fields of class,
No hidden fold slips through the grass.
The preamble counts with care,
Until lengths match in air.
Thump, thump—clean paths at last!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: stopping optimizer resolution of a forced instance's w_class header.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 rewrite-tracer

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.

@github-actions

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit 4dcad94).
Updated: 2026-07-31T11:05:07.999Z

Files in the reviewed diff
majit/majit-metainterp/src/optimizeopt/heap.rs
majit/majit-metainterp/src/optimizeopt/unroll.rs
majit/majit-metainterp/src/optimizeopt/virtualize.rs

1. Regressions to PyPy parity introduced by this patch

None.

2. Other mismatches introduced by this patch

None.

3. Pre-existing mismatches (already present before this patch)

  • majit/majit-metainterp/src/optimizeopt/unroll.rs:3868 ↔ rpython/jit/metainterp/optimizeopt/unroll.py:400 — Rust stops the short-preamble fix-point loop after 20 iterations ("fixpoint loop exceeded 20 iterations, breaking"); PyPy uses unconditional while 1 until convergence. This cap can return an incompletely processed preamble.

4. Structural adaptations

  • majit/majit-metainterp/src/optimizeopt/heap.rs:2207 ↔ rpython/jit/codewriter/jtransform.py:1004 — Pyre permits a forced-instance Python-level w_class header load to proceed through heap optimization; PyPy rewrites an RPython typeptr getfield to guard_class before optimization. The patch correctly removes the unsound Pyre-only forced-instance constant fold.

  • majit/majit-metainterp/src/optimizeopt/virtualize.rs:860 ↔ rpython/jit/codewriter/jtransform.py:1004 — Pyre folds w_class only while the object remains virtual; PyPy has no analogous optimizer case because its type-pointer read is eliminated by codewriter transformation. This is required by Pyre’s CPython-compatible object/compiler model.

  • majit/majit-metainterp/src/optimizeopt/unroll.rs:3998 ↔ rpython/jit/metainterp/optimizeopt/unroll.py:412 — Rust preserves a seeded mapping for colliding allocation OpRefs (skip_insert), while PyPy unconditionally assigns mapping[sop] = op. This compensates for Pyre’s flat OpRef namespace, where an allocation result can collide with a loop-input identity; PyPy’s distinct Box identities cannot collide.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4dcad9490b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +909 to +913
// the header write into `OptHeap`, where it sits in
// `CachedField::lazy_set`. A retag to a user subclass would be
// discarded and the base class answered instead. Resolving it
// in `OptHeap` does not close the hole either: reads and writes
// of this header carry different descr spellings, and the

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Flush retag stores before passing on forced-header reads

When a virtual with an overridden w_class is forced by a side-effect-free operation before a later class read, its materializing SETFIELD_GC is cached here under the layout descriptor while the subsequent GETFIELD_GC uses the standalone header descriptor. The read therefore misses the pending store, and because GETFIELD_GC is side-effect-free, OptHeap::emitting_operation does not flush lazy sets before emitting it; the generated order is the class load followed by the retag store, so the load observes the allocation's original base class and can make type/class guards silently take the wrong path. Merely removing the fold does not fix this case—the pending cross-spelling store must be forced before the read, or the descriptors must first be unified.

AGENTS.md reference: AGENTS.md:L14-L19

Useful? React with 👍 / 👎.

@youknowone
youknowone merged commit d14e80f into main Jul 31, 2026
19 checks passed
@youknowone
youknowone deleted the rewrite-tracer branch July 31, 2026 12:47
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