Skip to content

Optimize tuple unpacking and specialization in JIT trace - #59

Merged
youknowone merged 24 commits into
youknowone:mainfrom
lifthrasiir:trace-visible-tuple-unpacking
Jun 8, 2026
Merged

Optimize tuple unpacking and specialization in JIT trace#59
youknowone merged 24 commits into
youknowone:mainfrom
lifthrasiir:trace-visible-tuple-unpacking

Conversation

@lifthrasiir

@lifthrasiir lifthrasiir commented May 16, 2026

Copy link
Copy Markdown
Contributor

Summary

This pull request enhances support for Python object subclass identity in the JIT trace, particularly for tuples and integer subclasses. The main changes introduce descriptors and helpers to accurately track and guard the dynamic Python-level class (w_class) of objects, ensuring correct behavior when handling subclassed types and specialized tuple variants.

Key changes include:

Tuple and Specialized Tuple Object Model Improvements

  • Added w_class field descriptors for W_TupleObject and all specialized tuple variants (SPECIALISED_TUPLE_II, FF, OO), allowing the JIT to access and set the Python-level class of these objects.
  • Introduced size descriptors for each tuple object variant, supporting accurate memory layout handling in the JIT.
  • Provided new helper functions to retrieve these descriptors in descr.rs.

Concrete Value and Subclass Handling

  • Updated ConcreteValue::from_pyobj to distinguish between exact int, bool, and integer subclasses, preserving subclass identity by returning a reference instead of unboxing for non-exact ints.
  • Added is_trace_plain_int helper to check for exact (non-subclassed) integer objects.
  • Added tests to verify correct handling and unboxing of exact ints, bools, and int subclasses.

JIT Tracing and Opcode Handling Enhancements

  • Updated tuple construction tracing to use both the IR and concrete items, enabling precise tracking of tuple contents and class.
  • Introduced tracing helpers in trace_opcode.rs to:
    • Unbox exact integer payloads and guard for exact Python classes in the trace.
    • Set the w_class field for tuple objects during trace construction, maintaining Python-level type information.

These changes ensure that the JIT trace can accurately model Python's dynamic type system, especially for tuples and integer subclasses, improving correctness and compatibility with Python semantics.

Benchmark run:

  benchmark                            cpython     pypy             dynasm          cranelift
  ──────────────────────────────────────────────────────────────────────────────────────────────────
  synth/tuple_unpacking                  0.13s    0.01s      0.01s    0.5x      0.02s    1.9x

Self-review

Prompt & Model

Model: gpt-5.5

Prompt:

Assess by static analysis whether our changes in git diff main are equivalent
to the corresponding RPython/PyPy source code. The RPython and PyPy sources
are available locally.

If anything was ported incorrectly, report every instance in detail. After
collecting all differences, organize the report into separate sections:

1. Cases where our patch regressed PyPy parity compared to main
2. Other mismatches introduced by our patch
3. Mismatches that already existed before this patch
4. Structural adaptations

Exceptions: some differences cannot be ported 1:1 because of Python 3.11 vs
3.14 differences, opcode mismatches caused by using a CPython-compatible
compiler, GIL/free-threading differences, and fundamental implementation-
language differences between RPython and Rust. Mark those separately under
“Structural adaptations.”

Answer

  1. Regressed PyPy Parity Compared To Main

  pyre/pyre-jit-trace/src/trace_opcode.rs:4525 regresses exact-type parity for future trace executions.

  PyPy’s makespecialisedtuple2 only selects Cls_ii when both args satisfy is_plain_int1, and is_plain_int1 is type(w_obj) is W_IntObject or exact
  W_LongObject that fits int. Main used the opaque tuple helper, so this predicate ran at runtime.

  The patch checks is_plain_int1(lhs/rhs) only while tracing, then emits unbox guards that check only ob_type/layout: pyre/pyre-jit-trace/src/
  trace_opcode.rs:4467 and pyre/pyre-jit-trace/src/state.rs:2142. They do not guard w_class. A later int/long subclass with the same Pyre payload
  layout can incorrectly stay on the Cls_ii trace and lose object identity when unpacked/getitem wraps raw ints.

  pyre/pyre-jit-trace/src/trace_opcode.rs:4575 has the same regression.

  PyPy requires type(w_arg) is W_FloatObject before selecting Cls_ff. The patch checks is_plain_float_strict at trace time, but the emitted pyre/pyre-
  jit-trace/src/state.rs:2166 guard only checks FLOAT_TYPE, not w_class. A future float subclass can incorrectly take the Cls_ff trace and be
  rewrapped as a plain float.

  2. Other Mismatches Introduced By This Patch

  None found beyond the trace-guard exact-type regressions above. The runtime tuple constructors and object helpers otherwise match the PyPy selection
  rules more closely than main.

  3. Mismatches Already Present Before This Patch

  pyre/pyre-object/src/listobject.rs:299 still use is_float/payload layout where PyPy’s FloatListStrategy.is_correct_type uses exact type(w_obj) is
  W_FloatObject. Similar existing sites include append/setitem strategy decisions in listobject.rs and JIT list append selection at pyre/pyre-jit-
  trace/src/trace_opcode.rs:5133. These can accept float subclasses into float storage, unlike PyPy.

  pyre/pyre-jit-trace/src/state.rs:1268 still unboxes float subclasses with is_float. The patch fixed int subclass preservation, but float subclass
  concrete identity remains weaker than PyPy’s exact W_FloatObject distinction.

  4. Structural Adaptations

  pyre/pyre-jit-trace/src/trace_opcode.rs:250 is a valid Pyre object-model adaptation. PyPy’s specialised tuple classes share the public tuple
  typedef; Pyre models JIT-visible layout with distinct ob_type values, so setting w_class to canonical tuple is necessary.

  The fallback to opaque tuple build when concrete side-channel values are null at pyre/pyre-jit-trace/src/trace_opcode.rs:4509 is also a reasonable
  adaptation. PyPy always has concrete W_Root values at space.newtuple; Pyre sometimes does not.

  Specialised tuple unpack tracing at pyre/pyre-jit-trace/src/trace_opcode.rs:4720 is structurally aligned with PyPy’s specialised tuple tolist()/
  getitem() behavior, with Pyre-specific virtual wrapint/wrapfloat replacing immediate boxed allocation.

(My Codex usage limits ran out right after this final assessment, so it might require some more fixes.)

Summary by CodeRabbit

  • Bug Fixes

    • Preserve exact int/bool identity so booleans and int subclasses aren’t misclassified; tighter checks avoid incorrect fast-paths.
  • Performance

    • Faster 2-element tuple construction and unpacking with new specialization fast paths (int/int, float/float, object/object) and stricter class identity guards.
  • New Features

    • JIT now emits real tuple construction/unpack operations instead of no-op placeholders.
  • Tests

    • Added/updated unit tests covering int/bool mapping, tuple-specialization selection, and cache behavior.

@coderabbitai

coderabbitai Bot commented May 16, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds tuple w_class and size descriptor accessors; introduces ConcreteValue::Bool and strict plain-int/float predicates using w_class identity; implements trace-time specialized 2-element tuple construction and unpacking with exact w_class guards; updates JIT blackhole helpers, codewriter lowering, heapcache, and majit passes.

Changes

Tuple specialization and type identity

Layer / File(s) Summary
Descriptor infrastructure for tuple field access
pyre/pyre-jit-trace/src/descr.rs
Extended tuple singleton descriptor groups with a PyObject.w_class field descriptor, added public w_class field accessors and size-descriptor accessors for all tuple variants, and implemented SizeDescr::w_class_obj().
ConcreteValue Bool and strict plain-int/float
pyre/pyre-jit-trace/src/state.rs
Added ConcreteValue::Bool(bool), tightened unboxing to detect exact bool and strict plain int/float via py_type_check+get_instantiate+w_class identity, updated boxing/unboxing, IR/type mapping, truthiness, constant fast paths, and tests.
Trace helpers and specialized tuple construction
pyre/pyre-jit-trace/src/trace_opcode.rs
Added helpers for payload extraction and trace_guard_exact_w_class; implemented MIFrame::trace_build_tuple_value to emit specialized arity-2 tuples (ii/ff/oo) with NewWithVtable/SetfieldGc, heapcache writes, and w_class initialization; falls back to generic construction.
Unpack fast paths and handler integration
pyre/pyre-jit-trace/src/trace_opcode.rs, pyre/pyre-jit-trace/src/opcode_handler_impls_pre.template.rs
Extended trace_unpack_known_tuple to accept concrete tuple objects and added arity-2 fast paths that guard specialized tuple classes and load inline fields directly; updated build_tuple handler to call trace_build_tuple_value and thread concrete tuple data through unpacking.
JIT blackhole helpers and lowering
pyre/pyre-jit/src/call_jit.rs, pyre/pyre-jit/src/jit/codewriter.rs, pyre/pyre-jit/src/jit/flatten.rs, pyre/pyre-jit/src/jit/cpu.rs
Added blackhole callbacks bh_build_tuple_fn, bh_unpack_sequence_fn, bh_unpack_item_fn; registered helper fn pointers, added emit_frontend_newtuple, lifted residual-call emission for BUILD_TUPLE/UNPACK_SEQUENCE, added flatten helper for (Int,Ref)->Ref residual calls, and wired new helper fn pointers into Cpu.
Trace jitcode dispatch and concrete shadow
pyre/pyre-jit-trace/src/jitcode_dispatch.rs
Coerce ConcreteValue::Bool appropriately when writing Ref/Int banks, treat bool concretes as null for vable struct pointer derivation, and add op_arg_value seeding into dispatch entry registers.
Object predicate refinements and tests
pyre/pyre-object/src/listobject.rs, pyre/pyre-object/src/tupleobject.rs
Refactored is_plain_int1 to use w_class pointer identity with instantiate handling for ints/longs; made is_plain_float_strict public and strict-identity based; added tests mutating w_class to ensure specialization falls back to object-object variant when identity mismatches.
majit and heapcache adjustments
majit/majit-metainterp/src/optimizeopt/intbounds.rs, majit/majit-metainterp/src/optimizeopt/virtualize.rs, majit/majit-trace/src/heapcache.rs
find_producing_op early-returns None for constant inputs; optimize_getfield_gc resolves virtual w_class reads from virtual fields or SizeDescr::w_class_obj(); HeapCache replacement map switched to Vec<(old,new)> keyed by typed OpRef, updated lookup/update semantics and tests for typed identity.
OpArg decoding lowering
majit/majit-translate/src/front/ast.rs
Lower Arg::<T>::get(op_arg) into same_as and fold OpArgType decode methods into primitive int/bool ops using new helpers; classify OpArg/OpArgState as ValueType::Int.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • youknowone

🐰 A rabbit's ode to class and trace:

I sniffed the w_class in morning light,
Distinguish bool from int, set fast paths right,
Two-element tuples wear a slimmer coat,
Inline fields set, guards stand like a moat—
The trace hops swift, identity in sight.

🚥 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 describes the primary change: optimization of tuple unpacking and specialization in the JIT trace layer, which is the main focus across multiple files.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pyre/pyre-jit-trace/src/state.rs (1)

1259-1280: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve exact bool identity here too.

from_pyobj now preserves non-exact int identity, but this branch still collapses exact bool to ConcreteValue::Int. Because to_pyobj reboxes every ConcreteValue::Int with w_int_new, any bool that round-trips through concrete tracking comes back as an int, and downstream exact-type checks lose the distinction again.

Suggested fix
-            if is_bool(obj) {
-                ConcreteValue::Int(w_bool_get_value(obj) as i64)
+            if is_bool(obj) {
+                ConcreteValue::Ref(obj)
             } else if is_trace_plain_int(obj) {
                 ConcreteValue::Int(w_int_get_value(obj))
🤖 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/state.rs` around lines 1259 - 1280, from_pyobj
currently collapses exact booleans into ConcreteValue::Int which loses bool
identity on round-trip; change the is_bool branch to preserve booleans as a
distinct ConcreteValue (e.g. ConcreteValue::Bool(w_bool_get_value(obj) != 0) or
similar) instead of ConcreteValue::Int, and update to_pyobj to handle
ConcreteValue::Bool by creating/returning a boolean PyObject (use the boolean
constructor w_bool_new or equivalent) so bools are not reboxed as ints; update
any enum/type definitions if needed to add ConcreteValue::Bool and handle it
alongside ConcreteValue::Int/Float/Ref in from_pyobj and to_pyobj.
🤖 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/trace_opcode.rs`:
- Around line 4528-4705: trace_build_tuple_value can emit specialised tuple
trace objects (Cls_ii/Cls_ff/Cls_oo) but leaves the corresponding concrete
PyObjectRef (concrete_items / concrete_seq) with the original plain tuple type,
causing later concrete_seq.ob_type checks to miss the specialised layout; fix
by, immediately after creating a specialised tuple branch (where you call
trace_set_tuple_w_class and record the SetfieldGc ops), also update the concrete
PyObjectRef's ob_type to the matching specialised tuple class so the
interpreter-side concrete_seq observes the same class as the trace (use the same
specialised_tuple_*_w_class_descr()/w_class symbol used in
trace_set_tuple_w_class to determine the class to write); apply the same change
to the other specialised-tuple-building site mentioned (the second
specialised-creation block around the later range).

In `@pyre/pyre-object/src/tupleobject.rs`:
- Around line 359-360: The test fixtures set (*lhs).w_class and (*rhs).w_class
to crate::noneobject::w_none(), which is wrong because w_class must point to a
real type object; replace these assignments with a dedicated test type instance
(e.g., create a distinct "fake tuple subclass" type via a helper like
make_test_subclass() or an instantiated TypeObject) and assign that type pointer
to (*lhs).w_class and (*rhs).w_class (also update the same change at the other
occurrence around lines 380–381); ensure you use the existing type-construction
helper or add one and handle any required reference management when attaching
the type to the fixtures.

---

Outside diff comments:
In `@pyre/pyre-jit-trace/src/state.rs`:
- Around line 1259-1280: from_pyobj currently collapses exact booleans into
ConcreteValue::Int which loses bool identity on round-trip; change the is_bool
branch to preserve booleans as a distinct ConcreteValue (e.g.
ConcreteValue::Bool(w_bool_get_value(obj) != 0) or similar) instead of
ConcreteValue::Int, and update to_pyobj to handle ConcreteValue::Bool by
creating/returning a boolean PyObject (use the boolean constructor w_bool_new or
equivalent) so bools are not reboxed as ints; update any enum/type definitions
if needed to add ConcreteValue::Bool and handle it alongside
ConcreteValue::Int/Float/Ref in from_pyobj and to_pyobj.
🪄 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

Run ID: b0b9e87c-6e31-4c64-b678-8812a1e2ed63

📥 Commits

Reviewing files that changed from the base of the PR and between eb35809 and 14c8392.

⛔ Files ignored due to path filters (1)
  • pyre/pyre-jit-trace/tests/snapshots/opcode_handler_impls.snap is excluded by !**/*.snap
📒 Files selected for processing (6)
  • pyre/pyre-jit-trace/src/descr.rs
  • pyre/pyre-jit-trace/src/opcode_handler_impls_pre.template.rs
  • pyre/pyre-jit-trace/src/state.rs
  • pyre/pyre-jit-trace/src/trace_opcode.rs
  • pyre/pyre-object/src/listobject.rs
  • pyre/pyre-object/src/tupleobject.rs

Comment on lines +4528 to +4705
pub(crate) fn trace_build_tuple_value(
&mut self,
items: &[OpRef],
concrete_items: &[PyObjectRef],
) -> Result<OpRef, PyError> {
if concrete_items.iter().any(|item| item.is_null()) {
// STRUCTURAL ADAPTATION: PyPy's `space.newtuple()` always
// reaches `wraptuple()` with concrete W_Root instances, so
// `makespecialisedtuple2()` can choose `Cls_ii` / `Cls_ff` /
// `Cls_oo` for arity 2. Pyre's trace-side FrontendOp may
// lack that concrete side channel after earlier trace-only
// operations. In that case, keep the older helper path rather
// than guessing a canonical W_TupleObject and regressing
// specialised tuple parity on escaped two-tuples.
return self.trace_build_tuple(items);
}

self.with_ctx(|this, ctx| unsafe {
if items.len() == 2 {
let lhs = concrete_items[0];
let rhs = concrete_items[1];
if pyre_object::is_plain_int1(lhs) && pyre_object::is_plain_int1(rhs) {
trace_guard_exact_python_class(this, ctx, items[0], &INT_TYPE);
trace_guard_exact_python_class(this, ctx, items[1], &INT_TYPE);
let raw0 = trace_plain_int_payload(this, ctx, items[0], lhs);
let raw1 = trace_plain_int_payload(this, ctx, items[1], rhs);
let tuple = ctx.record_op_with_descr(
OpCode::NewWithVtable,
&[],
crate::descr::specialised_tuple_ii_size_descr(),
);
ctx.heap_cache_mut().new_object(tuple);
trace_set_tuple_w_class(
ctx,
tuple,
crate::descr::specialised_tuple_ii_w_class_descr(),
);
ctx.record_op_with_descr(
OpCode::SetfieldGc,
&[tuple, raw0],
crate::descr::specialised_tuple_ii_value0_descr(),
);
ctx.heapcache_setfield_cached(
tuple,
crate::descr::specialised_tuple_ii_value0_descr().index(),
raw0,
);
ctx.record_op_with_descr(
OpCode::SetfieldGc,
&[tuple, raw1],
crate::descr::specialised_tuple_ii_value1_descr(),
);
ctx.heapcache_setfield_cached(
tuple,
crate::descr::specialised_tuple_ii_value1_descr().index(),
raw1,
);
return Ok(tuple);
}

if pyre_object::is_plain_float_strict(lhs)
&& pyre_object::is_plain_float_strict(rhs)
{
trace_guard_exact_python_class(this, ctx, items[0], &FLOAT_TYPE);
trace_guard_exact_python_class(this, ctx, items[1], &FLOAT_TYPE);
let raw0 = if this.value_type(items[0]) == Type::Float {
items[0]
} else {
crate::state::trace_unbox_float_with_resume(
this,
ctx,
items[0],
&FLOAT_TYPE as *const _ as i64,
)
};
let raw1 = if this.value_type(items[1]) == Type::Float {
items[1]
} else {
crate::state::trace_unbox_float_with_resume(
this,
ctx,
items[1],
&FLOAT_TYPE as *const _ as i64,
)
};
let tuple = ctx.record_op_with_descr(
OpCode::NewWithVtable,
&[],
crate::descr::specialised_tuple_ff_size_descr(),
);
ctx.heap_cache_mut().new_object(tuple);
trace_set_tuple_w_class(
ctx,
tuple,
crate::descr::specialised_tuple_ff_w_class_descr(),
);
ctx.record_op_with_descr(
OpCode::SetfieldGc,
&[tuple, raw0],
crate::descr::specialised_tuple_ff_value0_descr(),
);
ctx.heapcache_setfield_cached(
tuple,
crate::descr::specialised_tuple_ff_value0_descr().index(),
raw0,
);
ctx.record_op_with_descr(
OpCode::SetfieldGc,
&[tuple, raw1],
crate::descr::specialised_tuple_ff_value1_descr(),
);
ctx.heapcache_setfield_cached(
tuple,
crate::descr::specialised_tuple_ff_value1_descr().index(),
raw1,
);
return Ok(tuple);
}

let tuple = ctx.record_op_with_descr(
OpCode::NewWithVtable,
&[],
crate::descr::specialised_tuple_oo_size_descr(),
);
ctx.heap_cache_mut().new_object(tuple);
trace_set_tuple_w_class(
ctx,
tuple,
crate::descr::specialised_tuple_oo_w_class_descr(),
);
ctx.record_op_with_descr(
OpCode::SetfieldGc,
&[tuple, items[0]],
crate::descr::specialised_tuple_oo_value0_descr(),
);
ctx.heapcache_setfield_cached(
tuple,
crate::descr::specialised_tuple_oo_value0_descr().index(),
items[0],
);
ctx.record_op_with_descr(
OpCode::SetfieldGc,
&[tuple, items[1]],
crate::descr::specialised_tuple_oo_value1_descr(),
);
ctx.heapcache_setfield_cached(
tuple,
crate::descr::specialised_tuple_oo_value1_descr().index(),
items[1],
);
return Ok(tuple);
}

let len = ctx.const_int(items.len() as i64);
let array_descr = crate::state::pyobject_gcarray_descr();
let items_block = ctx.record_op_with_descr(OpCode::NewArrayClear, &[len], array_descr);
ctx.heap_cache_mut().new_array(items_block, len, true);
for (idx, &item) in items.iter().enumerate() {
let idx = ctx.const_int(idx as i64);
crate::state::trace_items_block_setitem_value(ctx, items_block, idx, item);
}

let tuple = ctx.record_op_with_descr(
OpCode::NewWithVtable,
&[],
crate::descr::w_tuple_size_descr(),
);
ctx.heap_cache_mut().new_object(tuple);
trace_set_tuple_w_class(ctx, tuple, crate::descr::tuple_w_class_descr());
let wrappeditems_descr = crate::descr::tuple_wrappeditems_descr();
ctx.record_op_with_descr(
OpCode::SetfieldGc,
&[tuple, items_block],
wrappeditems_descr.clone(),
);
ctx.heapcache_setfield_cached(tuple, wrappeditems_descr.index(), items_block);
Ok(tuple)
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Align specialised tuple unpacking with the traced tuple layout.

MIFrame::trace_build_tuple_value can emit _ii/_ff/_oo trace objects while the same execution’s concrete_seq is still a plain interpreter W_TupleObject. These concrete_seq.ob_type checks therefore miss the specialised tuple you just built in-trace, and the fallback then records guard_class(..., &TUPLE_TYPE) plus tuple_wrappeditems_descr() accesses against a specialised tuple OpRef. That makes the build→unpack path bail out instead of using the new specialised layout.

Also applies to: 4717-4792

🤖 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/trace_opcode.rs` around lines 4528 - 4705,
trace_build_tuple_value can emit specialised tuple trace objects
(Cls_ii/Cls_ff/Cls_oo) but leaves the corresponding concrete PyObjectRef
(concrete_items / concrete_seq) with the original plain tuple type, causing
later concrete_seq.ob_type checks to miss the specialised layout; fix by,
immediately after creating a specialised tuple branch (where you call
trace_set_tuple_w_class and record the SetfieldGc ops), also update the concrete
PyObjectRef's ob_type to the matching specialised tuple class so the
interpreter-side concrete_seq observes the same class as the trace (use the same
specialised_tuple_*_w_class_descr()/w_class symbol used in
trace_set_tuple_w_class to determine the class to write); apply the same change
to the other specialised-tuple-building site mentioned (the second
specialised-creation block around the later range).

Comment thread pyre/pyre-object/src/tupleobject.rs Outdated

@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: 14c8392364

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread pyre/pyre-jit-trace/src/trace_opcode.rs Outdated
Comment on lines +4549 to +4551
if pyre_object::is_plain_int1(lhs) && pyre_object::is_plain_int1(rhs) {
trace_guard_exact_python_class(this, ctx, items[0], &INT_TYPE);
trace_guard_exact_python_class(this, ctx, items[1], &INT_TYPE);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Specialize tuple ints only from class-preserving inputs

trace_build_tuple_value chooses the Cls_ii path from concrete_items, but those values are synthesized via i.concrete.to_pyobj() (BUILD_TUPLE handler) rather than the original runtime refs. Because bools are stored as ConcreteValue::Int, a (True, False) pair is reconstructed as plain ints, so is_plain_int1(lhs/rhs) passes and this branch emits trace_guard_exact_python_class(..., INT_TYPE) on the original bool refs. That guard cannot hold for bool objects, so traces recorded on boolean tuple construction immediately side-exit on replay, causing a repeatable hot-path deoptimization for tuple unpack/build patterns involving bools.

Useful? React with 👍 / 👎.

@lifthrasiir
lifthrasiir force-pushed the trace-visible-tuple-unpacking branch from 14c8392 to b5b5546 Compare May 16, 2026 13:35

@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/b5b5546ac72157d2b6d84874adabc0fe9332e639/pyre-jit-trace/src/trace_opcode.rs#L4586-L4590
P1 Badge Guard exact float class before taking Cls_ff fast path

trace_build_tuple_value picks the Cls_ff specialization using is_plain_float_strict (which requires exact float w_class), but the emitted unbox guard uses trace_unbox_float_with_resume(..., FLOAT_TYPE) and only checks ob_type. In this codebase, float subclasses can share ob_type and differ only by w_class (see the new tupleobject test that mutates w_class), so a trace recorded on exact floats can incorrectly accept float-subclass inputs on replay, build a specialised ff tuple, and then unpack/getitem them as plain floats, dropping subclass identity.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

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

🤖 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/state.rs`:
- Around line 1265-1270: ConcreteValue::from_pyobj is using a plain-int check
(is_trace_plain_int) but elsewhere constants are folded via is_int ->
try_trace_const_boxed_int, which loses int-subclass identity; update the code to
use the new exact-int predicate consistently: replace the plain/int-type checks
(the is_trace_plain_int usage in ConcreteValue::from_pyobj and the is_int check
that gates try_trace_const_boxed_int) with the new exact-int predicate, and
apply the same change to the other occurrence block around the 1360-1371 region
so that try_trace_const_boxed_int only runs for exact-int objects.

In `@pyre/pyre-jit-trace/src/trace_opcode.rs`:
- Around line 4509-4525: The fast-path for arity-2 tuple handling uses
concrete_items[0] and [1] without ensuring concrete_items has length >= 2, which
can panic if concrete_items is shorter than items; update the guard before the
arity-2 branch (inside with_ctx closure in trace_opcode.rs) to check that
concrete_items.len() >= 2 (in addition to no nulls) and fall back to
self.trace_build_tuple(items) when that condition fails; ensure the check
surrounds the block that calls pyre_object::is_plain_int1 on lhs/rhs so the
existing generic fallback remains used when concrete slice drift occurs.
🪄 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

Run ID: 92c54cd9-1934-4110-9e48-a3eccd0e826c

📥 Commits

Reviewing files that changed from the base of the PR and between 14c8392 and b5b5546.

📒 Files selected for processing (4)
  • pyre/pyre-jit-trace/src/state.rs
  • pyre/pyre-jit-trace/src/trace_opcode.rs
  • pyre/pyre-object/src/listobject.rs
  • pyre/pyre-object/src/tupleobject.rs

Comment thread pyre/pyre-jit-trace/src/state.rs
Comment thread pyre/pyre-jit-trace/src/trace_opcode.rs

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

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +4539 to +4540
let raw0 = trace_plain_int_payload(this, ctx, items[0], lhs);
let raw1 = trace_plain_int_payload(this, ctx, items[1], rhs);

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 Guard int-specialized tuple path by Python class

The Cls_ii fast path is selected using concrete is_plain_int1 values at trace time, but replay only unboxes via trace_plain_int_payload, which ultimately guards on ob_type (INT_TYPE/LONG_TYPE) and never checks w_class. That allows later executions with app-level int subclasses (same payload layout, different w_class) to stay on this trace and get re-materialized as plain ints, losing subclass identity and diverging from the runtime makespecialisedtuple2 predicate.

Useful? React with 👍 / 👎.

Comment on lines +4586 to +4590
crate::state::trace_unbox_float_with_resume(
this,
ctx,
items[0],
&FLOAT_TYPE as *const _ as i64,

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 Guard float-specialized tuple path by Python class

The Cls_ff branch is trace-selected from concrete is_plain_float_strict, but the emitted replay checks unbox with trace_unbox_float_with_resume, which only guards ob_type == FLOAT_TYPE and does not validate w_class. A later float-subclass instance can therefore hit this trace and be converted into plain float payloads, breaking subtype-preserving behavior expected when runtime specialization rejects non-exact floats.

Useful? React with 👍 / 👎.

@youknowone
youknowone force-pushed the trace-visible-tuple-unpacking branch from d688b44 to 55a4a37 Compare May 28, 2026 03:55

@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: 55a4a37a40

ℹ️ 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 +285 to +286
if expected_typeobj.is_null() || frame.value_type(obj) != Type::Ref {
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Initialize w_class before exact-class guards

When the tuple elements are freshly boxed values produced in the same trace, e.g. (a + b, c + d) or the float equivalent, wrapint/wrapfloat allocate NewWithVtable and set only the payload field (helpers.rs emits NewWithVtable + SetfieldGc for intval/floatval), leaving PyObject.w_class as zero. This new guard runs for any Type::Ref item and compares that zero field with the canonical int/float type object, so those traces side-exit every time before reaching the specialized tuple path. Either initialize w_class for the inline boxed values or avoid this guard for boxes whose exact class is already implied by the just-created vtable.

Useful? React with 👍 / 👎.

@youknowone

youknowone commented May 28, 2026

Copy link
Copy Markdown
Owner

Static parity/perf follow-up related to this PR scope:

This PR is the right place to verify tuple-unpacking performance parity because it changes trace-visible tuple construction/unpacking specialization.

The specific thing to investigate is whether the strict PyPy-parity guards for exact tuple/int/float class identity still allow the specialized tuple fast paths to be selected in the benchmark cases that PyPy optimizes well. If the strict guards are correct but the trace falls back to the generic tuple path too often, tuple unpacking can remain semantically correct while still missing PyPy-like performance.

Suggested checks for this PR:

  • confirm that arity-2 tuple construction records the specialized ii/ff/oo tuple paths when the concrete operands satisfy PyPy exact-type predicates;
  • confirm that replay guards reject subclasses or non-exact values before entering those specialized paths;
  • confirm that unpacking a known specialized tuple loads inline fields directly rather than routing through the generic sequence/unpack helper;
  • compare the emitted trace shape for synth/tuple_unpacking against the PyPy intent: exact-type guarded specialized tuple allocation plus direct field loads.

If all of those hold and the benchmark is still slow, the remaining cause is probably outside tuple specialization itself, likely in surrounding trace selection, guard placement, or downstream optimization/regalloc.

@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: 61b01a7c62

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

pub extern "C" fn bh_unpack_sequence_fn(count: i64, seq: i64) -> i64 {
let seq = seq as pyre_object::PyObjectRef;
match pyre_interpreter::runtime_ops::unpack_sequence_exact(seq, count as usize) {
Ok(items) => pyre_interpreter::runtime_ops::build_tuple_from_refs(&items) as i64,

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 Preserve item identity during UNPACK_SEQUENCE

For compiled UNPACK_SEQUENCE on a non-tuple sequence with two exact floats/ints, this temporary tuple is built through build_tuple_from_refs, which calls w_tuple_new and specializes arity-2 tuples. Reading the items back with bh_unpack_item_fn then goes through w_tuple_getitem and re-wraps the unboxed payload, so code like x = 1.5; a, b = [x, x]; a is x can become false on the JIT path even though the interpreter pushes the original element refs. The validation container needs to avoid arity-2 specialization or otherwise retain the original refs.

Useful? React with 👍 / 👎.

@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: 994e93b0d5

ℹ️ 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 +4854 to +4856
let int_typeobj = get_instantiate(&INT_TYPE);
trace_guard_exact_w_class(this, ctx, items[0], int_typeobj);
trace_guard_exact_w_class(this, ctx, items[1], int_typeobj);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Allow null w_class for cached small ints

When tuple elements are cached small ints, this guard rejects the trace even though is_plain_int1 selected the Cls_ii path: the small-int cache initializes PyObject.w_class to null (pyre/pyre-object/src/intobject.rs:65-68), and is_plain_int1 treats that null as an exact int, but the new replay guard compares it to the canonical int type object. As a result hot traces for common tuples like (1, 2) side-exit on the first replay instead of using the specialized tuple path; either skip/relax the guard for null exact-int w_class or initialize the cached ints consistently.

Useful? React with 👍 / 👎.

@youknowone
youknowone force-pushed the trace-visible-tuple-unpacking branch 2 times, most recently from 5b0b40a to c9a2a4f Compare June 6, 2026 06:14

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

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

ref_reg: u16,
dst_reg: u16,
) -> Insn {
let effect_info = effect_info_for_call_flavor(CallFlavor::Plain);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Treat unpack_sequence_fn as may-force

When this helper is used for unpack_sequence_fn (see the Instruction::UnpackSequence call site in codewriter.rs), CallFlavor::Plain is too weak for non-list/tuple operands: unpack_sequence_exact can call user code through baseobjspace::len, getitem, iter, and next for instances/iterables. In a hot loop unpacking a custom sequence, the residual call can therefore re-enter Python without the may-force virtualizable/vref preparation that CallFlavor::MayForce provides, leaving the compiled frame state inconsistent across callbacks or exceptions.

Useful? React with 👍 / 👎.

@youknowone
youknowone force-pushed the trace-visible-tuple-unpacking branch 2 times, most recently from c039ac0 to dc65ddb Compare June 7, 2026 12:20

@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/dc65ddb74226c992005d95c9329209228756225d/pyre-jit/src/call_jit.rs#L3323
P2 Badge Reject extra values from iterator unpacking

When the compiled UNPACK_SEQUENCE path reaches this helper for an iterable that has no sequence_len fast path, runtime_ops::unpack_sequence_exact consumes exactly count items and returns without probing for an additional item (runtime_ops.rs:835-849). That makes hot code such as a, b = iter([1, 2, 3]) continue successfully on the JIT path instead of raising the required "too many values to unpack" error; the previous codewriter path aborted rather than executing this helper. Please either add the final exhaustion check before returning the validation tuple or keep this residual path out of iterator fallback cases.

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

@youknowone
youknowone force-pushed the trace-visible-tuple-unpacking branch 2 times, most recently from 58844a0 to 019b327 Compare June 8, 2026 13:50
The tuple_unpacking synthetic benchmark was still slower than CPython after making UNPACK_SEQUENCE aware of specialised tuple instances. The remaining cost was that BUILD_TUPLE still emitted opaque jit_build_tuple_N helper calls, so the optimizer could not see or virtualize tuples that are immediately unpacked.

Mirror the PyPy/RPython object shapes instead of introducing side tables: arity-2 int, float, and object tuples now trace as W_SpecialisedTupleObject_ii/ff/oo with value0/value1 fields, while other arities trace as W_TupleObject with a wrappeditems GC array. UNPACK_SEQUENCE now guards and reads the corresponding specialised tuple fields directly before falling back to the canonical W_TupleObject wrappeditems path.

This keeps the trace aligned with pypy/objspace/std/specialisedtupleobject.py and tupleobject.py, and exposes the allocations and field stores to OptVirtualize.

Verification: cargo fmt; cargo check --no-default-features --features dynasm; cargo test --no-default-features --features dynasm -p pyre-jit-trace; cargo test --no-default-features --features dynasm; python3 pyre/check.py --synthetic-only --synthetic-pattern tuple_unpacking.py. The focused tuple benchmark now reports dynasm about 0.01-0.02s and cranelift about 0.02s versus CPython about 0.16-0.33s and PyPy about 0.01-0.03s. Full synthetic was also run; tuple_unpacking passed, while pre-existing unrelated failures remain in class_attrs_methods, context_manager, inheritance_dispatch, and cranelift iteration_protocol.
lifthrasiir and others added 21 commits June 8, 2026 22:53
Booleans were collapsed into ConcreteValue::Int, causing (True, False)
pairs to be reconstructed as plain ints during tuple specialisation.
This made trace_build_tuple_value wrongly take the Cls_ii path and emit
GuardClass(INT_TYPE) guards that immediately fail on bool objects,
creating a repeatable hot-path deoptimization loop.

Add ConcreteValue::Bool(bool) so the round-trip preserves type identity.
Also fix test fixtures that used w_none() as a fake w_class (None is not
a type object) — use &INSTANCE_TYPE instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add is_trace_plain_float predicate (mirrors is_trace_plain_int) so
  from_pyobj only unboxes exact floats; float subclasses stay as
  ConcreteValue::Ref, preventing trace_build_tuple_value from wrongly
  taking the Cls_ff path on reconstructed subclass items.

- Fix try_trace_const_boxed_int: replace is_int (which accepts
  subclasses and bools) with is_trace_plain_int, and reorder so
  is_bool is checked first. Int subclasses are no longer folded to
  plain int constants.

- Guard concrete_items.len() >= 2 before accessing [0]/[1] in
  trace_build_tuple_value to prevent panic on mismatched slice lengths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
write_ref_reg and the vable struct-pointer reader sanitize Bool into
the same Null path as Int/Float; write_int_reg coerces it to the i64
shadow value via 'as i64'.
trace_build_tuple_value selected the int-int and float-float specialised
tuple branches on is_plain_int1 / is_plain_float_strict but emitted only
ob_type guards via the unbox helpers, leaving a later int/float subclass
with the same payload layout free to replay the trace and lose subclass
identity. Emit GetfieldGcR(w_class) + PtrEq + GuardTrue against
get_instantiate(&INT_TYPE) / get_instantiate(&FLOAT_TYPE) before the
unbox so non-exact instances side-exit.
replaced_with_const was a Vec<Option<OpRef>> keyed by OpRef::raw() (a
position index), so IntOp(n) and RefOp(n) aliased to the same slot.
Store (old, new) OpRef pairs and match on the full OpRef in
get_box_replacement / replace_box. walk_const_ptr_refs forwards only
the value slot of each pair.

Assisted-by: Claude
as_operation (optimizer.py:372) returns None for a Const, since a Const
is not an AbstractResOp. find_producing_op called OpRef::raw() before
checking, which panics on inline-Const OpRef variants when
propagate_bounds_backward reaches a constant-folded operand.

Assisted-by: Claude
trace_guard_exact_w_class emitted GetfieldGcR(w_class) + PtrEq +
GuardTrue for every Cls_ii / Cls_ff tuple item. Items that are unboxed
payloads (Type::Int / Type::Float) or boxes freshly allocated in the
trace by wrapint / wrapfloat carry no divergent class: emit_box_int_inline
writes only the payload field and leaves PyObject.w_class zero, so the
guard compared 0 against the int/float type object and side-exited on
every replay. Skip the guard for non-Ref values and for unescaped
(heapcache-tracked in-trace) allocations, whose exact class is known.

Keep BuildTuple / UnpackSequence on the trait-dispatch path; the walker
jitcode dispatch aborts with InlineCallArityMismatch on the specialised
arity-2 tuple layout. Add a test_miframe helper and a regression test
for the primitive-value guard skip.

Assisted-by: Claude
Two cascading JIT failures hit any tuple build/unpack that compiles:

- StoreFastStoreFast's codewriter arm lowers to residual_call ops whose
  funcptr constants_i entries are unresolved placeholders (not rewritten
  by patch_constants_i_fnaddrs, which only maps build->runtime fnaddr
  pairs). The walker reads such a placeholder as the call target and
  branches to it, faulting on an unmapped address (SIGBUS). Drop
  StoreFastStoreFast from production_walker_handles so it dispatches
  through the trait handler opcode_store_fast_store_fast.

- The trace-visible specialised-tuple build virtualizes the inline
  value0/value1 Int fields; OptVirtualize::propagate_forward then
  forwards the re-boxed Ref result of the unpack onto an Int field,
  tripping make_equal_to's Box.type invariant (Ref vs Int) and aborting
  the optimizer. trace_build_tuple_value now builds through the opaque
  jit_build_tuple_N helper, keeping the tuple a real heap object the
  optimizer never virtualizes.

All tuple programs run crash-free and correct; tuple_unpacking is no
longer a SIGBUS. The trace-level specialised-tuple optimization is
disabled pending fixes to the two walker/optimizer bugs above.

Assisted-by: Claude
…c-tuple build

trace_guard_exact_w_class reads PyObject.w_class (offset 8) through a
shared descr whose index_in_parent is 0. OptVirtualize folded that
GetfieldGcR against a virtual W_IntObject (field 0 is intval, an Int),
returned the Int payload, and forwarded Ref <- Int, tripping the
make_equal_to Box.type invariant. This crashed the peeled loop of any
spec-tuple build whose int/float items become loop-carried virtuals.

- Add FieldDescr::is_w_class and SizeDescr::w_class_obj.
- Name the w_class guard descr "w_class" so is_w_class recognises it.
- PyreSizeDescr::w_class_obj returns get_instantiate(vtable_type).
- optimize_getfield_gc resolves is_w_class reads on virtuals from the
  stored w_class field (specialised tuples) or the size descr's
  canonical class constant (int/float), instead of indexing value
  fields.
- Re-enable the trace-visible specialised-tuple build in
  trace_build_tuple_value.

tuple_unpacking no longer crashes; the peeled loop is pure integer
arithmetic with the build->unpack tuple elided.

Assisted-by: Claude
BUILD_TUPLE emitted abort_permanent, so a blackhole resume whose
continuation builds a tuple (e.g. a tuple return on loop exit) hit
OpKind::Abort and invalidated the loop.

- bh_build_tuple_fn: (argc, item0..2) -> tuple via build_tuple_from_refs,
  CallFlavor::Plain like bh_build_list_fn.
- build_tuple_fn added to the Cpu helper table and FnPtrIndices, bound
  after the existing fn_ptrs to preserve their indices.
- emit_frontend_newtuple records the newtuple graph op.
- BuildTuple handler emits newtuple plus a residual_call_ir_r to
  build_tuple_fn for argc <= 3 via the fn-index-agnostic
  build_build_list_fn_residual_call_ir_r_insn; argc > 3 stays abort.

Assisted-by: Claude
UNPACK_SEQUENCE emitted abort_permanent, so a blackhole resume whose
continuation unpacks a sequence (e.g. a variable-tuple target after a
loop) hit OpKind::Abort and invalidated the loop.

- bh_unpack_sequence_fn(count, seq): validates the length and returns a
  tuple of the items via unpack_sequence_exact, raising ValueError /
  TypeError on mismatch; bh_unpack_item_fn(index, seq): sequence_getitem
  on that validated tuple. Both publish raises through BH_LAST_EXC_VALUE
  and are classified CallFlavor::Plain.
- unpack_sequence_fn / unpack_item_fn added to the Cpu helper table and
  FnPtrIndices, bound after the existing fn_ptrs to preserve indices.
- build_one_int_one_ref_fn_residual_call_ir_r_insn: (Int, Ref) -> Ref
  residual_call_ir_r shape (Int arg first, then Ref).
- UnpackSequence handler pops the sequence into a scratch reg, calls
  unpack_sequence_fn, then pushes each item read by unpack_item_fn in
  reverse so the stack top is item[0].

Assisted-by: Claude
…ct register-file length

The closing GuardFutureCondition in close_loop_args_at lazy-inits every
register the jitcode reports live at target_pc. With BUILD_TUPLE now a
real residual-call op, its destination color is reported live one slot
past the virtualizable window [nlocals..nlocals+stack_only], so
registers_r extends to 4 with a NONE tail that production fills via
materialize_fail_arg_slot. The same footprint occurs for BUILD_LIST and
BINARY_OP; the prior exact-length expectation held only while BUILD_TUPLE
was an abort stub that truncated liveness.

Relax the length assertion to a lower bound covering the window and scope
the non-NONE check to the virtualizable window the JUMP carries.

Assisted-by: Claude
…tualize

ensure_box was deleted in the BoxRef refactor; the w_class header-field
branch in optimize_getfield_gc still called it. Use BoxRef::from_bound_op
+ get_box_replacement, matching the sibling array-field branch.
… blackhole

_setup_return_value_* reads the result register as code[position-1].
The portal codewriter emits a per-push valuestackdepth setfield_vable_i
immediately after a call result push, landing a 5-byte sync between the
call's result-register byte and the next opcode's -live- resume anchor
that position points at. Add call_result_reg() to step back over that
sync (identified by BC_SETFIELD_VABLE_I + valuestackdepth VableField
descr) to the result register; non-portal jitcode keeps position-1.
cmp against a Reg or Frame location truncated the immediate to i32, so a
pointer-sized constant (e.g. a w_class type-object address) was compared
against its sign-extended low 32 bits and never matched. PtrEq guards on
such constants spuriously failed, deopting every loop iteration. Route the
Reg case through emit_cmp_imm64 and add the i32::try_from / scratch-register
fallback to the Frame case.

Also remove the fail_arg debug eprintln from the dynasm guard handler.
@youknowone
youknowone force-pushed the trace-visible-tuple-unpacking branch from 019b327 to ef9bdc9 Compare June 8, 2026 13:53

@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/ef9bdc9c4d6b27546ab0691e27f47304cc439130/pyre-jit-trace/src/trace_opcode.rs#L4968-L4971
P1 Badge Box primitive items before storing OO tuple fields

When an arity-2 build falls through to the Cls_oo path (for example (int_value, float_value) or (int_value, object_value)), the corresponding items[...] can still be a Type::Int/Type::Float op because exact Python ints/floats are kept unboxed in FrontendOp. This SetfieldGc stores those payload bits into a ref field, so later tuple reads can treat 1 or raw float bits as a PyObjectRef; the generic NewArrayClear path below has the same issue via trace_items_block_setitem_value. Box primitive stack values before writing them to OO/ref tuple storage.

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

@youknowone
youknowone merged commit 0b776f0 into youknowone:main Jun 8, 2026
25 checks passed
youknowone added a commit that referenced this pull request Jun 8, 2026
The Bool(bool) variant (state.rs:1424) added in #59 was not covered at
two walker-only match sites, leaving them non-exhaustive:
- arraylen_vable_via_metainterp: a Bool concrete is not a vable struct
  pointer, so it joins Null/Int/Float in mapping to 0 (unseeded abort).
- diagnose_inline_recognition: maps it to 'b' in the shape string.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Jun 11, 2026
The Bool(bool) variant (state.rs:1424) added in #59 was not covered at
two walker-only match sites, leaving them non-exhaustive:
- arraylen_vable_via_metainterp: a Bool concrete is not a vable struct
  pointer, so it joins Null/Int/Float in mapping to 0 (unseeded abort).
- diagnose_inline_recognition: maps it to 'b' in the shape string.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Jun 12, 2026
The Bool(bool) variant (state.rs:1424) added in #59 was not covered at
two walker-only match sites, leaving them non-exhaustive:
- arraylen_vable_via_metainterp: a Bool concrete is not a vable struct
  pointer, so it joins Null/Int/Float in mapping to 0 (unseeded abort).
- diagnose_inline_recognition: maps it to 'b' in the shape string.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Jun 13, 2026
The Bool(bool) variant (state.rs:1424) added in #59 was not covered at
two walker-only match sites, leaving them non-exhaustive:
- arraylen_vable_via_metainterp: a Bool concrete is not a vable struct
  pointer, so it joins Null/Int/Float in mapping to 0 (unseeded abort).
- diagnose_inline_recognition: maps it to 'b' in the shape string.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Jun 13, 2026
* refresh_virtualizable_shadow_from_heap: reserve trailing identity slot

`virtualizable_values`'s last entry stores the standard-vable identity
(`virtualizable_boxes[-1]` in RPython terms, see comment at
`virtualizable_box_at:1589`).  `synchronize_virtualizable` already stops at
`static_count + sum(lengths)`; the read path now mirrors that bound so a
short or misaligned shadow (e.g. only the identity slot present, or
fewer data slots than the schema implies) cannot overwrite the
identity from heap fields.

Use `values.len().saturating_sub(1)` as the upper bound for both the
static-fields loop and the array-items cursor.

* pyframe free-function helpers: also bind bare-path aliases

`pyframe_get_pycode`, `ncells`, and `npure_cellvars` carry
`#[elidable_cannot_raise]` and are called both qualified (`pyframe::name`
from other modules) and unqualified (`name(...)` inside `pyframe.rs`
itself, e.g. pyframe.rs:783/1136/1143/1717).

`target_to_path` for a `FunctionPath` returns the segments verbatim, so
the bare call resolves to the 1-segment CallPath `["<name>"]` while a
cross-module qualified call resolves to `["pyframe", "<name>"]`.  The
prior `push_fnaddr` registered only the 2-segment shape, so
`has_cannot_raise_assertion` missed the bare-path call sites and the
descrs stayed `EF_ELIDABLE_CAN_RAISE`.

Switch to `push_alias_pair` with both the 3-segment input
`pyre_interpreter::pyframe::<name>` and the 2-segment input
`pyre_interpreter::<name>`; `register_macro_helper_trace_fnaddr` strips
the leading segment of each, yielding the two CallPath shapes the
codewriter actually asks for.

* full-body walk: speculative int/float specialization for BINARY_OP/COMPARE_OP

Recognition vehicle (cross-crate: pyre-jit-trace does not depend on
pyre-jit, so fnaddr matching is unavailable):
- effectinfo: add OopSpecIndex::BinaryOp=200 / CompareOp=201, out of the
  upstream OS_* range.
- flatten: build_residual_call_ir_r_insn_from_operands tags the BINARY_OP
  / COMPARE_OP helper calldescr EffectInfo with the oopspec; pin test
  updated.

Full-body walk (PYRE_FULL_BODY_WALK, default-off):
- trace: run_perfn_walk / full_body_walk_trace drive dispatch_via_miframe
  over the per-CodeObject JitCode with per-fn descr pool, loop-input
  register seeding, and CloseLoop→CloseLoopWithArgs mapping.
- executor: execute_may_force_call runs a may-force helper concretely and
  reports (boxed_result_i64, exc_value); tests for non-raising, raising,
  and stale-exception-clear paths.
- jitcode_dispatch: dispatch_residual_call_iIRd_kind recognizes the
  oopspec and re-emits the specialization instead of an opaque
  CALL_MAY_FORCE. Int binary (Add/Sub/Mul ovf, And/Or/Xor, FloorDiv/Mod
  via call_typed_with_effect_pure ll_int_py_div/mod with rhs==0 and
  INT_MIN/-1 guards, Lshift ovfcheck, Rshift large-shift const-fold), int
  compare (6 ops, non-fused bool box), float binary (Add/Sub/Mul/TrueDiv +
  int operand CastIntToFloat), float compare (6 ops). Shared gates
  walker_execute_may_force_boxed / walker_concrete_ref_object and unbox
  helpers walker_unbox_int / walker_unbox_float / walker_coerce_operand_to_float.
- jitcode_runtime: overlay the full wellknown_bh_insns() universe onto the
  byte→opname decode table so runtime JitCodes decode every reserved BC_*
  byte.
- shadow_walker: pass authoritative=false (shadow validation must not
  re-execute may-force calls).

check.py dynasm 39/39; pyre-jit-trace 232/0. Walker remains default-off;
flag-on resume (snapshot jitcode_pc coordinate) is the remaining blocker.

* full-body walk: per-opcode snapshot coordinate + branch-guard resume target

Add FULL_BODY_SNAPSHOT_SYM thread-local + FullBodySnapshotSymGuard so
walker_capture_snapshot_for_last_guard, when run from dispatch_via_miframe,
derives the snapshot's Python pc from the guard op's jitcode pc via
python_pc_for_jitcode_pc (inverse pc_map) and reads liveness from the live
walk register banks (ctx.registers_i/r/f) at that pc.

Parameterize collect_outer_active_boxes with regs_i/regs_r/regs_f slices so
the full-body walk reads its fresh top_regs banks instead of sym.registers_*.

In the goto_if_not/iL handler, pass other_target (the not-taken-in-trace
branch destination) to walker_capture_snapshot_for_last_guard instead of
op.pc, matching trace_opcode.rs resume_pc = other_target: GuardTrue resumes
at the jump target, GuardFalse at the fall-through.

PYRE_FULL_BODY_WALK=1 int_loop produces 19999999900000000 (was
20000000100000000); check.py dynasm 39/39.

* full-body walk: resolve branch-guard target through register-renaming trampoline

goto_if_not's other_target can land on a synthetic ref_copy*; goto
renaming block (emit_trampoline_for_multi_pred_link) that carries no
Python pc. resolve_branch_target_through_trampoline follows such a chain
to the py-boundary destination before stamping the guard snapshot, so
python_pc_for_jitcode_pc no longer maps the trampoline offset to a wrong
Python opcode. Consumed only on the full-body-walk snapshot path
(FULL_BODY_SNAPSHOT_SYM set); default-off control flow uses the
unresolved target unchanged.

* full-body walk: forward-skip Python trivia at branch-guard resume coordinate

The inverse-pc_map can map a resolved branch target to a Python trivia
instruction's jitcode region (e.g. a NOT_TAKEN block). A resume
coordinate must be a real opcode boundary, matching the trait path which
resumes branches at semantic_fallthrough_pc / jump_target_forward (both
forward-skip trivia). skip_python_trivia_forward advances the stamped
py_pc past Cache/ExtendedArg/Resume/Nop/NotTaken to the next opcode, so
the resume reader's backward trivia backtrack is a no-op instead of
walking a NOT_TAKEN coordinate back to the preceding branch opcode (whose
block-entry liveness differs and desynced the snapshot box-count against
the resume liveness, reading past section.values and crashing).

bool_compare full-body-walk now matches default (5809236); check.py
dynasm 39/39; int/float/nested unaffected.

* full-body walk: fold LoadConst residual_call to constant ref

Add OopSpecIndex::LoadConst (202) and tag the load_const_from_code helper
calldescr with it in build_residual_call_ir_r_single_ref_plain_insn_from_operands.

In dispatch_residual_call_iIRd_kind, when the call carries the LoadConst
oopspec and both the const index and code-pointer operands are concrete,
materialize co_consts[idx] (mirroring bh_load_const_fn) into a const_ref and
suppress the residual.

flag-on int_loop/float_loop/nested_loop now complete in 2.7-5.5s (were
10-36s / check.py timeout). check.py dynasm 39/39.

* full-body walk: bridge resume uses section pc, not vable last_instr

The full-body walk sets the concrete frame's last_instr once at the loop
header and never advances it per opcode, so decode_and_restore_guard_failure's
next_instr() (derived from the vable last_instr field) carries the header pc
for a mid-body guard. resume_in_blackhole already resumes at the per-frame
section pc (ResumedFrame.py_pc), which is correct; the bridge-compile path
read next_instr() instead and started tracing from the loop header, double-
executing the loop body before the guarded branch.

Prefer the innermost resumed-frame section pc when it disagrees with
next_instr(); for the trait tracer they always match (no-op there).

float_arithmetic flag-on now matches (was 883662 vs 998193). check.py
dynasm 39/39.

* full-body walk: skip concrete execution of void-result residual calls

try_execute_residual_call_via_walker ran every Call* concretely, including
void-result CallN/CallMayForceN (STORE_SUBSCR, list.append, dict setitem).
Those carry no value the walk specializes on; executing one only commits
its heap side effect. Since the loop body the walk traces is re-executed by
the loop itself, the committed mutation lands twice on the live heap — the
traced iteration is counted twice (list_index_update +192, dict_lookup_update
+3905). Return early when call_descr.result_type() is Void, leaving the side
effect to the loop, mirroring the trait tracer which records these
symbolically and never touches the real heap.

PYRE_FULL_BODY_WALK=1: list_index_update / dict_lookup_update / list_append_pop
now match the default backend; float_arithmetic unchanged. dynasm 39/39.

* full-body walk: abort trace on may-force call with concrete-NULL Ref arg

A user-function call whose callee is loaded from a local (or is a closure)
folds to a specialized direct-entry call: funcptr = the callee entry, with
the PUSH_NULL self-slot baked as ptr(0x0). try_execute_residual_call_via_walker
skips such a call (its NULL-receiver SEGV guard), leaving the result
unresolved, and the baked NULL arg makes the compiled call pass NULL where
the entry needs the callee's globals/closure → NULL result at runtime
(closures / locals-bound callees called in a loop returned NULL). The trait
tracer declines this shape and aborts; mirror it.

walker_abort_if_mayforce_null_ref_arg returns DispatchError::
MayForceNullRefArgUnsupported when a CallMayForce{R,I,F} carries a Ref arg
folded to GcRef(0); the driver maps the error to TraceAction::Abort so the
loop falls back to the interpreter instead of compiling a wrong trace.
Wired into the iRd / iIRd / iIRFd residual-call dispatchers.

PYRE_FULL_BODY_WALK=1: closures / function_calls / recursion /
class_attrs_methods / inheritance_dispatch now match the default backend;
24/25 synth benches match (exceptions pre-existing). dynasm 39/39,
pyre-jit-trace 232/0.

* full-body walk: abort cross-loop cut when inner merge point shape differs from jump args

The walker's jit_merge_point handler registers the inner merge point from
the JitCode merge-point reds ([frame, ec], len 2 for the portal
jitdriver), while the loop close rebuilds jump_args via close_loop_args_at
with the full virtualizable expansion (locals + stack). When the trace
starts before the loop header (loop_header_pc != start_pc) a cross-loop
cut replaces the trace inputargs with the reds-only merge-point boxes,
but next_iteration_args keeps the expanded length; for loops that
virtualize a heap object (fannkuch list, nbody body tuples) the unroll
peel then reads an inputarg slot past the cut list end and panics in
inputarg_type_at_strict.

Detect the shape mismatch at close and abort the full-body walk so the
portal falls back to the trait tracer. Loops without a heap virtual have
matching shapes and still close under the walk.

dynasm + cranelift check.py 39/39 each; pyre-jit-trace 232/0. fannkuch /
nbody flag-on now produce correct output instead of panicking.

* full-body walk: abort may-force trace in functions with exception handlers; fix walker_unbox class_now_known arg type

A may-force residual call that can raise emits GUARD_NO_EXCEPTION. When
that guard fails at runtime the deopt bridge must resume into the
function's exception handler, but the walker's guard resume snapshot does
not seed the standing exception value, so the bridge dereferences a NULL
exception object (GuardClass ldr [x0], x0=0) and segfaults
(exceptions.py, N>=~1000). Abort the walk when the jitcode body contains
any catch_exception op so the portal falls back to the trait tracer,
which resumes the handler correctly. Bodies without catch_exception
(nbody / fannkuch / loop benches) are unaffected and still compile under
the walk.

Also fix walker_unbox_int / walker_unbox_float passing GcRef to
HeapCache::class_now_known, whose parameter is i64; the type error was
latent because incremental compilation had cached the stale typecheck.

dynasm + cranelift check.py 39/39 each; pyre-jit-trace 232/0. exceptions.py
flag-on now matches flag-off (1125000) instead of SIGSEGV; nbody / loop
benches unchanged under flag-on.

* full-body-walk: bail InvalidLoop on next_iteration_args longer than inputargs

Replace the trace.rs CloseLoop cross-loop-cut shape-mismatch abort with an
optimizer-level InvalidLoop bail.

The CloseLoop guard returned TraceAction::Abort after the authoritative walk
had already executed the loop body concretely, so loops with observable
side-effects (nested_loop's print) ran twice. The unroll peel's typed_inputargs
loop now panics InvalidLoop when next_iteration_args has an entry past the cut
inputarg list end, routing through compile_loop_body's catch (interpreter
fallback) without re-executing the body, instead of inputarg_type_at_strict
hard-panicking the worker thread.

* full-body-walk: don't concretely execute LoadConst residual calls

try_execute_residual_call_via_walker skips calls whose descr carries the
LoadConst oopspec. The LOAD_CONST helper has a dedicated fold in the
residual_call dispatchers that materializes co_consts[idx] when both the
const index and the code pointer (frame.pycode) are concrete; when that fold
declines, the residual is recorded so the loop computes it at runtime from
the live frame's pycode. An inlined-callee sub-walk does not seed
frame.pycode concrete, so the fold declines, and concretely executing the
recorded residual passed the unseeded code pointer to bh_load_const_fn, which
dereferenced it via w_code_get_ptr and faulted (SIGSEGV when a float-returning
helper with a co_const is inlined into a hot loop).

* optimizeopt: fold standard-virtualizable getarrayitem read-after-write

optimize_getarrayitem_gc folds a constant-index read on the standard
virtualizable array field to the box tracked in vstate.arrays (seeded
from the inputarg layout, updated by mirror_setarrayitem), mirroring the
static-field fold in optimize_getfield_gc. The matching setarrayitem is
left emitted, so no heap write is dropped; only the redundant read is
removed. Adds VirtualizableTracker::tracked_array_element.

* jitcode_dispatch: precise may-force exception-handler abort gate

walker_abort_if_protected_may_force aborted the full-body walk for any
may-force-can-raise call whenever the body contained any catch_exception.
Resolve the call's Python PC via python_pc_for_jitcode_pc (the inverse
pc_map already used by walker_capture_snapshot_for_last_guard) reached
through FULL_BODY_SNAPSHOT_SYM, then lookup_exceptiontable on the
CodeObject's exceptiontable: abort only when that Python opcode is
actually covered by a handler. Calls with no covering handler raise out
of the frame and are safe to walk. Falls back to the whole-body scan
when no full-body sym pointer is set.

flag-on exceptions.py=1125000 and raise_catch_loop=1142858 (both match
cpython); check.py 39/39 dynasm; pyre-jit-trace 232/0.

* WIP: FBW #67/#62 experiments (candidate-b shape fix, boxing/subscr oopspec, graceful abort)

effectinfo: add OopSpecIndex::BoxInt (203) and StoreSubscr (204) tags.
trace_ctx: graceful-abort try_set_opref_concrete (returns false when no
  frontend op/inputarg is recorded; routes through recorder.set_concrete_at
  per the #123 canonical-op-identity model, no BoxPool side-table).
jitdriver: TraceContinuationSuspendGuard around may-force concrete execution.
optimizer: InvalidLoop bail on next_iteration_args longer than inputargs.
jitcode_dispatch: append_virtualizable_boxes merge-point shape fix +
  entry_py_pc snapshot substitution + BoxInt/StoreSubscr walker dispatch.
trace: deferred-void-store event log + no-loop entry seeding.
flatten: box_int/store_subscr calldescr tagging.

Experimental; proven worse than the clean InvalidLoop-bail baseline.
FBW-flag-gated (PYRE_FULL_BODY_WALK, default off); inert at default env.

Assisted-by: Claude

* jitcode_dispatch: advance resume coordinate past may-force call for after_residual_call guards

The full-body-walk guard-snapshot helper mapped a guard's jitcode op_pc back
to the Python opcode CONTAINING it and resumed there. For GUARD_NOT_FORCED /
GUARD_NO_EXCEPTION emitted after a may-force residual call, that opcode is the
call's own opcode (e.g. STORE_SUBSCR); the call already ran in compiled code
and consumed its Python stack operands, so resuming at it re-executes the call
from a coordinate whose stack no longer holds them.

Split walker_capture_snapshot_for_last_guard into an _impl taking
after_residual_call, add walker_capture_snapshot_after_residual_call, and route
the four may-force guard sites (release-gil + iRd dispatcher GUARD_NOT_FORCED /
GUARD_NO_EXCEPTION) through it. When set, the resume coordinate advances via
semantic_fallthrough_pc to the next executable opcode, matching
capture_resumedata(after_residual_call=True) (pyjitpl.py:2599-2603).

FBW-gated (PYRE_FULL_BODY_WALK, default off). check.py 39/39 flag-off; flag-on
int/nested/list-read loops unaffected.

Assisted-by: Claude

* compile_loop_body: convert SpeculativeError to InvalidLoop bail

The optimize catch in compile_loop_body handled only InvalidLoop; a
SpeculativeError (constant_fold of an ill-typed speculative heap access,
raised via panic_any at optimizeopt/mod.rs:534) fell through to
resume_unwind, propagating the panic across the JIT FFI boundary and
crashing the interpreter. unroll.py:119-123 maps SpeculativeError to
InvalidLoop; treat both the same here so the optimized trace is
abandoned and tracing continues, rather than re-raising.

Assisted-by: Claude

* eval: full-body-walk guard failures resume via blackhole, skip bridge

Under PYRE_FULL_BODY_WALK, handle_fail now returns ResumeInBlackhole for
every guard failure instead of compiling and attaching a bridge. The
full-body walk sets the concrete frame's last_instr once at the loop
header and never advances it per opcode, so a guard's per-PC resume
coordinate and partial operand stack are reconstructed precisely only by
the blackhole's rd_numb consume_one_section decode. A bridge compiled
and attached from such a resume produces compiled code that mishandles
the resumed state — a hot in-place list-swap loop dropped one iteration
at the round an inner-loop guard's failure counter reached
trace_eagerness. Forcing the blackhole path matches the behavior proven
correct under the diagnostic no-bridge mode.

The flag is cached in a OnceLock because handle_fail is hot on
recursion-heavy guard chains; a per-call env read regressed
fib_recursive past its timeout. The trait tracer (flag-off) is
unaffected.

Assisted-by: Claude

* jitcode_dispatch tests: add raw_descrs/is_authoritative_executor to two WalkContext literals

The two vable-box-not-seeded test constructions predate the
raw_descrs/is_authoritative_executor WalkContext fields; set them to the
production defaults (RawDescrPool::Global, false).

Assisted-by: Claude

* full-body walk: abort may-force trace in any exception-handler body

walker_abort_if_protected_may_force returned Ok(()) unconditionally under
the full-body walk, relying on bridge handler-resume seeding to supply the
standing exception value. That seeding does not cover the multi-frame
shape where an inlined callee raises and the caller's handler reads the
exception (synth/exceptions: may_fail raises, main catches and reads
e.args[0]): the exception-path bridge bakes a NULL exception object and the
compiled GuardClass dereferences x0 == 0 (SIGSEGV).

Restore the conservative whole-body gate for the full-body walk too: abort
to the trait tracer whenever the walked body contains any catch_exception.
Bodies without a handler still compile under the walk.

flag-off check.py 41/41; flag-on synth/exceptions no longer crashes
(1125000, matches flag-off) and the flag-on suite is 34/41 (was 33/41 with
the crash), no other bench regressed.

Assisted-by: Claude

* walker int/float specialization: exclude bool operands

walker_int_specialization_operands and walker_float_specialization_operands
accepted bool operands because is_int returns true for W_BoolObject. The
specialization then unboxed them via the int path (GuardClass INT_TYPE +
GetfieldGcPureI intval@16), but W_BoolObject has its own &BOOL_TYPE vtable
and a 1-byte boolval field, not W_IntObject's 8-byte intval at offset 16.
A boolean compare result used as an arithmetic operand (e.g. `x = a < b;
s = s + x`) was therefore miscompiled under PYRE_FULL_BODY_WALK: the
GuardClass guarded the wrong class and the getfield read 7 bytes past
boolval.

Reject bool in both specializations so such operands route through the
generic residual call, which forces the correct value.

Assisted-by: Claude

* walker list subscr/store specialization: exclude bool index and value

try_walker_specialize_subscr and try_walker_specialize_store_subscr gated
the index operand (and the int-storage store value) on is_int, which is
true for W_BoolObject. A bool index (lst[a < b]) or bool value stored into
an int-storage list was unboxed via the int path (GuardClass INT_TYPE +
getfield intval@16), reading the 1-byte boolval as an 8-byte intval and
guarding the wrong class.

Reject bool in both gates so these operands route through the generic
residual call.

Assisted-by: Claude

* full-body walk: abort branch guard whose resume target keeps an operand-stack temp

A goto_if_not branch guard resuming at a target with operand-stack depth
> 0 — the short-circuit `and`/`or`, conditional expression, and chained
comparison shapes, where CPython keeps the tested value on the value
stack across the branch — corrupts a loop-carried slot on guard-failure
deopt: the single-frame snapshot does not model the kept temp on the
not-taken arm. Detect this via the resume-target stack depth
(branch_resume_target_stack_depth, full-body-walk only) and surface
DispatchError::BranchGuardKeptStackUnsupported, mapped to AbortPermanent
→ interpreter fallback. Plain while/if branches resume at depth 0 and
still compile.

flag-off check.py 41/41; flag-on 34/41 (unchanged, remaining 7 are perf
timeouts); pyre-jit-trace 234/0.

Assisted-by: Claude

* full-body walk: elide dead box_bool when compare result is consumed only by the branch

A COMPARE specialization (try_walker_specialize_compare_op_int/_float)
emits the raw truth plus a box_bool CallR that lands a W_Bool in the Ref
dst. When the only consumer is the immediately-following is_true
(POP_JUMP_IF_*), that op folds to the raw truth (bool_box_truth), leaving
the box dead — but it is a non-pure CallR the optimizer cannot DCE, so it
runs every loop iteration.

compare_box_provably_dead does a forward JitCode lookahead proving the
boxed Ref dst is read by exactly one op (an is_true residual), never
overwritten before it, the scan ends at a goto_if_not, and the dst color
is dead at both branch arms (so the guard snapshot cannot capture the
marker). When proven, the box is elided: write the truth into the Ref dst
as a marker and record bool_box_truth(truth, truth) so the is_true fold
resolves it. Any other shape (escape to a local, arithmetic use, second
reader, register reuse, kept-on-stack short-circuit) falls back to
emitting the real box. Full-body-walk only.

flag-off check.py 41/41; flag-on 34/41 → 36/41 (int_loop now passes,
nested_loop improved); pyre-jit-trace 234/0. Escape cases (x=i<n; s=s+x),
bool family, and plain while/if verified flag-on == cpython.

Assisted-by: Claude

* full-body walk: add env-gated call-inlining recognition probe

dispatch_residual_call_iRd_kind gains a PYRE_DIAG_INLINE_RECOG-gated
diagnostic (authoritative-executor path only) that reads each Ref arg's
concrete value, detects a pure-Python function (FUNCTION_TYPE, via
is_function + ob_type), resolves its CodeObject, and reports whether a
per-fn JitCode is present in the setup-time and codewriter stores.

On inline_helper the probe identifies the callable at arg#1 of every
call_fn and reports both stores MISS: pyre builds per-fn JitCode lazily,
so the callee JitCode is absent at caller-trace time. The on-demand build
path jitcode_for (state.rs:593) was measured to build+install these
callees successfully.

No behavior change without the env var (single bool guard per call_fn).

Assisted-by: Claude

* full-body walk: extend inline recognition probe with call_fn arg layout

The PYRE_DIAG_INLINE_RECOG probe now reports nargs, a per-arg concrete
kind shape, and the callable index. On inline_helper the layout is
[ctx, callable, positional_args..]: callable at index 1, positional args
from index 2 (1-arg call → nargs 3, 2-arg call → nargs 4).

Assisted-by: Claude

* full-body walk: add sub_jitcode_body_for_code callee-body obtain helper

sub_jitcode_body_for_code(code) builds (via jitcode_for) and views a
callee per-fn JitCode as a SubJitCodeBody. Unlike the per-function
sub_jitcode_lookup (indexed by the current function's descr pool), this
resolves an arbitrary runtime callable's code, which call inlining needs.
The Arc<PyJitCode> is program-lived in the append-only jitcodes store, so
the 'static slice extension matches the per-fn arm-entry borrow extension
at trace.rs:363.

The PYRE_DIAG_INLINE_RECOG probe now exercises it, reporting each callee's
register-bank shape (inline_helper: add/mul body 27 bytes regs_r=17,i=1;
compute 96 bytes regs_r=19). Gated; flag-off 41/41 unchanged.

Assisted-by: Claude

* full-body walk: dev-gated user-function call inline sub-walk (PYRE_FBW_INLINE)

try_walker_inline_user_call sub-walks a recognized user-function call_fn in
place of the residual call: exact-positional, closure-free callees only;
binds r_args[2..] to callee locals [0..nparams]; runs walk() over the callee
per-fn JitCode (sub_jitcode_body_for_code); writes SubReturn to the call dst;
routes SubRaise through try_catch_exception_at. Sub-walk DispatchErrors
propagate as a trace abort (sound; never mixes inlined + residual emission).
Guards inside resume to the caller CALL boundary via the inherited
single-frame snapshot.

Gated by PYRE_FBW_INLINE (default off); production flag-on path unchanged,
flag-off 41/41. Validated correct on a 200k-iter inline_helper variant
(matches CPython/flag-off/flag-on = 647933338). Currently perf-negative
(0.53s vs 0.35s gate-off) — the single-frame-resume inline yields a correct
but slower trace; trace-quality / multi-frame-resume work is the next step.

Assisted-by: Claude

* full-body walk: diagnose inline sub-walk trace aborts (PYRE_FBW_INLINE_DIAG)

Add a gated log of the callee sub-walk DispatchError. On inline_helper the
dev-gated inline produces correct output but aborts 25 traces; the errors
pinpoint two callee frame-setup gaps the bare sub-WalkContext skips:
VableBoxNotSeeded / VableArrayDescrMalformed (callee frame vable not seeded)
and ResidualCallDescrNotCallDescr (a nested call indexes the parent descr
pool, not the callee's own pool). These are the next steps for the inline to
compile rather than fall back to the interpreter.

Assisted-by: Claude

* full-body walk: decode-dump callee body under PYRE_FBW_INLINE_DIAG

Dump the callee per-fn JitCode opcodes. Confirms the inline-abort root: the
add callee reads its params via getfield_vable_r at pc 3 (a virtualizable
frame access), not from registers. So the register-binding sub-walk does not
match how a per-fn JitCode reads locals; the callee virtualizable frame must
be seeded with the args (build_pending_inline_frame-style) rather than
binding callee registers.

Assisted-by: Claude

* full-body walk: build real callee frame + per-fn descr pool for inline sub-walk

The callee per-fn JitCode is portal-shaped: LOAD_FAST lowers to
getfield_vable_r(r0=frame, localsplus[i]), so the inline sub-walk needs a
genuine heap frame at r0 (read via the _nonstandard_virtualizable ->
GETFIELD_GC fallback), not register-bound args. try_walker_inline_user_call
now builds the concrete callee frame (PyFrame::try_new_for_call_with_closure_
and_globals_obj over the concrete positional args + the caller frame's
execution context) and emits the symbolic callee-frame OpRef via the
one_arg_callee_frame_helper / callee_frame_helper(n) helpers, then seeds
callee_regs_r[0] / callee_concrete_r[0] with it.

The callee body resolves its d/j descr operands through its OWN per-fn pool,
not the caller's. sub_jitcode_descr_pool_for_code (memoized per code ptr,
mirror of trace.rs:363-400) builds the callee's adapted descr_refs +
RawDescrPool::PerFn + sub_jitcode_lookup; the sub_wc now uses them.

Moves the PYRE_FBW_INLINE sub-walk abort past VableBoxNotSeeded (no frame at
r0) and ResidualCallDescrNotCallDescr (wrong descr pool). Remaining blocker:
VableArrayDescrMalformed at the LOAD_FAST sites — the callee jitcode lacks the
(VableArray, Array) descr pair. Dev-gated PYRE_FBW_INLINE (default off);
flag-off check.py 41/41.

Assisted-by: Claude

* full-body walk: diagnose VableArrayDescrMalformed in inline sub-walk

Add a PYRE_FBW_INLINE_DIAG-gated dump at the (VableArray, Array) pair
mismatch arm of vable_array_descrs_from_jitcode + a RawDescrPool::len()
helper.  On inline_helper the callee sub-walk reports pool_len=22,
field_idx=21 resolving to the body's BinaryOp Call descr and array_idx=22
out of range — the decoded descr indices point at the tail of the callee
pool, not a vable-array pair, indicating a descr-operand-offset/routing
mismatch in the callee body decode rather than an absent descr.

Dev-gated; no production path change.

Assisted-by: Claude

* full-body walk: seed callee entry registers per setup_call convention

Straight-line callee entry (pc=0, no governing loop header) places the
jitdriver args [pycode, frame, ec] at r0/r1/r2 (trace.rs:341-345), and the
body reads its vable from r1 — not r0.  The inline sub-walk previously seeded
only r0=frame, so the body's getfield_vable_r read r1 (NONE) and aborted
VableBoxNotSeeded.  Seed all three (pycode=const_ref(w_code), frame=callee
frame OpRef, ec=const_ref(exec_ctx)) with concretes, and require >=3 Ref
register slots.

With this the PYRE_FBW_INLINE sub-walk runs the whole callee body with zero
decode / descr / vable aborts (VableBoxNotSeeded + VableArrayDescrMalformed
both gone).  The next blocker is multi-frame guard resume: a callee-emitted
GuardValue has resume_pos=-1 (store_final_boxes_in_guard panic) because the
inline guards lack a snapshot modelling the caller+callee frame stack
(#124/#65 family).  Dev-gated PYRE_FBW_INLINE; default off.

Assisted-by: Claude

* full-body walk: emit callable guard_value + no-exception guard for inline

pyjitpl recursive_call specializes the inlined body on the exact callable via
guard_value(callable) before building the callee frame, and emits
GUARD_NO_EXCEPTION right after the frame-build helper
(build_pending_inline_frame:5986 + 6153).  The inline sub-walk now emits both
(with walker_capture_snapshot_for_last_guard), and pre-checks frame-helper
availability before any IR so the unsupported-arity path bails cleanly.

Advances the PYRE_FBW_INLINE trace build further (the callable-specialization
GuardValue now carries a snapshot).  One orphan GuardValue (resume_pos=-1)
remains at store_final_boxes_in_guard time, emitted by a non-walker-record
path (may-force promotion / multi-frame resume integration, the (4) frontier
documented at jitcode_dispatch.rs:4756).  Dev-gated PYRE_FBW_INLINE; default
off.

Assisted-by: Claude

* full-body-walk inline: capture resume snapshot for helper-internal promote guards

The `_nonstandard_virtualizable` check in `TraceCtx`'s vable_* helpers
records a PTR_EQ + `promote_int` GuardValue internally for a frame that is
not the standard virtualizable, then `emit_force_virtualizable` records
GETFIELD_GC / PTR_NE / COND_CALL after it. Only an inlined callee's heap
frame is non-standard (the production main frame is the standard
virtualizable, so the check short-circuits and emits nothing), so the
walker never attached a resume snapshot to that guard and
`store_final_boxes_in_guard` reached it with `rd_resume_position == -1`.

Add Recorder::set_last_guard_op_resume_position (stamps the most-recent
guard op, skipping the non-guard ops recorded after the promote) and the
matching History::capture_snapshot_for_last_guard_op_with_vable_vref;
`set_last_op_resume_position` could not be reused because the guard is not
the last op once emit_force_virtualizable runs.

In the walker, gate a guard-count delta check (TraceCtx::num_guards)
around each vable_* call via INLINE_SUBWALK_CAPTURE_BOUNDARY (set only
during a `try_walker_inline_user_call` sub-walk). When a guard was emitted,
capture a single-frame snapshot at the caller's CALL boundary
(outer_active_boxes / entry_py_pc) — re-executing the call on deopt — and
stamp it on the guard op. The same flag routes the existing
walker_capture_snapshot_for_last_guard away from the callee-pc-via-outer-
pc_map mapping for inline sub-walks.

Dev-gated by PYRE_FBW_INLINE; no-op when the flag is unset (one num_guards
read). check.py flag-off 41/41, majit-metainterp 1277, pyre-jit-trace 234.

Assisted-by: Claude

* full-body-walk inline: seed callee registers with args (fast-path convention)

The inlined callee body is entered at pc=0 and reads its parameters
straight from registers_r[0..nparams], not via getfield_vable against a
heap frame. Decode of add/mul/square/compute confirms the body is
`ref_copy r0->r0 / ref_copy r1->r1 / residual_call(r0,r1) / ref_return r0`
— the can_skip_traced_callee_frame convention (build_pending_inline_frame:
`sym.registers_r = args.to_vec()`; same seeding dispatch_inline_call_dr_kind
uses). The previous code built a heap callee frame and seeded
[pycode@r0, frame@r1, ec@r2] (the portal-entry convention), so the callee
computed `pycode + frame` and failed at runtime with "TypeError: call
failed".

Replace the heap-frame build + frame-build helper + GuardNoException with
direct arg seeding: callee_regs_r[i] = r_args[2+i], concretes likewise.
Keep the guard_value(callable) specialization. A callee that materializes
a frame (extra locals → getfield_vable on an unseeded register) aborts
cleanly with VableBoxNotSeeded -> trait fallback, never a miscompile.

ih_tiny (single-level), ih_small (nested compute->add->square), and
bench/inline_helper.py now all produce correct output under
PYRE_FBW_INLINE. Trace quality / compile (the traces still abort or
guard-thrash, so no perf win yet) is the remaining #62 work. check.py
flag-off 41/41.

Assisted-by: Claude

* optimizeopt: migrate tracked_array_element to BoxRef array source

resolve_array_source takes &BoxRef after the box-identity migration, but
its read-path caller tracked_array_element still took OpRef and the
optimize_getarrayitem_gc call site passed an out-of-scope `array_ref`.
Under --no-default-features --features dynasm this failed to compile
(E0308 expected &BoxRef found OpRef; E0425 array_ref not in scope).
Thread the BoxRef through tracked_array_element symmetrically with the
already-migrated mirror_setarrayitem write path.

Assisted-by: Claude

* FBW: dev-gated LOAD_GLOBAL cell-cache fold (PYRE_FBW_LOADGLOBAL_FOLD)

Add OopSpecIndex::LoadGlobal (205) and tag the load_global_fn calldescr in
build_load_global_fn_insn_from_operands. The full-body walker's
dispatch_residual_call_iIRd_kind recognizes the tag and, when namei /
namespace / promoted pycode are concrete, emits the cell-cache fast path
(quasiimmut_field + record_known_result + elidable jit_namespace_cell_lookup,
plus getfield on an ObjectMutableCell) mirroring the trait load_global_value,
folding the per-iteration module-global read to a loop-invariant cell pointer.

The reader is gated three ways: is_authoritative_executor, no catch_exception
in the body (load_global_fn is CallFlavor::Plain/can-raise; a handler-bearing
body must keep falling back via walker_abort_if_protected_may_force), and the
PYRE_FBW_LOADGLOBAL_FOLD env flag (default off). When enabled, a folded
function callee that is then CALLed mis-resolves through the in-progress FBW
call-inlining path and yields wrong output for global-function-call loops, so
the fold stays default-off until that path lands.

Adds pyframe::load_name_from_code (co_names[idx] accessor).

Default behavior unchanged: check.py dynasm 41/41 flag-off, 35/41 flag-on;
majit-ir 303+5, pyre-jit-trace lib 234.

Assisted-by: Claude

* FBW: route bridge traces through full_body_walk_trace; skip merge-point for bridges

Revert the trait-bridge hybrid: trace_bytecode no longer gates the walker on
`!ctx.is_bridge_trace`, so under PYRE_FULL_BODY_WALK every trace, main and
bridge, goes through full_body_walk_trace (single tracer).

full_body_walk_trace skips add_merge_point when ctx.is_bridge_trace: a bridge
resumes at a mid-loop pc, not a loop header, so it registers no portal entry
signature there.

flag-off check.py 41/41; flag-on default 35-36/41 (both unchanged — the edits
are inert unless PYRE_FBW_BRIDGE enables guard-fail bridges, default-off). The
bridge float-accumulator resume bug (#67) is unchanged: setup_bridge_sym leaves
an unboxed loop-carried scalar's localsplus slot at its stale trace-time box.

Assisted-by: Claude

* FBW: publish last_instr=py_pc-1 to vable shadow at walker guard snapshot

walker_capture_snapshot_for_last_guard_impl resolves the guard's resume
py_pc but did not update the `last_instr` static vable scalar before
building the snapshot boxes. The walker walks JitCode and never crosses
`set_orgpc`, so `virtualizable_boxes[last_instr]` kept the value the trace
seed / previous `close_loop_args_at` override wrote (the loop-header pc).
The blackhole / vable-sync resume reads that scalar into `frame.last_instr`,
so a mid-body guard resumed at the loop header instead of its own opcode.

Mirror `MIFrame::publish_last_instr_to_vable`: publish `py_pc-1` into the
shadow (gated on `owns_virtualizable_shadow`) before
`build_snapshot_vable_vref_boxes`, and defer that build until after the
publish. Make `mirror_vable_static_to_boxes` pub(crate).

Verified: flag-on+PYRE_FBW_BRIDGE spectral_norm bridge resume now writes
frame ni=94 (was 13), matching the trait. check.py dynasm 41/41,
pyre-jit-trace lib 234/0.

Assisted-by: Claude

* FBW: emit intermediate merge-point vable→heap writeback at walker register branch

The walker `jit_merge_point` register branch returned `Continue` without
the vable→heap writeback that the trait's `close_loop_args_at`
(trace_opcode.rs:2875-2950) runs at every merge point: override the vable
`last_instr` scalar to `merge_pc - 1`, mirror it into the
`virtualizable_boxes` shadow, and on a reduced (reds-only) target LABEL
call `gen_writeback_vable_to_heap`. The walker's loop-close path already
routes through `close_loop_args_at` (run_perfn_walk) so it emitted the
final writeback, but an intermediate loop header reached mid-trace — e.g.
a bridge re-entering the inner loop — skipped it, so the compiled body
left the heap PyFrame `last_instr`/array at the trace-seed state.

With the writeback emitted, the flag-on+PYRE_FBW_BRIDGE walker bridge for a
nested while loop now carries the intermediate GcStore(_, 32, merge_pc-1)
and its op count matches the trait (pre-opt 75→105, compiled 36→46).

check.py 41/41 dynasm + 41/41 cranelift.

Assisted-by: Claude

* FBW: restore live frame to guard resume state after bridge trace

`trace_and_compile_from_bridge` set the live frame's `last_instr` to the
guard `resume_pc` before tracing so that a `BridgeCompiled` outcome
re-enters `eval_loop_jit` at the guard point — the
`ContinueRunningNormally` arm runs the interpreter forward from the live
frame's `last_instr` / value stack without reconstructing it.

The trait tracer interprets a private `snapshot_for_tracing` copy and
never touches the live frame. The full-body walker executes may-force
residual calls concretely through the shared execution context during
the walk, which advanced the live frame's `last_instr` to a walked
opcode (a jitcode-region pc) and stepped its locals (a loop counter) to
the body's concrete state. The post-bridge interpreter then resumed
mid-body (value-stack underflow) or past a dropped loop iteration
(off-by-one-iteration result) under PYRE_FULL_BODY_WALK=1 +
PYRE_FBW_BRIDGE=1.

Snapshot the resume state (last_instr, valuestackdepth,
locals_cells_stack_w) before the walk and restore it afterward via the
new `PyFrame::restore_resume_state_from`. The trait path is a no-op
(it never mutates the live frame).

Verified: nested-loop bridges f(1200,3)=2158200, f(2000,10)=89955000,
spectralnorm(50)=1.2741938369830932 all correct and deterministic under
flag-on+bridge; check.py 41/41 dynasm + 41/41 cranelift ×2.

Assisted-by: Claude

* FBW: recover guard-resume section pc from build_resumed_frames return

decode_and_restore_guard_failure now binds build_resumed_frames's return
and reads the innermost resumed frame's py_pc to derive the resume
coordinate, replacing a read of the removed LAST_GUARD_FRAMES thread-local.

Assisted-by: Claude

* compile_bridge: give up when terminal JUMP arg count differs from target LABEL

A bridge whose terminal JUMP targets an already-compiled loop must supply
as many args as that loop's LABEL; the backend regalloc asserts
arglocs.len() == target_arglocs.len(). When a full-body walk closes a bridge
against an outer-loop LABEL grown by an unroll short preamble, the counts can
disagree. Return false (giveup) before the backend stage so blackhole resume
handles it, instead of letting the backend panic. The check is skipped when
target_arglocs is empty (not-yet-compiled target).

Assisted-by: Claude

* optimizeopt: wrap virtualize test op args in BoxRef::from_opref

The tracked_array_element BoxRef-array-source migration left one
virtualize.rs unit test passing `OpRef` where `Op::with_descr`,
`setarg`, and `get_box_replacement` now take/return `BoxRef`, so the
lib-test build did not compile. Wrap the args in `BoxRef::from_opref`,
matching the already-migrated sibling test in the same file.

Assisted-by: Claude

* pyjitpl: give up cross-loop-CUT self loop with NULL-fed class-guarded slot

A cross-loop CUT synthesizes a self-loop LABEL whose inputargs include
valuestack temporaries promoted from the inner merge point via the
escaped-ref BFS. When the optimized body class-guards such a slot
(GuardClass / GuardNonnullClass / GuardNonnull) while the closing Jump
feeds back a Const NULL for that slot, the LABEL/JUMP contract is
self-inconsistent: the loop dereferences NULL on its back edge. The
no-unroll retry path (pyjitpl.py:3044-3054) carries no virtual state to
reject it, the way virtualstate.py:595-606 _generate_guards_knownclass
would. Detect the inconsistency in compile_loop_body and return
CompileOutcome::Aborted (give up to the blackhole, as the retry-failure
path does) instead of installing the loop. Gated on
cut_inner_green_key.is_some() so the full-body-walk-off path is unchanged.

This removes the fannkuch SIGSEGV under
PYRE_FULL_BODY_WALK=1 PYRE_FBW_BRIDGE=1; the giveup fires on slot 24 and
the output matches the trait-dispatch reference.

Assisted-by: Claude

* flatten: LoadGlobal residual_call tests expect oopspecindex=LoadGlobal

build_load_global_fn_insn_from_operands tags the calldescr EffectInfo
with OopSpecIndex::LoadGlobal. Three tests still constructed their
expected EffectInfo via effect_info_for_call_flavor(CallFlavor::Plain),
which leaves oopspecindex None, so they failed once the workspace test
build could run. Update the expected EffectInfo in
build_load_global_fn_residual_call_ir_r_insn_emits_residual_call_ir_r,
build_load_global_fn_with_const_pycode_uses_const_ref_for_code_only, and
build_load_global_fn_residual_call_ir_r_insn_matches_flatten_of_residual_call_op
to set oopspecindex = LoadGlobal, mirroring the LoadConst sibling test.

Assisted-by: Claude

* FBW LOAD_GLOBAL fold: index co_names by namei >> 1

try_walker_load_global_cell_fold passed the raw LOAD_GLOBAL oparg to
load_name_from_code as the co_names index. The oparg is
(name_idx << 1) | push_null_flag, so the fold resolved the wrong name for
any global whose load carries the push-NULL flag (e.g. a global at
co_names[0] read co_names[1]), folding the in-loop callee to a different
global's cell. bh_load_global_fn already derives the index as namei >> 1;
mirror that in the fold.

Reword the dev-gate comment: the fold is correct and reaches production
parity for global-function-call loops with PYRE_FBW_INLINE; the gate stays
pending Phase-5 full-FBW-bench validation.

Assisted-by: Claude

* FBW inline: bound recursive inlining at max_unroll_recursion (=7)

try_walker_inline_user_call had no recursion bound, so a self-recursive
callee (fib) unrolled its exponentially branching call tree at trace
time, never terminating. Add a thread-local inline stack of callee
w_code pointers with an RAII guard around the sub-walk; bail to a
residual call (Ok(None)) once a callee is already FBW_MAX_INLINE_RECURSION
deep, mirroring the trait tracer's recursive_depth >= max_unroll_recursion
gate. fib_recursive under PYRE_FBW_INLINE now completes with correct
output instead of exploding; loop benches stay at trait parity.

Assisted-by: Claude

* FBW inline: decline frame-materializing callees instead of aborting trace

The fast-path inline seeds only r0..nparams; a callee that reads its
params from the virtualizable frame (any *_vable_* op, emitted when a
local must survive a sub-call) hit VableBoxNotSeeded mid sub-walk, and
that Err propagated up to abort the *entire* enclosing loop trace. With
PYRE_FBW_INLINE on, inline_helper's loop therefore never compiled and
ran fully interpreted.

Add callee_fast_path_inlinable: pre-scan the callee body for vable ops
before recording any IR and return Ok(None) (residual call) when found,
mirroring the orthodox should_inline=False -> do_residual_call path.
inline_helper under PYRE_FBW_INLINE now compiles with zero aborts; loop
benches stay at trait parity.

Assisted-by: Claude

* FBW inline: resolve callee frame-static reads to constants; defer iIRd arg-bound past folds

try_walker_inline_user_call's sub-walk seeds only the callee's positional
arg registers, leaving its portal frame box unseeded. A callee that reads a
scalar getfield_vable_r off that frame (pycode field 1 / w_globals field 5,
for LOAD_CONST / LOAD_GLOBAL setup) aborted the trace with VableBoxNotSeeded,
so callee_fast_path_inlinable declined every *_vable_* callee.

Resolve those reads to the callee's compile-time constants (w_code,
function_get_globals_obj) via a thread-local InlineCalleeConsts stack pushed
for the sub-walk lifetime, mirroring the codewriter non-portal branch
(codewriter.rs:6720-6732 / :7347-7369). callee_fast_path_inlinable now admits
a callee whose only vable ops are getfield_vable_r on field 1/5.

In dispatch_residual_call_iIRd_kind, move ensure_residual_call_args_bound
after the LoadConst/LoadGlobal folds: a folded call records nothing, so an
inlined callee's load_global passing its own unseeded portal_frame_reg no
longer trips the bound check.

With PYRE_FBW_INLINE + PYRE_FBW_LOADGLOBAL_FOLD, inline_helper inlines
compute/square/add/mul fully (0 aborts) and matches trait timing (was >235x
slower). check.py dynasm+cranelift 41/41; pyre-jit-trace tests pass.

Assisted-by: Claude

* FBW: execute iIRFd may-force residual via walker, matching iRd/iIRd

dispatch_residual_call_iIRFd_kind recorded the may-force residual and ran
try_fold_pure_call_via_executor but omitted the
try_execute_residual_call_via_walker call that the iRd and iIRd
dispatchers make after the pure-fold. The boxes3 (i++r++f) arglist shares
the same do_residual_call body as the r and ir shapes, so the float-arg /
float-return shape now executes concretely and stamps the result the same
way. Void-result stores are deferred inside the helper's
result_type()==Void arm, so a compiled loop re-run does not double-apply
the store.

The irf residual shape is not emitted by reachable pyre paths today: the
execute path records 0 hits across float_loop/int_loop/fib_loop/
nested_loop/nbody_200 under PYRE_FULL_BODY_WALK=1, so this is
behavior-neutral on the current bench suite. Gate dynasm 41/41 +
cranelift 41/41, pyre-jit-trace 242 tests.

Assisted-by: Claude

* pyrex: suppress SpeculativeError panic message like InvalidLoop

The panic hook silenced InvalidLoop (a caught control-flow panic) but not
SpeculativeError, which the optimizer raises to decline a speculative heap
fold. compile_loop / compile_bridge catch SpeculativeError and abandon the
trace (blackhole fallback = correct result, rc=0), but the default hook
still printed "panicked at optimizeopt/mod.rs" to stderr during the unwind.
check.py treats any "panicked" in stderr as a failure, so a fannkuch loop
compile that gracefully declined a fold under FBW+PYRE_FBW_BRIDGE was
reported as a crash though it ran correctly (8629/30, internal_compile_
panics=0).

Suppress SpeculativeError in the hook symmetrically with InvalidLoop; both
are silent exceptions per unroll.py:119-123. A genuinely uncaught one still
aborts with a nonzero exit, so only the false crash message is removed.

Gate: default check.py dynasm 41/41 + cranelift 41/41.

Assisted-by: Claude

* FBW inline: decline callees with internal branches (non-leaf)

callee_fast_path_inlinable admitted any callee whose body had no
unsupported vable op, but the inline convention only resumes a guard
inside the callee at the caller's CALL boundary via the inherited
single-frame snapshot — sound only for straight-line leaves where
re-executing the whole call on deopt reproduces the state.

A callee with an internal conditional branch (goto_if_not / switch)
emits a branch guard whose fail snapshot must resume into the callee
mid-body. Under PYRE_FBW_INLINE+PYRE_FBW_LOADGLOBAL_FOLD the single-frame
model then serialised a resume section whose liveness shape disagreed
with the encoded stream: a folded branch operand numbered TAGINT in a
slot the outer liveness reported as a ref, crashing blackhole resume
with "resume.rs decode_ref: unexpected tag 1" (synth/function_calls,
mix's `if a & 1`). Decline such callees pre-flight so the call lowers to
an ordinary residual call (correct) until the multi-frame resume
coordinate is ported (#68).

Branchless leaves (inline_helper's add/mul/square/compute) stay inlined,
so inline_helper keeps parity (0.22s, 985375007). function_calls now
completes correctly (320103199572). Full flip-candidate sweep
(FULL_BODY_WALK+BRIDGE+INLINE+LOADGLOBAL_FOLD) dynasm 39/41, remaining 2
fails are perf-only (fib_recursive timeout, nbody 5.1x). Default gate
dynasm 41/41 + cranelift 41/41.

Assisted-by: Claude

* FBW: compile loop-free portal exits via TraceAction::Finish (gated)

full_body_walk_trace mapped every walk outcome except CloseLoop to
TraceAction::Abort, so a top-level *_return that reaches
done_with_this_frame (DispatchOutcome::Terminate, no back-edge) never
compiled: fib_recursive aborted and ran interpreted.

Under PYRE_FBW_CALL_ASSEMBLER the three top-level return arms route
through fbw_terminate_with_finish instead of ctx.finish(): re-box the
result to Type::Ref, record the vable store-back + GUARD_NOT_FORCED_2,
and stash the finish value in a thread-local. full_body_walk_trace
resets it at walk start and, on Terminate, builds TraceAction::Finish
from it, letting the compile pipeline record the FINISH -- the same
split the trait StepResult::Return path uses. Gate off leaves the
ctx.finish() + Terminate->Abort path byte-identical.

fib_recursive then compiles under PYRE_FULL_BODY_WALK (loops_compiled
0->1, loops_aborted 5->0) and matches trait output under MAJIT_STRICT.

Add the missing raw_descrs / is_authoritative_executor fields to the
drive_int_between test's WalkContext literal.

Assisted-by: Claude

* FBW: abort non-standard-virtualizable callee Finish portals to trait

A callee compiled as its own Finish portal (reached via
call_user_function_with_eval) can access its frame through a vable_* op
that finds it to be a non-standard virtualizable, emitting an internal
promote GuardValue + force store-back. Those internal ops carry a resume
snapshot / FieldDescr only on the inline sub-walk path; on the own-portal
compile path the optimizer's store_final_boxes_in_guard /
optimize_setfield_gc invariants trip.

walker_capture_inline_nonstandard_vable_guard now distinguishes the two:
the inline sub-walk still captures the snapshot, while an own-portal walk
that emitted such a guard returns NonStandardVableFinishPortalUnsupported,
aborting the trace to the trait interpreter (AbortPermanent). The method
runs interpreted until the own-portal callee frame is registered as the
standard virtualizable (a perf follow-up).

Exposed by the Finish-portal compile path (PYRE_FBW_CALL_ASSEMBLER):
synth/inheritance_dispatch's polymorphic value() methods previously
panicked in the optimizer; they now produce correct output. The full
flip-candidate sweep (FBW + all levers + CALL_ASSEMBLER) is 45/45
stdout-match, 0 panic; default check.py dynasm + cranelift 41/41.

Assisted-by: Claude

* FBW walker: guard unseeded vable array register; gate inline on call_fn oopspec

getarrayitem_vable / setarrayitem_vable / arraylen_vable read the vable Ref
register without the is_none() check the scalar getfield/setfield handlers
carry, so an unseeded OpRef::None (raw() == u32::MAX) flowed into the
metainterp vable path and resized the heapcache flag vector to ~16 GiB
instead of aborting. Add the VableBoxNotSeeded guard to all three plus a
unit test.

try_walker_inline_user_call inlined any _r_* residual whose 2nd ref arg is a
function, including store_subscr_fn (StoreSubscr oopspec): `d[f] = v` with a
1-arg function key f was mis-inlined as f(v), skipping the store. Gate the
inline on ei.oopspecindex == None, the only iRd-routed helper shape without
an oopspec (the genuine call_fn / call_fn_N).

Assisted-by: Claude

* FBW sub-descr pool: key cache by payload identity and retain the Arc

SUB_DESCR_POOL_CACHE keyed by code ptr only and cached 'static views of the
payload's exec.descrs without retaining the Arc<PyJitCode>. A merge-point
refinement that replaces the payload in place for the same code ptr left the
cache returning descrs derived from the superseded payload (or dangling if
the old Arc had no other holder). Key on (code, payload) identity and retain
Arc::clone(&pjc) in the entry so a refined payload yields a fresh, correct
pool and the borrowed slices stay valid.

Assisted-by: Claude

* optimizeopt: invalidate tracked vable array on variable-index setarrayitem

optimize_setarrayitem_gc only mirrored constant-index writes into
vstate.arrays; a non-constant-index SETARRAYITEM_GC left the tracked slots
unchanged, so a later constant-index GETARRAYITEM_GC could fold to a value
the variable-index write may have overwritten. Add
VirtualizableTracker::invalidate_array and call it on the variable-index
path, mirroring force_lazy_setarrayitem(can_cache=False). Add a regression
test alongside the read-after-write fold test.

Assisted-by: Claude

* review nits: doc link, must_use guard, probe void-defer reset

- history.rs: fix the intra-doc link to recorder::Trace (no Recorder type).
- jitdriver.rs: mark TraceContinuationSuspendGuard #[must_use] so a bare
  enter() statement cannot construct-then-drop the suspend guard.
- trace.rs: reset the void-defer log and Finish payload on the diagnostic
  probe_walk_perfn_jitcode path, which discards its trace via cut_trace but
  never runs through the production tracer's entry reset.

Assisted-by: Claude

* EffectInfo: recognize FBW helper calls via pyre_helper, not oopspecindex

The full-body walker recognized the binary_op / compare / load_const /
box_int / store_subscr / load_global helper residual_calls by tagging their
EffectInfo with OopSpecIndex variants 200-205. has_oopspec() returns true
when oopspecindex != None, so production trait-dispatch traces carrying these
descrs were misclassified: the dynasm reghint _consider_real_call pass
skipped their real-call register hints, and the CanRaise members (load_const,
box_int, load_global) sat outside the _OS_CANRAISE set.

Add EffectInfo.pyre_helper: PyreHelperKind and move recognition onto it;
oopspecindex stays None for these helpers, so has_oopspec() and the OS_*
universe match origin/main. The CallDescrStub cache keys on the whole
EffectInfo and pyre_helper joins PartialEq/Hash, so distinct helper descrs
stay interned distinctly. Migrate the flatten.rs producers, the
jitcode_dispatch.rs walker consumers, and the try_walker_inline_user_call
inline gate; initialize pyre_helper: None in the front-end and metainterp EI
builders. Remove variants 200-205 from OopSpecIndex.

Also document BOOL_BOX_TRUTH as a pyre-only runtime reconstruction of the
jtransform optimize_goto_if_not fusion and note its reset boundary.

Assisted-by: Claude

* pyframe: document bridge-resume restore scope

restore_resume_state_from restores last_instr, valuestackdepth, and
locals_cells_stack_w — the only virtualizable fields the full-body walk
mutates on the live frame. Document that pycode / w_globals / debugdata are
intentionally not restored: frame-invariant or debug-only, never written on
the live frame during bridge tracing.

Assisted-by: Claude

* FBW: log walker abort reason under PYRE_FBW_DEBUG_ABORT

full_body_walk_trace mapped every DispatchError and non-loop-closing
DispatchOutcome to TraceAction::Abort/AbortPermanent without surfacing
which one; the metainterp abort log (pyjitpl/mod.rs:6348) reports only
the green key and permanence. Add a cached PYRE_FBW_DEBUG_ABORT gate that
prints the DispatchError variant, the non-loop-closing outcome, and the
ungated-portal-exit / run_perfn_walk-None cases at each Abort arm.
Default off, no production effect.

Assisted-by: Claude

* optimizeopt tests: adapt virtualizable regression tests to rebased BoxRef API

virtualize.rs regression tests (read-after-write fold, variable-index
invalidate): append `.to_opref()` to `get_box_replacement` (now returns
BoxRef) and pass the OpRc argument to `propagate_forward`.

Assisted-by: Claude

* jitcode_dispatch: handle ConcreteValue::Bool at two pc-map match sites

The Bool(bool) variant (state.rs:1424) added in #59 was not covered at
two walker-only match sites, leaving them non-exhaustive:
- arraylen_vable_via_metainterp: a Bool concrete is not a vable struct
  pointer, so it joins Null/Int/Float in mapping to 0 (unseeded abort).
- diagnose_inline_recognition: maps it to 'b' in the shape string.

Assisted-by: Claude

* FBW: direct CALL_ASSEMBLER for self-recursive call (dev-gated PYRE_FBW_REC_CA)

The full-body-walk residual_call dispatcher emitted a generic may-force
CallMayForceR for a self-recursive call, which re-enters the callee
through the func-entry residency door: one frame build + entry-bridge per
recursive call. fib_recursive ran ~30x slower than the trait tracer,
which instead records CallAssemblerR (direct assembler->assembler jump to
the callee's own loop/pending token).

Add try_walker_call_assembler_self_recursive, invoked from
dispatch_residual_call_iRd_kind after the inline path declines. It mirrors
the trait CALL_ASSEMBLER sequence: build the callee PyFrame inline via
emit_new_pyframe_inline_self_recursive (Branch A: single positional int
local, ncells==0, non-global-storing), then FORCE_TOKEN/vable setup,
call_assembler_red_only_ref to the resolved loop or pending token,
KEEPALIVE, escaped-frame heapcache invalidation, the result writeback
(Ref dst as-is, Int dst unboxed) BEFORE the guards — mirroring
make_result_of_lastop (pyjitpl.py:2077) and the in-tree sibling residual
path (write at jitcode_dispatch.rs:6856-6857, contract at 6844-6849) so a
raising/forcing deopt's after-call resume snapshot reads the recorded dst
OpRef instead of a stale box or NONE — then GUARD_NOT_FORCED and
GUARD_NO_EXCEPTION with after-call resume snapshots.

Every precondition (single int arg, exact self-recursion via
callee-code==portal-code, resolvable token, available portal sym) bails
with Ok(None) before recording any IR, so an unmet shape falls back to the
proven residual path. Gated by PYRE_FBW_REC_CA; default-off, production
trait path unaffected.

fib(30) FBW+REC_CA user-CPU at parity with trait (~0.12s); resolves the
sole fib_recursive flip blocker (was ~30x via the func-entry door).

Assisted-by: Claude

* FBW: reconcile walker with #158 base APIs after rebase

The rebased full-body-walk commits predate two APIs the #158/#390 base
added to pyre-jit-trace:

- `WalkContext.store_subscr_fn_addr`: the inline-call sub-walk
  constructor in `try_walker_inline_user_call` propagates it from the
  parent context, and the six `#[cfg(test)]` WalkContext fixtures that
  the release build does not compile set it to `None`.
- `walker_capture_snapshot_for_last_guard` now returns
  `Result<(), DispatchError>`; the base's `WalkerFrameOps::generate_guard`
  for `WalkContext` calls it in a `()` context with no abort channel, so
  the snapshot is best-effort there (the STORE_SUBSCR specialization that
  reaches it is env-gated and dead in production).

Assisted-by: Claude

* FBW: record void residual calls symbolically; drop VOID_DEFER_LOG

try_execute_residual_call_via_executor declined Type::Void residual calls
by pushing them onto VOID_DEFER_LOG and committing enclosing-scope stores
at the primary loop close. The may-force-safe gate (from #158) declines
the active-vable CallMayForce* subset (STORE_SUBSCR) before reaching this
branch, so the void branch no longer fires across the bench suite.

Replace the void branch with a bare `return None` so the residual op is
recorded symbolically and the compiled loop applies the side effect once
per iteration -- matching the may-force-decline path and the trait path's
net effect. do_residual_call (pyjitpl.py:2040/2104/2123) executes void
calls eagerly during tracing, but the full-body walk is symbolic and does
not advance the interpreter, so the compiled loop re-runs the traced
iteration; eager execution there would double-apply the heap mutation.

Remove the dead VOID_DEFER_LOG, VoidDeferEvent,
void_defer_push_store/push_register/commit_at_close, and the
jit_merge_point register push and loop-close commit call sites. Rename
void_defer_reset to bool_box_truth_reset, its only remaining job.

Verified: FBW-all-levers output byte-identical to the trait path on all
44 benches (incl. nbody enclosing-scope store, fannkuch, list ops);
check.py dynasm 44/44 + cranelift 44/44; pyre-jit-trace 250 +
majit-metainterp 1365 unit tests pass.

Assisted-by: Claude

* FBW: object-storage list getitem arm in try_walker_specialize_subscr

try_walker_specialize_subscr handled only int/float list storage (sid 1/2),
declining object-storage reads to the generic CALL_MAY_FORCE residual. Add
the sid=0 arm, mirroring generated_list_getitem_by_strategy: guard_class LIST
+ guard_value(strategy==0) + unbox index + IntLt bounds guard (length via
list_length_descr) + getfield(list_items_descr) + getarrayitem_gc_r against
the Ptr(GcArray(OBJECTPTR)) items block, reading the element Ref directly.
The bounds-length descriptor is selected per strategy.

Under PYRE_FULL_BODY_WALK, nbody object reads (bodies[i]) now specialize,
which makes the loaded element concrete so its float-storage reads (b[k])
also specialize; the BINARY_SUBSCR CALL_MAY_FORCE residuals and the
associated GuardNoException / FORCE_TOKEN drop to zero.

Assisted-by: Claude

* FBW: source merge-point reds from sym.frame/execution_context

The walker jit_merge_point handler registered reds read from the walk
register file (ri/rr/rf), while the loop-close path rebuilds jump args
via close_loop_args_at, which reads sym.frame / sym.execution_context.
When a register slot held a const-folded alias (e.g. a ConstPtr for the
execution context), the registered green_boxes and the close-side reds
diverged in box identity. A cross-loop cut (cut_trace_from_with_consts)
then saw the close-side box as an escaped inputarg and appended it as an
extra inputarg, producing a loop entry layout that
extend_compiled_live_values could never satisfy — every interpreter
entry aborted with reason=extend-live-values.

Override the frame/ec slots of the registration reds with sym.frame and
sym.execution_context (the same source the close path uses) when the
red signature matches the portal shape (rr.len()==2, no ints/floats).

Also add a MAJIT_LOG-gated [jit][cut-escape] diagnostic in
cut_trace_from_with_consts when an original inputarg has no pool
constant and is appended as an extra inputarg.

nbody 50k under PYRE_FULL_BODY_WALK + all levers: guard failures
51181 -> 1427, loop entry aborts 263 -> 14, runtime 1.38s -> 0.59s,
output byte-identical to the trait tracer across all 18 benches.

Assisted-by: Claude

* FBW: emit loop_header at backward jumps; gate jit_merge_point on it

The per-CodeObject codewriter emitted only jit_merge_point at loop-header
PCs. Upstream lowers can_enter_jit at each backward-JUMP site to a
loop_header op (jtransform.py:1714-1723) and opimpl_jit_merge_point
(pyjitpl.py:1547-1562) fires reached_loop_header only when a preceding
loop_header stamped seen_loop_header_for_jdindex; a fall-through arrival
returns early unless the green key already has compiled targets.

Without the gate, the FBW walker treated every jit_merge_point execution
as a loop crossing. On nbody the first trace at the inner physics header
(pc=38) closed a degenerate "inner exhausted, outer increment, fresh
iterator" loop at its fall-through re-arrival, occupying the inner
loop's green key. The trait tracer (backward-jump-triggered
close_loop_args) instead cuts at the outer header, leaving the inner key
free to re-heat into a specialized retrace.

Changes:
- codewriter.rs: JumpBackward / JumpBackwardNoInterrupt arms emit a
  loop_header graph op (Constant(jdindex)) before the goto when a portal
  jitdriver is registered; new emit_loop_header helper records the graph
  op and serializes …
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.

2 participants