Skip to content

jit/interp: tagged-int groundwork, warmup fixes, wasm oversized-module decline#417

Merged
youknowone merged 22 commits into
mainfrom
miframe
Jul 9, 2026
Merged

jit/interp: tagged-int groundwork, warmup fixes, wasm oversized-module decline#417
youknowone merged 22 commits into
mainfrom
miframe

Conversation

@youknowone

@youknowone youknowone commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Branch miframe — 9 commits ahead of main, grouped by theme.

Tagged-int enablement groundwork (inert, CAN_BE_TAGGED=false)

Slices 1–4 of the tagged-int epic. All land harmlessly behind the disabled flag until a later enablement flip; no behavior change yet.

  • object: tag-guard type-probe primitives ahead of tagged-int enablement
  • object/jit: tag-aware GC walker, int maker, and GC config wiring for tagged ints
  • objspace: document tagged-int identity in is_w

Interpreter warmup fixes (follow-up to gh#394)

  • interp: cache npure_cellvars on PyCode to avoid per-stack-op recompute
  • interp: skip in-place special lookup for exact-builtin numeric lhs
  • interp: skip dunder method-cache lookup for exact-builtin numeric operands

JIT

  • jit: move (not clone) Phase 2 snapshot/emit buffers in unroll optimizer
  • synth: add nested FOR_ITER test coverage
  • majit-wasm: decline compile when host rejects the emitted modulejit_compile_wasm returns handle 0 when the wasm runtime rejects an emitted module (e.g. a function body exceeding the parser's size limit, which a trace within trace_limit can still produce after the optimizer peels/unrolls the loop). compile_loop/compile_bridge now return BackendError::Unsupported on handle 0 so execute_token does not dispatch dead table slot 0 and leave frame[0] unwritten (was surfacing as consume_vable_info: get_total_size=15 != vable_size-1=10 on nested_foriter_call).

Verification

  • check.py: dynasm 187/187, cranelift 187/187, wasm 187/187
  • nested_foriter_call → 9940050000 (was panicking on wasm)

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of small immediate integers so object lookup, type checks, iteration, and dictionary behavior avoid crashes and behave consistently.
    • Fixed trace compilation fallbacks so unsupported host responses no longer record invalid handles.
    • Reduced incorrect dispatch on tagged integer values during tracing and unboxing.
  • New Features

    • Added several benchmark programs covering nested loop and iteration patterns.
  • Performance

    • Streamlined internal value handling to avoid unnecessary copying and improve runtime efficiency.

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@youknowone, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 55 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 68175593-be1f-4506-bb74-8122dfcf05ae

📥 Commits

Reviewing files that changed from the base of the PR and between 7e5cc9b and ffb5aa6.

📒 Files selected for processing (32)
  • majit/majit-backend-cranelift/src/compiler.rs
  • majit/majit-backend-dynasm/src/runner.rs
  • majit/majit-backend-wasm/src/lib.rs
  • majit/majit-backend/src/model.rs
  • majit/majit-gc/src/collector.rs
  • majit/majit-gc/src/lib.rs
  • majit/majit-metainterp/src/optimizeopt/info.rs
  • majit/majit-metainterp/src/optimizeopt/rewrite.rs
  • majit/majit-metainterp/src/optimizeopt/unroll.rs
  • pyre/bench/synth/foriter_in_while.py
  • pyre/bench/synth/nested_foriter_call.py
  • pyre/bench/synth/nested_foriter_poly.py
  • pyre/bench/synth/nested_foriter_range.py
  • pyre/pyre-interpreter/src/baseobjspace.rs
  • pyre/pyre-interpreter/src/objspace/descroperation.rs
  • pyre/pyre-interpreter/src/pycode.rs
  • pyre/pyre-interpreter/src/pyframe.rs
  • pyre/pyre-interpreter/src/typedef.rs
  • pyre/pyre-jit-trace/src/helpers.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch.rs
  • pyre/pyre-jit-trace/src/state.rs
  • pyre/pyre-jit-trace/src/trace_opcode.rs
  • pyre/pyre-jit-trace/src/walker_frame_ops.rs
  • pyre/pyre-jit/src/eval.rs
  • pyre/pyre-object/src/celldict.rs
  • pyre/pyre-object/src/dictmultiobject.rs
  • pyre/pyre-object/src/identitydict.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/sliceobject.rs

Walkthrough

This PR introduces tagged-immediate integer representation across pyre-object, pyre-interpreter, and pyre-jit-trace/pyre-jit crates, adding short-circuit checks in type/identity predicates, GC configuration, and JIT-trace guard/unbox emission. It also adds a cached pure-cellvars count to PyCode/PyFrame, wasm backend rejection handling for zero handles, Phase-2 optimizer ownership transfer, and new benchmark scripts.

Changes

Tagged Integer Support

Layer / File(s) Summary
Core type predicates and int constructor tagging
pyre/pyre-object/src/intobject.rs, pyre/pyre-object/src/pyobject.rs, pyre/pyre-interpreter/src/typedef.rs
w_int_new returns a tagged immediate when it fits; is_exact_builtin_instance, is_exact_type, ll_isinstance, py_type_check, and typedef::r#type/name resolution short-circuit for tagged ints.
Container/collection tagged-int guards
pyre/pyre-object/src/celldict.rs, dictmultiobject.rs, identitydict.rs, iterobject.rs, listobject.rs, sliceobject.rs
Cell unwrapping, GC root-walk, identity comparison, iterator/list/slice type checks each add early returns for tagged ints to avoid dereferencing object headers.
Attribute lookup and numeric dispatch fixes
pyre/pyre-interpreter/src/baseobjspace.rs, objspace/descroperation.rs
object_getattr_miss/is_w use tag-safe type resolution; new is_exact_numeric_builtin/numeric_base_type_of_overriding_subclass helpers gate numeric override and in-place dispatch for exact builtins.
JIT-trace tagged-int guard emission and unboxing
pyre/pyre-jit-trace/src/helpers.rs, jitcode_dispatch.rs, state.rs
New emit_tag_lowbit_test/emit_untag_int helpers back an early dispatch path and unboxing logic that guard and untag tagged-int operands instead of using class-based checks.
JIT GC configuration and virtual materialization
pyre/pyre-jit/src/eval.rs
build_gc wires GcConfig.taggedpointers to CAN_BE_TAGGED; comments document boxed materialization for tagged immediates.

Cached Pure Cellvars Count

Layer / File(s) Summary
PyCode field and PyFrame integration
pyre/pyre-interpreter/src/pycode.rs, pyframe.rs
Adds npure_cellvars field computed at construction (with u32::MAX sentinel), a w_code_npure_cellvars accessor, and updates PyFrame::ncells to use the cached value with fallback.

WASM Backend Rejection and Optimizer Ownership

Layer / File(s) Summary
WASM host rejection handling
majit/majit-backend-wasm/src/lib.rs
compile_loop and compile_bridge return BackendError::Unsupported when glue::compile_module returns a zero handle/slot.
Optimizer state move semantics
majit/majit-metainterp/src/optimizeopt/unroll.rs
Phase 2 initialization moves preamble/exporter state via std::mem::take instead of cloning.

Synthetic FOR_ITER Benchmarks

Layer / File(s) Summary
New FOR_ITER benchmark scripts
pyre/bench/synth/foriter_in_while.py, nested_foriter_call.py, nested_foriter_poly.py, nested_foriter_range.py
Adds four benchmark scripts, each with a main() performing nested/while-wrapped loop accumulation and printing an expected result.

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

Possibly related issues

Possibly related PRs

  • youknowone/pyre#218: Overlaps with typedef.rs changes for type resolution used by tagged-int handling.
  • youknowone/pyre#312: Both modify compile_loop/compile_bridge to produce/handle BackendError::Unsupported declines.
  • youknowone/pyre#407: Both modify compile_bridge to decline wasm bridge compilation under specific handle/slot conditions.

Poem

A tag upon an int so small,
No boxing needed after all! 🐇
Through guards and shifts it hops so light,
Past dicts and slices, out of sight.
Cellvars cached, wasm declined with grace—
This bunny thumps at review pace! 🥕

🚥 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 is concise and accurately captures the PR’s main themes: tagged-int groundwork, interpreter warmup fixes, and the wasm module rejection fix.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate 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.

@youknowone youknowone changed the title jit/interp: tagged-int groundwork (S1-S4), warmup fixes, wasm oversized-module decline jit/interp: tagged-int groundwork, warmup fixes, wasm oversized-module decline Jul 8, 2026
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

🤖 Codex parity review

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

1. Regressions to PyPy parity introduced by this patch

None.

2. Other mismatches introduced by this patch

None.

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

None.

4. Structural adaptations

  • pyre/pyre-object/src/intobject.rs:90 ↔ pypy/objspace/std/intobject.py:903: pyre may return a tagged immediate from w_int_new (return crate::tagged_int::tag_int(value)), while PyPy wrapint returns W_IntObject(x) or a prebuilt W_IntObject. This is the tagged-pointer representation adaptation, gated by CAN_BE_TAGGED.

  • pyre/pyre-jit-trace/src/helpers.rs:850 ↔ rpython/rtyper/lltypesystem/rtagged.py:155: pyre emits explicit trace IR for the low-bit test (CastPtrToInt, IntAnd), while RPython expresses it as if lltype.cast_ptr_to_int(instance) & 1. Equivalent tagged-pointer lowering.

  • pyre/pyre-jit-trace/src/state.rs:2961 ↔ rpython/rtyper/lltypesystem/rtagged.py:147: pyre’s int unbox path emits a guard plus IntRshift(CastPtrToInt(obj), 1), while RPython’s helper is direct ll_unboxed_to_int(p): return lltype.cast_ptr_to_int(p) >> 1. This is the Rust trace-generation form of the same operation.

  • majit/majit-gc/src/collector.rs:3071 ↔ rpython/memory/gc/base.py:380: pyre exposes is_tagged_immediate and makes can_move return false for tagged immediates; RPython folds that into is_valid_gc_object via taggedpointers && (addr & 1 == 0). Same GC validity rule, split across Rust trait hooks.

  • majit/majit-metainterp/src/optimizeopt/info.rs:415 ↔ rpython/jit/metainterp/optimizeopt/info.py:763: pyre returns None for a tagged-immediate constant class to avoid header dereference; PyPy returns cpu.cls_of_box(self._const) after the check_is_object gate. Conservative optimization loss only; runtime guards preserve behavior.

  • majit/majit-metainterp/src/optimizeopt/rewrite.rs:332 ↔ rpython/jit/metainterp/optimizeopt/autogenintrules.py:1113: pyre adds a tagged-int-only fold for IntRshift(IntOr(IntAddOvf(x,x),1),1) => x; PyPy’s generated optimize_INT_RSHIFT has no such tagged-box/unbox round-trip rule. This is a representation-specific optimizer adaptation.

  • pyre/pyre-interpreter/src/pyframe.rs:1871 ↔ pypy/interpreter/pyframe.py:386: pyre caches and uses npure_cellvars + freevars.len() for stack base; PyPy uses co_nlocals + len(co_cellvars) + len(co_freevars). This is due to pyre’s CPython locals-plus/cellvar overlap layout.

  • pyre/pyre-interpreter/src/objspace/descroperation.rs:2522 ↔ pypy/objspace/descroperation.py:825: pyre skips the lhs __i*__ lookup for exact builtin numerics; PyPy always performs space.lookup(w_lhs, specialname) first. This is a performance shortcut relying on exact builtin numeric types not defining in-place slots; subclasses and mutable sequences still follow the PyPy path.

@youknowone youknowone force-pushed the miframe branch 2 times, most recently from cb5fddb to 1e0a402 Compare July 8, 2026 08:01

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

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

// Decline the compile so the metainterp keeps the interpreter fallback —
// a backend capability limit, reported like any other unsupported shape.
#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
if func_handle == 0 {

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 Convert web wasm compile failures to the zero sentinel

When running the wasm backend through the web glue, this sentinel check is never reached for the oversized/invalid-module case it is meant to handle: majit/majit-backend-wasm/js/jit_glue.js constructs new WebAssembly.Module(bytes) before any catch, and the fallback instantiation can also throw, so a browser CompileError/instantiation error propagates out of glue::compile_module instead of returning 0. The same import is used for bridges, so browser wasm still aborts rather than returning BackendError::Unsupported unless the web glue catches compile/instantiate failures and returns the zero handle.

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: 7e5cc9b64d

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

// code-invariant and cached on the `PyCode` wrapper; `freevars.len()`
// is a direct read. Falls back to the full walk for stub frames
// whose `pycode` wrapper carries the `u32::MAX` sentinel.
let code = unsafe { &*pyframe_get_pycode(self) };

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 Check the PyCode sentinel before dereferencing

When self.pycode is a wrapper that took the new u32::MAX sentinel path (w_code_new(null) or an unaligned test stub), pyframe_get_pycode(self) returns a null/invalid code pointer, but this line forms &* before w_code_npure_cellvars can return None. That makes the advertised fallback unreachable and turns any ncells()/stack_base() call on such a stub frame into UB or a crash; check the cache/sentinel before dereferencing the raw code pointer.

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

🤖 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 `@majit/majit-backend-wasm/src/lib.rs`:
- Around line 1211-1218: The fail-descriptor registration in the trace/bridge
install flow is happening too early, before the host acceptance checks. Update
the install logic around the func_handle and bridge_slot zero-checks so
register_fail_descrs(...) runs only after those checks pass, and keep it before
any guard cell state is flipped. Apply the same reordering in both affected
paths so unsupported/rejected modules do not publish descriptors for traces or
bridges that will never be installed.
🪄 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: 70f85eec-69c0-4284-afb0-b1a06d4a95c7

📥 Commits

Reviewing files that changed from the base of the PR and between 7e3db1c and 7e5cc9b.

📒 Files selected for processing (23)
  • majit/majit-backend-wasm/src/lib.rs
  • majit/majit-metainterp/src/optimizeopt/unroll.rs
  • pyre/bench/synth/foriter_in_while.py
  • pyre/bench/synth/nested_foriter_call.py
  • pyre/bench/synth/nested_foriter_poly.py
  • pyre/bench/synth/nested_foriter_range.py
  • pyre/pyre-interpreter/src/baseobjspace.rs
  • pyre/pyre-interpreter/src/objspace/descroperation.rs
  • pyre/pyre-interpreter/src/pycode.rs
  • pyre/pyre-interpreter/src/pyframe.rs
  • pyre/pyre-interpreter/src/typedef.rs
  • pyre/pyre-jit-trace/src/helpers.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch.rs
  • pyre/pyre-jit-trace/src/state.rs
  • pyre/pyre-jit/src/eval.rs
  • pyre/pyre-object/src/celldict.rs
  • pyre/pyre-object/src/dictmultiobject.rs
  • pyre/pyre-object/src/identitydict.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/sliceobject.rs

Comment thread majit/majit-backend-wasm/src/lib.rs
@youknowone youknowone force-pushed the miframe branch 3 times, most recently from 25bba27 to f230969 Compare July 8, 2026 15:08

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

ℹ️ 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 +3071 to +3072
fn is_tagged_immediate(&self, addr: usize) -> bool {
MiniMarkGC::is_tagged_immediate(self, addr)

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 Short-circuit tagged refs before type-id lookup

When taggedpointers is enabled, exposing is_tagged_immediate here is not enough because this same allocator still lets check_is_object call get_actual_typeid, whose foreign-object fallback dereferences gcref.0 at offset 0. Any path that reaches majit_gc::check_is_object or get_actual_typeid without doing the new precheck (for example speculative GC-type validation) will crash or read from an odd tagged-int address; the collector should reject tagged immediates before attempting type-id/classptr reads.

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: f10b512baa

ℹ️ 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 +1217 to +1218
if func_handle == 0 {
return Err(BackendError::Unsupported(

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 Defer wasm fail-descr registration until compile succeeds

When this new Unsupported branch is taken on the browser wasm backend, register_fail_descrs(&fail_descrs) has already appended this rejected trace's exits to the global fail-index registry above. The loop compile error path is explicitly non-terminal (pyjitpl.rs:5960-5964), so a hot loop whose module is consistently rejected can retry and leak another batch of dead fail descriptors each time before falling back; the bridge path has the same ordering with bridge_descrs. Instantiate/check the zero sentinel before publishing descriptors so rejected modules do not consume global fail-index entries.

Useful? React with 👍 / 👎.

youknowone added 16 commits July 9, 2026 07:53
- nested_foriter_range: for(range) × for(range), homogeneous
- nested_foriter_poly: for(range) × for(list), polymorphic type flip
- nested_foriter_call: for(range) × for(range) + function call
- foriter_in_while: while > for(range), the common real-world pattern

All four verify JIT correctness against PYRE_JIT=0 oracle.

Assisted-by: Claude
…rands

numeric_operand_overrides / needs_numeric_unaryop_dispatch ran
lookup_where_with_method_cache(type, "__add__"/…) for every numeric
binop/unary operand, including exact int/float. That lookup interns the
dunder name via box_str_constant(Wtf8::new(name)) (a UTF-8 validation)
on each call, up to 4x per binop, on every pre-threshold iteration.

Extract numeric_base_type_of_overriding_subclass, gated on
is_exact_builtin_instance — the same exact-builtin guard already opening
subclass_special_override and is_true. An exact builtin numeric instance
cannot override a special method, so it returns None without the
string-keyed lookup.

Measured on `while i < N: total += i + 1; i += 1` (PYRE_JIT=0, 3M iters,
aarch64): ~700-808 -> ~287-300 ns/iter (~2.4x), result byte-identical.
30-fn warmup: ~1.12 -> ~0.72-0.99 ms/fn.

check.py dynasm 187/187, cranelift 187/187.

Assisted-by: Claude
Phase 2 of the unroll optimizer is the last reader of self.phase1_emit_ops,
snapshot_boxes, snapshot_frame_sizes, snapshot_vable_boxes,
snapshot_vref_boxes, snapshot_frame_pcs, and call_pure_results, so hand
them to opt_p2 via std::mem::take instead of clone. The GC re-root
(replace_compile_snapshot_roots over collect_snapshot_const_ptr_slots)
stays after the in-place remap and before optimize, preserving the raw
*mut OpRef slot addresses. trace_inputargs stays a clone (re-read at the
Phase 2 debug_assert and p2_inputarg_types).

Snapshot clone cost measured at ~13us on small traces, so this is not a
warmup lever; the change is a strictly-cheaper, safer form. Mirrors the
InvalidLoop simple_opt move precedent in pyjitpl.rs.

check.py dynasm 187/187, cranelift 187/187.

Assisted-by: Claude
try_inplace_special ran lookup_type_special(lhs, "__iadd__"/…) for every
augmented numeric assignment (total += …), a string-keyed method-cache
probe that interns the dunder name via box_str_constant. int/long/float/
complex/bool define no in-place special, so the lookup is always a miss.

Gate on is_exact_numeric_builtin (is_exact_builtin_instance + numeric
storage): exact numerics skip straight to the binary op. Builtin sequences
(list/bytearray) define __iadd__/__imul__ and subclasses may override the
in-place slot, so both fall through to the lookup unchanged.

Measured on `while i < n: total += i + 1; i += 1` (PYRE_JIT=0, aarch64):
~296-300 -> ~240-257 ns/iter, cumulative from baseline ~700-808 -> ~3.0x.
Parity verified vs py3.14 (JIT on/off): list/bytearray += keep identity,
int/float/bigint/complex += correct, IAddInt/list-subclass __iadd__
overrides dispatch, int += float reflected fall-through.

check.py dynasm 187/187, cranelift 187/187.

Assisted-by: Claude
PyFrame::ncells() ran npure_cellvars(code) — an O(cellvars × varnames)
overlap count — on every pop_value/peek_at (twice per binop). The count
is code-invariant, so compute it once in w_code_new_with_hidden_applevel
and store it as a u32 on the PyCode wrapper (u32::MAX sentinel for
null/unaligned code_ptr stubs). PyFrame::ncells() reads it via
w_code_npure_cellvars and adds freevars.len(), falling back to the full
walk for stub frames.

The new field sits after fast_natural_arity, before the cache pointers;
CODE_PTR_OFFSET / CODE_W_GLOBALS_OFFSET (the only JIT-baked offsets) are
unaffected, and a u32 is not a GC-walked reference.

Measured on `while i < n: total += i + 1; i += 1` (PYRE_JIT=0, aarch64):
~240-257 -> ~227-229 ns/iter; npure_cellvars gone from the profile.
Cumulative from baseline ~700-808 -> ~3.2x. Parity verified vs py3.14
including a closure loop (non-empty cellvars).

check.py dynasm 187/187, cranelift 187/187.

Assisted-by: Claude
py_type_check, ll_isinstance, is_exact_type, and is_exact_builtin_instance
short-circuit on a tagged immediate before the ob_type deref. Gated on
tagged_int::CAN_BE_TAGGED (const false), so the guards fold away and the
deref path is unchanged until enablement.

Assisted-by: Claude
…tagged ints

- is_seq_iter short-circuits on a tagged immediate before the ob_type
  deref, so the GC value-stack walker (walk_raw_seq_iter_roots) never
  dereferences one; the other walk_raw_* predicates route through the
  already-tag-guarded py_type_check/ll_isinstance.
- w_int_new returns a tagged immediate for values that fit, instead of
  allocating; w_int_new_unique documented as never-tag (subclass identity);
  the JIT virtual-reconstruction allocators documented as stay-boxed.
- The GC constructor mirrors CAN_BE_TAGGED into GcConfig.taggedpointers so
  the collector-core immediate guards go live in lockstep with the maker.

All gated on tagged_int::CAN_BE_TAGGED (const false); behavior unchanged
until enablement. dynasm+cranelift 187/187.

Assisted-by: Claude
is_w's int branch already handles tagged immediates without change: two
equal-valued immediates match the ptr::eq fast path, and an immediate vs a
boxed int of the same value compares equal by value through the tag-aware
w_int_get_value. No behavioral change (CAN_BE_TAGGED still false).

Assisted-by: Claude
jit_compile_wasm returns handle 0 when the wasm runtime rejects the
emitted module (e.g. a function body exceeding the parser's size limit,
which a trace within trace_limit can still produce after the optimizer
peels/unrolls the loop). Return BackendError::Unsupported in compile_loop
and compile_bridge when compile_module returns 0, so execute_token does
not dispatch dead table slot 0 and leave frame[0] unwritten.

Assisted-by: Claude
Eight object→type accessors and dict/slice value-readers dereferenced
(*obj).ob_type / .w_class after only a null check, faulting on a tagged
immediate. A PYRE_JIT=0 flip of CAN_BE_TAGGED=true SIGSEGVs at stdlib
bootstrap through these sites, which the type-probe primitives hardened
earlier do not cover (they route through py_type_check; these do a direct
field read).

Guarded, all gated on CAN_BE_TAGGED (default false, inert):
- typedef::r#type returns gettypefor(&INT_TYPE) for a tagged int
- celldict::unwrap_cell returns the immediate unchanged
- listobject::is_plain_int1 returns true (always an exact int)
- dictmultiobject::key_compares_by_identity / identitydict::is_correct_type
  return false (an int compares by value)
- sliceobject::is_slice returns false
- baseobjspace::object_getattr_miss reads the class via typedef::r#type

typedef::init_type_type and object_getattr_miss error messages now name
the value's type via the tag-safe typedef::r#type instead of a raw
(*obj).ob_type read.

Verified with CAN_BE_TAGGED=true, PYRE_JIT=0: all 177 synth scripts run
crash-free and byte-identical to the boxed build (object-repr addresses
aside). Inert build: check.py 187/187 dynasm + cranelift.

Assisted-by: Claude
is_trace_plain_int dereferences (*obj).w_class after a py_type_check that
already returns true for a tagged immediate, faulting when the trace-entry
value classifier (ConcreteValue::from_pyobj) inspects a tagged int on the
value stack. A tagged immediate is always an exact int, so return true
before the deref. Gated on CAN_BE_TAGGED (default false, inert).

Assisted-by: Claude
walk_module_value_slot dereferenced (*w_value).ob_type without the
tagged-immediate skip its sibling unwrap_cell (celldict.rs:206) already
carries. A tagged int in a module-dict value slot faulted the ob_type
read during a minor-GC root walk (walk_pyframe_roots ->
walk_module_dicts_gc -> w_module_dict_walk_gc_cells). Forward the slot
as-is on a tagged immediate. Gated on CAN_BE_TAGGED (default false).

Assisted-by: Claude
The walker and MIFrame int-unbox paths emitted GuardClass +
GetfieldGcI unconditionally, dereferencing the operand header. A
tagged immediate has no header, so those loads fault once ints tag.
Before the class guard, dispatch on the operand's observed concrete:
a tagged immediate emits CastPtrToInt + IntAnd(1) + GuardTrue then
IntRshift(1) arithmetic untag (rtagged.py ll_unboxed_to_int) and skips
the class guard/getfield entirely; a boxed operand emits the same
low-bit test with GuardFalse, then falls through to the unchanged
GuardClass + GetfieldGcI. int_or_bool_unbox_type_descr takes the INT
arm for a tagged immediate before the is_bool header deref. All gated
on CAN_BE_TAGGED (default false); no new opcodes, no backend changes.

Assisted-by: Claude
…f (gated taggedpointers)

Add a public `majit_gc::is_tagged_immediate(addr)` free shim delegating
through a new `ACTIVE_IS_TAGGED_IMMEDIATE` thread-local callback, wired
into `ActiveGcGuardHooks` and installed by the dynasm, cranelift, and
wasm backends alongside `check_is_object`. The installed callbacks reach
the active GC's `GcAllocator::is_tagged_immediate`, which MiniMarkGC
answers from `config.taggedpointers && (addr & 1 == 1)` and other
backends default to false.

In `PtrInfo::get_known_class`'s `Constant` arm, return `None` for a
tagged-immediate constant before the `check_is_object` / `cls_of_gcref`
offset-0 reads, so the odd-valued address is not dereferenced as an
object header. Inert while taggedpointers is off.

Assisted-by: Claude
…ntAddOvf(x,x),1),1) -> x (gated taggedpointers)

Add an `IntRshift` arm to `OptRewrite::propagate_forward` that, when the
active GC has `taggedpointers` enabled, folds the box/unbox round-trip
`IntRshift(IntOr(IntAddOvf(x, x), 1), 1)` to `x` via `make_equal_to` +
`Remove`. The arm peels `IntOr` then `IntAddOvf` with `get_producing_op`,
requires both the `IntOr` and `IntRshift` second args to be the constant
`1`, and checks the `IntAddOvf` args with `same_box`. The box ops stay;
only the redundant unbox is removed. Gated on the new
`majit_gc::taggedpointers_enabled()` accessor (probes the installed
`is_tagged_immediate` callback with an odd sentinel), so it returns
`PassOn` and is byte-identical with the config off.

Add `majit_gc::taggedpointers_enabled()` free shim.

Assisted-by: Claude
…_payload, guard_int_object_value) (gated CAN_BE_TAGGED)

Assisted-by: Claude
…s on a tagged-immediate operand (gated CAN_BE_TAGGED)

Assisted-by: Claude
x86_64-windows enforces the stack guard page strictly: a trace body whose
prologue moves %rsp past the 4096-byte guard in a single `sub` (deep
nested-FOR_ITER bodies reach 150KB-540KB fixed_frame_storage) skips the guard
page, so the first write below it faults 0xC0000005. Linux/macOS grow the
stack on the fault instead. Enable inline probestack in Compiler::new so the
prologue touches each page.

Scoped to Windows via the runtime `cfg!(windows)` macro rather than a `#[cfg]`
attribute so the flag_builder.set calls compile on every platform; non-Windows
codegen is unchanged (verified cranelift check.py 159/159 on aarch64/x86_64
darwin). probestack_size_log2 defaults to 12 (= the 4096 guard page).

Assisted-by: Claude
@youknowone youknowone merged commit 69df974 into main Jul 9, 2026
29 of 30 checks passed
@youknowone youknowone deleted the miframe branch July 9, 2026 00:58
youknowone added a commit that referenced this pull request Jul 9, 2026
…e decline (#417)

* synth: add nested FOR_ITER test coverage

- nested_foriter_range: for(range) × for(range), homogeneous
- nested_foriter_poly: for(range) × for(list), polymorphic type flip
- nested_foriter_call: for(range) × for(range) + function call
- foriter_in_while: while > for(range), the common real-world pattern

All four verify JIT correctness against PYRE_JIT=0 oracle.

Assisted-by: Claude

* interp: skip dunder method-cache lookup for exact-builtin numeric operands

numeric_operand_overrides / needs_numeric_unaryop_dispatch ran
lookup_where_with_method_cache(type, "__add__"/…) for every numeric
binop/unary operand, including exact int/float. That lookup interns the
dunder name via box_str_constant(Wtf8::new(name)) (a UTF-8 validation)
on each call, up to 4x per binop, on every pre-threshold iteration.

Extract numeric_base_type_of_overriding_subclass, gated on
is_exact_builtin_instance — the same exact-builtin guard already opening
subclass_special_override and is_true. An exact builtin numeric instance
cannot override a special method, so it returns None without the
string-keyed lookup.

Measured on `while i < N: total += i + 1; i += 1` (PYRE_JIT=0, 3M iters,
aarch64): ~700-808 -> ~287-300 ns/iter (~2.4x), result byte-identical.
30-fn warmup: ~1.12 -> ~0.72-0.99 ms/fn.

check.py dynasm 187/187, cranelift 187/187.

Assisted-by: Claude

* jit: move (not clone) Phase 2 snapshot/emit buffers in unroll optimizer

Phase 2 of the unroll optimizer is the last reader of self.phase1_emit_ops,
snapshot_boxes, snapshot_frame_sizes, snapshot_vable_boxes,
snapshot_vref_boxes, snapshot_frame_pcs, and call_pure_results, so hand
them to opt_p2 via std::mem::take instead of clone. The GC re-root
(replace_compile_snapshot_roots over collect_snapshot_const_ptr_slots)
stays after the in-place remap and before optimize, preserving the raw
*mut OpRef slot addresses. trace_inputargs stays a clone (re-read at the
Phase 2 debug_assert and p2_inputarg_types).

Snapshot clone cost measured at ~13us on small traces, so this is not a
warmup lever; the change is a strictly-cheaper, safer form. Mirrors the
InvalidLoop simple_opt move precedent in pyjitpl.rs.

check.py dynasm 187/187, cranelift 187/187.

Assisted-by: Claude

* interp: skip in-place special lookup for exact-builtin numeric lhs

try_inplace_special ran lookup_type_special(lhs, "__iadd__"/…) for every
augmented numeric assignment (total += …), a string-keyed method-cache
probe that interns the dunder name via box_str_constant. int/long/float/
complex/bool define no in-place special, so the lookup is always a miss.

Gate on is_exact_numeric_builtin (is_exact_builtin_instance + numeric
storage): exact numerics skip straight to the binary op. Builtin sequences
(list/bytearray) define __iadd__/__imul__ and subclasses may override the
in-place slot, so both fall through to the lookup unchanged.

Measured on `while i < n: total += i + 1; i += 1` (PYRE_JIT=0, aarch64):
~296-300 -> ~240-257 ns/iter, cumulative from baseline ~700-808 -> ~3.0x.
Parity verified vs py3.14 (JIT on/off): list/bytearray += keep identity,
int/float/bigint/complex += correct, IAddInt/list-subclass __iadd__
overrides dispatch, int += float reflected fall-through.

check.py dynasm 187/187, cranelift 187/187.

Assisted-by: Claude

* interp: cache npure_cellvars on PyCode to avoid per-stack-op recompute

PyFrame::ncells() ran npure_cellvars(code) — an O(cellvars × varnames)
overlap count — on every pop_value/peek_at (twice per binop). The count
is code-invariant, so compute it once in w_code_new_with_hidden_applevel
and store it as a u32 on the PyCode wrapper (u32::MAX sentinel for
null/unaligned code_ptr stubs). PyFrame::ncells() reads it via
w_code_npure_cellvars and adds freevars.len(), falling back to the full
walk for stub frames.

The new field sits after fast_natural_arity, before the cache pointers;
CODE_PTR_OFFSET / CODE_W_GLOBALS_OFFSET (the only JIT-baked offsets) are
unaffected, and a u32 is not a GC-walked reference.

Measured on `while i < n: total += i + 1; i += 1` (PYRE_JIT=0, aarch64):
~240-257 -> ~227-229 ns/iter; npure_cellvars gone from the profile.
Cumulative from baseline ~700-808 -> ~3.2x. Parity verified vs py3.14
including a closure loop (non-empty cellvars).

check.py dynasm 187/187, cranelift 187/187.

Assisted-by: Claude

* object: tag-guard type-probe primitives ahead of tagged-int enablement

py_type_check, ll_isinstance, is_exact_type, and is_exact_builtin_instance
short-circuit on a tagged immediate before the ob_type deref. Gated on
tagged_int::CAN_BE_TAGGED (const false), so the guards fold away and the
deref path is unchanged until enablement.

Assisted-by: Claude

* object/jit: tag-aware GC walker, int maker, and GC config wiring for tagged ints

- is_seq_iter short-circuits on a tagged immediate before the ob_type
  deref, so the GC value-stack walker (walk_raw_seq_iter_roots) never
  dereferences one; the other walk_raw_* predicates route through the
  already-tag-guarded py_type_check/ll_isinstance.
- w_int_new returns a tagged immediate for values that fit, instead of
  allocating; w_int_new_unique documented as never-tag (subclass identity);
  the JIT virtual-reconstruction allocators documented as stay-boxed.
- The GC constructor mirrors CAN_BE_TAGGED into GcConfig.taggedpointers so
  the collector-core immediate guards go live in lockstep with the maker.

All gated on tagged_int::CAN_BE_TAGGED (const false); behavior unchanged
until enablement. dynasm+cranelift 187/187.

Assisted-by: Claude

* objspace: document tagged-int identity in is_w

is_w's int branch already handles tagged immediates without change: two
equal-valued immediates match the ptr::eq fast path, and an immediate vs a
boxed int of the same value compares equal by value through the tag-aware
w_int_get_value. No behavioral change (CAN_BE_TAGGED still false).

Assisted-by: Claude

* majit-wasm: decline compile when host rejects the emitted module

jit_compile_wasm returns handle 0 when the wasm runtime rejects the
emitted module (e.g. a function body exceeding the parser's size limit,
which a trace within trace_limit can still produce after the optimizer
peels/unrolls the loop). Return BackendError::Unsupported in compile_loop
and compile_bridge when compile_module returns 0, so execute_token does
not dispatch dead table slot 0 and leave frame[0] unwritten.

Assisted-by: Claude

* object/interp: tag-guard interpreter raw-deref sites for tagged ints

Eight object→type accessors and dict/slice value-readers dereferenced
(*obj).ob_type / .w_class after only a null check, faulting on a tagged
immediate. A PYRE_JIT=0 flip of CAN_BE_TAGGED=true SIGSEGVs at stdlib
bootstrap through these sites, which the type-probe primitives hardened
earlier do not cover (they route through py_type_check; these do a direct
field read).

Guarded, all gated on CAN_BE_TAGGED (default false, inert):
- typedef::r#type returns gettypefor(&INT_TYPE) for a tagged int
- celldict::unwrap_cell returns the immediate unchanged
- listobject::is_plain_int1 returns true (always an exact int)
- dictmultiobject::key_compares_by_identity / identitydict::is_correct_type
  return false (an int compares by value)
- sliceobject::is_slice returns false
- baseobjspace::object_getattr_miss reads the class via typedef::r#type

typedef::init_type_type and object_getattr_miss error messages now name
the value's type via the tag-safe typedef::r#type instead of a raw
(*obj).ob_type read.

Verified with CAN_BE_TAGGED=true, PYRE_JIT=0: all 177 synth scripts run
crash-free and byte-identical to the boxed build (object-repr addresses
aside). Inert build: check.py 187/187 dynasm + cranelift.

Assisted-by: Claude

* jit-trace: tag-guard is_trace_plain_int trace-entry classifier

is_trace_plain_int dereferences (*obj).w_class after a py_type_check that
already returns true for a tagged immediate, faulting when the trace-entry
value classifier (ConcreteValue::from_pyobj) inspects a tagged int on the
value stack. A tagged immediate is always an exact int, so return true
before the deref. Gated on CAN_BE_TAGGED (default false, inert).

Assisted-by: Claude

* object: tag-guard walk_module_value_slot GC root walker

walk_module_value_slot dereferenced (*w_value).ob_type without the
tagged-immediate skip its sibling unwrap_cell (celldict.rs:206) already
carries. A tagged int in a module-dict value slot faulted the ob_type
read during a minor-GC root walk (walk_pyframe_roots ->
walk_module_dicts_gc -> w_module_dict_walk_gc_cells). Forward the slot
as-is on a tagged immediate. Gated on CAN_BE_TAGGED (default false).

Assisted-by: Claude

* jit-trace: emit primitive-IR tag branch in int unbox paths

The walker and MIFrame int-unbox paths emitted GuardClass +
GetfieldGcI unconditionally, dereferencing the operand header. A
tagged immediate has no header, so those loads fault once ints tag.
Before the class guard, dispatch on the operand's observed concrete:
a tagged immediate emits CastPtrToInt + IntAnd(1) + GuardTrue then
IntRshift(1) arithmetic untag (rtagged.py ll_unboxed_to_int) and skips
the class guard/getfield entirely; a boxed operand emits the same
low-bit test with GuardFalse, then falls through to the unchanged
GuardClass + GetfieldGcI. int_or_bool_unbox_type_descr takes the INT
arm for a tagged immediate before the is_bool header deref. All gated
on CAN_BE_TAGGED (default false); no new opcodes, no backend changes.

Assisted-by: Claude

* jit-trace: box int-binop result as a tagged immediate when it fits (gated CAN_BE_TAGGED)

Assisted-by: Claude

* optimizer: recognize a tagged immediate's class without a header deref (gated taggedpointers)

Add a public `majit_gc::is_tagged_immediate(addr)` free shim delegating
through a new `ACTIVE_IS_TAGGED_IMMEDIATE` thread-local callback, wired
into `ActiveGcGuardHooks` and installed by the dynasm, cranelift, and
wasm backends alongside `check_is_object`. The installed callbacks reach
the active GC's `GcAllocator::is_tagged_immediate`, which MiniMarkGC
answers from `config.taggedpointers && (addr & 1 == 1)` and other
backends default to false.

In `PtrInfo::get_known_class`'s `Constant` arm, return `None` for a
tagged-immediate constant before the `check_is_object` / `cls_of_gcref`
offset-0 reads, so the odd-valued address is not dereferenced as an
object header. Inert while taggedpointers is off.

Assisted-by: Claude

* optimizer: fold the tagged-int box/unbox round-trip IntRshift(IntOr(IntAddOvf(x,x),1),1) -> x (gated taggedpointers)

Add an `IntRshift` arm to `OptRewrite::propagate_forward` that, when the
active GC has `taggedpointers` enabled, folds the box/unbox round-trip
`IntRshift(IntOr(IntAddOvf(x, x), 1), 1)` to `x` via `make_equal_to` +
`Remove`. The arm peels `IntOr` then `IntAddOvf` with `get_producing_op`,
requires both the `IntOr` and `IntRshift` second args to be the constant
`1`, and checks the `IntAddOvf` args with `same_box`. The box ops stay;
only the redundant unbox is removed. Gated on the new
`majit_gc::taggedpointers_enabled()` accessor (probes the installed
`is_tagged_immediate` callback with an odd sentinel), so it returns
`PassOn` and is byte-identical with the config off.

Add `majit_gc::taggedpointers_enabled()` free shim.

Assisted-by: Claude

* jit-trace: tag-guard the int-payload unbox helpers (trace_guarded_int_payload, guard_int_object_value) (gated CAN_BE_TAGGED)

Assisted-by: Claude

* jit-trace: decline the spec_ii tuple and int-storage list-append folds on a tagged-immediate operand (gated CAN_BE_TAGGED)

Assisted-by: Claude

* jit: tag-guard the cls_of_gcref typeptr read for tagged immediates (gated taggedpointers)

Assisted-by: Claude

* cranelift: enable inline stack probes on Windows

x86_64-windows enforces the stack guard page strictly: a trace body whose
prologue moves %rsp past the 4096-byte guard in a single `sub` (deep
nested-FOR_ITER bodies reach 150KB-540KB fixed_frame_storage) skips the guard
page, so the first write below it faults 0xC0000005. Linux/macOS grow the
stack on the fault instead. Enable inline probestack in Compiler::new so the
prologue touches each page.

Scoped to Windows via the runtime `cfg!(windows)` macro rather than a `#[cfg]`
attribute so the flag_builder.set calls compile on every platform; non-Windows
codegen is unchanged (verified cranelift check.py 159/159 on aarch64/x86_64
darwin). probestack_size_log2 defaults to 12 (= the 4096 guard page).

Assisted-by: Claude

* interp: read binop-error operand type names via tag-safe ll_type (gated CAN_BE_TAGGED)

Assisted-by: Claude

* gc: return false from can_move for tagged immediates (gated taggedpointers)

Assisted-by: Claude
youknowone added a commit that referenced this pull request Jul 9, 2026
…e decline (#417)

* synth: add nested FOR_ITER test coverage

- nested_foriter_range: for(range) × for(range), homogeneous
- nested_foriter_poly: for(range) × for(list), polymorphic type flip
- nested_foriter_call: for(range) × for(range) + function call
- foriter_in_while: while > for(range), the common real-world pattern

All four verify JIT correctness against PYRE_JIT=0 oracle.

Assisted-by: Claude

* interp: skip dunder method-cache lookup for exact-builtin numeric operands

numeric_operand_overrides / needs_numeric_unaryop_dispatch ran
lookup_where_with_method_cache(type, "__add__"/…) for every numeric
binop/unary operand, including exact int/float. That lookup interns the
dunder name via box_str_constant(Wtf8::new(name)) (a UTF-8 validation)
on each call, up to 4x per binop, on every pre-threshold iteration.

Extract numeric_base_type_of_overriding_subclass, gated on
is_exact_builtin_instance — the same exact-builtin guard already opening
subclass_special_override and is_true. An exact builtin numeric instance
cannot override a special method, so it returns None without the
string-keyed lookup.

Measured on `while i < N: total += i + 1; i += 1` (PYRE_JIT=0, 3M iters,
aarch64): ~700-808 -> ~287-300 ns/iter (~2.4x), result byte-identical.
30-fn warmup: ~1.12 -> ~0.72-0.99 ms/fn.

check.py dynasm 187/187, cranelift 187/187.

Assisted-by: Claude

* jit: move (not clone) Phase 2 snapshot/emit buffers in unroll optimizer

Phase 2 of the unroll optimizer is the last reader of self.phase1_emit_ops,
snapshot_boxes, snapshot_frame_sizes, snapshot_vable_boxes,
snapshot_vref_boxes, snapshot_frame_pcs, and call_pure_results, so hand
them to opt_p2 via std::mem::take instead of clone. The GC re-root
(replace_compile_snapshot_roots over collect_snapshot_const_ptr_slots)
stays after the in-place remap and before optimize, preserving the raw
*mut OpRef slot addresses. trace_inputargs stays a clone (re-read at the
Phase 2 debug_assert and p2_inputarg_types).

Snapshot clone cost measured at ~13us on small traces, so this is not a
warmup lever; the change is a strictly-cheaper, safer form. Mirrors the
InvalidLoop simple_opt move precedent in pyjitpl.rs.

check.py dynasm 187/187, cranelift 187/187.

Assisted-by: Claude

* interp: skip in-place special lookup for exact-builtin numeric lhs

try_inplace_special ran lookup_type_special(lhs, "__iadd__"/…) for every
augmented numeric assignment (total += …), a string-keyed method-cache
probe that interns the dunder name via box_str_constant. int/long/float/
complex/bool define no in-place special, so the lookup is always a miss.

Gate on is_exact_numeric_builtin (is_exact_builtin_instance + numeric
storage): exact numerics skip straight to the binary op. Builtin sequences
(list/bytearray) define __iadd__/__imul__ and subclasses may override the
in-place slot, so both fall through to the lookup unchanged.

Measured on `while i < n: total += i + 1; i += 1` (PYRE_JIT=0, aarch64):
~296-300 -> ~240-257 ns/iter, cumulative from baseline ~700-808 -> ~3.0x.
Parity verified vs py3.14 (JIT on/off): list/bytearray += keep identity,
int/float/bigint/complex += correct, IAddInt/list-subclass __iadd__
overrides dispatch, int += float reflected fall-through.

check.py dynasm 187/187, cranelift 187/187.

Assisted-by: Claude

* interp: cache npure_cellvars on PyCode to avoid per-stack-op recompute

PyFrame::ncells() ran npure_cellvars(code) — an O(cellvars × varnames)
overlap count — on every pop_value/peek_at (twice per binop). The count
is code-invariant, so compute it once in w_code_new_with_hidden_applevel
and store it as a u32 on the PyCode wrapper (u32::MAX sentinel for
null/unaligned code_ptr stubs). PyFrame::ncells() reads it via
w_code_npure_cellvars and adds freevars.len(), falling back to the full
walk for stub frames.

The new field sits after fast_natural_arity, before the cache pointers;
CODE_PTR_OFFSET / CODE_W_GLOBALS_OFFSET (the only JIT-baked offsets) are
unaffected, and a u32 is not a GC-walked reference.

Measured on `while i < n: total += i + 1; i += 1` (PYRE_JIT=0, aarch64):
~240-257 -> ~227-229 ns/iter; npure_cellvars gone from the profile.
Cumulative from baseline ~700-808 -> ~3.2x. Parity verified vs py3.14
including a closure loop (non-empty cellvars).

check.py dynasm 187/187, cranelift 187/187.

Assisted-by: Claude

* object: tag-guard type-probe primitives ahead of tagged-int enablement

py_type_check, ll_isinstance, is_exact_type, and is_exact_builtin_instance
short-circuit on a tagged immediate before the ob_type deref. Gated on
tagged_int::CAN_BE_TAGGED (const false), so the guards fold away and the
deref path is unchanged until enablement.

Assisted-by: Claude

* object/jit: tag-aware GC walker, int maker, and GC config wiring for tagged ints

- is_seq_iter short-circuits on a tagged immediate before the ob_type
  deref, so the GC value-stack walker (walk_raw_seq_iter_roots) never
  dereferences one; the other walk_raw_* predicates route through the
  already-tag-guarded py_type_check/ll_isinstance.
- w_int_new returns a tagged immediate for values that fit, instead of
  allocating; w_int_new_unique documented as never-tag (subclass identity);
  the JIT virtual-reconstruction allocators documented as stay-boxed.
- The GC constructor mirrors CAN_BE_TAGGED into GcConfig.taggedpointers so
  the collector-core immediate guards go live in lockstep with the maker.

All gated on tagged_int::CAN_BE_TAGGED (const false); behavior unchanged
until enablement. dynasm+cranelift 187/187.

Assisted-by: Claude

* objspace: document tagged-int identity in is_w

is_w's int branch already handles tagged immediates without change: two
equal-valued immediates match the ptr::eq fast path, and an immediate vs a
boxed int of the same value compares equal by value through the tag-aware
w_int_get_value. No behavioral change (CAN_BE_TAGGED still false).

Assisted-by: Claude

* majit-wasm: decline compile when host rejects the emitted module

jit_compile_wasm returns handle 0 when the wasm runtime rejects the
emitted module (e.g. a function body exceeding the parser's size limit,
which a trace within trace_limit can still produce after the optimizer
peels/unrolls the loop). Return BackendError::Unsupported in compile_loop
and compile_bridge when compile_module returns 0, so execute_token does
not dispatch dead table slot 0 and leave frame[0] unwritten.

Assisted-by: Claude

* object/interp: tag-guard interpreter raw-deref sites for tagged ints

Eight object→type accessors and dict/slice value-readers dereferenced
(*obj).ob_type / .w_class after only a null check, faulting on a tagged
immediate. A PYRE_JIT=0 flip of CAN_BE_TAGGED=true SIGSEGVs at stdlib
bootstrap through these sites, which the type-probe primitives hardened
earlier do not cover (they route through py_type_check; these do a direct
field read).

Guarded, all gated on CAN_BE_TAGGED (default false, inert):
- typedef::r#type returns gettypefor(&INT_TYPE) for a tagged int
- celldict::unwrap_cell returns the immediate unchanged
- listobject::is_plain_int1 returns true (always an exact int)
- dictmultiobject::key_compares_by_identity / identitydict::is_correct_type
  return false (an int compares by value)
- sliceobject::is_slice returns false
- baseobjspace::object_getattr_miss reads the class via typedef::r#type

typedef::init_type_type and object_getattr_miss error messages now name
the value's type via the tag-safe typedef::r#type instead of a raw
(*obj).ob_type read.

Verified with CAN_BE_TAGGED=true, PYRE_JIT=0: all 177 synth scripts run
crash-free and byte-identical to the boxed build (object-repr addresses
aside). Inert build: check.py 187/187 dynasm + cranelift.

Assisted-by: Claude

* jit-trace: tag-guard is_trace_plain_int trace-entry classifier

is_trace_plain_int dereferences (*obj).w_class after a py_type_check that
already returns true for a tagged immediate, faulting when the trace-entry
value classifier (ConcreteValue::from_pyobj) inspects a tagged int on the
value stack. A tagged immediate is always an exact int, so return true
before the deref. Gated on CAN_BE_TAGGED (default false, inert).

Assisted-by: Claude

* object: tag-guard walk_module_value_slot GC root walker

walk_module_value_slot dereferenced (*w_value).ob_type without the
tagged-immediate skip its sibling unwrap_cell (celldict.rs:206) already
carries. A tagged int in a module-dict value slot faulted the ob_type
read during a minor-GC root walk (walk_pyframe_roots ->
walk_module_dicts_gc -> w_module_dict_walk_gc_cells). Forward the slot
as-is on a tagged immediate. Gated on CAN_BE_TAGGED (default false).

Assisted-by: Claude

* jit-trace: emit primitive-IR tag branch in int unbox paths

The walker and MIFrame int-unbox paths emitted GuardClass +
GetfieldGcI unconditionally, dereferencing the operand header. A
tagged immediate has no header, so those loads fault once ints tag.
Before the class guard, dispatch on the operand's observed concrete:
a tagged immediate emits CastPtrToInt + IntAnd(1) + GuardTrue then
IntRshift(1) arithmetic untag (rtagged.py ll_unboxed_to_int) and skips
the class guard/getfield entirely; a boxed operand emits the same
low-bit test with GuardFalse, then falls through to the unchanged
GuardClass + GetfieldGcI. int_or_bool_unbox_type_descr takes the INT
arm for a tagged immediate before the is_bool header deref. All gated
on CAN_BE_TAGGED (default false); no new opcodes, no backend changes.

Assisted-by: Claude

* jit-trace: box int-binop result as a tagged immediate when it fits (gated CAN_BE_TAGGED)

Assisted-by: Claude

* optimizer: recognize a tagged immediate's class without a header deref (gated taggedpointers)

Add a public `majit_gc::is_tagged_immediate(addr)` free shim delegating
through a new `ACTIVE_IS_TAGGED_IMMEDIATE` thread-local callback, wired
into `ActiveGcGuardHooks` and installed by the dynasm, cranelift, and
wasm backends alongside `check_is_object`. The installed callbacks reach
the active GC's `GcAllocator::is_tagged_immediate`, which MiniMarkGC
answers from `config.taggedpointers && (addr & 1 == 1)` and other
backends default to false.

In `PtrInfo::get_known_class`'s `Constant` arm, return `None` for a
tagged-immediate constant before the `check_is_object` / `cls_of_gcref`
offset-0 reads, so the odd-valued address is not dereferenced as an
object header. Inert while taggedpointers is off.

Assisted-by: Claude

* optimizer: fold the tagged-int box/unbox round-trip IntRshift(IntOr(IntAddOvf(x,x),1),1) -> x (gated taggedpointers)

Add an `IntRshift` arm to `OptRewrite::propagate_forward` that, when the
active GC has `taggedpointers` enabled, folds the box/unbox round-trip
`IntRshift(IntOr(IntAddOvf(x, x), 1), 1)` to `x` via `make_equal_to` +
`Remove`. The arm peels `IntOr` then `IntAddOvf` with `get_producing_op`,
requires both the `IntOr` and `IntRshift` second args to be the constant
`1`, and checks the `IntAddOvf` args with `same_box`. The box ops stay;
only the redundant unbox is removed. Gated on the new
`majit_gc::taggedpointers_enabled()` accessor (probes the installed
`is_tagged_immediate` callback with an odd sentinel), so it returns
`PassOn` and is byte-identical with the config off.

Add `majit_gc::taggedpointers_enabled()` free shim.

Assisted-by: Claude

* jit-trace: tag-guard the int-payload unbox helpers (trace_guarded_int_payload, guard_int_object_value) (gated CAN_BE_TAGGED)

Assisted-by: Claude

* jit-trace: decline the spec_ii tuple and int-storage list-append folds on a tagged-immediate operand (gated CAN_BE_TAGGED)

Assisted-by: Claude

* jit: tag-guard the cls_of_gcref typeptr read for tagged immediates (gated taggedpointers)

Assisted-by: Claude

* cranelift: enable inline stack probes on Windows

x86_64-windows enforces the stack guard page strictly: a trace body whose
prologue moves %rsp past the 4096-byte guard in a single `sub` (deep
nested-FOR_ITER bodies reach 150KB-540KB fixed_frame_storage) skips the guard
page, so the first write below it faults 0xC0000005. Linux/macOS grow the
stack on the fault instead. Enable inline probestack in Compiler::new so the
prologue touches each page.

Scoped to Windows via the runtime `cfg!(windows)` macro rather than a `#[cfg]`
attribute so the flag_builder.set calls compile on every platform; non-Windows
codegen is unchanged (verified cranelift check.py 159/159 on aarch64/x86_64
darwin). probestack_size_log2 defaults to 12 (= the 4096 guard page).

Assisted-by: Claude

* interp: read binop-error operand type names via tag-safe ll_type (gated CAN_BE_TAGGED)

Assisted-by: Claude

* gc: return false from can_move for tagged immediates (gated taggedpointers)

Assisted-by: Claude
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.

JIT: compile_loop pipeline is ~7.6x slower than PyPy per-function warmup — optimizer + codegen dominates

1 participant