Skip to content

jit: #22 tagged-int Dir3 trace-boundary machinery + 4 gated deref guards (flag-false inert)#524

Merged
youknowone merged 2 commits into
mainfrom
miframe
Jul 13, 2026
Merged

jit: #22 tagged-int Dir3 trace-boundary machinery + 4 gated deref guards (flag-false inert)#524
youknowone merged 2 commits into
mainfrom
miframe

Conversation

@youknowone

@youknowone youknowone commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Summary

Direction-3 tagged-int trace-boundary machinery plus tag-immediate guards at four object-deref boundaries. All changes are gated on pyre_object::tagged_int::CAN_BE_TAGGED (default false) and are dead code at the flag's default, so flag-false trace emission and interpreter behavior are byte-identical to before.

Groundwork toward enabling tagged small ints (#22): representing small int as an immediate (v<<1)|1 value to eliminate int-boxing malloc. This branch closes the four flag-true SIGSEGV crashes that blocked the enablement flip. The flip itself (CAN_BE_TAGGED=true) is not in this branch.

Commits

  1. Dir3 tagged-int trace-boundary machinery — a hot-loop-carried tagged int enters the JIT in the flag-false heap representation (never carries a tagged pointer in a trace):

    • untag_tagged_frame_locals (eval.rs): convert a frame's tagged-immediate local slots to heap W_IntObject before a compiled loop reads them.
    • record-time seed (state.rs): convert tagged locals on the snapshot_for_tracing copy so the recorded body matches the runtime-converted frame.
    • wrapint class-record, box_int_payload heap-only, walker_unbox_int_typed two-arm split, bridge ec-color seed fix.
  2. Gated tag-immediate guards at 4 deref boundaries — each fixes one flag-true crash class:

    • majit-gc get_actual_typeid: return None for a tagged immediate before the header read (mirrors can_move).
    • interpreter builtin_str: stringify a tagged int to decimal before the ob_type derefs (mirrors py_str_wtf8/py_repr_obj).
    • jit-trace loop_inlines_abort_permanent_callee: skip a tagged immediate in the globals-dict and cell scans.
    • jit assembler untag_const_ref_value: re-realize a tagged immediate as a heap W_IntObject (w_int_new_unique) before it enters the GC-root-walked jitcode constants_r pool, at each ConstRef pool-bake site.

Verification

  • flag-false (shipped default) check.py --backend dynasm,cranelift: cranelift 163/163, dynasm 162/163 — the one red is the raise_catch vs-pypy 1.5x timing gate, a boundary jitter flake (re-ran 3×: 1.5x/1.4x/1.0x), not a correctness regression. Slice is gated dead-code so the tree is byte-identical to base.
  • flag-true (CAN_BE_TAGGED=true, local only) check.py --backend dynasm,cranelift: 163/163 both backends. All four formerly-crashing tests (fannkuch, nested_for_varying_trip, polymorphic_slot_retype, str_fstring) pass; fannkuch still JITs (loops_compiled=5).

Follow-ups (documented, for the eventual flip, not in this branch)

  • builtin_str tag-guard sits after the encoding/errors block that derefs ob_type; str(tagged, encoding=) would still deref tagged at flip-true (no test reaches it). Move the guard earlier at flip time.
  • Why the majit optimizer folds a loop-carried const to Operand::ConstRef(tagged) at record time instead of a heap-box const — the assembler-boundary normalization covers it; the optimizer-side root is the single-root ideal for cleanup.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of tagged integer values across interpretation and compiled execution.
    • Fixed integer string conversion to consistently produce decimal representations.
    • Prevented tagged integers from being misidentified as objects, function references, or closure cells.
    • Improved JIT tracing, bridge resumes, constant handling, and deoptimization reliability.
    • Reduced risks of invalid memory access and guard failures when processing small integers.

…D, flag-false inert)

Direction-3 machinery so a hot-loop-carried tagged small int enters the JIT in
the flag-false heap representation, all gated on `tagged_int::CAN_BE_TAGGED`
(default false) so the flag-false trace emission is byte-identical:

- eval.rs `untag_tagged_frame_locals`: convert a frame's tagged-immediate local
  slots to heap `W_IntObject` in place before a compiled loop reads them; called
  from `execute_assembler` and `try_function_entry_jit`.
- state.rs `PyreSym` record-time seed: convert tagged locals (locals region only,
  `item_idx < nlocals`) to heap boxes on the `snapshot_for_tracing` copy so the
  recorded body matches the runtime-converted frame.
- state.rs `wrapint`: record the JIT-made int box's class (`INT_TYPE`) so a later
  unbox takes the `GetfieldGc` arm that folds through `SetfieldGc` at loop-close
  instead of the `CastPtrToInt` tag-arith arm.
- trace_opcode.rs `box_int_payload`: always box via heap `wrapint`; drop the
  in-trace tag-arith emitter (`IntAddOvf`/`IntOr`/`CastIntToPtr`).
- jitcode_dispatch.rs `box_int_concrete`, `walker_unbox_int_typed` two-arm split,
  `walker_inputarg_is_converted_local` (reads `ctx.fbw_mode.snapshot_sym` after
  the #509 FBW-TLS removal).
- trace.rs bridge seed: when `bridge_registers_r` names a genuine operand at a
  reserved EC color (free-regalloc reused the EC color for a live operand at a
  PC with no live EC read), seed the real operand instead of stranding the stale
  `ConstPtr(ec)`; the frame color keeps its unconditional #124 skip. Fixes the
  fib_recursive flag-true SIGSEGV where `fib(n-1)+fib(n-2)` consumed the EC
  pointer as the Add's int lhs.
- resoperation.rs `OpRef::is_input_arg` helper.

check.py dynasm 162/162, cranelift 162/162 at flag-false.

Assisted-by: Claude
All four guards are gated on `pyre_object::tagged_int::CAN_BE_TAGGED`
(currently false) and are dead code at the flag's default, so flag-false
trace output and interpreter behavior are byte-identical.

- majit-gc collector get_actual_typeid: return None for a tagged
  immediate before the offset-0 header read, mirroring can_move's guard
  (gc/base.py:380 is_valid_gc_object).
- interpreter builtin_str: stringify a tagged int to its decimal value
  before the is_str/unwrap_cell/ob_type derefs, mirroring py_str_wtf8
  and py_repr_obj.
- jit-trace loop_inlines_abort_permanent_callee: skip a tagged immediate
  in the globals-dict scan and the cell-contents scan before the
  ob_type / is_cell derefs.
- jit assembler untag_const_ref_value: re-realize a tagged immediate as
  a distinct heap W_IntObject (w_int_new_unique) before it enters the
  jitcode constants_r pool, at each ConstRef pool-bake site. The pool is
  GC-root-walked (walk_jitcode_constants_refs), so it must hold only real
  gcrefs; the bake is byte-identical to the flag-false w_int_new path.

Assisted-by: Claude
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Tagged immediate integers are handled explicitly across object inspection, string conversion, tracing, frame initialization, compiled execution, and assembler constant handling. JIT paths now preserve consistent logical and heap-concrete integer representations while avoiding unsafe pointer dereferences and unnecessary tag guards.

Changes

Tagged integer handling

Layer / File(s) Summary
Immediate-value safety boundaries
majit/majit-gc/src/collector.rs, majit/majit-ir/src/resoperation.rs, pyre/pyre-interpreter/src/builtins.rs, pyre/pyre-jit-trace/src/trace.rs, pyre/pyre-jit-trace/src/trace_opcode.rs
Tagged immediates are excluded from pointer-based inspection, recognized by OpRef, formatted directly by str(), and no longer treated as heap candidates or cells.
Trace boxing and concrete representation
pyre/pyre-jit-trace/src/jitcode_dispatch.rs, pyre/pyre-jit-trace/src/state.rs
Integer boxing, class-cache updates, concrete opref seeding, and tag-guard emission now distinguish logical boxed values from heap-concrete values.
Compiled-entry local normalization
pyre/pyre-jit/src/eval.rs
Compiled execution entry points convert tagged integers in frame locals into heap integer objects before running machine code.
Constant and residual-value normalization
pyre/pyre-jit/src/jit/assembler.rs
Assembler constant-reference paths convert tagged integer constants into unique heap objects before pool insertion and reference operations.

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

Sequence Diagram(s)

sequenceDiagram
  participant Frame
  participant CompiledEntry
  participant JITTrace
  participant Assembler
  participant CompiledCode
  Frame->>CompiledEntry: provide locals and constants
  CompiledEntry->>Frame: replace tagged local integers with heap objects
  CompiledEntry->>JITTrace: execute normalized frame
  JITTrace->>Assembler: emit concrete integer and constant references
  Assembler->>CompiledCode: run code with normalized representations
Loading

Possibly related PRs

  • youknowone/pyre#417: Extends tagged-immediate handling across the same JIT tracing and boxing paths.
  • youknowone/pyre#471: Adds related tagged-immediate checks in callee candidate scanning.

Poem

A bunny found a tagged-up bit,
And taught the JIT to care for it.
Heap-bound ints hop safely through,
Guards grow wise and pointers too.
“No rogue deref!” the rabbit sings—
“Clean little values, clever things!”

🚥 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 captures the tagged-int trace-boundary work and the four gated dereference guards in the changeset.
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.
✨ 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

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ast-grep (0.44.1)
pyre/pyre-jit-trace/src/jitcode_dispatch.rs

ast-grep timed out on this file


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

❤️ Share

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

@github-actions

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit 440b08d).

Files in the reviewed diff
majit/majit-gc/src/collector.rs
majit/majit-ir/src/resoperation.rs
pyre/pyre-interpreter/src/builtins.rs
pyre/pyre-jit-trace/src/jitcode_dispatch.rs
pyre/pyre-jit-trace/src/state.rs
pyre/pyre-jit-trace/src/trace.rs
pyre/pyre-jit-trace/src/trace_opcode.rs
pyre/pyre-jit/src/eval.rs
pyre/pyre-jit/src/jit/assembler.rs

1. Regressions to PyPy parity introduced by this patch

  • pyre/pyre-jit-trace/src/trace_opcode.rs:5333 ↔ rpython/rtyper/lltypesystem/rtagged.py:144box_int_payload now always emits wrapint/NewWithVtable; upstream’s tagged representation deterministically returns the immediate value * 2 + 1. This removes the previously literal tagged-boxing parity.

2. Other mismatches introduced by this patch

  • pyre/pyre-jit/src/eval.rs:5313 ↔ rpython/rtyper/lltypesystem/rtagged.py:144 — converting each tagged local with w_int_new_unique breaks identity: equal tagged immediates map to the same deterministic pointer upstream, while two local slots holding the same value become distinct heap objects. Thus identity-sensitive code such as a = 1; b = 1; a is b can change result in compiled execution when tagging is enabled. The same noncanonical conversion is also introduced while recording at pyre/pyre-jit-trace/src/state.rs:4869 and for JitCode constants at pyre/pyre-jit/src/jit/assembler.rs:1937.

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

None.

4. Structural adaptations

  • majit/majit-gc/src/collector.rs:3549 ↔ rpython/memory/gc/base.py:380 — the new tagged-immediate rejection prevents treating an odd immediate as a GC object before dereferencing its header. This is the necessary Rust-side form of RPython’s taggedpointers validity check.

  • majit/majit-ir/src/resoperation.rs:194 ↔ rpython/jit/metainterp/resoperation.py:716OpRef::is_input_arg() is a Rust enum-tag implementation of AbstractInputArg.is_inputarg().

  • pyre/pyre-interpreter/src/builtins.rs:4802 ↔ pypy/objspace/std/intobject.py:664 — tagged integers require a pre-dereference decimal-string path because Pyre represents enabled small integers as non-pointer immediates; PyPy obtains the equivalent string from the integer payload.

@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/5584174af37448cd5755f8c50c819573e9f7aecb/pyre-interpreter/src/builtins.rs#L4802
P2 Badge Move tagged-int str guard before decoding

When CAN_BE_TAGGED is enabled and callers supply encoding or errors, builtin_str takes the decoding branch before reaching this new guard; for a tagged-int obj, buffer_as_bytes_like(obj) falls through and the TypeError path reads (*obj).ob_type, dereferencing the odd immediate instead of reporting int found. str(1, "utf-8")/keyword variants can therefore crash under the tagged-int flag, so the tagged-int handling needs to run before the has_encoding || has_errors path or that path must synthesize the int type name safely.


https://github.com/youknowone/pyre/blob/5584174af37448cd5755f8c50c819573e9f7aecb/pyre-jit/src/jit/assembler.rs#L1937-L1939
P2 Badge Preserve identity when normalizing tagged ConstRefs

With CAN_BE_TAGGED enabled, this creates a fresh heap W_IntObject every time the same tagged immediate passes through untag_const_ref_value, before add_const_r can deduplicate by raw pointer. If a jitcode bakes the same ConstRef(tag_int(n)) in multiple places, those constants become distinct heap addresses, so later ptr_eq/Python is can observe false even though the interpreter tagged representation is the same immediate value. Reuse a materialized box per tagged payload/jitcode before inserting into constants_r.

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

Copy link
Copy Markdown
Owner Author

Codex parity-review disposition (team-lead)

Ran the codex parity review against upstream/main. Note: the current
upstream/main (37cc318) lags origin/main (aa36172), so the diff base pulls in
#523 (always-portal) and #447 (gc-hook) on top of this slice's two commits
(ba2dac7110d Dir3 machinery, d4f5e51826b 4 gated guards). Findings adjudicated
per-section:

§1 Parity regressions (3) — all on the tagged-int machinery, all conditioned "when tagging enabled"; the committed slice ships flag-FALSE (dead code, byte-identical), so none affect the default.

  • §1-1 box_int_payload heap-only (trace_opcode.rs) → reclassified §4 (intentional, won't-fix documented). This is Direction-B by design: we deliberately rejected Direction-A (in-trace tagged pointer per rtagged.py) because it would force GcConfig.taggedpointers ON and make the JIT carry tagged pointers in traces — something the mainline int path never does — and it regressed hot int loops. Dir-B: the JIT never carries a tagged int; it untags once at the concrete boundary to a virtualizable heap box, trace stays byte-identical to flag-false, box virtualizes away.
  • §1-2 frame-local untag → distinct heap object → identity/is/hash (eval.rs, state.rs) → false-positive, empirically refuted. With tagging live, is/identity compares the immediate bits (v<<1)|1, identical for equal values — value-identity holds without an address compare. The heap box the untag creates is consumed only by the in-trace GuardClass deref; it is never exposed to a Python is.
  • §1-3 const-pool untag_const_ref_value (assembler.rs) → false-positive, same refutation. The const-pool box exists solely so GC-walked constants_r holds a real gcref for a GuardClass deref; Python is still compares the original tagged immediate.

Verified live on a flag-true build vs CPython 3.14, all matching: a=1000;b=1000; a is b→True; 256 is 256→True; big=10**18; a is copy→True; loop-accumulator is literal→True; [7,7,7] elements identical; d[42] lookup correct.

§2 Other mismatches (1) — codewriter always-Portal (codewriter.rs:5745) → out of scope. This is #523 (aa36172bc1f, already merged to origin/main), leaked in by the stale upstream/main base. Neither commit in this slice touches codewriter.rs.

§3 Pre-existing (1) — single-frame inline collapse (jitcode_dispatch.rs:14729) → defer. Pre-existing on upstream/main, tracked under the multiframe/walker-arch work.

§4 Structural adaptations (4) — free-threading GC (gc_sync, shadow_stack, collector, stack_check) → won't-fix documented. STW/per-thread adaptations, orthogonal to this slice.

Net: no §1/§2 fix lands on the tagged-int files — 1 intentional design (reclassified), 2 refuted false-positives, 1 out-of-scope. The committed slice stands as-is (flag stays OFF).

commented by Claude

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
pyre/pyre-interpreter/src/builtins.rs (1)

4783-4807: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Move the tagged-int guard before the decode branch. has_encoding || has_errors still reaches is_str(obj) and buffer_as_bytes_like(obj) first, so a tagged immediate can be treated like a pointer before the new fast path runs. Put the tagged-int check right after obj is resolved.

🤖 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/builtins.rs` around lines 4783 - 4807, Move the
tagged-integer guard using pyre_object::tagged_int::is_tagged_int immediately
after obj is resolved, before the has_encoding || has_errors decode branch.
Return the formatted untagged decimal string there, and remove the later
duplicate guard while leaving normal decoding behavior unchanged.
🤖 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.

Outside diff comments:
In `@pyre/pyre-interpreter/src/builtins.rs`:
- Around line 4783-4807: Move the tagged-integer guard using
pyre_object::tagged_int::is_tagged_int immediately after obj is resolved, before
the has_encoding || has_errors decode branch. Return the formatted untagged
decimal string there, and remove the later duplicate guard while leaving normal
decoding behavior unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c89fb80a-ea6a-4099-9224-a1f587111dbe

📥 Commits

Reviewing files that changed from the base of the PR and between aa36172 and 5584174.

📒 Files selected for processing (9)
  • majit/majit-gc/src/collector.rs
  • majit/majit-ir/src/resoperation.rs
  • pyre/pyre-interpreter/src/builtins.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch.rs
  • pyre/pyre-jit-trace/src/state.rs
  • pyre/pyre-jit-trace/src/trace.rs
  • pyre/pyre-jit-trace/src/trace_opcode.rs
  • pyre/pyre-jit/src/eval.rs
  • pyre/pyre-jit/src/jit/assembler.rs

@youknowone youknowone merged commit f255891 into main Jul 13, 2026
29 of 30 checks passed
@youknowone youknowone deleted the miframe branch July 13, 2026 10:41
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