Skip to content

jit-trace: bridge the codewriter's PyObject.w_class read to the walker descr (append loop 40 → 33 ops) - #931

Merged
youknowone merged 2 commits into
mainfrom
rewrite-tracer
Aug 1, 2026
Merged

jit-trace: bridge the codewriter's PyObject.w_class read to the walker descr (append loop 40 → 33 ops)#931
youknowone merged 2 commits into
mainfrom
rewrite-tracer

Conversation

@youknowone

@youknowone youknowone commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Recovers most of what #922 gave up, and this time by deleting a duplicate read rather than folding past a pending write.

The duplicate

make_descr_from_bh already redirects a codewriter-lowered body's field descr to the walker's canonical one — for W_ListObject.int_items.*, ItemsBlock.capacity and the box payloads. The reason is stated in that function: the heapcache and the optimizer's heap pass both key on descr identity, so a field carrying two descrs silently breaks aliasing between them. "One field is one descr."

The shared PyObject header's w_class had no such arm, and it is read from both sides in the same loop:

  • orthodox_list_append_commit (specialize.rs:6446) pins the appended value's class — reads offset 8 through w_class_descr(), guards it to a constant, replace_box.
  • the sub-walk then evaluates is_plain_int1(value) (pyre-object/src/listobject.rs), whose value.w_class read comes through the modelled PyObject parent — a different identity for the same field.

So the pinned constant never reached the second read. In the steady body that showed up as the load appearing twice, the second followed by its own null test and equality test:

v233 = GetfieldGcR(v231) descr=<PyreFieldDescr { offset: 8 …     ← walker
GuardValue(v233, ptr(0x102954938))
v235 = GetfieldGcR(v231) descr=<SimpleFieldDescr { index: 1 …    ← layout entry
v236 = PtrEq(v235, ptr(0x0));        v237 = IntIsTrue; GuardFalse(v237)
v238 = PtrEq(v235, ptr(0x102954938)); v239 = IntIsTrue; GuardTrue(v239)

The fix

Bridge ("PyObject" | "pyre_object::pyobject::PyObject", "w_class") to w_class_descr().

Placed ahead of the parent-group lookup, for the reason the neighbouring int_items.* arm documents: when the codewriter does model the parent, that lookup answers with the parent's own entry for the same offset and re-creates the split. Putting the arm in the later (owner, name) match — where the box-payload bridges live — measured no change at all, because the parented path returns first.

Result

Append loop steady body 40 → 33 ops, so 7 of the 9 #922 gave up.

The remaining pair is the single genuine read and its guard. guard_class cannot replace it: a builtin subclass instance shares the payload ob_type and passes guard_class, retagging only w_class (walker_frame_ops.rs:196-200). Removing it needs the object-model split behind builtin subclass shares the base layout, not another descr change.

Verification

  • check.py --backend dynasm,cranelift: dynasm 352/352, cranelift 352/352
  • cargo test --release -p pyre-jit-trace --features dynasm: 314 passed, 0 failed, including a new test asserting both owner spellings bridge to the same Arc
  • retag_force.py matches the PYRE_NO_JIT oracle on both backends (25000 50000 1249975000 tuple MyTuple); w_subclass2.py and synth/callee_store_global_read_after_call unchanged
  • Re-measured on the current base after the branch was rebased mid-work, not on the tree the first measurement used

authored by Claude

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of object class descriptors for supported object layouts.
    • Ensured compatible descriptors are reused correctly while preserving distinct descriptors when their sizes differ.
  • Tests

    • Added coverage for descriptor reuse and size-mismatch behavior.

…r descr

`make_descr_from_bh` already redirects a codewriter-lowered body's field descr
to the walker's canonical one for `W_ListObject.int_items.*`,
`ItemsBlock.capacity` and the box payloads, because the heapcache and the
optimizer's heap pass key on descr identity and a field carrying two descrs
breaks aliasing between them. The shared `PyObject` header's `w_class` had no
such arm.

`orthodox_list_append_commit` pins the appended value's class by reading
offset 8 through `w_class_descr()` and guarding it to a constant. The sub-walk
then evaluates `is_plain_int1(value)` (listobject.rs), whose `value.w_class`
read comes through the modelled `PyObject` parent — a different identity for
the same field, so the pinned constant never reached it. The steady loop body
carried the read twice, the second one followed by its own null test and
equality test.

Bridge `("PyObject" | "pyre_object::pyobject::PyObject", "w_class")` to
`w_class_descr()`. Placed ahead of the parent-group lookup for the reason the
neighbouring `int_items.*` arm documents: when the codewriter does model the
parent, that lookup answers with the parent's own entry and re-creates the
split.

Append loop steady body 40 -> 33 ops. The remaining pair is the single genuine
read and guard; `guard_class` cannot replace it because a builtin subclass
shares `ob_type` and only retags `w_class`.

Assisted-by: Claude
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

make_descr_from_bh now recognizes PyObject.w_class fields from both supported owner spellings. It reuses the canonical descriptor only when offset, width, and type match. Tests cover matching and width-mismatch cases.

Changes

w_class descriptor matching

Layer / File(s) Summary
w_class matching and validation
pyre/pyre-jit-trace/src/descr.rs
make_descr_from_bh validates PyObject.w_class owner, offset, width, and type before canonical descriptor reuse. Tests cover canonical reuse and separately sized descriptors for width mismatches.

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

Suggested reviewers: lifthrasiir

Poem

A rabbit checked the class field bright,
Matched its width and type just right.
Shared descriptors hopped in line,
Mismatched widths kept their own design.
“Tests pass!” cried Bun beneath the moon.

🚥 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 identifies the PyObject.w_class descriptor bridge and its append-loop operation reduction.
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.

@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

https://github.com/youknowone/pyre/blob/63875f6ed156de2e54746c99fdc5fe4fe7c3f801/pyre-jit-trace/src/descr.rs#L4360
P1 Badge Preserve the wasm32 layout when bridging w_class

On the wasm32 backend, the incoming codewriter descriptor uses the target layout (a 4-byte pointer and the wasm w_class offset), but this unconditional return replaces it with w_class_descr(), whose new_w_class_field_descr deliberately uses the host-derived offset and hard-coded field_size: 8. Consequently, translated PyObject.w_class operations such as is_plain_int1 can access the wrong wasm field or include adjacent payload bytes, miscompiling the generated JIT even though the interpreter uses the correct layout. Either make the canonical descriptor target-layout-correct or avoid this bridge when its offset/size differs from the BhDescr.

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

ℹ️ 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".

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit b54316a).
Updated: 2026-07-31T17:25:06.616Z

Files in the reviewed diff
pyre/pyre-jit-trace/src/descr.rs

1. Regressions to PyPy parity introduced by this patch

None.

2. Other mismatches introduced by this patch

  • Test-only descriptor metadata mismatch: descr.rs:3641 constructs PyObject.w_class as ArrayFlag::Signed despite field_type: Type::Ref; RPython classifies GC pointers as FLAG_POINTER in descr.py:241-244. The production bridge derives pointer-ness from Type::Ref, so this does not change generated JIT behavior, but the new test fixture is not faithful.

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

  • descr.rs:1929 hardcodes w_class to an 8-byte field, while RPython obtains each field’s target-specific size through descr.py:223-224. On 32-bit targets this remains a real mismatch; this patch deliberately declines to bridge the 4-byte codewriter descriptor rather than fixing it.

4. Structural adaptations

  • descr.rs:4398-4409 manually unifies the codewriter BhDescr and walker Arc<dyn Descr> representations for PyObject.w_class. This is the Rust equivalent of RPython returning the cache-owned descriptor object for a (STRUCT, fieldname) pair in descr.py:218-239; the manual bridge is required because Pyre has separate descriptor representations.
  • descr.rs:3637-3649 models a two-word PyObject header containing ob_type and w_class, whereas RPython’s root object has only typeptr in rclass.py:149-165. This is a Pyre/Rust object-model adaptation, not a regression caused by the bridge.

…s width

`new_w_class_field_descr` hardcodes `field_size: 8` while the codewriter sizes
a pointer field by `layout::target_word_size()` (`call.rs:7448-7452
get_type_flag`), so on a 32-bit target the two spellings of `PyObject.w_class`
describe different accesses at the same offset: a 4-byte load from the
codewriter and an 8-byte load from the canonical descr. The bridge returned
the canonical descr unconditionally, widening the read over four bytes of the
adjacent payload.

Return the canonical descr only when its offset, width, and field type all
equal the incoming `BhDescr`'s. The width split is deliberate and documented
at `new_w_class_field_descr`; targets where it applies keep the descr they had
before the bridge existed.

Adds a decline test alongside the existing bridge test, both now deriving
offset and width from the canonical descr instead of hardcoding them.

Raised as P1 on #931.

Assisted-by: Claude
@youknowone

Copy link
Copy Markdown
Owner Author

The P1 is correct — and the tree already documented the trap it names.

new_w_class_field_descr (descr.rs:1918-1938) carries this on the very field in question:

⚠️ WORD on paper — the field is a *mut PyObject, so 4 bytes on wasm32, and the build-time descr pool already sizes it that way (call.rs get_type_flaglayout::target_word_size()). Deriving it here to match makes synth/exception_traceback_loop_forms lose one iteration's e.__traceback__ on the wasm backend, so the two universes stay deliberately out of step until that is understood.

Verified both halves:

  • call.rs:7448-7452 get_type_flag gives every pointer field crate::layout::target_word_size() — 4 on wasm32.
  • new_w_class_field_descr hardcodes field_size: 8.

So on a 32-bit target the bridge replaced a 4-byte load with an 8-byte load at the same offset, pulling four bytes of the adjacent payload into the class pointer. Same family as the arraydescrof_concrete literal-8 fallback that mis-strided a pointer array on wasm32 and surfaced as an invalid type_id GC panic.

Fix taken: the second of the two you offered. Making the canonical descr target-layout-correct is the first option, and the comment above records that it was already tried and regressed synth/exception_traceback_loop_forms on wasm — so it is not a bounded change here. Instead the bridge now returns the canonical descr only when offset, width, and field type all match the incoming BhDescr:

let canonical = &*W_CLASS_FIELD_DESCR;
if canonical.offset() == *offset
    && canonical.field_size() == *field_size
    && canonical.field_type() == *field_type
{
    return w_class_descr();
}

Where the widths disagree the bridge declines and the target keeps exactly the descr it had before #931 — the documented "two universes out of step" state stays as-is rather than being silently merged.

Test coverage: the existing bridge test and a new decline test now both derive offset and width from the canonical descr instead of hardcoding 8, so neither can go vacuously green if the layout moves.

Verification on this commit: check.py dynasm 352/352, cranelift 352/352; cargo test --release -p pyre-jit-trace --features dynasm green. The 64-bit win is unaffected — the append loop steady body stays at 33 ops, since the widths agree there.

commented by Claude

@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 `@pyre/pyre-jit-trace/src/descr.rs`:
- Around line 3631-3693: Extend
make_descr_from_bh_declines_w_class_bridge_on_a_width_mismatch to also construct
bridge descriptors with mismatched offset and field_type, then assert each
result is not Arc-pointer-equal to the canonical w_class_descr. Preserve the
existing width-mismatch coverage and verify the returned descriptors retain
their requested access properties.
🪄 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: 86f0a094-fd60-45ae-9574-3de8b230ef9f

📥 Commits

Reviewing files that changed from the base of the PR and between c7c8003 and b54316a.

📒 Files selected for processing (1)
  • pyre/pyre-jit-trace/src/descr.rs

Comment on lines +3631 to +3693
/// A `PyObject.w_class` `BhDescr` describing the same access as the
/// canonical header descr, for both owner spellings the codewriter emits.
fn w_class_bh(owner: &str, field_size: usize) -> majit_translate::jitcode::BhDescr {
use majit_ir::descr::ArrayFlag;
use majit_translate::jitcode::BhDescr;

BhDescr::Field {
offset: pyre_object::pyobject::W_CLASS_OFFSET,
field_size,
field_type: Type::Ref,
field_flag: ArrayFlag::Signed,
is_field_signed: false,
is_immutable: false,
is_quasi_immutable: false,
// slot 0 is `ob_type`; `w_class` is slot 1 of the header.
index_in_parent: 1,
parent: None,
name: "w_class".into(),
owner: owner.into(),
}
}

/// The shared `PyObject` header's `w_class` bridges to the same descr the
/// walker pins a value's class through, so a codewriter-lowered subclass
/// test (`is_plain_int1`) reads the header the walker already guarded
/// instead of emitting a second, uncacheable read of the same offset.
#[test]
fn make_descr_from_bh_bridges_pyobject_w_class_to_the_walker_descr() {
let canonical = w_class_descr();
let width = W_CLASS_FIELD_DESCR.field_size();

for owner in ["PyObject", "pyre_object::pyobject::PyObject"] {
let descr = make_descr_from_bh(&w_class_bh(owner, width));
assert!(
std::sync::Arc::ptr_eq(&descr, &canonical),
"{owner}.w_class must bridge to the walker's w_class descr Arc",
);
}
}

/// …but only when the two spellings describe the same access. The canonical
/// descr hardcodes an 8-byte width while the codewriter sizes a pointer by
/// `target_word_size()`, so on a 32-bit target the incoming descr is a
/// narrower load at the same offset. Bridging there would widen the read
/// over the adjacent payload, so the mismatch declines instead.
#[test]
fn make_descr_from_bh_declines_w_class_bridge_on_a_width_mismatch() {
let canonical = w_class_descr();
let narrower = W_CLASS_FIELD_DESCR.field_size() / 2;

for owner in ["PyObject", "pyre_object::pyobject::PyObject"] {
let descr = make_descr_from_bh(&w_class_bh(owner, narrower));
assert!(
!std::sync::Arc::ptr_eq(&descr, &canonical),
"{owner}.w_class must not bridge to a descr of a different width",
);
assert_eq!(
descr.as_field_descr().map(|f| f.field_size()),
Some(narrower),
"the declined descr must keep the width the codewriter asked for",
);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover every bridge rejection guard.

The tests only reject a mismatched field_size. Add rejection cases for a mismatched offset and field_type. Both cases must return a noncanonical descriptor. This protects the access-compatibility contract at Lines 4405-4407.

🤖 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 `@pyre/pyre-jit-trace/src/descr.rs` around lines 3631 - 3693, Extend
make_descr_from_bh_declines_w_class_bridge_on_a_width_mismatch to also construct
bridge descriptors with mismatched offset and field_type, then assert each
result is not Arc-pointer-equal to the canonical w_class_descr. Preserve the
existing width-mismatch coverage and verify the returned descriptors retain
their requested access properties.

@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: b54316acf5

ℹ️ 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".

&& canonical.field_size() == *field_size
&& canonical.field_type() == *field_type
{
return w_class_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 Keep w_class stores valid for virtual objects

When a codewriter-lowered body stores PyObject.w_class on a freshly allocated virtual object—for example, while retagging a builtin-subclass instance—this unconditional descriptor bridge also redirects the SetfieldGc, not just the intended read. w_class_descr() has no parent descriptor, while OptVirtualize::optimize_setfield_gc treats every field except typeptr as positional and calls get_parent_descr().expect(...); the resulting trace therefore panics during optimization. Make the canonical descriptor valid for virtual stores or avoid applying this read-oriented bridge to store operands.

AGENTS.md reference: AGENTS.md:L205-L207

Useful? React with 👍 / 👎.

@youknowone
youknowone merged commit f062790 into main Aug 1, 2026
18 of 19 checks passed
@youknowone
youknowone deleted the rewrite-tracer branch August 1, 2026 00:26
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