Skip to content

review: act on the #904 findings, and scope the descr type_id split out - #921

Merged
youknowone merged 4 commits into
mainfrom
gc-decouple
Jul 31, 2026
Merged

review: act on the #904 findings, and scope the descr type_id split out#921
youknowone merged 4 commits into
mainfrom
gc-decouple

Conversation

@youknowone

@youknowone youknowone commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Acts on the code review left on #904, after verifying each finding against post-merge main rather than against the PR diff.

Twelve findings were filed (1 by Codex, 11 by CodeRabbit). Five did not survive verification, two are style suggestions whose advice does not apply, one is epic-sized and is deliberately left alone. The four here are the ones worth landing.

What lands

get_field_descr cache hit: gate the index arbitration on debug_assertions.
The hit path derived expected_index_in_parent by walking the parent SizeDescr's whole all_fielddescrs() list and comparing field keys. Its only reader is the debug_assert! directly below it, which cfg!(debug_assertions) compiles away — so release builds performed the walk and discarded the result. make_simple_descr_group_keyed_with_headerless calls get_field_descr once per field spec while holding the gc_cache lock, and every field after the first publication is a cache hit, so this is not off the hot path.

Recognize the Result ctor through the front-side rule.
jtransform's malloc + __discriminant lowering gated on owner_path.last().starts_with("Result"). That also accepts an enum whose leaf merely begins with the literal — ResultCode, say — which, with Ok/Err variants, would be rerouted through the Result-specific lowering. No name in the current LLBC corpus does that. front::result_exc::result_ctor_kind already decides the same question precisely: it anchors the owner path head at core::result and compares the instantiation-stripped leaf for equality. Reuses it, so the two gates cannot drift.

Name the graph_jit_shapes discriminant.
drain_unfinished_graphs recorded ConstEncodingOverflow as the bare literal 3; cached_unsupported_jit_shape decoded it with a hand-written match over 0..=3. Both numberings tracked UnsupportedJitShape's implicit discriminants by hand, in two files, with nothing linking them — renumbering or removing a variant misclassifies every cached frame shape with no build error and no test signal. Adds call::JIT_SHAPE_CONST_ENCODING_OVERFLOW beside the map, pins it to the enum in eval.rs with const _: () = assert!(..), and names the decode arms after the discriminants themselves. Written value and decode unchanged.

Assert the nested virtual's link, not the rd_virtuals count.
test_guard_fail_args_virtual_array_with_nested_virtual_item ended at virtuals.len() >= 2, which also holds when the array and the item are numbered as two unrelated virtuals. What the bridge decoder follows is the link: it reads the array's item slot, resolves that TAGVIRTUAL number back into rd_virtuals under the negative-index rule, and materializes the entry it lands on. The test now asserts that path — locating the array by its VArrayInfo* variant rather than by slot order, requiring the resolved slot to be a different one holding the nested VirtualInfo, and requiring its one live field slot to be field_descr(42) carrying a TAGBOX that resolves to the SETFIELD_GC value i40. Both descr references must be live, since a None on either silently drops the materialization. Passes on the current encoding unchanged.

Deliberately not fixed

CodeRabbit filed the fielddescrof test gap as a trivial coverage nit. Verifying it surfaced something larger: the SizeDescr type_id is minted two ways.

bh_size_spec_from_callcontrol : path_hash(strip_generic_args(owner))   <- the call-site spelling
fielddescrof                  : struct_id_for_name(owner).as_u64()     =  path_hash(strip_crate_prefix(path))

OpKind::New / OpKind::NewWithVtable ship the first verbatim. The tell is inside that function: it fetches the layout through cc.struct_layout_for, which is struct_id_for_name(name)? -> struct_layouts[sid], and then stamps a different identity onto that layout. "W_FloatObject" alone splits into path_hash("W_FloatObject") versus path_hash("floatobject::W_FloatObject"). A NewWithVtable and the payload setfield_gc emitted one instruction later on the same object therefore mint two disjoint Arc<SimpleFieldDescr> sets for the same offsets — and OptHeap keys cached_fields on Arc identity, so the store does not invalidate the load's cache slot.

Converging the keys is not inert, which is why it is not in this PR: it moves every New spec onto the same _cache_size key the runtime group already occupies, activating register_keyed_size arbitration where it is dormant today. That tiebreak is vtable wins, else more fields wins, and the analyzer's list is longer by exactly the GC-header words — so a NewWithVtable spec would win and replace the orthodox headerless runtime group. This belongs with the header-convention unification, as its own change with a full gate.

Findings that did not survive

Codex's P1 asked to keep the nested-gateway inline_subwalk path disabled, on the evidence that synth/sre_pattern_methods printed 279999 instead of 280000. That benchmark now passes — but it passes because the branch was rebased onto a main carrying #874/#905/#906/#907, not because the blamed commits were reverted, so "fixed" and "masked" had to be separated. The path is unreachable: compute_inline_caller_frame has exactly two callers, both under try_walker_inline_user_call, which declines unless pyre_helper is CallFn/CallKw/CallFunctionEx; PyreHelperKind::CallFn is assigned only in pyre-jit/src/jit/flatten.rs, while majit-translate/src/codewriter/call.rs — which builds the gateway body's residuals — writes PyreHelperKind::None at all three of its construction sites.

One residue is worth recording: what makes that safe today is an unrelated helper-kind filter, not a check at the resolution site. The sibling guard-capture path in resume_snapshot.rs gates the same hazard explicitly (if !inline_subwalk && !full_body_sym.is_null()), and compute_inline_caller_frame maps the same kind of op_pc without it; upstream traps the class outright at pyjitpl.py:199.

Four more were misreads: Descr::index() and FieldDescr::index_in_parent() are different axes, not a mismatched identity; the _helper_frame guard does outlive the fbw_mode restore but no call in that tail reads framestack.last(); float.as_integer_ratio(1) already raises TypeError because builtin_code_call rejects any positional count differing from the recorded arity, in both directions; and no ruff configuration covers pyre/bench/synth, so B007 never runs there.

Verification

cargo test --all --no-default-features --features dynasm — exit 0, 100 test binaries, no failures.
python3 pyre/check.py --backend dynasm — 349/349.
Both against a freshly extracted LLBC whose pyre-object / pyre-interpreter / pyre-jit fingerprints match the working tree, run at base 59d87af4d9. The base has since moved by one commit (#920, optimizeopt/virtualize.rs); cargo test -p majit-metainterp --lib was re-run on the current base — 1434 passed.

authored by Claude

Summary by CodeRabbit

  • Bug Fixes

    • Corrected handling of Result variants so unrelated enum types are no longer misidentified as Ok or Err.
    • Improved JIT shape decoding consistency for constant-encoding overflow cases.
    • Preserved reliable field descriptor lookup behavior in release builds.
  • Tests

    • Expanded validation of nested virtual arrays and their encoded fields.
    • Added checks to keep JIT shape status values synchronized across components.

…ebug builds

The cache-hit path derives `expected_index_in_parent` by walking the parent
SizeDescr's whole `all_fielddescrs()` list and comparing field keys. Its only
reader is the `debug_assert!` directly below, which `cfg!(debug_assertions)`
compiles away, so a release build performed the walk and discarded the result.

The walk is not off the hot path: `make_simple_descr_group_keyed_with_headerless`
calls `get_field_descr` once per field spec while holding the `gc_cache` lock,
and every field after the first publication is a cache hit.

Spells the same `cfg!(debug_assertions)` gate out at the binding, so a release
build reads the cached descr's own slot number and skips the walk.

Assisted-by: Claude
`jtransform`'s malloc + `__discriminant` lowering gated on
`owner_path.last().starts_with("Result")` plus a `matches!(name, "Ok" | "Err")`
test. The leaf test also accepts an enum whose name merely begins with the
literal — `ResultCode`, say — and such an enum with `Ok`/`Err` variants would be
rerouted through the Result-specific lowering. No name in the current LLBC
corpus does that.

`front::result_exc::result_ctor_kind` already decides the same question and
decides it precisely: it anchors the owner path head at `core::result` and
compares the instantiation-stripped leaf for equality. Reuses it here rather
than tightening the second spelling test, so the two gates cannot drift.

Assisted-by: Claude
`drain_unfinished_graphs` recorded `ConstEncodingOverflow` into
`CallControl.graph_jit_shapes` as the bare literal `3`, and
`cached_unsupported_jit_shape` decoded it with a hand-written match over the
literals `0..=3`. Both numberings only track `UnsupportedJitShape`'s implicit
discriminants by hand, in two files, with nothing linking them: renumbering,
inserting or removing a variant misclassifies every cached frame shape with no
build error and no test signal.

Adds `call::JIT_SHAPE_CONST_ENCODING_OVERFLOW` beside the map, used at the write
site, and pins it to the enum in `eval.rs` with `const _: () = assert!(..)` —
the only module that sees both. Names the decode arms after the discriminants
themselves so the read side carries no literals either.

The written value and the decode are unchanged.

Assisted-by: Claude
`test_guard_fail_args_virtual_array_with_nested_virtual_item` ended at
`virtuals.len() >= 2`, which also holds when the array and the item are numbered
as two unrelated virtuals. What the bridge decoder follows is the link between
them: it reads the array's item slot, resolves that TAGVIRTUAL number back into
`rd_virtuals` under the negative-index rule, and materializes the entry it lands
on.

Asserts that path instead: the array is located by its `VArrayInfo*` variant
rather than by slot order, its single item slot must untag to TAGVIRTUAL, the
slot that resolves to must be a different one and hold the nested `VirtualInfo`,
and that virtual's one live field slot must be `field_descr(42)` carrying a
TAGBOX that resolves to the `SETFIELD_GC` value `i40`. Both descr references are
required to be live, since a `None` on either silently drops the
materialization.

Passes on the current encoding unchanged.

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: 3c4e305f-7ac9-4e55-9d12-f8ecfb4fbee1

📥 Commits

Reviewing files that changed from the base of the PR and between 93a7239 and 1281d0e.

📒 Files selected for processing (7)
  • majit/majit-ir/src/descr.rs
  • majit/majit-metainterp/src/optimizeopt/virtualize.rs
  • majit/majit-translate/src/codewriter/jtransform.rs
  • majit/majit-translate/src/front/result_exc.rs
  • pyre/pyre-jit/src/eval.rs
  • pyre/pyre-jit/src/jit/call.rs
  • pyre/pyre-jit/src/jit/codewriter.rs

Walkthrough

The changes optimize debug-only descriptor validation, strengthen nested virtual-array resume-data checks, use exact Result constructor resolution, and replace JIT shape discriminant literals with shared constants and compile-time validation.

Changes

MAJIT correctness

Layer / File(s) Summary
Debug-only descriptor arbitration
majit/majit-ir/src/descr.rs
Debug builds continue parent-field validation. Release builds use the cached field index without scanning the parent list.
Nested virtual-array validation
majit/majit-metainterp/src/optimizeopt/virtualize.rs
The test now validates nested virtual entries, descriptors, field slots, tag types, index resolution, and the expected leaf argument.

Result constructor lowering

Layer / File(s) Summary
Frontend Result constructor resolution
majit/majit-translate/src/front/result_exc.rs, majit/majit-translate/src/codewriter/jtransform.rs
Result lowering now uses the crate-visible frontend resolver and exact Ok or Err constructor matching.

JIT shape encoding

Layer / File(s) Summary
Shared overflow discriminant
pyre/pyre-jit/src/jit/call.rs, pyre/pyre-jit/src/eval.rs
The JIT defines the overflow discriminant and checks it against UnsupportedJitShape::ConstEncodingOverflow.
Cached shape discriminant usage
pyre/pyre-jit/src/eval.rs, pyre/pyre-jit/src/jit/codewriter.rs
Shape decoding and overflow recording use named constants instead of hard-coded numeric values.

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

Possibly related PRs

  • youknowone/pyre#563: Related to UnsupportedJitShape::ConstEncodingOverflow and JIT shape-cache encoding.
  • youknowone/pyre#606: Related to Result-constructor handling in the frontend and codewriter.
  • youknowone/pyre#876: Related to GcCache::get_field_descr and descriptor lookup behavior.

Suggested reviewers: lifthrasiir

Poem

A rabbit checks the fields in flight,
Finds nested tags aligned just right.
Result paths choose the proper door,
JIT numbers match the enum’s score.
Hop, hop—consistent code once more!

🚥 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 accurately summarizes acting on the prior review findings and separating the unresolved descr type_id split.
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 gc-decouple

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 1281d0e).
Updated: 2026-07-31T10:55:42.731Z

Files in the reviewed diff
majit/majit-ir/src/descr.rs
majit/majit-metainterp/src/optimizeopt/virtualize.rs
majit/majit-translate/src/codewriter/jtransform.rs
majit/majit-translate/src/front/result_exc.rs
pyre/pyre-jit/src/eval.rs
pyre/pyre-jit/src/jit/call.rs
pyre/pyre-jit/src/jit/codewriter.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)

None.

4. Structural adaptations

  • majit/majit-translate/src/codewriter/jtransform.rs:2897 ↔ rpython/jit/codewriter/jtransform.py:406 — Rust/Charon exposes a synthetic Result::Ok/Err constructor before RPython’s rtyper lowering; pyre reconstructs "new + __discriminant field write" rather than receiving RPython’s already-lowered allocation operations.

  • majit/majit-ir/src/descr.rs:1370 ↔ rpython/jit/backend/llsupport/descr.py:228 — pyre derives index_in_parent from the published parent descriptor to reconcile independent Charon/runtime numberers; RPython has one authoritative heaptracker.get_fielddescr_index_in() source.

  • pyre/pyre-jit/src/eval.rs:5983 ↔ rpython/jit/codewriter/codewriter.py:74 — pyre memoizes runtime CodeObject JIT-shape eligibility and may interpret frames whose bytecode cannot fit the one-byte encoding; RPython constructs bounded interpreter graphs at translation time.

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