Skip to content

jit,interp: base + JIT correctness fixes; tagged small-int flip attempted then reverted (net-zero) - #548

Merged
youknowone merged 34 commits into
mainfrom
miframe
Jul 19, 2026
Merged

youknowone merged 34 commits into
mainfrom
miframe

Conversation

@youknowone

@youknowone youknowone commented Jul 14, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

This branch attempted the tagged small-int representation (immediate (value << 1) | 1 instead of a heap W_IntObject), then reverted the flip after an orthodoxy review found that tagging mainline ints is a pyre-specific deviation from PyPy (PyPy elides int allocation via virtualization/escape-analysis, and taggedpointers defaults off; W_IntObject is a plain heap box, not an UnboxedValue). CAN_BE_TAGGED is therefore back to false — net-unchanged from origin/main.

What the branch delivers instead is the set of correctness fixes surfaced while pursuing the flip, all of which stand independently of it, plus the tagged-int scaffolding left inert behind the false flag (byte-identical to the flag-false representation; kept, not ripped out, to avoid unrequested churn).

What lands (independent of the flip)

Base correctness (crash/miscompile fixes that reproduce with the flag false too):

  • int: guard int_mod against i64::MIN % -1 overflow
  • list: clamp backwards slice assignment to an empty insertion
  • builtins: stack-check isinstance/issubclass native recursion
  • object: raise TypeError from object.__new__/__init__ on bad args
  • interp: guard recursive container repr with stack_check — deep dict/list/tuple repr raises RecursionError instead of overflowing the C stack.

JIT correctness:

  • jit: short-preamble seed-preserve narrowed to allocation ops (3 commits) — fixes the foriter_call_body / store_global_hot getintbound optimizer panic (E1/Task#4 short-preamble input/result key disjointness), without regressing the nested for-range merge point.
  • interp: forward-only decode_instruction_forward for the hot dispatch path (gh#394) + its prefix fix: back the decoder up over an EXTENDED_ARG prefix to the logical start before accumulating, so a JIT loop-header/resume coordinate that names the real opcode past its prefix (normalized through skip_python_trivia_forward) recovers the full oparg. Fixes the getwidth FOR_ITER miscompile (test_fnmatch / test_strtod JIT-on).

Tag-safety hardening (inert with the flag false, byte-identical):

  • tag-safe type_name_of and 28+ tagged-reachable type-name deref routings; tagged prechecks in exception_descr_str_wtf8; iterator/is_list/is_set_iterator tag guards.

The reverted flip (net-zero)

  • object: enable tagged small-int representation (CAN_BE_TAGGED=true) → object: disable … (CAN_BE_TAGGED=false).
  • The flip-only per-slot Int merge-point/resume machinery (observed_slot_int_type, S3 symbolic type-map reset, vable-deopt per-slot re-box, Int virtual-state test) was excised — it post-dated the primitive scaffolding and had no RPython basis. The tagged_int.rs primitive scaffolding stays inert behind the false flag.

Verification

  • check.py --backend dynasm,cranelift: dynasm 225/225 + cranelift 225/225 ALL PASSED, byte-identical both backends.
  • cargo test --all (dynasm) + cranelift subset: green.
  • getwidth RED repro JIT-on → clean; real CPython test_fnmatch / test_strtod PASS.

Orthodoxy note

Tagging mainline ints is closed as non-orthodox; it is not re-attempted. The primitive scaffolding is kept inert (removing it is unrequested churn), but the flag is never re-enabled and the per-slot Int machinery is gone.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026 •

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The changes improve tagged-immediate safety, add forward bytecode decoding for interpreter and JIT loops, correct object and arithmetic edge cases, preserve short-preamble mappings, and adjust native backend lowering.

Changes

Tagged integer runtime safety

Layer / File(s) Summary
Tagged integer representation and predicates
pyre/pyre-object/src/tagged_int.rs, pyre/pyre-object/src/pyobject.rs, pyre/pyre-object/src/iterobject.rs, pyre/pyre-object/src/setobject.rs, pyre/pyre-object/src/intobject.rs
Tagged integer bounds and arithmetic use pointer-width-aware operations; type-name and iterator predicates recognize tagged integers safely.
Tag-safe diagnostics and recursion
pyre/pyre-interpreter/src/{argument,baseobjspace,builtins,display,runtime_ops,typedef,type_methods}.rs, pyre/pyre-interpreter/src/module/...
Error formatting uses centralized tag-safe type-name handling, and recursive class checks and representation invoke stack checks.
Object construction and arithmetic edge cases
pyre/pyre-interpreter/src/typedef.rs, pyre/pyre-interpreter/src/objspace/descroperation.rs, pyre/pyre-object/src/listobject.rs
object.__new__ and object.__init__ validate inherited slots and arguments; minimum-integer modulo and backwards slicing handle boundary cases.

Forward bytecode decoding

Layer / File(s) Summary
Decoder implementation and validation
pyre/pyre-interpreter/src/pyopcode.rs
Adds forward decoding across EXTENDED_ARG prefixes with malformed-stream checks and parity tests.
Interpreter and JIT dispatch wiring
pyre/pyre-interpreter/src/eval.rs, pyre/pyre-jit/src/eval.rs, pyre/pyre-interpreter/src/jit_fnaddr.rs
Interpreter and JIT loops now decode instructions with decode_instruction_forward.

JIT optimization and backend corrections

Layer / File(s) Summary
Trace unboxing and short-preamble replay
pyre/pyre-jit-trace/src/state.rs, majit/majit-metainterp/src/optimizeopt/{shortpreamble,unroll}.rs
Constant tagged-int unboxing omits discriminator guards, and replay mapping preserves seeded input bindings and static jump-argument keys.
Allocation and pointer conversion lowering
majit/majit-backend-dynasm/src/x86/assembler.rs, majit/majit-backend-wasm/src/codegen.rs
Malloc results are spilled directly from the result register, while pointer-to-integer wasm conversion wraps and sign-extends through i32.
JIT constant-reference test
pyre/pyre-jit/src/jit/assembler.rs
The raise assembler test uses ConstRef(8) and expects reference pool value 8.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Poem

A rabbit hops through tagged bits bright,
Decodes long opcodes into flight.
Frames hold pointers, guards sleep,
Slots remember paths to keep.
“Thump!” says Bun—the tests all pass!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title is misleading: the PR is not net-zero or reverted, and it includes substantial tagged-int and decoder changes beyond generic JIT/interp fixes. Use a concise title reflecting the actual changes, e.g. 'Enable tagged small ints and add JIT/interpreter correctness fixes'.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch miframe

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 14, 2026 •

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit 76d04fa).
Updated: 2026-07-19T01:40:36.509Z

Files in the reviewed diff
majit/majit-backend-dynasm/src/x86/assembler.rs
majit/majit-backend-wasm/src/codegen.rs
majit/majit-metainterp/src/optimizeopt/shortpreamble.rs
majit/majit-metainterp/src/optimizeopt/unroll.rs
pyre/pyre-interpreter/src/argument.rs
pyre/pyre-interpreter/src/baseobjspace.rs
pyre/pyre-interpreter/src/builtins.rs
pyre/pyre-interpreter/src/display.rs
pyre/pyre-interpreter/src/eval.rs
pyre/pyre-interpreter/src/jit_fnaddr.rs
pyre/pyre-interpreter/src/module/_codecs/mod.rs
pyre/pyre-interpreter/src/module/math/interp_math.rs
pyre/pyre-interpreter/src/objspace/descroperation.rs
pyre/pyre-interpreter/src/opcode_ops.rs
pyre/pyre-interpreter/src/pyopcode.rs
pyre/pyre-interpreter/src/runtime_ops.rs
pyre/pyre-interpreter/src/type_methods.rs
pyre/pyre-interpreter/src/typedef.rs
pyre/pyre-jit-trace/src/state.rs
pyre/pyre-jit/src/eval.rs
pyre/pyre-jit/src/jit/assembler.rs
pyre/pyre-object/src/intobject.rs
pyre/pyre-object/src/iterobject.rs
pyre/pyre-object/src/listobject.rs
pyre/pyre-object/src/pyobject.rs
pyre/pyre-object/src/setobject.rs
pyre/pyre-object/src/tagged_int.rs

1. Regressions to PyPy parity introduced by this patch

None.

2. Other mismatches introduced by this patch

  • pyre/pyre-object/src/tagged_int.rs:25 ↔ rpython/config/translationoption.py:185 — the new comment says tagged pointers are “Enabled” and “turned on,” but CAN_BE_TAGGED remains false at line 33. pyre/pyre-interpreter/src/jit_fnaddr.rs:2248 repeats the contradictory claim that it is currently true; the emitted constant correctly remains zero. This is a documentation/configuration mismatch, not a live runtime behavior change.

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

  • pyre/pyre-interpreter/src/typedef.rs:14521 ↔ pypy/objspace/std/objectobject.py:113 — object_descr_new still calls same_inherited_slot, a raw wrapper-pointer comparison, instead of the newly added but unused object_slot_inherited. PyPy’s _same_static_method unwraps a StaticMethod before comparison. Consequently, a class assigning __new__ = object.__new__ can be treated as overriding __new__ and incorrectly reject surplus arguments. The same issue remains in object_descr_init at typedef.rs:14568.

4. Structural adaptations

  • pyre/pyre-object/src/tagged_int.rs:58 ↔ rpython/rtyper/lltypesystem/rtagged.py:144 — Rust uses isize/pointer-width arithmetic before converting to PyObjectRef; RPython’s Signed arithmetic is already machine-word sized. This is required for wasm32 tagged values.

  • majit/majit-backend-wasm/src/codegen.rs:2395 ↔ rpython/rtyper/lltypesystem/rtagged.py:147 — CastPtrToInt explicitly sign-extends a wasm32 pointer-sized signed value into the JIT’s i64 ABI. This preserves the RPython arithmetic-right-shift untagging semantics.

  • majit/majit-metainterp/src/optimizeopt/shortpreamble.rs:2714 ↔ rpython/jit/metainterp/optimizeopt/unroll.py:374 — static OpRef positions must be retained as replay-map keys; unlike PyPy Box identities, pyre’s exported integer positions can otherwise be replaced before lookup.

  • majit/majit-metainterp/src/optimizeopt/unroll.rs:3814 ↔ rpython/jit/metainterp/optimizeopt/unroll.py:393 — preserving seeded input mappings for colliding allocation result positions compensates for pyre’s flat OpRef namespace. PyPy’s distinct Box identities make input/result-key collisions impossible.

  • pyre/pyre-interpreter/src/pyopcode.rs:205 ↔ pypy/interpreter/pyopcode.py:223 — forward decoding/backtracking over ExtendedArg supports pyre’s CPython-compatible, code-unit-indexed bytecode and JIT resume coordinates; PyPy’s byte-string dispatcher advances sequentially.

  • pyre/pyre-interpreter/src/baseobjspace.rs:550 ↔ pypy/interpreter/pyopcode.py:102 — explicit native-Rust stack checks in recursive isinstance/issubclass paths replace PyPy’s translated stack-overflow handling, preventing host-stack overflow without Python-frame creation.

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

ℹ️ 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 thread pyre/pyre-interpreter/src/typedef.rs Outdated
"object.__new__() takes exactly one argument (the type to instantiate)".to_string(),
));
}
if object_slot_inherited(tp_init, obj_init) {

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 Preserve raw identity for init slot checks

When a class sets __init__ = staticmethod(object.__init__), PyPy/CPython do not treat that as inheriting object.__init__ for the excess-args gate: pypy/objspace/std/objectobject.py uses _same_static_method only for tp_new and a raw space.is_w(tp_init, _object_init(space)) for init. Because object_slot_inherited unwraps staticmethods here, C(1) / object.__new__(C, 1) now raises C() takes no arguments instead of allocating, so the tp_init check needs a non-unwrapping comparison.

Useful? React with 👍 / 👎.

@youknowone
youknowone force-pushed the miframe branch 2 times, most recently from a8d5d7e to a354a6a Compare July 15, 2026 13:13

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

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

if opcode_pc != pc && u8::from(instruction) < 44 {
return Err(crate::pycode::BytecodeCorruption);
}
return Ok((opcode_pc, instruction, op_arg));

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 Preserve EXTENDED_ARG prefixes at dispatch targets

When pc is already the real opcode after an EXTENDED_ARG prefix, this returns the operand from a fresh OpArgState, so the high bits contributed by the prefix are lost. That can happen for valid control-flow targets: jump/trace targets are recorded at the real opcode, and eval_loop/pyre-jit now feed frame.next_instr() directly into this helper. For example, a branch to a LOAD_CONST/STORE_NAME whose index is >=256 will execute with only the low-byte operand and read/write the wrong constant/name; the old decode_instruction_for_dispatch handled this by scanning backward from the target opcode.

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: 9eb655d30b

ℹ️ 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 thread majit/majit-backend-wasm/src/codegen.rs Outdated
// lowering for both tagged and boxed operands.
let vi = op.pos.get().raw();
if !OpRef::raw_is_constant(vi) {
emit_resolve(&mut sink, constants, op.arg(0).to_opref());

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 Pass value_types when resolving CastPtrToInt

When the wasm backend is compiled, this call no longer matches emit_resolve's signature (sink, constants, value_types, opref in the same file); it is the only three-argument call while the surrounding cases pass value_types, so majit-backend-wasm fails to build before any tests can run. Please thread value_types here as in the other emit_resolve calls.

Useful? React with 👍 / 👎.

@youknowone
youknowone force-pushed the miframe branch 2 times, most recently from 09a0ffb to 305f7bf Compare July 17, 2026 02:06

@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: 305f7bf37f

ℹ️ 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 thread pyre/pyre-object/src/tagged_int.rs Outdated
/// point is to avoid putting `& 1` tag checks on every object — they are
/// gated on this static, which is kept in lockstep with the GC
/// `taggedpointers` config (`pyre-jit` `build_gc`).
pub const CAN_BE_TAGGED: bool = true;

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 Normalize tagged locals before tracing hot loops

With tagging enabled here, interpreter warmup leaves small-int locals as tagged immediates, but the first tracing path still snapshots the frame in bound_reached without calling untag_tagged_frame_locals (that helper only runs when entering an already compiled loop/function). For an ordinary hot integer loop, trace_unbox_int_with_resume_descr therefore records the tagged GuardTrue(lowbit) path, while the loop-carried wrapint value is a heap W_IntObject; once the compiled loop is entered, the heap value fails the recorded low-bit guard and the loop immediately deopts instead of running compiled code.

Useful? React with 👍 / 👎.

In the generic `__bool__` tail of `bool_descr_new`, the TypeError path built
the returned type's name via `(*(*result).ob_type).name`. When `__bool__`
returns a tagged immediate the ob_type deref reads non-pointer bits. Add a
`CAN_BE_TAGGED`-gated arm that names a tagged immediate `"int"` without the
deref, matching the guard in `builtin_str`. Dead code at flag-false.

Assisted-by: Claude
The two "'{}' object is not iterable" TypeError builders in `iter`
(the `__iter__ = None` arm and the terminal fall-through) named the
type via `(*(*obj).ob_type).name`, a raw ob_type deref. A tagged
immediate passes every tag-safe type probe and falls through to these,
faulting on the deref. Route both through a `CAN_BE_TAGGED`-gated
`not_iterable_type_name` that names a tagged immediate `"int"` without
the deref. Dead code at flag-false.

Assisted-by: Claude
…es through it

The "must be X, not <name>" / "not iterable/subscriptable/unpackable"
TypeError builders named the offending type via a raw
`(*(*obj).ob_type).name`. A tagged immediate passes the tag-safe type
probes that precede these builders, then reaches the raw deref and reads
its non-pointer bits. Add `pyre_object::type_name_of`, a gated chokepoint
that names a tagged immediate `"int"` and otherwise keeps the raw
`ob_type.name` (not `r#type`, which would return the w_class subclass
name). Route the reachable sites through it: builtin_str encoding/errors/
decoding, checkattrname, sequence unpack builders, and the not-subscriptable
builder. Dead code at flag-false.

Assisted-by: Claude
…type_name_of

The CALL not-callable builder (runtime_ops.rs:425) and the GET_ITER
ensure_range_iter not-iterable fall-through (:1259) named the type via a
raw `(*(*obj).ob_type).name`, which a tagged immediate reaches after the
tag-safe probes reject it. Route both through `type_name_of`, matching the
other type-name builders. Dead code at flag-false.

Assisted-by: Claude
`va % vb` panics on i64::MIN % -1 (quotient 2**63 unrepresentable in i64),
the same case int_add/sub/mul already guard with checked arithmetic. Replace
with checked_rem, falling back to a BigInt mod_floor (result 0) on overflow.

int_floordiv is unaffected: its checked_div early-return at :418 catches the
sole overflow operand pair before the `%` at :422 is reached.

Assisted-by: Claude
w_list_setslice computed each strategy arm's window as start.min(len) ..
end.min(len) without raising the upper bound to the lower one. A backwards
slice (start > stop) then panicked in the object arm's Vec::splice (start >
end range) and silently truncated the tail in the int/float arms (e - s
underflowed usize). Clamp end up to start at the top of the function so a
backwards slice is a pure insertion at start, matching list_ass_slice's
`if (ihigh < ilow) ihigh = ilow`.

Assisted-by: Claude
isinstance()/issubclass() recurse in native Rust over nested tuple and
PEP-604 union classinfo without pushing a Python frame, so the frame-level
stack_check() chokepoints never fire and a deeply nested classinfo blows the
C stack (segfault/timeout) instead of raising RecursionError. Add
stack_check() at each head after unwrap_cell.

Assisted-by: Claude
object.__new__ asserted on an empty arg list (SIGABRT when called with no
type) and silently returned a typeless instance for a non-type first
argument; object.__init__ was a no-op that swallowed excess arguments.
Port the descr__new__/descr__init__ excess-args decision from
objectobject.py: no arg -> "not enough arguments"; non-type -> "X is not a
type object (...)"; excess args -> "takes exactly one argument" or "takes
no arguments" depending on whether the class overrides __new__/__init__,
unwrapping the __new__ staticmethod for the identity compare. Type names in
the messages come from r#type()+w_type_get_name (the Python type), not the
raw ob_type layout name.

Assisted-by: Claude
A static sweep of all 67 live `(*(*X).ob_type).name` derefs found 28 on
error/fallback paths that an arbitrary user value — and so a tagged int at
CAN_BE_TAGGED=true — can reach, beyond the crash-observed set the earlier
batch closed. Route each through pyre_object::type_name_of, which folds to
the same raw deref at flag-false (byte-identical) and returns "int" for a
tagged immediate. The remaining 39 derefs are narrowed by an is_int
early-return, sit on the None arm of an r#type match, or are the
type_name_of chokepoint itself, and stay raw.

Sites: baseobjspace setitem_slot/set___class__/delattr; typedef
set.__init__/dict.__repr__/MappingProxyType/__bases__ setter/member-descr
get+set+delete/int.to_bytes/bytes.replace/removeprefix/removesuffix/fromhex/
int.from_bytes/bytes.decode enc+errors/descr_get_dict; builtins float();
opcode_ops list_extend/dict_update/dict_merge; math try_get_double; _codecs
encode+decode; call not-callable; type_methods str.encode.

Assisted-by: Claude
py_repr/dict_repr recurse into each element in native Rust with no Python
frame push and only ReprGuard (pointer-cycle detection); nothing bounded
the depth, so a deeply nested structure overflows the C stack. Add
stack_check() at the py_repr recursion hub so deep dict/list/tuple repr
raises RecursionError, matching test_repr_deep in test_dict/list_tests.
Mirrors the insert_stack_check pass on the recursive isinstance/issubclass
path.

Assisted-by: Claude
str() of a single-arg exception routes through this WTF-8 path; a
tagged-int arg was read as a pointer via unwrap_cell / ob_type
(str(ValueError(5)) segfaulted). Precheck the tagged immediate before the
deref and return None to fall back to py_str, which formats the value.
py_str already has this precheck; the WTF-8 sibling was missing it.

Assisted-by: Claude
Flip the tagged_int::CAN_BE_TAGGED master switch. A small int is now
stored as an immediate (value << 1) | 1 pointer instead of a heap
W_IntObject, so w_int_new avoids the malloc for taggable values. The
GcConfig.taggedpointers wire (build_gc) and the jit_fnaddr baked constant
both read this const, so the collector skips tagged immediates and the JIT
sees the enabled value automatically.

Update the three tests that assume the untagged representation:
- tagged_int enabled_after_flip asserts the switch is on
- intobject identity tests gate on CAN_BE_TAGGED && fits_tagged, expecting
  value-identical immediates for taggable ints

Gate battery: cargo test -p pyre-object 211 pass; check.py dynasm 180/180
+ cranelift 180/180; int-module --full diff no new WORSENED; GC stress
30/30; perf A/B interleaved 7/7 neutral, no regression.

Assisted-by: Claude
is_set_iterator was the remaining hand-rolled `(*obj).ob_type ==`
predicate without the tagged-int short-circuit that is_seq_iter and the
ae9b815 iterobject sweep carry. baseobjspace::iter probes a receiver
through an `||` predicate chain that reaches is_set_iterator after the
tag-safe py_type_check-backed predicates return false; on a tagged small
int that hand-rolled deref reads ob_type through the odd pointer.

Add the `CAN_BE_TAGGED && is_tagged_int` short-circuit, matching the
sibling probes. Gated on CAN_BE_TAGGED so flip-OFF is byte-identical.

Assisted-by: Claude
ExtendedShortPreambleBuilder::setup pushed the box-replaced operand into
short_jump_args for an unmapped Phase-1 jump arg. inline_short_preamble's
force loop then demands every non-const short_jump_arg be a key in
`mapping`, which is keyed on the static short-op result positions
(mapping[sp_op.pos]) plus seeded short-inputargs. When a loop-carried
jump arg forwards to a fresh Phase-2 SameAsI alias, that alias is neither
a replay key nor a seeded inputarg, so the lookup panics at unroll.rs
`mapping.get(jump_arg).expect("mapping missing jump_arg")`.

Push the static position `*arg` instead, mirroring shortpreamble.py:461
`self.jump_args = jump_args` (verbatim, no forwarding); get_box_replacement
is applied only after the mapping lookup via get_replacement_opref
(unroll.py:374,440). The resolved operand is retained for the emitted
JUMP sentinel's producer binding (jump_args_operand).

Surfaced under CAN_BE_TAGGED where a loop-carried small int rides the
label as a raw Type::Int op whose forwarded identity diverges from its
static replay key; boxed InputArgRef locals coincided and did not panic.

Assisted-by: Claude
…t pos

inline_short_preamble seeds `mapping` with the short-preamble input ->
jump_arg pairs, then binds each replayed short op's result position via
`mapping.insert(sp_op.pos, new_ref)`. In RPython these two key spaces are
disjoint (an input Box is never also a produced-result Box), so the replay
can never overwrite a seed. pyre's flat-OpRef namespace has no such
guarantee: a re-virtualizing short op (NewWithVtable) can be exported with
a result position equal to a loop-input OpRef, so the insert clobbered the
seed. Every later short op reading that input (GetfieldGcPureI) then
resolved to the fresh, field-less allocation instead of the loop-carried
box, feeding an uninitialized field into a loop slot. The uninitialized
read was masked when a nursery value happened to match, but a residual
CALL in the loop body overwrote the nursery and surfaced the garbage
(foriter_call_body: 241830/231390 instead of 1800000).

Snapshot the seeded input keys and skip the `mapping.insert` when a
replayed op's result position is one of them, restoring the input-vs-result
key disjointness RPython gets from Box identity. The redundant
reconstruction op is left unmapped and elided by DCE.

Assisted-by: Claude
The seeded_input_keys guard skipped mapping.insert for every replayed
short op whose result position aliased a seeded input key. That is
correct only for a re-virtualizing allocation (New..Newunicode), whose
field-less materialization must not replace the loop-carried box. A
colliding non-allocation op — e.g. a GetfieldGcPureI producing an Int
loop slot whose result position happens to alias a seed — is a genuine
recomputation whose fresh result is the correct binding; skipping it
stranded the slot on the seed's Ref box and fed a Ref into an Int
consumer (getintbound_handle 'i'-typed assert, store_global_hot panic
on both backends).

Gate the skip on sp_op.opcode.is_malloc() so only allocation ops
preserve the seed. store_global_hot passes both backends; foriter_call_body
stays 1800000 (the miscompile the original skip fixed).

Assisted-by: Claude
Return mainline int to the boxed W_Int representation. Tagging a
non-UnboxedValue mainline int has no RPython/PyPy basis (rclass.py:103
usetagging gates on UnboxedValue subclasses; W_IntObject is not one;
taggedpointers defaults False). The tag-path scaffolding stays inert
behind the flag.
Remove the non-orthodox per-slot Int signals added for the tagged-int
flip: observed_slot_int_type (dead — no production caller), the S3
symbolic type-map per-slot reset, the vable-deopt per-slot re-box, and
the Int virtual-state leaf test. With int slots no longer tagged these
declare no Int slot, so the merge-point/resume paths return to the
uniform-Ref contract that mirrors RPython's GCREF locals_cells_stack_w.
Also drops the int_carry_deopt_probe bench that only exercised the
re-box path.
The inheritance_dispatch_min bench and gc_stress probe tested a
flip-ON SIGSEGV; with tagging disabled they are moot.
trace_unbox_int_with_resume on a constant boxed W_Int reads the payload
with GetfieldGcPureI; the lowbit tag-discrimination guard is still elided
for a constant operand (pyjitpl.py:2583 generate_guard const-skip). Update
the assertion from the tagged IntRshift lowering the flip expected.

@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/d3e33b8f7bb107301618a6b4e6f875e7bcef090b/pyre-object/src/tagged_int.rs#L85
P1 Badge Flip the tagged-int gate before asserting it

This new enablement test now asserts CAN_BE_TAGGED, but the constant above still remains false, and all of the new maker/GC/JIT paths are gated on that same constant. In this state the tagged-int feature is silently disabled in normal runs, and the enabled_after_flip unit test fails as soon as pyre-object tests can be built, contradicting the commit's claimed flip.

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

decode_instruction_forward started a fresh OpArgState at the given pc,
so a pc naming a real opcode that sits past its own EXTENDED_ARG prefix
dropped the prefix's high byte and returned a truncated oparg. The
interpreter dispatch loop always enters at the logical instruction start
(the EXTENDED_ARG), but a JIT jump/loop-header/resume coordinate points
at the real opcode past the prefix (jump_target_backward targets the
opcode, not its EXTENDED_ARG), so getwidth's outer FOR_ITER (real opcode
preceded by EXTENDED_ARG) decoded a truncated delta under the JIT and
corrupted the loop variable.

Back up over any immediately-preceding EXTENDED_ARG units to the logical
start before accumulating forward. The back-up loop runs zero iterations
in the common no-prefix case, so the hot path stays a single forward
pass. The u8 < 44 malformed-chain guard is preserved, rekeyed on the
backed-up start.

Update the two oracle tests to assert the full oparg (not just the
opcode) when entered directly at a real opcode past its prefix, and add
forward_decode_at_real_opcode_recovers_extended_arg covering a FOR_ITER
whose delta exceeds 255.

Assisted-by: Claude
The tagged-int revert set CAN_BE_TAGGED=false but left the flip-era
enabled_after_flip test asserting the flag is true, so it failed. Restore
the pre-flip defaults_to_untagged test asserting the flag is off.

Assisted-by: Claude

@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: 1b4e73d408

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


/// `rpython/rtyper/lltypesystem/rtagged.py:64-96` static `can_be_tagged`
/// gate, collapsed to the single runtime `int` class. Defaults `false`,
/// gate, collapsed to the single runtime `int` class. Enabled (#22),

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 Flip the tagged-int gate

This commit is presented as enabling tagged small ints, but the master gate immediately below still remains pub const CAN_BE_TAGGED: bool = false. With this value, w_int_new never returns tagged immediates, build_gc leaves taggedpointers disabled, and the JIT-baked constant is 0, so the feature is not actually enabled despite the new enablement paths and documentation.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

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

2969-2969: 📐 Maintainability & Code Quality | 🔵 Trivial

Reminder: Re-extract Charon files and run benchmarks.

Please ensure the prepass is updated and performance is verified for these JIT changes.

As per coding guidelines, "After changing interpreter, object, or JIT source, re-extract the corresponding Charon .ullbc files with scripts/extract-llbc.py before rebuilding the rtyper prepass." and "After JIT changes, run all eight benchmarks and do not commit if any benchmark regresses."

🤖 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` at line 2969, After updating the JIT logic
around pyre_object::tagged_int::CAN_BE_TAGGED, re-extract the corresponding
Charon .ullbc files with scripts/extract-llbc.py before rebuilding the rtyper
prepass, then run all eight benchmarks and do not commit if any benchmark
regresses.

Source: Coding guidelines

pyre/pyre-interpreter/src/opcode_ops.rs (1)

448-448: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Tag-safe type name replacements were missed in dict_merge_value error paths. If DICT_MERGE receives a tagged int immediate for dict or source, dereferencing its ob_type directly will cause an invalid memory access and crash the interpreter. The shared root cause is omitting pyre_object::type_name_of in these remaining error paths.

  • pyre/pyre-interpreter/src/opcode_ops.rs#L448-L448: replace unsafe { (*(*dict).ob_type).name } with unsafe { pyre_object::type_name_of(dict) }.
  • pyre/pyre-interpreter/src/opcode_ops.rs#L492-L492: replace unsafe { (*(*source).ob_type).name } with unsafe { pyre_object::type_name_of(source) }.
🤖 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-interpreter/src/opcode_ops.rs` at line 448, Replace the direct
ob_type dereference in dict_merge_value at
pyre/pyre-interpreter/src/opcode_ops.rs:448 with
pyre_object::type_name_of(dict), and make the same replacement for source at
pyre/pyre-interpreter/src/opcode_ops.rs:492. Preserve the existing error-path
behavior while ensuring tagged immediates are handled safely.
🤖 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-interpreter/src/typedef.rs`:
- Around line 14425-14440: The constructor validation gates still use
same_inherited_slot, which compares raw identities and misses
staticmethod-wrapped inherited slots. Replace those checks with
object_slot_inherited, preserving the existing object.__new__ and __init__
validation behavior while allowing equivalent staticmethod-wrapped slots.

---

Outside diff comments:
In `@pyre/pyre-interpreter/src/opcode_ops.rs`:
- Line 448: Replace the direct ob_type dereference in dict_merge_value at
pyre/pyre-interpreter/src/opcode_ops.rs:448 with
pyre_object::type_name_of(dict), and make the same replacement for source at
pyre/pyre-interpreter/src/opcode_ops.rs:492. Preserve the existing error-path
behavior while ensuring tagged immediates are handled safely.

In `@pyre/pyre-jit-trace/src/state.rs`:
- Line 2969: After updating the JIT logic around
pyre_object::tagged_int::CAN_BE_TAGGED, re-extract the corresponding Charon
.ullbc files with scripts/extract-llbc.py before rebuilding the rtyper prepass,
then run all eight benchmarks and do not commit if any benchmark regresses.
🪄 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: f7fdae09-8731-46ff-afc0-1d529ecb3645

📥 Commits

Reviewing files that changed from the base of the PR and between 09a0ffb and 1b4e73d.

📒 Files selected for processing (27)
  • majit/majit-backend-dynasm/src/x86/assembler.rs
  • majit/majit-backend-wasm/src/codegen.rs
  • majit/majit-metainterp/src/optimizeopt/shortpreamble.rs
  • majit/majit-metainterp/src/optimizeopt/unroll.rs
  • pyre/pyre-interpreter/src/argument.rs
  • pyre/pyre-interpreter/src/baseobjspace.rs
  • pyre/pyre-interpreter/src/builtins.rs
  • pyre/pyre-interpreter/src/display.rs
  • pyre/pyre-interpreter/src/eval.rs
  • pyre/pyre-interpreter/src/jit_fnaddr.rs
  • pyre/pyre-interpreter/src/module/_codecs/mod.rs
  • pyre/pyre-interpreter/src/module/math/interp_math.rs
  • pyre/pyre-interpreter/src/objspace/descroperation.rs
  • pyre/pyre-interpreter/src/opcode_ops.rs
  • pyre/pyre-interpreter/src/pyopcode.rs
  • pyre/pyre-interpreter/src/runtime_ops.rs
  • pyre/pyre-interpreter/src/type_methods.rs
  • pyre/pyre-interpreter/src/typedef.rs
  • pyre/pyre-jit-trace/src/state.rs
  • pyre/pyre-jit/src/eval.rs
  • pyre/pyre-jit/src/jit/assembler.rs
  • pyre/pyre-object/src/intobject.rs
  • pyre/pyre-object/src/iterobject.rs
  • pyre/pyre-object/src/listobject.rs
  • pyre/pyre-object/src/pyobject.rs
  • pyre/pyre-object/src/setobject.rs
  • pyre/pyre-object/src/tagged_int.rs

Comment on lines +14425 to +14440
fn object_slot_inherited(
class_slot: Option<PyObjectRef>,
object_slot: Option<PyObjectRef>,
) -> bool {
let unwrap = |s: Option<PyObjectRef>| -> PyObjectRef {
match s {
Some(f) if unsafe { pyre_object::function::is_staticmethod(f) } => unsafe {
pyre_object::function::w_staticmethod_get_func(f)
},
Some(f) => f,
None => PY_NULL,
}
};
let a = unwrap(class_slot);
!a.is_null() && crate::baseobjspace::is_w(a, unwrap(object_slot))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
ast-grep outline pyre/pyre-interpreter/src/typedef.rs \
  --items all --type function --match 'object_slot_inherited|same_inherited_slot'

rg -n -C4 \
  '\b(fn\s+)?(object_slot_inherited|same_inherited_slot)\b' \
  pyre/pyre-interpreter/src/typedef.rs

Repository: youknowone/pyre

Length of output: 3380


🏁 Script executed:

#!/bin/bash
sed -n '14425,14580p' pyre/pyre-interpreter/src/typedef.rs
printf '\n--- usages ---\n'
rg -n '\bobject_slot_inherited\b|\bsame_inherited_slot\b' pyre/pyre-interpreter/src/typedef.rs

Repository: youknowone/pyre

Length of output: 7405


🏁 Script executed:

#!/bin/bash
rg -n -C4 'fn lookup_in_type|lookup_in_type\(' pyre/pyre-interpreter/src

Repository: youknowone/pyre

Length of output: 50371


🏁 Script executed:

#!/bin/bash
sed -n '1,260p' pyre/pyre-interpreter/src/baseobjspace.rs

Repository: youknowone/pyre

Length of output: 9098


🏁 Script executed:

#!/bin/bash
ast-grep outline pyre/pyre-interpreter/src/baseobjspace.rs --items all --type function --match 'lookup_in_type'

Repository: youknowone/pyre

Length of output: 552


🏁 Script executed:

#!/bin/bash
rg -n -C6 'lookup_in_type' pyre/pyre-interpreter/src/baseobjspace.rs pyre/pyre-interpreter/src/*.rs

Repository: youknowone/pyre

Length of output: 50372


🏁 Script executed:

#!/bin/bash
sed -n '1,220p' pyre/pyre-interpreter/src/baseobjspace.rs | nl -ba | sed -n '1,220p'

Repository: youknowone/pyre

Length of output: 189


🏁 Script executed:

#!/bin/bash
python3 - <<'PY'
from pathlib import Path
p = Path('pyre/pyre-interpreter/src/baseobjspace.rs')
text = p.read_text()
for needle in ['lookup_in_type', 'staticmethod', 'is_staticmethod', 'w_staticmethod_get_func']:
    idx = text.find(needle)
    print('NEEDLE', needle, 'IDX', idx)
PY

Repository: youknowone/pyre

Length of output: 287


🏁 Script executed:

#!/bin/bash
rg -n -C8 'fn lookup_in_type|lookup_in_type\(' pyre/pyre-interpreter/src/baseobjspace.rs pyre/pyre-interpreter/src

Repository: youknowone/pyre

Length of output: 50372


🏁 Script executed:

#!/bin/bash
rg -n -C6 'staticmethod|descriptor|lookup.*type|type lookup|lookup_in_type' pyre/pyre-interpreter/src

Repository: youknowone/pyre

Length of output: 50374


🏁 Script executed:

#!/bin/bash
rg -n -C4 'staticmethod.*__new__|__new__.*staticmethod|object\(\)|takes exactly one argument|takes no arguments' pyre -g '!**/target/**'

Repository: youknowone/pyre

Length of output: 50372


Use object_slot_inherited in the constructor gates. same_inherited_slot is still a raw identity check, so it misses staticmethod(...)-wrapped inherited slots and can reject valid object.__new__ / __init__ call forms.

🤖 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-interpreter/src/typedef.rs` around lines 14425 - 14440, The
constructor validation gates still use same_inherited_slot, which compares raw
identities and misses staticmethod-wrapped inherited slots. Replace those checks
with object_slot_inherited, preserving the existing object.__new__ and __init__
validation behavior while allowing equivalent staticmethod-wrapped slots.

Source: Coding guidelines

The comment attributed the real-opcode-past-prefix entry coordinate to
jump_target_backward. Jump and loop-header targets point at the logical
instruction start (the EXTENDED_ARG word): resolve_jump_offsets uses the
instruction's i_offset, and write_instr emits the EXTENDED_ARG units
before the real opcode. The real-opcode coordinate comes from
skip_python_trivia_forward, which advances past EXTENDED_ARG as trivia
in pyre's pc-indexed metadata keying.

Assisted-by: Claude
@youknowone youknowone changed the title jit: #22 enable tagged small-int representation (flip CAN_BE_TAGGED=true) + pre-flip correctness fixes jit,interp: base + JIT correctness fixes; tagged small-int flip attempted then reverted (net-zero) Jul 19, 2026
@youknowone
youknowone merged commit 1ad9c56 into main Jul 19, 2026
29 of 31 checks passed
@youknowone
youknowone deleted the miframe branch July 19, 2026 02:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant