Skip to content

majit: resolve a forced instance's w_class in optimize_getfield, not virtualize (#915 P1) - #920

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

youknowone merged 2 commits into
mainfrom
rewrite-tracer

Conversation

@youknowone

@youknowone youknowone commented Jul 31, 2026 •

Copy link
Copy Markdown
Collaborator

Follow-up to #915, which merged before its Codex review could be acted on. The P1 raised there is real, and worse than the report described — the fold it names is live on main right now.

The defect

#915's f20f9925c0 extended the w_class header fold from PtrInfo::Virtual to PtrInfo::Instance, so the class stayed usable after an allocation escaped. But virtualize.py:184-195 optimize_GETFIELD_GC_* folds only under opinfo.is_virtual() and otherwise emits, leaving a non-virtual's read to heap.py. That layering is load-bearing for this field:

  • force_box installs the forced instance with an empty field list (info.rs:1113-1118 — deliberately, so heap's do_setfield cannot MUST_ALIAS-elide the materializing SETFIELD_GC),
  • and emits the header write as an ordinary SetfieldGc, which lands in CachedField::lazy_set,
  • while OptVirtualize runs a pass earlier than OptHeap.

So the Instance arm saw an empty slot on every forced instance and answered from the size descr's canonical class. Two consequences: the recorded-value branch was effectively dead code, and the entire 43 → 37 op win came from the unsound fallback. An instance retagged to a user subclass and then forced would read back as the base class — a silent miscompile of a class check, not a crash.

The fix

Both resolutions move into optimize_getfield, after the cache consult: a value already in the instance's own slot wins, and only when the header was never written does the layout's canonical class answer — gated on no w_class store still being pending.

The pending check scans every is_w_class() descr rather than the one being read. The caches are keyed by Arc::as_ptr (heap.rs cached_field_pos_for_descr) and pyre mints more than one descr for this header (a walker singleton with no parent, and the size descr's own all_fielddescrs entry), so a miss on the read descr does not prove the field is untouched — the store may sit in the other spelling's lazy_set.

The slot is still looked up through the layout's own w_class fielddescr, never the read descr's index: a parent-bearing spelling has index_in_parent == 0, which is the first value field, and folding through it forwards Ref <- Int.

One wrong turn worth recording

The first gate also refused on !cached_structs.is_empty() and dropped the loop to 40 ops. Plain reads fill cached_structs via register_info, so the first w_class read blocked every later fold. Only lazy_set distinguishes a pending write; a store that has already been flushed is readable from the instance slot instead. Gating on lazy_set alone restores 31 ops.

Regression coverage

w_subclass2.py does not cover this shape — it passed throughout the unsound window. Added retag_force.py: MyTuple/MyInt interleaved with their base types, class read both before and after the forcing store.

NO_JIT (oracle) : 25000 50000 1249975000 tuple MyTuple
dynasm JIT      : 25000 50000 1249975000 tuple MyTuple
cranelift JIT   : 25000 50000 1249975000 tuple MyTuple

Also here

majit: exit the short-preamble force loop on a non-growing arg list — the loop reads grown[num_short_jump_args..] immediately after testing grown.len() == num_short_jump_args, so a list that ever came back shorter would slice out of bounds instead of taking the "did not grow" exit. Raised on #556, which added the surrounding guard.

Verification

  • check.py --backend dynasm,cranelift: dynasm 349/349, cranelift 349/349
  • cargo test --release -p majit-metainterp --features dynasm: 1425 passed, 0 failed
  • Append loop steady body stays at 31 ops; w_subclass2.py and synth/callee_store_global_read_after_call (240008) unchanged on both backends and under PYRE_NO_JIT

Not addressed

The parity review's §2 on unroll.rs (signal_invalid_loop where upstream's mapping[box] raises KeyError) is left as-is. #556 already ported upstream's own answer for an unmapped jump arg in this function — unroll.py:425-434, which defers to the outer replay loop rather than raising — and the same carrier is used 25 lines earlier for optimizer.flush(). That reads as a documented structural adaptation (§4), not a new divergence.

— authored by Claude

Summary by CodeRabbit

  • Bug Fixes
    • Improved optimization of class-field reads by reusing known stored values when safe.
    • Preserved unresolved or pending class-field reads for correct later processing.
    • Fixed short-preamble handling when jump arguments shrink, preventing invalid processing.
    • Corrected forced object reads so they are handled safely during subsequent heap processing.

…virtualize

`virtualize.py:184-195 optimize_GETFIELD_GC_*` folds a field read only under
`opinfo.is_virtual()` and otherwise emits, leaving a non-virtual's read to
`heap.py`. The w_class arm added in f20f992 folded for `PtrInfo::Instance`
too, which runs a pass earlier than that layering allows.

`force_box` installs the forced instance with an EMPTY field list
(info.rs:1113-1118, so heap's `do_setfield` cannot MUST_ALIAS-elide the
materializing SETFIELD_GC) and emits the header write as an ordinary
`SetfieldGc`, where it sits in `CachedField::lazy_set`. The virtualize arm
therefore saw no stored value on every forced instance and answered from the
size descr's canonical class, discarding a pending retag to a user subclass.

Move both resolutions into `optimize_getfield`, after the cache consult: a
value already in the instance's own slot wins, otherwise the layout's class
answers only when no `w_class` store is still pending. The pending check scans
every `is_w_class()` descr rather than the one being read, because the caches
are keyed by `Arc::as_ptr` (`cached_field_pos_for_descr`) and pyre mints more
than one descr for this header, so a miss on the read descr does not prove the
field is untouched. The slot is looked up through the layout's own w_class
fielddescr, never the read descr's index.

Append loop steady body stays at 31 ops.

Assisted-by: Claude
The loop reads `grown[num_short_jump_args..]` right after testing
`grown.len() == num_short_jump_args`, so a list that ever came back shorter
would slice out of bounds instead of taking the "did not grow" exit. Test with
`<=`. Raised on #556, which added the surrounding guard.

Assisted-by: Claude
@coderabbitai

coderabbitai Bot commented Jul 31, 2026 •

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The optimizer now preserves forced w_class reads for heap processing, folds eligible reads using stored or canonical class values, detects pending stores across descriptor spellings, and safely terminates short-preamble fixpoint iteration when arguments do not grow.

Changes

w_class read handling

Layer / File(s) Summary
Preserve forced reads
majit/majit-metainterp/src/optimizeopt/virtualize.rs
Forced PtrInfo::Instance w_class reads remain emitted for downstream heap processing.
Heap-level read folding
majit/majit-metainterp/src/optimizeopt/heap.rs
Heap optimization detects pending w_class stores and folds eligible reads to stored values or canonical layout classes.

Short-preamble fixpoint

Layer / File(s) Summary
Fixpoint termination
majit/majit-metainterp/src/optimizeopt/unroll.rs
The loop terminates when the jump-argument list is unchanged or shorter, avoiding an invalid growth-dependent slice.

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

Possibly related PRs

Suggested reviewers: lifthrasiir

Poem

A rabbit checked each class-field trail,
Kept forced reads on the heap’s next rail.
Stored values fold, pending stores stay,
Canonical classes guide the way.
Short preambles stop when lists don’t grow—
Hop, hop, safer paths now flow!

🚥 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 moving forced-instance w_class resolution to optimize_getfield, which is the primary change.
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 54e8fc9).
Updated: 2026-07-31T09:39:39.638Z

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

  • majit/majit-metainterp/src/optimizeopt/heap.rs:2281 ↔ rpython/jit/metainterp/optimizeopt/heap.py:103 — an alternate-descriptor pending w_class store is detected but neither returned nor forced. The code suppresses canonical folding via w_class_set_pending(), then falls through to the descriptor-identity cache lookup at heap.rs:2307; a GETFIELD_GC using the other spelling can therefore be emitted before the delayed SETFIELD_GC. PyPy’s matching cache returns the lazy store RHS for a must-alias read.

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

None.

4. Structural adaptations

  • majit/majit-metainterp/src/optimizeopt/virtualize.rs:850 ↔ rpython/jit/metainterp/optimizeopt/virtualize.py:184 — Pyre’s extra PyObject.w_class header is distinct from its dispatch ob_type; PyPy has no corresponding separately loaded field. Resolving a virtual object’s Python-level class through the layout is a necessary object-model adaptation.

  • majit/majit-metainterp/src/optimizeopt/heap.rs:2230 ↔ rpython/jit/metainterp/optimizeopt/heap.py:641 — moving forced-instance w_class handling to heap preserves PyPy’s pass layering: virtualize.py only folds virtual fields, while non-virtual reads proceed to heap. The added branch is required by Pyre’s separate class header.

  • majit/majit-metainterp/src/optimizeopt/unroll.rs:4046 ↔ rpython/jit/metainterp/optimizeopt/unroll.py:425 — Rust uses <= instead of PyPy’s == before slicing newly appended jump arguments. Under the shared monotonic-growth invariant this is equivalent; the broader condition is a Rust bounds-safety adaptation for an otherwise-invalid shrink.

@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: 54e8fc976e

ℹ️ 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 +2281 to +2282
if !self.w_class_set_pending() {
if let Some(w_class) = size_descr

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 the pending w_class store before emitting the load

When forcing a virtual whose w_class was retagged, force_box can queue the materializing SETFIELD_GC under the layout descriptor while the subsequent read uses the walker descriptor. Detecting that pending store here merely suppresses the constant fold; execution then falls through and emits the GETFIELD_GC before the lazy store is flushed, so the load observes the allocation's canonical class and a following guard emits the retag too late. Resolve the pending value for the same object or force its store before allowing the load to be emitted.

AGENTS.md reference: AGENTS.md:L14-L20

Useful? React with 👍 / 👎.

Comment on lines +4043 to +4046
// `<=`, not `==`: the list only grows here, but the slice below
// indexes from `num_short_jump_args`, so a shrink would panic
// rather than fall into the "did not grow" exit.
if grown.len() <= num_short_jump_args {

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 Restore the upstream equality check

Upstream rpython/jit/metainterp/optimizeopt/unroll.py:425 uses equality here, and the new comment itself states that this list can only grow, so the <= branch adds behavior only for an unreachable shrink rather than fixing a real slicing path. Keep the literal upstream condition instead of introducing an unnecessary structural divergence in this line-by-line port.

AGENTS.md reference: AGENTS.md:L194-L196

Useful? React with 👍 / 👎.

@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 `@majit/majit-metainterp/src/optimizeopt/heap.rs`:
- Around line 2250-2269: Update the special `w_class` lookup in the surrounding
heap optimization logic to derive the layout slot via `Self::field_slot_index()`
rather than `index_in_parent()`, matching `force_lazy_set_field` and
`w_class_descr()`. Ensure `w_class_set_pending()` checks pending stores under
both the parentless and parented `w_class` descriptor cache entries.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 763a0aa7-e5d9-4441-b6c9-c87f0f374fc0

📥 Commits

Reviewing files that changed from the base of the PR and between 59d87af and 54e8fc9.

📒 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

Comment on lines +2250 to +2269
if descr.as_field_descr().is_some_and(|fd| fd.is_w_class()) {
let obj_box = ctx.get_box_replacement_operand(obj);
if let Some(crate::optimizeopt::info::PtrInfo::Instance(iinfo)) =
ctx.peek_ptr_info(&obj_box)
{
let size_descr = iinfo.descr.as_ref().and_then(|d| d.as_size_descr());
let stored = size_descr
.and_then(|sd| {
sd.all_fielddescrs()
.iter()
.find(|fd| fd.is_w_class())
.map(|fd| fd.index_in_parent() as u32)
})
.and_then(|widx| {
iinfo
.fields
.iter()
.find(|(idx, _)| *idx == widx)
.map(|(_, e)| e.clone())
});

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 | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Compare w_class descr spellings and the slot index used by reads vs writes.
set -euo pipefail

# All w_class descr constructions and their parent/index setup.
rg -nP -C 6 '\bis_w_class\b' --type=rust

# Slot index helper and every structinfo write site.
rg -nP -C 4 'fn field_slot_index|structinfo_setfield\(' --type=rust

Repository: youknowone/pyre

Length of output: 153


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "tracked rust files around majit:"
git ls-files | rg '(^|/)majit/majit-metainterp/src/optimizeopt/heap\.rs$|.*\.rs$' | rg 'majit|maji' | head -200 || true

echo
repo_files=$(git ls-files '*.rs' | wc -l)
echo "total rust tracked files: $repo_files"
if [ "$repo_files" -gt 2000 ]; then
  echo "limitting rust search by repository dirs"
fi

echo
echo "locating is_w_class and w_class-related text in Rust files:"
rg -n --glob '*.rs' -C 4 'is_w_class|w_class|field_slot_index|structinfo_setfield|force_lazy_set_field|lazy_set' . || true

echo
echo "file size for target file:"
wc -l majit/majit-metainterp/src/optimizeopt/heap.rs 2>/dev/null || true

Repository: youknowone/pyre

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

target="majit/majit-metainterp/src/top/optimizeopt/heap.rs"
if [ -f "$target" ]; then
  echo "FOUND $target"
  wc -l "$target"
else
  echo "target missing at $target"
  git ls-files | rg 'majit/.*/optimizeopt/heap\.rs|maxit/.*/optimizeopt/heap\.rs|.*opt/heap\.rs$' || true
fi

echo
echo "search is_w_class in target if present:"
if [ -f "$target" ]; then
  rg -n -C 6 'is_w_class|field_slot_index|structinfo_setfield|force_lazy_set_field|w_class_set_pending|lazy_set|PendingFieldInfo|PENDINGFIELDSTRUCT' "$target"
fi

echo
echo "line ranges around cited code if present:"
if [ -f "$target" ]; then
  sed -n '880,955p' "$target" | nl -ba -v880
  echo "---"
  sed -n '2230,2290p' "$target" | nl -ba -v2230
  echo "---"
  sed -n '2710,2760p' "$target" | nl -ba -v2710
fi

Repository: youknowone/pyre

Length of output: 353


🏁 Script executed:

#!/bin/bash
set -euo pipefail

target="majit/majit-metainterp/src/optimizeopt/heap.rs"
wc -l "$target"

echo
echo "targeted search in heap.rs:"
rg -n -C 4 'is_w_class|field_slot_index|structinfo_setfield|force_lazy_set_field|w_class_set_pending|lazy_set|PENDINGFIELDSTRUCT' "$target"

echo
echo "cited line ranges in heap.rs:"
sed -n '880,955p' "$target" | nl -ba -v880
echo "---"
sed -n '2230,2290p' "$target" | nl -ba -v2230
echo "---"
sed -n '2710,2760p' "$target" | nl -ba -v2710

echo
echo "file size and module-level imports for types:"
sed -n '1,120p' "$target" | nl -ba -v1

Repository: youknowone/pyre

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

target="majit/majit-metainterp/src/optimizeopt/heap.rs"

echo "field descriptions around header:"
sed -n '190,300p' "$target" | nl -ba -v190

echo
echo "structinfo_setfield implementations:"
rg -n -C 12 'fn structinfo_setfield|structinfo_setfield\(' majit/majit-metainterp/src/optimizeopt majit/majit-lower src -g '*.rs' || true

echo
echo "cached_fields keys and w_class lookup implementation:"
sed -n '22,27p' "$target" | nl -ba -v22
sed -n '533,565p' "$target" | nl -ba -v533
sed -n '846,930p' "$target" | nl -ba -v846
sed -n '1030,1065p' "$target" | nl -ba -v1030
sed -n '2240,2305p' "$target" | nl -ba -v2240
sed -n '2358,2480p' "$target" | nl -ba -v2358
sed -n '2522,2555p' "$target" | nl -ba -v2522

Repository: youknowone/pyre

Length of output: 227


🏁 Script executed:

#!/bin/bash
set -euo pipefail

target="majit/majit-metainterp/src/optimizeopt/heap.rs"

echo "field descriptions around header:"
sed -n '190,300p' "$target"

echo
echo "structinfo_setfield implementations:"
rg -n -C 12 'fn structinfo_setfield|structinfo_setfield\(' majit/majit-metainterp/src/optimizeopt majit/majit-lower pyre/pyre-jit-trace/src -g '*.rs' || true

echo
echo "cached_fields keys and w_class lookup implementation:"
sed -n '190,230p' "$target"
echo "---"
sed -n '533,565p' "$target"
echo "---"
sed -n '846,930p' "$target"
echo "---"
sed -n '1030,1065p' "$target"
echo "---"
sed -n '2240,2305p' "$target"
echo "---"
 sed -n '2358,2480p' "$target"
echo "---"
sed -n '2522,2555p' "$target"
echo "---"
sed -n '2701,2775p' "$target"

Repository: youknowone/pyre

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "OptContext::structinfo_setfield full implementation:"
sed -n '8124,8170p' majit/majit-metainterp/src/optimizeopt/mod.rs

echo
echo "cached_fields tuple semantics:"
sed -n '22,28p' majit/majit-metainterp/src/optimizeopt/heap.rs

echo
echo "pyre w_class descriptor construction relevant parts:"
sed -n '1908,1954p' pyre/pyre-jit-trace/src/descr.rs
sed -n '1038,1007p' pyre/pyre-jit-trace/src/descr.rs || sed -n '996,1058p' pyre/pyre-jit-trace/src/descr.rs

echo
echo "read-only verifier of field_slot_index semantics for w_class spellings:"
python3 - <<'PY'
class FieldDescr:
    def __init__(self, index, parent):
        self.index = index
        self.parent = parent
    def get_parent_descr(self):
        return self.parent
    def index_in_parent(self):
        return self.index_in_parent

def field_slot_index(descr):
    if descr.get_parent_descr() is None:
        return descr.index
    return descr.index_in_parent()

# Mirrors pyre: singleton PyObject.w_class has index_in_parent=0 and parent=None.
parentless = type("Descr", (), {"index": 10, "as_field_descr": lambda self: type("Fd", (), {"index_in_parent": lambda: 0, "get_parent_descr": lambda: None})()})()
# Mirrors size_descr sibling/w_class field: has parent; parent-local index = 1.
parented = type("Descr", (), {"index": 0, "as_field_descr": lambda self: type("Fd", (), {"index_in_parent": lambda: 1, "get_parent_descr": lambda: object()})()})()

print("parentless_w_class_slot", field_slot_index(parentless))
print("size_sibling_w_class_slot", field_slot_index(parented))
print("parented_structure_match", field_slot_index(parented) == 1)
PY

Repository: youknowone/pyre

Length of output: 5431


Align the flushed w_class stored field slot with the layout spelling.

force_lazy_set_field restores flushed w_class stores with Self::field_slot_index(&put_back_op.getdescr().unwrap()), which uses index() for the parentless w_class_descr(). The special lookup here reads only the parented layout spelling, and w_class_set_pending() only checks that cache entry. Use Self::field_slot_index() for the layout’s w_class fielddescr and check pending stores across both w_class cachings.

🤖 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 `@majit/majit-metainterp/src/optimizeopt/heap.rs` around lines 2250 - 2269,
Update the special `w_class` lookup in the surrounding heap optimization logic
to derive the layout slot via `Self::field_slot_index()` rather than
`index_in_parent()`, matching `force_lazy_set_field` and `w_class_descr()`.
Ensure `w_class_set_pending()` checks pending stores under both the parentless
and parented `w_class` descriptor cache entries.

@youknowone
youknowone merged commit 93a7239 into main Jul 31, 2026
19 checks passed
@youknowone
youknowone deleted the rewrite-tracer branch July 31, 2026 10:48
@youknowone

Copy link
Copy Markdown
Collaborator Author

Both P1s and the CodeRabbit finding are correct, and this merged before they could be acted on. Addressed in #922.

They converge on one conclusion: the heap-side fold is still unsound in two distinct ways. When a store is pending under the other descr spelling, suppressing the fold only declines the constant — the GETFIELD_GC is then emitted ahead of the unflushed SETFIELD_GC, so the load reads the pre-retag word. And when a store has already been flushed, put_field_back_to_info slots it by field_slot_index(write_descr) — Descr::index() for the parentless walker spelling, not the index_in_parent() the lookup reads — so the value is invisible and lazy_set is empty by then.

Neither is patchable without stacking another special case on the descr split, and upstream has no counterpart to port: jtransform.py:1004-1009 handle_getfield_typeptr deletes this read at codewriter time. #922 removes both arms. Cost is 31 → 40 ops on the append loop — 9, not the 6 the original 43 → 37 measurement implied, so the fold was also carrying part of the _immutable_fields_ win.

The <= on unroll.rs is reverted there too: unroll.py:425 uses equality and the list can only grow, so it was an unreachable branch bought with a divergence in a line-by-line port.

— commented by Claude

youknowone added a commit that referenced this pull request Jul 31, 2026
…izer (#920 review) (#922)

* Revert "majit: exit the short-preamble force loop on a non-growing arg 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

* majit: stop resolving a forced instance's w_class header in the optimizer

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