Skip to content

Audit RPython parity in OptVirtualize and fix deviations - #5

Merged
youknowone merged 7 commits into
youknowone:mainfrom
lifthrasiir:optvirtualize
May 7, 2026
Merged

Audit RPython parity in OptVirtualize and fix deviations#5
youknowone merged 7 commits into
youknowone:mainfrom
lifthrasiir:optvirtualize

Conversation

@lifthrasiir

@lifthrasiir lifthrasiir commented May 7, 2026

Copy link
Copy Markdown
Contributor

This pull request removes the unused imported_virtual_heads field from the OptContext struct and updates related logic to align with RPython's handling of virtual heads during optimization. It also clarifies how virtualizables are treated in the optimizer, ensuring consistency with RPython's semantics.

Key changes:

Refactoring and Cleanup:

  • Removed the imported_virtual_heads field from OptContext and all related initialization code, as imported virtual heads are now managed differently and no longer require a side table.
  • Updated the logic in Optimizer::import_state to avoid populating imported_virtual_heads, with explanatory comments about how replaying short preambles populates the necessary cache instead.

Behavioral Clarification:

  • Added documentation and an early return in Optimizer::force_box to clarify that "virtualizable" objects should not be treated as true virtuals, preventing destructive state changes and maintaining RPython parity.

Summary by CodeRabbit

  • Optimization & Performance

    • Enhanced virtual object allocation tracking and optimization pipeline
    • Improved cache handling for field access patterns
  • Bug Fixes

    • Fixed virtual allocation forcing logic to prevent state tracking corruption
  • Chores

    • Refactored virtualizable frame initialization and tracking for improved maintainability
    • Updated internal virtual reference modeling and field tracking

lifthrasiir and others added 5 commits May 7, 2026 17:45
Line-by-line parity audit of OptVirtualize against
rpython/jit/metainterp/optimizeopt/virtualize.py.

Fixes:
- Remove MAJIT_PROBE_LIVENESS env-var debug probes (~80 lines of
  conditional eprintln! in setarrayitem_gc / getarrayitem_gc)
- Add InvalidLoop for reading uninitialized virtual array items
  (virtualize.py:282-284, 394-396)
- Add missing make_nonnull calls in arraylen_gc, getarrayitem_gc,
  getinteriorfield_gc, setinteriorfield_gc (virtualize.py:273,287,399,413)
- Add pure_from_args registration in optimize_new_array non-virtual path
  (virtualize.py:220)
- Route GETARRAYITEM_GC_PURE_I/R/F through optimize_getarrayitem_gc
  (virtualize.py:294-296)
- Remove RecordExactClass/RecordExactValueI/RecordExactValueR handlers
  (belong in rewrite.py:376-395, already handled by rewrite.rs)
- Remove optimize_strlen (belong in vstring.py:519-520, already handled
  by vstring.rs)
- Remove dead is_phase2 field (unroll.rs:572 confirms gating was removed)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…trInfo parity)

RPython virtualize.py:123-125 creates vrefs via make_virtual() →
InstancePtrInfo(descr, known_class, is_virtual=True), which forces as
NEW_WITH_VTABLE. Pyre was using VirtualStruct + manual type_tag field,
forcing as bare NEW without vtable.

Changes:
- optimize_virtual_ref: VirtualStruct → Virtual with known_class set
  to VREF_TYPE_TAG (the vtable identity for JitVirtualRef)
- VRefSizeDescr: add vtable() → VREF_TYPE_TAG, is_object() → true
  so NEW_WITH_VTABLE writes the typeptr at offset 0 automatically
- Remove VREF_TYPE_TAG_FIELD_INDEX: typeptr is no longer a tracked
  virtual field (handled by allocation, matching RPython)
- Dense field indices (0, 1) instead of (1, 2): matches RPython's
  all_fielddescrs() which excludes typeptr
- optimize_virtual_ref_finish, optimize_jit_force_virtual: match
  PtrInfo::Virtual instead of VirtualStruct

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…_box

RPython parity: virtualize.py's dispatch table defaults to emit(op) for
calls and other escaping operations. The actual arg forcing happens in
_emit_operation (optimizer.py:623-625), not in the virtualize pass.

Pyre had a redundant optimize_escaping_op in OptVirtualize that manually
forced all virtual args before emitting. This duplicated
Optimizer::emit_operation's force_box path. The only reason it existed
was to skip the virtualizable frame ref — force_box would have destroyed
the Virtualizable PtrInfo via take_ptr_info.

Fix: add Virtualizable guard to Optimizer::force_box so it returns the
frame ref unchanged without taking its PtrInfo. This makes
optimize_escaping_op redundant.

Changes:
- Optimizer::force_box: skip Virtualizable PtrInfo (existing heap object
  with tracked fields, not a deferred allocation)
- Remove OptVirtualize::optimize_escaping_op + force_virtual +
  force_virtualizable + clear_forced_field_caches (-71 lines)
- All call/escaping-op match arms now return PassOn instead of
  optimize_escaping_op(op, ctx)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…hortpreamble replay)

The imported_virtual_heads lookup table in OptVirtualize was a shortcut
that resolved getfield(pool, descr) → imported virtual head directly.

This was redundant: inline_short_preamble already replays the getfield
ops through send_extra_operation, which populates OptHeap's field cache.
The body's getfield then folds naturally through the standard heap cache
path — no side table needed.

Confirmed: all unit tests and all 14 check.py benchmarks pass without
the shortcut on both dynasm and cranelift backends.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
RPython's virtualize.py (OptVirtualize) does NOT track virtualizable
field values — it only removes COND_CALL/CALL for OS_JIT_FORCE_VIRTUALIZABLE.
Field tracking happens in pyjitpl.py's virtualizable_boxes, not in the
optimizer.

Pyre fused ~200 lines of virtualizable field tracking into OptVirtualize.
This is a PRE-EXISTING-ADAPTATION because pyre's tracing model carries
virtualizable fields as trace input args, requiring optimizer-level mapping.

Extract all virtualizable-specific code into a separate VirtualizableTracker
struct to mirror RPython's separation of concerns (virtualizable.py is
separate from virtualize.py):

- VirtualizableTracker owns: config, init, ensure_setup, is_standard_ref,
  array_idx_for_offset, resolve_array_source, mirror_setarrayitem,
  should_passthrough_raw
- OptVirtualize holds: Option<VirtualizableTracker> (was 3 separate fields)
- propagate_forward delegates to VirtualizableTracker methods
- Document convergence path: port RPython's virtualizable_boxes model

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@coderabbitai

coderabbitai Bot commented May 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Rate limit exceeded

@lifthrasiir has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 15 minutes and 47 seconds before requesting another review.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 20095468-492b-460d-ab23-54ad28fe4a4e

📥 Commits

Reviewing files that changed from the base of the PR and between 1f2e752 and 5cfc206.

📒 Files selected for processing (2)
  • majit/majit-metainterp/src/optimizeopt/rewrite.rs
  • majit/majit-metainterp/src/optimizeopt/virtualize.rs

Walkthrough

This PR refactors virtualizable frame tracking in the optimization pass. It introduces VirtualizableTracker to manage state initialization, restructures JitVirtualRef from three tracked fields to a two-field model (virtual_token, forced), and reworks array/field optimizations to directly consume and produce virtual state instead of relying on side-table lookups. The OptContext constructors are updated to reflect struct layout changes.

Changes

Virtualizable Frame Tracking Refactor

Layer / File(s) Summary
Virtual Reference Field Layout
majit/majit-metainterp/src/optimizeopt/virtualize.rs
JitVirtualRef tracked field indices change from three fields (including type tag) to two fields: virtual_token (index 0) and forced (index 1); vtable identity now comes from descriptor.
VirtualizableTracker Infrastructure
majit/majit-metainterp/src/optimizeopt/virtualize.rs
Introduces VirtualizableTracker struct that lazily seeds PtrInfo::Virtualizable during optimization by mapping configured byte offsets to virtual field indices and input typed slots; adds helper methods to determine standard virtualizable ref, map array indices, and mirror Setarrayitem* writes into tracked state.
OptVirtualize Struct Refactor
majit/majit-metainterp/src/optimizeopt/virtualize.rs
Consolidates virtualizable tracking into vable: Option<VirtualizableTracker>; removes public is_phase2 field and prior vable_config/vable_initialized/needs_vable_setup fields; makes all fields private.
Virtualizable Setup Integration
majit/majit-metainterp/src/optimizeopt/virtualize.rs
propagate_forward entry setup calls VirtualizableTracker::ensure_setup() to initialize state on demand.
Array and Field Operation Optimizations
majit/majit-metainterp/src/optimizeopt/virtualize.rs
optimize_setarrayitem_gc removes ops that update existing PtrInfo::VirtualArray slots and mirrors writes into PtrInfo::Virtualizable via tracker; optimize_getarrayitem_gc returns InvalidLoop for out-of-range/uninitialized indices; optimize_getfield_gc, optimize_getinteriorfield_gc, and optimize_setinteriorfield_gc resolve and forward PtrInfo::VirtualArray/VirtualArrayStruct values; ArraylenGc is registered as pure operation.
Virtual Reference Creation and Finalization
majit/majit-metainterp/src/optimizeopt/virtualize.rs
optimize_virtual_ref now creates struct with two-field layout using VRefSizeDescr for vtable; optimize_virtual_ref_finish updates forced and clears virtual_token to null on normal finish; emits SetfieldGc for escaped values.
Optimizer Integration and Context Updates
majit/majit-metainterp/src/optimizeopt/optimizer.rs, majit/majit-metainterp/src/optimizeopt/mod.rs
force_box short-circuits for virtualizable_via_box to preserve tracked state; install_imported_virtuals no longer pushes to side table, relying on short preamble replay; OptContext::new and OptContext::with_num_inputs_and_start_pos initializers updated to reflect struct layout changes.

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A rabbit hops through virtual frames,
Tracking tokens, forced and free,
Arrays mirror, fields align,
Two-field structs dance gracefully,
Optimization flows like spring! 🌱

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: a comprehensive audit of RPython parity in OptVirtualize and correction of identified deviations throughout the optimization pipeline.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

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

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (3)
majit/majit-metainterp/src/optimizeopt/virtualize.rs (3)

169-201: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Length-mismatch between parallel array config vectors silently truncates.

init zips array_field_offsets with array_lengths, so any mismatch in their lengths silently drops entries; array_field_descrs.get(array_idx) then falls back to a synthetic make_field_index_descr, which loses the parent_descr backreference that static_field_descrs/the comment at lines 30–47 emphasizes is required for the force path. Consider asserting array_field_offsets.len() == array_lengths.len() (and ideally == array_item_types.len() == array_field_descrs.len()) at config construction or at the top of init to catch misconfigured VirtualizableConfig builders before the optimizer silently swaps in placeholder descrs.

🛡️ Proposed assertion
     fn init(&mut self, ctx: &mut OptContext) {
         self.initialized = true;
         if ctx.num_inputs() <= 1 {
             return;
         }
+        debug_assert_eq!(
+            self.config.array_field_offsets.len(),
+            self.config.array_lengths.len(),
+            "VirtualizableConfig: array_field_offsets/array_lengths must be parallel",
+        );
+        debug_assert!(
+            self.config.array_field_descrs.is_empty()
+                || self.config.array_field_descrs.len()
+                    == self.config.array_field_offsets.len(),
+            "VirtualizableConfig: array_field_descrs must be empty or parallel to array_field_offsets",
+        );
🤖 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 `@majit/majit-metainterp/src/optimizeopt/virtualize.rs` around lines 169 - 201,
The loop in init that zips self.config.array_field_offsets with
self.config.array_lengths can silently drop entries and cause
array_field_descrs.get(array_idx) to fall back to make_field_index_descr (losing
required parent_descr backrefs); fix by adding an assertion (or explicit
length-check and early error) that self.config.array_field_offsets.len() ==
self.config.array_lengths.len() (and ideally also ==
self.config.array_item_types.len() and == self.config.array_field_descrs.len())
either when constructing VirtualizableConfig or at the top of init in
virtualize.rs so misconfigured VirtualizableConfig builders fail fast instead of
generating placeholder descrs.

664-694: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Negative constant index falls through to mirror_setarrayitem and over-allocates.

When index is a constant negative integer, idx = index as usize becomes ~usize::MAX. The idx < vinfo.items.len() check on the virtual-array branch correctly fails, but execution then drops into the mirror_setarrayitem(array_ref, index, ...) call with the negative i64. Inside, elem_idx = index as usize is huge, and set_array_element performs elems.resize(elem_idx + 1, OpRef::NONE) / vec![OpRef::NONE; elem_idx + 1] (lines 1756 and 1760), which either panics on elem_idx + 1 overflow in debug, or attempts to allocate ~usize::MAX elements in release.

Note that the symmetric optimize_getarrayitem_gc does guard this case (line 707, returns InvalidLoop); the setter path should at minimum reject negatives before mirroring.

🛡️ Proposed fix to skip negative indices on the mirror path
         if let Some(index) = ctx.get_constant_int(index_ref) {
             let idx = index as usize;
             let did_virtual_write = ctx
                 .with_ptr_info_mut(array_ref, |info| {
                     if let PtrInfo::VirtualArray(vinfo) = info {
                         if idx < vinfo.items.len() {
                             vinfo.items[idx] = value_ref;
                             return true;
                         }
                     }
                     false
                 })
                 .unwrap_or(false);
             if did_virtual_write {
                 return OptimizationResult::Remove;
             }
-            if let Some(ref vt) = self.vable {
+            if index >= 0 && let Some(ref vt) = self.vable {
                 vt.mirror_setarrayitem(array_ref, index, value_ref, ctx);
             }
         }

(Adjust split-if form if MSRV does not include the let-chain stabilization; equivalent guard via nested if.)


1042-1063: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Type mismatch breaks optimize_jit_force_virtual — token field written as Ref but read as Int.

VRefFieldDescr::field_type declares VREF_VIRTUAL_TOKEN_FIELD_INDEX as Type::Int (line 1652), and optimize_jit_force_virtual reads it via ctx.get_constant_int(token_ref).is_some_and(|v| v == 0) (line 1121). However, both finish paths write using ctx.emit_constant_ref(majit_ir::GcRef(0)) (lines 1042 and 1061), which creates a Value::Ref-typed constant. Since get_constant_int() only returns Some for Value::Int and returns None for any other type, the read will always fail, and the optimization silently never fires on the normal vref-finish path — the case it is designed to handle.

The comment at lines 1119–1120 explicitly claims the value uses emit_constant_int(0), which contradicts the actual code.

Change the writers to use ctx.emit_constant_int(0) and update the comment, or change the reader to extract the Ref-typed value and adjust the field type accordingly.

🤖 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 `@majit/majit-metainterp/src/optimizeopt/virtualize.rs` around lines 1042 -
1063, The token field VREF_VIRTUAL_TOKEN_FIELD_INDEX is declared as Type::Int
but the code writes a Ref constant (ctx.emit_constant_ref(majit_ir::GcRef(0)))
so get_constant_int in optimize_jit_force_virtual can never see a 0; change the
writers to emit an integer constant using ctx.emit_constant_int(0) for both
places where null_ref is created/set (the earlier early-return path and the
set_token creation), and update the adjacent comment that currently claims
emit_constant_int(0) to reflect the corrected writer; ensure references to
VREF_VIRTUAL_TOKEN_FIELD_INDEX, optimize_jit_force_virtual,
ctx.emit_constant_ref and ctx.emit_constant_int are adjusted accordingly.
🤖 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-metainterp/src/optimizeopt/virtualize.rs`:
- Around line 91-134: The struct field `initialized` on VirtualizableTracker is
unused; remove the `initialized` field and all assignments to it (in
VirtualizableTracker::new, VirtualizableTracker::setup, and
VirtualizableTracker::init) and keep behavior of ensure_setup as-is, or
alternatively if idempotency was intended, change ensure_setup to call init only
when `!self.initialized` and set `self.initialized = true` inside `init`; pick
one approach and update VirtualizableTracker, its new(), setup(), init(), and
ensure_setup() accordingly to eliminate the dead state.

---

Outside diff comments:
In `@majit/majit-metainterp/src/optimizeopt/virtualize.rs`:
- Around line 169-201: The loop in init that zips
self.config.array_field_offsets with self.config.array_lengths can silently drop
entries and cause array_field_descrs.get(array_idx) to fall back to
make_field_index_descr (losing required parent_descr backrefs); fix by adding an
assertion (or explicit length-check and early error) that
self.config.array_field_offsets.len() == self.config.array_lengths.len() (and
ideally also == self.config.array_item_types.len() and ==
self.config.array_field_descrs.len()) either when constructing
VirtualizableConfig or at the top of init in virtualize.rs so misconfigured
VirtualizableConfig builders fail fast instead of generating placeholder descrs.
- Around line 1042-1063: The token field VREF_VIRTUAL_TOKEN_FIELD_INDEX is
declared as Type::Int but the code writes a Ref constant
(ctx.emit_constant_ref(majit_ir::GcRef(0))) so get_constant_int in
optimize_jit_force_virtual can never see a 0; change the writers to emit an
integer constant using ctx.emit_constant_int(0) for both places where null_ref
is created/set (the earlier early-return path and the set_token creation), and
update the adjacent comment that currently claims emit_constant_int(0) to
reflect the corrected writer; ensure references to
VREF_VIRTUAL_TOKEN_FIELD_INDEX, optimize_jit_force_virtual,
ctx.emit_constant_ref and ctx.emit_constant_int are adjusted accordingly.
🪄 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: b5a35005-1206-42e7-bc82-cf07b1c9d740

📥 Commits

Reviewing files that changed from the base of the PR and between 64aa654 and 1f2e752.

📒 Files selected for processing (3)
  • majit/majit-metainterp/src/optimizeopt/mod.rs
  • majit/majit-metainterp/src/optimizeopt/optimizer.rs
  • majit/majit-metainterp/src/optimizeopt/virtualize.rs

Comment thread majit/majit-metainterp/src/optimizeopt/virtualize.rs Outdated
Tighten InvalidLoop checks for virtual array reads to match RPython:

- optimize_getarrayitem_gc: raise InvalidLoop for negative or
  out-of-range constant index on virtual arrays, not just for
  uninitialized slots. Matches info.py:580-582 getitem() which
  returns None for `index < 0 or index >= len(self._items)`.

- optimize_getinteriorfield_gc: same — raise InvalidLoop for negative
  or out-of-range element index on virtual array-of-structs. Matches
  info.py:651-656 _compute_index() which returns -1 for
  `index < 0 or index >= self.length`.

Previously these cases silently fell through to PassOn, letting the
backend emit a read that RPython would have rejected as InvalidLoop.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@youknowone

Copy link
Copy Markdown
Owner

generated review:

 - RECORD_EXACT_VALUE_I/R optimization was removed.
    Current majit/majit-metainterp/src/optimizeopt/virtualize.rs:1430 says
    RECORD_EXACT_CLASS / RECORD_EXACT_VALUE_* are handled by OptRewrite, but
    actual majit/majit-metainterp/src/optimizeopt/rewrite.rs:2878 only handles
    RecordExactClass; it has no RecordExactValueI/R handling. RPython handles
    this in rpython/jit/metainterp/optimizeopt/rewrite.py:388 via
    optimize_record_exact_value, making the box constant to the expected const.
    main still had behavior for RecordExactValueI/R, even though it lived in
    OptVirtualize rather than the RPython-matching pass, so this patch is a
    parity regression relative to main.

…395 parity)

RecordExactValueI/R was previously handled in OptVirtualize (wrong pass).
The earlier parity audit removed it from virtualize.rs but failed to add
the counterpart in rewrite.rs, causing a parity regression: the ops
would fall through to the default PassOn and be silently dropped.

Add the handler in rewrite.rs matching RPython rewrite.py:388-395
optimize_record_exact_value: make_constant(box, expectedconstbox).

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@lifthrasiir

Copy link
Copy Markdown
Contributor Author

generated review:

 - RECORD_EXACT_VALUE_I/R optimization was removed.
    Current majit/majit-metainterp/src/optimizeopt/virtualize.rs:1430 says
    RECORD_EXACT_CLASS / RECORD_EXACT_VALUE_* are handled by OptRewrite, but
    actual majit/majit-metainterp/src/optimizeopt/rewrite.rs:2878 only handles
    RecordExactClass; it has no RecordExactValueI/R handling. RPython handles
    this in rpython/jit/metainterp/optimizeopt/rewrite.py:388 via
    optimize_record_exact_value, making the box constant to the expected const.
    main still had behavior for RecordExactValueI/R, even though it lived in
    OptVirtualize rather than the RPython-matching pass, so this patch is a
    parity regression relative to main.

Fixed in 5cfc206.

@youknowone
youknowone merged commit 2f2246a into youknowone:main May 7, 2026
17 of 24 checks passed
youknowone added a commit that referenced this pull request May 11, 2026
#13: Fix bridge knowledge lost by setup() — store as pending, apply after setup
#12: Add OpRef remapping for bridge knowledge using guard fail_args
#14: Set PtrInfo fields when deserializing bridge knowledge
#3: Add UNKNOWN_ALIAS force in getfield_from_cache (heap.py:109-111)
#4: Add post-force recheck in do_setfield (heap.py:84-101)
#5: Skip invalidate for is_always_pure fields (heap.py:189-191)
#6: Check postponed_op before emit in force_lazy_set (heap.py:131-135)
youknowone added a commit that referenced this pull request May 11, 2026
…ng_from position, original_boxes

#1 ResumeAtPositionDescr + rd_resume_position:
- Op: add rd_resume_position field (resoperation.py GuardResOp parity)
- Descr trait: add is_resume_at_position() marker method
- fail_descr.rs: add ResumeAtPositionDescr struct
- majit-opt/lib.rs: add OptResumeAtPositionDescr + make_resume_at_position_descr()
- unroll.rs: extra_guards and short preamble guards now copy rd_resume_position
  from patchguardop and set ResumeAtPositionDescr as descr (instead of
  copying descr wholesale)
- compile_bridge: inline_short_preamble = !fail_descr.is_resume_at_position()

#2 retracing_from → TracePosition:
- retracing_from changed from Option<u64> to Option<TracePosition>
- retrace_needed: self.retracing_from = self.potential_retrace_position
- close_and_compile retrace branch: compare merge point position against
  retracing_from (not green_key), matching pyjitpl.py:2994
- compile_retrace: extract green_key from tracing ctx instead of retracing_from

#5 MergePoint.original_boxes:
- MergePoint: add original_boxes: Vec<OpRef> field
- add_merge_point: takes live_args parameter
- Constructors: initialize with inputarg OpRefs
- pyre-jit: pass close_loop_args result to add_merge_point
youknowone added a commit that referenced this pull request May 11, 2026
#3 rawptrinfo: Add BoxEnv trait with is_virtual_ref() and is_virtual_raw()
   matching RPython's getptrinfo(box).is_virtual() and
   getrawptrinfo(box).is_virtual() type-dispatched virtual checks.

#4 NONE handling: Removed — _number_boxes no longer handles OpRef::NONE.
   RPython snapshots never contain null boxes. Callers must filter.

#5 Constant identification: BoxEnv::is_const() replaces HashMap lookup.
   RPython uses isinstance(box, Const); we use a trait method that
   implementations can back with OpRef range checks or HashMap.

#7/#9 Multi-frame + frame boundaries: Snapshot struct with framestack
   (Vec<SnapshotFrame>). Each frame records jitcode_index, pc, boxes.
   number() serializes slot_count per frame for rebuild_from_numbering
   to correctly split tagged values across frames.

SimpleBoxEnv: test implementation with constants/replacements/types/
virtuals HashMaps. Production BoxEnv will be implemented by OptContext.

5 tests including multi-frame roundtrip.
youknowone added a commit that referenced this pull request May 11, 2026
Issue #3 (high) — drop ImplTrait → dyn handling.  Rust `impl Trait`
is a static opaque type; the compiler monomorphizes each call site to
a single concrete impl.  RPython `indirect_call` is reserved for
runtime polymorphic callees (`rpython/jit/codewriter/call.py:103
graphs_from`).  type_root_ident / extract_dyn_trait_root_with_context
no longer treat ImplTrait as a trait object so method calls on `impl
Trait` parameters lower to CallTarget::Method.  full_type_string and
qualified_full_type_string still render the bound name for type-string
identity, but without the misleading `dyn ` prefix.

Issue #5 (medium) — receiver detection for field / index / wrapper
returns.  dyn_trait_root_for_receiver gains:

  * Expr::Field — `self.handler.run()` now resolves the field's
    declared type via struct_fields and recognizes `dyn T` /
    `Box<dyn T>` etc.
  * Expr::Index — `handlers[i].run()` reads the indexed local's full
    type from local_array_types, strips the container with the
    existing extract_element_type_from_str helper, then runs the dyn
    check on the element.
  * Wrapper return types — chained `make_boxed().run()` and
    `obj.foo().bar()` now accept `Box<dyn T>` / `Rc<dyn T>` /
    `Arc<dyn T>` returns in addition to plain `dyn T`.

A new `dyn_trait_root_from_type_str` helper centralises the wrapper /
prefix recognition so all three paths share the same parser.

Tests cover impl_trait_param_does_not_lower_to_indirect_call (Issue
#3) and dyn_receiver_via_field_index_and_box_return (Issue #5,
self.handler / handlers[i] / make_boxed()).
youknowone added a commit that referenced this pull request May 11, 2026
…ase 4 + P5.1)

model.rs: scaffold SomeValue enum + SomeObjectTrait + primitive Some*
variants (SomeInteger/Bool/Float/Char/UnicodeCodePoint/String); port
SomeList/Tuple/Dict/Iterator; port SomeInstance/PBC/Builtin/None/
Exception/WeakRef/TypeOf + s_None/s_ImpossibleValue singletons; port
union() / unionof / contains 2D pair dispatch including Instance,
Exception, PBC, WeakRef pairs; port ClassDef stub + SomeInstance flags
dict. Covers rpython/annotator/model.py ~855 LOC. Test port:
tests/test_annotator_model.py subset.

listdef.rs / dictdef.rs / exception.rs: port rpython/annotator/listdef.py
+ dictdef.py + exception.py; ListDef/DictDef read_item/generalize/agree
land alongside fail-fast r_dict semantics (Phase 5 P5.1).

flowspace/operation.rs: fix GetAttr.pure() misclassification + document
constfold gaps surfaced by parity review #5+#6.
youknowone pushed a commit that referenced this pull request May 11, 2026
* optimizeopt/virtualize: RPython parity audit — fix 8 deviations

Line-by-line parity audit of OptVirtualize against
rpython/jit/metainterp/optimizeopt/virtualize.py.

Fixes:
- Remove MAJIT_PROBE_LIVENESS env-var debug probes (~80 lines of
  conditional eprintln! in setarrayitem_gc / getarrayitem_gc)
- Add InvalidLoop for reading uninitialized virtual array items
  (virtualize.py:282-284, 394-396)
- Add missing make_nonnull calls in arraylen_gc, getarrayitem_gc,
  getinteriorfield_gc, setinteriorfield_gc (virtualize.py:273,287,399,413)
- Add pure_from_args registration in optimize_new_array non-virtual path
  (virtualize.py:220)
- Route GETARRAYITEM_GC_PURE_I/R/F through optimize_getarrayitem_gc
  (virtualize.py:294-296)
- Remove RecordExactClass/RecordExactValueI/RecordExactValueR handlers
  (belong in rewrite.py:376-395, already handled by rewrite.rs)
- Remove optimize_strlen (belong in vstring.py:519-520, already handled
  by vstring.rs)
- Remove dead is_phase2 field (unroll.rs:572 confirms gating was removed)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* optimizeopt/virtualize: vref uses PtrInfo::Virtual (RPython InstancePtrInfo parity)

RPython virtualize.py:123-125 creates vrefs via make_virtual() →
InstancePtrInfo(descr, known_class, is_virtual=True), which forces as
NEW_WITH_VTABLE. Pyre was using VirtualStruct + manual type_tag field,
forcing as bare NEW without vtable.

Changes:
- optimize_virtual_ref: VirtualStruct → Virtual with known_class set
  to VREF_TYPE_TAG (the vtable identity for JitVirtualRef)
- VRefSizeDescr: add vtable() → VREF_TYPE_TAG, is_object() → true
  so NEW_WITH_VTABLE writes the typeptr at offset 0 automatically
- Remove VREF_TYPE_TAG_FIELD_INDEX: typeptr is no longer a tracked
  virtual field (handled by allocation, matching RPython)
- Dense field indices (0, 1) instead of (1, 2): matches RPython's
  all_fielddescrs() which excludes typeptr
- optimize_virtual_ref_finish, optimize_jit_force_virtual: match
  PtrInfo::Virtual instead of VirtualStruct

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* optimizeopt: remove optimize_escaping_op, skip Virtualizable in force_box

RPython parity: virtualize.py's dispatch table defaults to emit(op) for
calls and other escaping operations. The actual arg forcing happens in
_emit_operation (optimizer.py:623-625), not in the virtualize pass.

Pyre had a redundant optimize_escaping_op in OptVirtualize that manually
forced all virtual args before emitting. This duplicated
Optimizer::emit_operation's force_box path. The only reason it existed
was to skip the virtualizable frame ref — force_box would have destroyed
the Virtualizable PtrInfo via take_ptr_info.

Fix: add Virtualizable guard to Optimizer::force_box so it returns the
frame ref unchanged without taking its PtrInfo. This makes
optimize_escaping_op redundant.

Changes:
- Optimizer::force_box: skip Virtualizable PtrInfo (existing heap object
  with tracked fields, not a deferred allocation)
- Remove OptVirtualize::optimize_escaping_op + force_virtual +
  force_virtualizable + clear_forced_field_caches (-71 lines)
- All call/escaping-op match arms now return PassOn instead of
  optimize_escaping_op(op, ctx)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* optimizeopt: remove imported_virtual_heads shortcut (redundant with shortpreamble replay)

The imported_virtual_heads lookup table in OptVirtualize was a shortcut
that resolved getfield(pool, descr) → imported virtual head directly.

This was redundant: inline_short_preamble already replays the getfield
ops through send_extra_operation, which populates OptHeap's field cache.
The body's getfield then folds naturally through the standard heap cache
path — no side table needed.

Confirmed: all unit tests and all 14 check.py benchmarks pass without
the shortcut on both dynasm and cranelift backends.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* optimizeopt/virtualize: extract VirtualizableTracker from OptVirtualize

RPython's virtualize.py (OptVirtualize) does NOT track virtualizable
field values — it only removes COND_CALL/CALL for OS_JIT_FORCE_VIRTUALIZABLE.
Field tracking happens in pyjitpl.py's virtualizable_boxes, not in the
optimizer.

Pyre fused ~200 lines of virtualizable field tracking into OptVirtualize.
This is a PRE-EXISTING-ADAPTATION because pyre's tracing model carries
virtualizable fields as trace input args, requiring optimizer-level mapping.

Extract all virtualizable-specific code into a separate VirtualizableTracker
struct to mirror RPython's separation of concerns (virtualizable.py is
separate from virtualize.py):

- VirtualizableTracker owns: config, init, ensure_setup, is_standard_ref,
  array_idx_for_offset, resolve_array_source, mirror_setarrayitem,
  should_passthrough_raw
- OptVirtualize holds: Option<VirtualizableTracker> (was 3 separate fields)
- propagate_forward delegates to VirtualizableTracker methods
- Document convergence path: port RPython's virtualizable_boxes model

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* optimizeopt/virtualize: RPython parity audit — fix 8 deviations

Tighten InvalidLoop checks for virtual array reads to match RPython:

- optimize_getarrayitem_gc: raise InvalidLoop for negative or
  out-of-range constant index on virtual arrays, not just for
  uninitialized slots. Matches info.py:580-582 getitem() which
  returns None for `index < 0 or index >= len(self._items)`.

- optimize_getinteriorfield_gc: same — raise InvalidLoop for negative
  or out-of-range element index on virtual array-of-structs. Matches
  info.py:651-656 _compute_index() which returns -1 for
  `index < 0 or index >= self.length`.

Previously these cases silently fell through to PassOn, letting the
backend emit a read that RPython would have rejected as InvalidLoop.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* optimizeopt/rewrite: add RecordExactValueI/R handler (rewrite.py:388-395 parity)

RecordExactValueI/R was previously handled in OptVirtualize (wrong pass).
The earlier parity audit removed it from virtualize.rs but failed to add
the counterpart in rewrite.rs, causing a parity regression: the ops
would fall through to the default PassOn and be silently dropped.

Add the handler in rewrite.rs matching RPython rewrite.py:388-395
optimize_record_exact_value: make_constant(box, expectedconstbox).

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
youknowone added a commit that referenced this pull request May 24, 2026
…riant test

The synthetic stub pygraph built by `build_stub_pygraph_for_unsafe_fn`
returns a single default-`Constant` link.  Reviewer round 2026-05-24
item #5 flagged the concern that if this graph reaches `make_jitcodes`
as an executable JITCode, the default-Constant body would run instead
of the actual Rust unsafe-fn helper.

Audit result: the stub is annotator-only and cannot reach the
codewriter.  `register_unsafe_fn_stubs` writes to `PyreCallRegistry`
only — `CallControl::function_graphs` is populated exclusively by
safe-fn `build_flow` output (`build_flow.rs:215` rejects unsafe
bodies).  `CallControl::find_all_graphs`' BFS walks
`function_graphs.keys()` and resolves each call target via
`target_to_path_and_graph` (`jit_codewriter/call.rs:2601`); a path
absent from `function_graphs` returns `None` and the BFS `continue`s
past the call, so the unsafe stub never enters `candidate_graphs`
and never reaches `transform_graph_to_jitcode`.

This commit:
- extends the doc on `register_unsafe_fn_stubs` with the layering
  argument that closes the audit;
- adds
  `register_unsafe_fn_stubs_does_not_populate_callcontrol_function_graphs`
  which mirrors the production call shape (registry +
  `callcontrol.unsafe_fn_stubs`) and asserts the path is reachable
  via the registry but absent from `function_graphs` /
  `candidate_graphs`.
youknowone added a commit that referenced this pull request May 24, 2026
…riant test

The synthetic stub pygraph built by `build_stub_pygraph_for_unsafe_fn`
returns a single default-`Constant` link.  Reviewer round 2026-05-24
item #5 flagged the concern that if this graph reaches `make_jitcodes`
as an executable JITCode, the default-Constant body would run instead
of the actual Rust unsafe-fn helper.

Audit result: the stub is annotator-only and cannot reach the
codewriter.  `register_unsafe_fn_stubs` writes to `PyreCallRegistry`
only — `CallControl::function_graphs` is populated exclusively by
safe-fn `build_flow` output (`build_flow.rs:215` rejects unsafe
bodies).  `CallControl::find_all_graphs`' BFS walks
`function_graphs.keys()` and resolves each call target via
`target_to_path_and_graph` (`jit_codewriter/call.rs:2601`); a path
absent from `function_graphs` returns `None` and the BFS `continue`s
past the call, so the unsafe stub never enters `candidate_graphs`
and never reaches `transform_graph_to_jitcode`.

This commit:
- extends the doc on `register_unsafe_fn_stubs` with the layering
  argument that closes the audit;
- adds
  `register_unsafe_fn_stubs_does_not_populate_callcontrol_function_graphs`
  which mirrors the production call shape (registry +
  `callcontrol.unsafe_fn_stubs`) and asserts the path is reachable
  via the registry but absent from `function_graphs` /
  `candidate_graphs`.
youknowone added a commit that referenced this pull request May 24, 2026
…riant test

The synthetic stub pygraph built by `build_stub_pygraph_for_unsafe_fn`
returns a single default-`Constant` link.  Reviewer round 2026-05-24
item #5 flagged the concern that if this graph reaches `make_jitcodes`
as an executable JITCode, the default-Constant body would run instead
of the actual Rust unsafe-fn helper.

Audit result: the stub is annotator-only and cannot reach the
codewriter.  `register_unsafe_fn_stubs` writes to `PyreCallRegistry`
only — `CallControl::function_graphs` is populated exclusively by
safe-fn `build_flow` output (`build_flow.rs:215` rejects unsafe
bodies).  `CallControl::find_all_graphs`' BFS walks
`function_graphs.keys()` and resolves each call target via
`target_to_path_and_graph` (`jit_codewriter/call.rs:2601`); a path
absent from `function_graphs` returns `None` and the BFS `continue`s
past the call, so the unsafe stub never enters `candidate_graphs`
and never reaches `transform_graph_to_jitcode`.

This commit:
- extends the doc on `register_unsafe_fn_stubs` with the layering
  argument that closes the audit;
- adds
  `register_unsafe_fn_stubs_does_not_populate_callcontrol_function_graphs`
  which mirrors the production call shape (registry +
  `callcontrol.unsafe_fn_stubs`) and asserts the path is reachable
  via the registry but absent from `function_graphs` /
  `candidate_graphs`.
youknowone added a commit that referenced this pull request May 25, 2026
…riant test

The synthetic stub pygraph built by `build_stub_pygraph_for_unsafe_fn`
returns a single default-`Constant` link.  Reviewer round 2026-05-24
item #5 flagged the concern that if this graph reaches `make_jitcodes`
as an executable JITCode, the default-Constant body would run instead
of the actual Rust unsafe-fn helper.

Audit result: the stub is annotator-only and cannot reach the
codewriter.  `register_unsafe_fn_stubs` writes to `PyreCallRegistry`
only — `CallControl::function_graphs` is populated exclusively by
safe-fn `build_flow` output (`build_flow.rs:215` rejects unsafe
bodies).  `CallControl::find_all_graphs`' BFS walks
`function_graphs.keys()` and resolves each call target via
`target_to_path_and_graph` (`jit_codewriter/call.rs:2601`); a path
absent from `function_graphs` returns `None` and the BFS `continue`s
past the call, so the unsafe stub never enters `candidate_graphs`
and never reaches `transform_graph_to_jitcode`.

This commit:
- extends the doc on `register_unsafe_fn_stubs` with the layering
  argument that closes the audit;
- adds
  `register_unsafe_fn_stubs_does_not_populate_callcontrol_function_graphs`
  which mirrors the production call shape (registry +
  `callcontrol.unsafe_fn_stubs`) and asserts the path is reachable
  via the registry but absent from `function_graphs` /
  `candidate_graphs`.
youknowone added a commit that referenced this pull request May 25, 2026
…riant test

The synthetic stub pygraph built by `build_stub_pygraph_for_unsafe_fn`
returns a single default-`Constant` link.  Reviewer round 2026-05-24
item #5 flagged the concern that if this graph reaches `make_jitcodes`
as an executable JITCode, the default-Constant body would run instead
of the actual Rust unsafe-fn helper.

Audit result: the stub is annotator-only and cannot reach the
codewriter.  `register_unsafe_fn_stubs` writes to `PyreCallRegistry`
only — `CallControl::function_graphs` is populated exclusively by
safe-fn `build_flow` output (`build_flow.rs:215` rejects unsafe
bodies).  `CallControl::find_all_graphs`' BFS walks
`function_graphs.keys()` and resolves each call target via
`target_to_path_and_graph` (`jit_codewriter/call.rs:2601`); a path
absent from `function_graphs` returns `None` and the BFS `continue`s
past the call, so the unsafe stub never enters `candidate_graphs`
and never reaches `transform_graph_to_jitcode`.

This commit:
- extends the doc on `register_unsafe_fn_stubs` with the layering
  argument that closes the audit;
- adds
  `register_unsafe_fn_stubs_does_not_populate_callcontrol_function_graphs`
  which mirrors the production call shape (registry +
  `callcontrol.unsafe_fn_stubs`) and asserts the path is reachable
  via the registry but absent from `function_graphs` /
  `candidate_graphs`.
youknowone added a commit that referenced this pull request May 25, 2026
…riant test

The synthetic stub pygraph built by `build_stub_pygraph_for_unsafe_fn`
returns a single default-`Constant` link.  Reviewer round 2026-05-24
item #5 flagged the concern that if this graph reaches `make_jitcodes`
as an executable JITCode, the default-Constant body would run instead
of the actual Rust unsafe-fn helper.

Audit result: the stub is annotator-only and cannot reach the
codewriter.  `register_unsafe_fn_stubs` writes to `PyreCallRegistry`
only — `CallControl::function_graphs` is populated exclusively by
safe-fn `build_flow` output (`build_flow.rs:215` rejects unsafe
bodies).  `CallControl::find_all_graphs`' BFS walks
`function_graphs.keys()` and resolves each call target via
`target_to_path_and_graph` (`jit_codewriter/call.rs:2601`); a path
absent from `function_graphs` returns `None` and the BFS `continue`s
past the call, so the unsafe stub never enters `candidate_graphs`
and never reaches `transform_graph_to_jitcode`.

This commit:
- extends the doc on `register_unsafe_fn_stubs` with the layering
  argument that closes the audit;
- adds
  `register_unsafe_fn_stubs_does_not_populate_callcontrol_function_graphs`
  which mirrors the production call shape (registry +
  `callcontrol.unsafe_fn_stubs`) and asserts the path is reachable
  via the registry but absent from `function_graphs` /
  `candidate_graphs`.
youknowone added a commit that referenced this pull request May 25, 2026
…riant test

The synthetic stub pygraph built by `build_stub_pygraph_for_unsafe_fn`
returns a single default-`Constant` link.  Reviewer round 2026-05-24
item #5 flagged the concern that if this graph reaches `make_jitcodes`
as an executable JITCode, the default-Constant body would run instead
of the actual Rust unsafe-fn helper.

Audit result: the stub is annotator-only and cannot reach the
codewriter.  `register_unsafe_fn_stubs` writes to `PyreCallRegistry`
only — `CallControl::function_graphs` is populated exclusively by
safe-fn `build_flow` output (`build_flow.rs:215` rejects unsafe
bodies).  `CallControl::find_all_graphs`' BFS walks
`function_graphs.keys()` and resolves each call target via
`target_to_path_and_graph` (`jit_codewriter/call.rs:2601`); a path
absent from `function_graphs` returns `None` and the BFS `continue`s
past the call, so the unsafe stub never enters `candidate_graphs`
and never reaches `transform_graph_to_jitcode`.

This commit:
- extends the doc on `register_unsafe_fn_stubs` with the layering
  argument that closes the audit;
- adds
  `register_unsafe_fn_stubs_does_not_populate_callcontrol_function_graphs`
  which mirrors the production call shape (registry +
  `callcontrol.unsafe_fn_stubs`) and asserts the path is reachable
  via the registry but absent from `function_graphs` /
  `candidate_graphs`.
youknowone added a commit that referenced this pull request May 26, 2026
…riant test

The synthetic stub pygraph built by `build_stub_pygraph_for_unsafe_fn`
returns a single default-`Constant` link.  Reviewer round 2026-05-24
item #5 flagged the concern that if this graph reaches `make_jitcodes`
as an executable JITCode, the default-Constant body would run instead
of the actual Rust unsafe-fn helper.

Audit result: the stub is annotator-only and cannot reach the
codewriter.  `register_unsafe_fn_stubs` writes to `PyreCallRegistry`
only — `CallControl::function_graphs` is populated exclusively by
safe-fn `build_flow` output (`build_flow.rs:215` rejects unsafe
bodies).  `CallControl::find_all_graphs`' BFS walks
`function_graphs.keys()` and resolves each call target via
`target_to_path_and_graph` (`jit_codewriter/call.rs:2601`); a path
absent from `function_graphs` returns `None` and the BFS `continue`s
past the call, so the unsafe stub never enters `candidate_graphs`
and never reaches `transform_graph_to_jitcode`.

This commit:
- extends the doc on `register_unsafe_fn_stubs` with the layering
  argument that closes the audit;
- adds
  `register_unsafe_fn_stubs_does_not_populate_callcontrol_function_graphs`
  which mirrors the production call shape (registry +
  `callcontrol.unsafe_fn_stubs`) and asserts the path is reachable
  via the registry but absent from `function_graphs` /
  `candidate_graphs`.
youknowone added a commit that referenced this pull request May 26, 2026
…riant test

The synthetic stub pygraph built by `build_stub_pygraph_for_unsafe_fn`
returns a single default-`Constant` link.  Reviewer round 2026-05-24
item #5 flagged the concern that if this graph reaches `make_jitcodes`
as an executable JITCode, the default-Constant body would run instead
of the actual Rust unsafe-fn helper.

Audit result: the stub is annotator-only and cannot reach the
codewriter.  `register_unsafe_fn_stubs` writes to `PyreCallRegistry`
only — `CallControl::function_graphs` is populated exclusively by
safe-fn `build_flow` output (`build_flow.rs:215` rejects unsafe
bodies).  `CallControl::find_all_graphs`' BFS walks
`function_graphs.keys()` and resolves each call target via
`target_to_path_and_graph` (`jit_codewriter/call.rs:2601`); a path
absent from `function_graphs` returns `None` and the BFS `continue`s
past the call, so the unsafe stub never enters `candidate_graphs`
and never reaches `transform_graph_to_jitcode`.

This commit:
- extends the doc on `register_unsafe_fn_stubs` with the layering
  argument that closes the audit;
- adds
  `register_unsafe_fn_stubs_does_not_populate_callcontrol_function_graphs`
  which mirrors the production call shape (registry +
  `callcontrol.unsafe_fn_stubs`) and asserts the path is reachable
  via the registry but absent from `function_graphs` /
  `candidate_graphs`.
youknowone added a commit that referenced this pull request May 27, 2026
…riant test

The synthetic stub pygraph built by `build_stub_pygraph_for_unsafe_fn`
returns a single default-`Constant` link.  Reviewer round 2026-05-24
item #5 flagged the concern that if this graph reaches `make_jitcodes`
as an executable JITCode, the default-Constant body would run instead
of the actual Rust unsafe-fn helper.

Audit result: the stub is annotator-only and cannot reach the
codewriter.  `register_unsafe_fn_stubs` writes to `PyreCallRegistry`
only — `CallControl::function_graphs` is populated exclusively by
safe-fn `build_flow` output (`build_flow.rs:215` rejects unsafe
bodies).  `CallControl::find_all_graphs`' BFS walks
`function_graphs.keys()` and resolves each call target via
`target_to_path_and_graph` (`jit_codewriter/call.rs:2601`); a path
absent from `function_graphs` returns `None` and the BFS `continue`s
past the call, so the unsafe stub never enters `candidate_graphs`
and never reaches `transform_graph_to_jitcode`.

This commit:
- extends the doc on `register_unsafe_fn_stubs` with the layering
  argument that closes the audit;
- adds
  `register_unsafe_fn_stubs_does_not_populate_callcontrol_function_graphs`
  which mirrors the production call shape (registry +
  `callcontrol.unsafe_fn_stubs`) and asserts the path is reachable
  via the registry but absent from `function_graphs` /
  `candidate_graphs`.
youknowone added a commit that referenced this pull request May 27, 2026
…riant test

The synthetic stub pygraph built by `build_stub_pygraph_for_unsafe_fn`
returns a single default-`Constant` link.  Reviewer round 2026-05-24
item #5 flagged the concern that if this graph reaches `make_jitcodes`
as an executable JITCode, the default-Constant body would run instead
of the actual Rust unsafe-fn helper.

Audit result: the stub is annotator-only and cannot reach the
codewriter.  `register_unsafe_fn_stubs` writes to `PyreCallRegistry`
only — `CallControl::function_graphs` is populated exclusively by
safe-fn `build_flow` output (`build_flow.rs:215` rejects unsafe
bodies).  `CallControl::find_all_graphs`' BFS walks
`function_graphs.keys()` and resolves each call target via
`target_to_path_and_graph` (`jit_codewriter/call.rs:2601`); a path
absent from `function_graphs` returns `None` and the BFS `continue`s
past the call, so the unsafe stub never enters `candidate_graphs`
and never reaches `transform_graph_to_jitcode`.

This commit:
- extends the doc on `register_unsafe_fn_stubs` with the layering
  argument that closes the audit;
- adds
  `register_unsafe_fn_stubs_does_not_populate_callcontrol_function_graphs`
  which mirrors the production call shape (registry +
  `callcontrol.unsafe_fn_stubs`) and asserts the path is reachable
  via the registry but absent from `function_graphs` /
  `candidate_graphs`.
youknowone added a commit that referenced this pull request May 28, 2026
…riant test

The synthetic stub pygraph built by `build_stub_pygraph_for_unsafe_fn`
returns a single default-`Constant` link.  Reviewer round 2026-05-24
item #5 flagged the concern that if this graph reaches `make_jitcodes`
as an executable JITCode, the default-Constant body would run instead
of the actual Rust unsafe-fn helper.

Audit result: the stub is annotator-only and cannot reach the
codewriter.  `register_unsafe_fn_stubs` writes to `PyreCallRegistry`
only — `CallControl::function_graphs` is populated exclusively by
safe-fn `build_flow` output (`build_flow.rs:215` rejects unsafe
bodies).  `CallControl::find_all_graphs`' BFS walks
`function_graphs.keys()` and resolves each call target via
`target_to_path_and_graph` (`jit_codewriter/call.rs:2601`); a path
absent from `function_graphs` returns `None` and the BFS `continue`s
past the call, so the unsafe stub never enters `candidate_graphs`
and never reaches `transform_graph_to_jitcode`.

This commit:
- extends the doc on `register_unsafe_fn_stubs` with the layering
  argument that closes the audit;
- adds
  `register_unsafe_fn_stubs_does_not_populate_callcontrol_function_graphs`
  which mirrors the production call shape (registry +
  `callcontrol.unsafe_fn_stubs`) and asserts the path is reachable
  via the registry but absent from `function_graphs` /
  `candidate_graphs`.
youknowone added a commit that referenced this pull request May 29, 2026
…riant test

The synthetic stub pygraph built by `build_stub_pygraph_for_unsafe_fn`
returns a single default-`Constant` link.  Reviewer round 2026-05-24
item #5 flagged the concern that if this graph reaches `make_jitcodes`
as an executable JITCode, the default-Constant body would run instead
of the actual Rust unsafe-fn helper.

Audit result: the stub is annotator-only and cannot reach the
codewriter.  `register_unsafe_fn_stubs` writes to `PyreCallRegistry`
only — `CallControl::function_graphs` is populated exclusively by
safe-fn `build_flow` output (`build_flow.rs:215` rejects unsafe
bodies).  `CallControl::find_all_graphs`' BFS walks
`function_graphs.keys()` and resolves each call target via
`target_to_path_and_graph` (`jit_codewriter/call.rs:2601`); a path
absent from `function_graphs` returns `None` and the BFS `continue`s
past the call, so the unsafe stub never enters `candidate_graphs`
and never reaches `transform_graph_to_jitcode`.

This commit:
- extends the doc on `register_unsafe_fn_stubs` with the layering
  argument that closes the audit;
- adds
  `register_unsafe_fn_stubs_does_not_populate_callcontrol_function_graphs`
  which mirrors the production call shape (registry +
  `callcontrol.unsafe_fn_stubs`) and asserts the path is reachable
  via the registry but absent from `function_graphs` /
  `candidate_graphs`.
youknowone added a commit that referenced this pull request May 29, 2026
…riant test

The synthetic stub pygraph built by `build_stub_pygraph_for_unsafe_fn`
returns a single default-`Constant` link.  Reviewer round 2026-05-24
item #5 flagged the concern that if this graph reaches `make_jitcodes`
as an executable JITCode, the default-Constant body would run instead
of the actual Rust unsafe-fn helper.

Audit result: the stub is annotator-only and cannot reach the
codewriter.  `register_unsafe_fn_stubs` writes to `PyreCallRegistry`
only — `CallControl::function_graphs` is populated exclusively by
safe-fn `build_flow` output (`build_flow.rs:215` rejects unsafe
bodies).  `CallControl::find_all_graphs`' BFS walks
`function_graphs.keys()` and resolves each call target via
`target_to_path_and_graph` (`jit_codewriter/call.rs:2601`); a path
absent from `function_graphs` returns `None` and the BFS `continue`s
past the call, so the unsafe stub never enters `candidate_graphs`
and never reaches `transform_graph_to_jitcode`.

This commit:
- extends the doc on `register_unsafe_fn_stubs` with the layering
  argument that closes the audit;
- adds
  `register_unsafe_fn_stubs_does_not_populate_callcontrol_function_graphs`
  which mirrors the production call shape (registry +
  `callcontrol.unsafe_fn_stubs`) and asserts the path is reachable
  via the registry but absent from `function_graphs` /
  `candidate_graphs`.
youknowone added a commit that referenced this pull request May 30, 2026
…riant test

The synthetic stub pygraph built by `build_stub_pygraph_for_unsafe_fn`
returns a single default-`Constant` link.  Reviewer round 2026-05-24
item #5 flagged the concern that if this graph reaches `make_jitcodes`
as an executable JITCode, the default-Constant body would run instead
of the actual Rust unsafe-fn helper.

Audit result: the stub is annotator-only and cannot reach the
codewriter.  `register_unsafe_fn_stubs` writes to `PyreCallRegistry`
only — `CallControl::function_graphs` is populated exclusively by
safe-fn `build_flow` output (`build_flow.rs:215` rejects unsafe
bodies).  `CallControl::find_all_graphs`' BFS walks
`function_graphs.keys()` and resolves each call target via
`target_to_path_and_graph` (`jit_codewriter/call.rs:2601`); a path
absent from `function_graphs` returns `None` and the BFS `continue`s
past the call, so the unsafe stub never enters `candidate_graphs`
and never reaches `transform_graph_to_jitcode`.

This commit:
- extends the doc on `register_unsafe_fn_stubs` with the layering
  argument that closes the audit;
- adds
  `register_unsafe_fn_stubs_does_not_populate_callcontrol_function_graphs`
  which mirrors the production call shape (registry +
  `callcontrol.unsafe_fn_stubs`) and asserts the path is reachable
  via the registry but absent from `function_graphs` /
  `candidate_graphs`.
youknowone added a commit that referenced this pull request May 30, 2026
…riant test

The synthetic stub pygraph built by `build_stub_pygraph_for_unsafe_fn`
returns a single default-`Constant` link.  Reviewer round 2026-05-24
item #5 flagged the concern that if this graph reaches `make_jitcodes`
as an executable JITCode, the default-Constant body would run instead
of the actual Rust unsafe-fn helper.

Audit result: the stub is annotator-only and cannot reach the
codewriter.  `register_unsafe_fn_stubs` writes to `PyreCallRegistry`
only — `CallControl::function_graphs` is populated exclusively by
safe-fn `build_flow` output (`build_flow.rs:215` rejects unsafe
bodies).  `CallControl::find_all_graphs`' BFS walks
`function_graphs.keys()` and resolves each call target via
`target_to_path_and_graph` (`jit_codewriter/call.rs:2601`); a path
absent from `function_graphs` returns `None` and the BFS `continue`s
past the call, so the unsafe stub never enters `candidate_graphs`
and never reaches `transform_graph_to_jitcode`.

This commit:
- extends the doc on `register_unsafe_fn_stubs` with the layering
  argument that closes the audit;
- adds
  `register_unsafe_fn_stubs_does_not_populate_callcontrol_function_graphs`
  which mirrors the production call shape (registry +
  `callcontrol.unsafe_fn_stubs`) and asserts the path is reachable
  via the registry but absent from `function_graphs` /
  `candidate_graphs`.
youknowone added a commit that referenced this pull request May 31, 2026
…riant test

The synthetic stub pygraph built by `build_stub_pygraph_for_unsafe_fn`
returns a single default-`Constant` link.  Reviewer round 2026-05-24
item #5 flagged the concern that if this graph reaches `make_jitcodes`
as an executable JITCode, the default-Constant body would run instead
of the actual Rust unsafe-fn helper.

Audit result: the stub is annotator-only and cannot reach the
codewriter.  `register_unsafe_fn_stubs` writes to `PyreCallRegistry`
only — `CallControl::function_graphs` is populated exclusively by
safe-fn `build_flow` output (`build_flow.rs:215` rejects unsafe
bodies).  `CallControl::find_all_graphs`' BFS walks
`function_graphs.keys()` and resolves each call target via
`target_to_path_and_graph` (`jit_codewriter/call.rs:2601`); a path
absent from `function_graphs` returns `None` and the BFS `continue`s
past the call, so the unsafe stub never enters `candidate_graphs`
and never reaches `transform_graph_to_jitcode`.

This commit:
- extends the doc on `register_unsafe_fn_stubs` with the layering
  argument that closes the audit;
- adds
  `register_unsafe_fn_stubs_does_not_populate_callcontrol_function_graphs`
  which mirrors the production call shape (registry +
  `callcontrol.unsafe_fn_stubs`) and asserts the path is reachable
  via the registry but absent from `function_graphs` /
  `candidate_graphs`.
youknowone added a commit that referenced this pull request May 31, 2026
…riant test

The synthetic stub pygraph built by `build_stub_pygraph_for_unsafe_fn`
returns a single default-`Constant` link.  Reviewer round 2026-05-24
item #5 flagged the concern that if this graph reaches `make_jitcodes`
as an executable JITCode, the default-Constant body would run instead
of the actual Rust unsafe-fn helper.

Audit result: the stub is annotator-only and cannot reach the
codewriter.  `register_unsafe_fn_stubs` writes to `PyreCallRegistry`
only — `CallControl::function_graphs` is populated exclusively by
safe-fn `build_flow` output (`build_flow.rs:215` rejects unsafe
bodies).  `CallControl::find_all_graphs`' BFS walks
`function_graphs.keys()` and resolves each call target via
`target_to_path_and_graph` (`jit_codewriter/call.rs:2601`); a path
absent from `function_graphs` returns `None` and the BFS `continue`s
past the call, so the unsafe stub never enters `candidate_graphs`
and never reaches `transform_graph_to_jitcode`.

This commit:
- extends the doc on `register_unsafe_fn_stubs` with the layering
  argument that closes the audit;
- adds
  `register_unsafe_fn_stubs_does_not_populate_callcontrol_function_graphs`
  which mirrors the production call shape (registry +
  `callcontrol.unsafe_fn_stubs`) and asserts the path is reachable
  via the registry but absent from `function_graphs` /
  `candidate_graphs`.
youknowone added a commit that referenced this pull request May 31, 2026
…riant test

The synthetic stub pygraph built by `build_stub_pygraph_for_unsafe_fn`
returns a single default-`Constant` link.  Reviewer round 2026-05-24
item #5 flagged the concern that if this graph reaches `make_jitcodes`
as an executable JITCode, the default-Constant body would run instead
of the actual Rust unsafe-fn helper.

Audit result: the stub is annotator-only and cannot reach the
codewriter.  `register_unsafe_fn_stubs` writes to `PyreCallRegistry`
only — `CallControl::function_graphs` is populated exclusively by
safe-fn `build_flow` output (`build_flow.rs:215` rejects unsafe
bodies).  `CallControl::find_all_graphs`' BFS walks
`function_graphs.keys()` and resolves each call target via
`target_to_path_and_graph` (`jit_codewriter/call.rs:2601`); a path
absent from `function_graphs` returns `None` and the BFS `continue`s
past the call, so the unsafe stub never enters `candidate_graphs`
and never reaches `transform_graph_to_jitcode`.

This commit:
- extends the doc on `register_unsafe_fn_stubs` with the layering
  argument that closes the audit;
- adds
  `register_unsafe_fn_stubs_does_not_populate_callcontrol_function_graphs`
  which mirrors the production call shape (registry +
  `callcontrol.unsafe_fn_stubs`) and asserts the path is reachable
  via the registry but absent from `function_graphs` /
  `candidate_graphs`.
youknowone added a commit that referenced this pull request May 31, 2026
…riant test

The synthetic stub pygraph built by `build_stub_pygraph_for_unsafe_fn`
returns a single default-`Constant` link.  Reviewer round 2026-05-24
item #5 flagged the concern that if this graph reaches `make_jitcodes`
as an executable JITCode, the default-Constant body would run instead
of the actual Rust unsafe-fn helper.

Audit result: the stub is annotator-only and cannot reach the
codewriter.  `register_unsafe_fn_stubs` writes to `PyreCallRegistry`
only — `CallControl::function_graphs` is populated exclusively by
safe-fn `build_flow` output (`build_flow.rs:215` rejects unsafe
bodies).  `CallControl::find_all_graphs`' BFS walks
`function_graphs.keys()` and resolves each call target via
`target_to_path_and_graph` (`jit_codewriter/call.rs:2601`); a path
absent from `function_graphs` returns `None` and the BFS `continue`s
past the call, so the unsafe stub never enters `candidate_graphs`
and never reaches `transform_graph_to_jitcode`.

This commit:
- extends the doc on `register_unsafe_fn_stubs` with the layering
  argument that closes the audit;
- adds
  `register_unsafe_fn_stubs_does_not_populate_callcontrol_function_graphs`
  which mirrors the production call shape (registry +
  `callcontrol.unsafe_fn_stubs`) and asserts the path is reachable
  via the registry but absent from `function_graphs` /
  `candidate_graphs`.
youknowone added a commit that referenced this pull request May 31, 2026
…e coverage (#91)

* model: bind_variable resize loop iterates only new slots

Capture old_len before resize_with and walk only old_len..new_len for
variable_to_vid registration.  Pre-existing slots already have their
variable_to_vid entry from the original allocation site, so the prior
full-vec walk repeated O(n) work per resize call without changing
any pre-existing mapping (the entry().or_insert shape made the
rescan a no-op for older slots but still touched every slot every
time).  Worst-case grows quadratic in graph size on construction
flows that bind_variable one slot at a time.

Carried forward from ssa-repr's 09183694f3 (CodeRabbit Nitpick on
PR #43 round-2).  The companion prune_dead_phis hunk in that commit
was retired upstream: origin/main's prune_dead_phis now keys on
Variable directly (HashSet<Variable>), no longer routes the
ExitSwitch::Value read through value_id_of, so the silent-drop
shape the panic-on-unregistered guard was protecting against no
longer exists.

* model: FunctionGraph.owner_root + with_owner_root; build_function_graph copies self_ty_root

* annotator: Bookkeeper.host_classes_by_root + host_class_for_root via IndexMap; derive_subject_inputcells optionally narrows self via getuniqueclassdef when ClassDef.attrs non-empty

* vecmap

* target_to_path: leaf-match fallback for FunctionPath + Method targets

PyPy `flowcontext.py:845-866 LOAD_GLOBAL` analog: when an exact
CallPath lookup misses, try a unique leaf-segment match across
function_graphs keys.  Returns the bare path unchanged when the
match is ambiguous (multiple keys with the same leaf) so BFS /
call-control walker reports the miss instead of silently picking
one.

FunctionPath fallback covers cross-module bare callsites whose
caller-side `canonical_call_target` qualification points at the
caller's module while the callee lives elsewhere.

Method fallback mirrors the same shape for `self.method()` whose
receiver carries a bare type token while the inherent-method
registry holds the qualified `[module, Type, method]` path.

test_make_jitcodes_produces_graph_keyed_output: Invariant 4 now
checks for `["__opcode_dispatch__", ...]` arm registration
(produced by `build_canonical_opcode_dispatch`) instead of the
no-longer-separately-registered `["execute_opcode_step"]` wrapper.

* target_to_path leaf-match: free-fn filter + multi-match name dedup; reset annotation cells on dual-gate Skip; portal selection prefers shortest qualified path

target_to_path FunctionPath leaf-match: filter candidates to free-function graphs (FunctionGraph.owner_root.is_none()) so impl-method same-leaf collisions (Type::copy 2-arg vs std::ptr::copy 3-arg) don't misroute the caller. Guard out segments.len() > 2 to keep explicitly-qualified callsites verbatim or Residual. Multi-match disambiguation by FunctionGraph.name catches alias-clone publications of the same source graph.

dual_gate_publish_concretetypes Skip arm: clear every graph.iter_variables() annotation cell to None before legacy_annotator::annotate runs. The real-path attempt inside dual_gate_check_with_registry can partially populate cells before panicking on known-unported shapes; reset matches PyPy RPythonAnnotator.__init__ starting from empty bindings.

lib.rs portal selection: choose min_by_key(segments.len()) among qualified aliases instead of HashMap::keys().find(), so the canonical [module, name] shape wins deterministically over crate-prefixed [crate_alias, module, name] aliases.

* register/classdesc: project ItemStruct named fields into REGISTERED_STRUCT_FIELD_ATTRS side-table; _init_classdef pre-populates ClassDef.attrs from snapshot via generalize_attr + modified, mirroring FORCE_ATTRIBUTES_INTO_CLASSES eager-installation shape

* register tests: cover struct_field_attrs_snapshot round-trip (named-field ValueType projection, re-registration overwrite, tuple/unit struct skip)

* flowspace_adapter::translate_op: add explicit `OpKind::Abort` arm that returns Ok(Vec::new()) and drop the variant from `post_rtyper_jtransform_variant_name`; unmapped result_var surfaces "undefined operand ValueId" via downstream consumer (already Skip-classified by `is_known_unported`)

* struct_field_attrs_snapshot: bare-leaf fallback when literal lookup misses

`build_host_class_from_struct` stores `REGISTERED_STRUCT_FIELD_ATTRS`
entries under the bare `ItemStruct.ident` because the module-walker has
no `prefix` threaded in.  The consumer at
`classdesc.rs::ClassDesc::_init_classdef` queries via `pyobj.qualname()`,
which for trait-impl methods is the module-qualified `owner_root`
(e.g. `pyframe::PyFrame`) that `parse.rs::collect_trait_impls_from_items`
derives through `qualify_type_name_with_imports`.  Without the fallback,
the lookup misses and `ClassDef.attrs` stays empty, so
`derive_subject_inputcells`' impl-method-self narrowing gate skips even
for structs whose fields are statically known.

When the literal key has a `::`, retry with the segment after the final
`::`.  Adds a test covering both single-segment and multi-segment
qualified lookups and confirms absent struct names still miss.

* target_to_path + canonical_call_target + getcalldescr: tighten three call-resolution surfaces

front/ast.rs::canonical_call_target consults `ctx.use_imports` for
single-ident callsites before qualifying with `module_prefix`, so
`use foo::bar; bar();` produces `["foo", "bar"]` and matches the
registered path verbatim instead of relying on the cross-module
leaf-match fallback.

call.rs::target_to_path keeps the leaf-match for the remaining
`["bare"]` / `["caller_module", "bare"]` cases the `use_imports`
expansion does not cover.  `PYRE_STRICT_TARGET_TO_PATH=1` (audit-only)
short-circuits both the FunctionPath and Method receiver-leaf
fallbacks to the verbatim path.

call.rs::getcalldescr restricts the `result_type != expected_result`
leniency to `result_type=Ref` against `expected_result=Int|Float`
(the front-end's `Unknown→Ref` fallback shape).  `Type::Void`
declared vs `Type::Ref` actual now hard-fails — that shape has no
plausible type-info-gap origin since the front-end emits
`Type::Void` directly from a real `()` return spelling.
`PYRE_STRICT_GETCALLDESCR=1` disables the leniency outright.

* flowspace_adapter: skip OpKind::Abort result_var in value_to_var seeding

build_value_to_variable_map and the per-block translation loop both
seeded a fresh flowspace::Variable for every legacy op result.
OpKind::Abort's translate_op arm emits no flowspace op (Ok(Vec::new())),
so seeding the result_var left consumer args referencing a Variable
that no flowspace op defines — checkgraph then panicked with
"variable used before definition" at the consumer's arg slot.

Skip the seed when the producer is OpKind::Abort.  The first consumer's
lookup_operand now surfaces "undefined operand ValueId" instead, which
is_known_unported already Skip-classifies (cutover.rs:592-593) at the
producer-adjacent site.

Skip-event taxonomy on test_codegen_output shifts from one root cause
(808 "variable used before definition" cascade traced to a single
sub-graph) to the orthodox per-category split:
- 340 "adapter cross-block body Input" (Cat 2.1)
- 215 "not registered in PyreCallRegistry" (Path C)
- 134 "complete_pending_blocks failed"
- 67 "undefined operand ValueId" (Path D producer-side, this commit's
  newly-exposed orthodox category)
- 44 "compute_at_fixpoint failed"
- 2 "AnnotatorError"

* PyreCallRegistry::lookup_with_leaf_match: registry-build-stage analog of target_to_path leaf-match

flowspace_adapter::translate_op's Call arm previously consulted only
the verbatim `call_registry.lookup(&key)` + alias indirection,
declaring "not registered in PyreCallRegistry" on any miss.  The
codewriter-side `CallControl::target_to_path` (call.rs:3043-3104) has
a cross-module leaf-match fallback for the
`["caller_module", "bare"]` shape but that fallback lives on a
different registry (`CallControl.function_graphs` vs
`PyreCallRegistry.entries`), so registry-build-stage call resolution
missed callees that the codewriter resolves via leaf-match.

Add `lookup_with_leaf_match` on `PyreCallRegistry`: verbatim + alias
lookup first; on miss, scan `entries` for free-function
(`HostObject::is_user_function()`) entries whose key's last segment
matches the queried leaf, restricted to single- or two-segment keys
(matching the codewriter-side `segments.len() <= 2` restriction).
Multi-alias clusters of the same `host_object` identity converge on a
single resolution.  Gated behind the same `PYRE_STRICT_TARGET_TO_PATH`
audit env so strict-mode keeps registry-build and codewriter
resolution consistent.

Skip taxonomy on test_codegen_output (default mode):
- 338 adapter cross-block body Input  (was 340)
- 211 not registered in PyreCallRegistry  (was 215)
- 134 complete_pending_blocks failed
-  67 undefined operand ValueId
-  47 compute_at_fixpoint failed  (was 44)
-   1 AnnotatorError

Lib: 2762/2762 PASS; check.py: 39/39+39/39+2/2 PASS.

* jtransform: coerce Ref operands to Int for int comparisons

`OpKind::BinOp` with op in `lt|le|gt|ge|eq|ne` reached `op_kind_to_opname`
with one or both operand kinds resolving to `'r'` after the broader
graph population from `vecmap` / target_to_path leaf-match / Path D
Abort skip / struct_field_attrs_snapshot bare-leaf fallback exposed
graphs whose comparand `concretetype` is left as `Unknown` (defaults
to `'r'`).  The assembler unconditionally prefixes `int_` and appends
the operand kinds, leaking `int_eq/ir>i` / `int_eq/ri>i` /
`int_le/ri>i` / `int_le/rr>i` / `int_ne/ri>i` into `pipeline.insns`
where no RPython blackhole handler exists.

Mirror the existing `mod` / `floordiv` PRE-EXISTING-ADAPTATION at
`jtransform.rs:1119`: when any operand is `'r'` (and floats are not
involved — that arm matched earlier), coerce via
`coerce_operand_to_int` (emits `cast_ptr_to_int` per `rfloat.py`-style
rtyper-layer cast that pyre's lighter rtyper omitted) and re-emit the
same `BinOp` with int operands.  `eq` / `ne` with both `'r'` operands
keeps falling through to the existing `ptr_eq` / `ptr_ne` arm.

`default_bh_builder_unwired_set_matches_task_85_snapshot` goes from
5 unwired entries back to 0.  `cargo test -p majit-translate --lib`
2762/2762 PASS.  `check.py` dynasm 39/39 + cranelift 39/39 + 2/2.

* register: add extract_unsafe_fn_signatures helper

Pure-extract walker over a parsed `syn::File` collecting
`(path-segments, Signature)` for every top-level `unsafe fn` and unsafe
impl-method.  Z2.5 Path C slice 1: no registration consumer yet — the
slice 3 follow-on (cutover.rs::populate_call_registry_from_call_graphs)
will drive the registration once the stub PyGraph builder lands.

Free fns key as [prefix, ident] (or [ident] with empty prefix); impl
methods key as [ImplTy, method] via extract_impl_target_path.

Factors extract_argnames body into extract_argnames_from_sig so the new
helper reuses arg-name extraction across `syn::ItemFn` and
`syn::ImplItemFn` (both expose `sig: syn::Signature`).

Seven unit tests cover the helper boundaries.

* cutover: add build_stub_pygraph_for_unsafe_fn + default_constvalue_for_lltype

Z2.5 Path C slice 2 — synthesize a minimal flowed PyGraph for a callee
whose real body cannot be lowered (unsafe fn in pyre-object /
pyre-interpreter).  The stub has a single Link from startblock to
returnblock carrying a Constant of the declared return lltype.

default_constvalue_for_lltype projects:
- integer family + Char/UniChar/Address     -> ConstValue::Int(0)
- Bool                                       -> ConstValue::Bool(false)
- float family                               -> ConstValue::float(0.0)
- Void                                       -> ConstValue::None
- Ptr(_)                                     -> ConstValue::None (null)
- Func/Struct/Array/Opaque/Fwd/Interior      -> None (caller skips)

Six unit tests cover both helpers.  No consumer wired yet — slice 3
follow-on will drive registration from populate_call_registry_from_call_graphs.

* register + cutover: Path C slice 3 helpers (return-type projection, stub registrar)

Slice 3a (register.rs): simple_return_type_to_lltype maps syn::ReturnType
to LowLevelType.  Coverage at this slice: Default -> Void, unit tuple
() -> Void, single-segment "bool" -> Bool.  All other return shapes
surface None and the caller skips.

Slice 3b (register.rs): extract_unsafe_fn_stubs combines
extract_unsafe_fn_signatures with simple_return_type_to_lltype to
produce (segments, signature, return_lltype) spec tuples ready for
registration.  Per-fn discard when the return shape is unsupported.

Slice 3c (cutover.rs): register_unsafe_fn_stubs wraps each spec
through build_stub_pygraph_for_unsafe_fn + PyreCallRegistry::
register_callee.  Existing entries are preserved (no overwrite);
compound-lltype specs are silently skipped.

Eight new unit tests cover the boundary cases.  Slice 3d (wiring into
analyze_pipeline_from_parsed + dual_gate_registry) follows.

* codewriter: wire CallControl.unsafe_fn_stubs into dual_gate_registry

Add Vec<(segments, Signature, LowLevelType)> carrier on CallControl,
populate from analyze_pipeline_from_parsed via extract_unsafe_fn_stubs
per parsed file, and consume from dual_gate_registry after the
function_graphs populate pass.  register_unsafe_fn_stubs lookup-skips
specs whose segment key is already registered (e.g. impl-method
entries reaching function_graphs via inherent/trait impl extraction).

Brings OpKind::Call::FunctionPath "not registered" Skip events from
218 down to 14 (only std:: paths + ambiguous bare-leaf cases remain).

* flowspace/model: register std.ptr / std.mem / std.alloc modules in HOST_ENV

Add std.ptr (null_mut, copy_nonoverlapping), std.mem (align_of), and
std.alloc (dealloc) to bootstrap_std_modules so 3-segment callsites
"std::*" resolve through flowspace_adapter Branch 3b (module path
joined with `.`, attribute via module_get).  Closes the remaining 5
OpKind::Call::FunctionPath "not registered" events whose paths start
with `std`.

Skip total drops 14 → 11.  Remaining 11 events are bare-leaf
multi-match (is_int/is_type/etc.) and `crate::` prefix leaf-match
ambiguity, not std-lib.

* flowspace/model: add std.ptr.eq + BigInt.from HOST_ENV stubs

Branch 3b of flowspace_adapter::translate_op resolves 2-segment and
3-segment callsites via HOST_ENV.import_module(joined).module_get(leaf).
Registering "BigInt"/from and adding "eq" to std.ptr closes the
["BigInt", "from"] (×4, longobject/tupleobject) and
["std", "ptr", "eq"] (×1) "not registered" events.

Skip count for OpKind::Call::FunctionPath drops 5 → 1; only
["baseobjspace", "is_none"] remains.

* codewriter: document register_unsafe_fn_stubs ordering vs populate

Expand the inline rationale on why register_unsafe_fn_stubs runs
AFTER populate_call_registry_from_call_graphs rather than before.
Empirical: swapping the order trips populate's `registry.alias()`
invariant ("alias key already a canonical entry") on impl-method
stubs that overlap with `function_graphs` aliases.  The residual
cost — one ["baseobjspace", "is_none"] Skip event surfacing as
recorded lazy-failure on the safe-fn caller's cachedgraph — is the
2026-05-23 floor of this category.

* Add OpKind::NewTuple variant; route Expr::Tuple through it

model.rs: add `NewTuple { args: Vec<Variable> }` OpKind variant.

Match-exhaustiveness arms (8 sites):
- assembler.rs: opkind_tag "NewTuple", flowspace opname "newtuple"
- jit_codewriter/call.rs raise_class: RaiseClass::No (newtuple is PureOperation)
- jit_codewriter/jtransform.rs: remap NewTuple args through aliases
- inline.rs: remap_op_kind clones args, op_variable_refs returns args,
  is_pure_op returns true
- legacy_annotator.rs infer_op_type: ValueType::Ref

front/ast.rs::Expr::Tuple: lower each element to a Variable via
pushvid/popvid_var and push `OpKind::NewTuple { args: elem_vars }`
instead of `continue_with_unknown(UnsupportedExprKind::Tuple)`.

flowspace_adapter::translate_op: NewTuple arm emits legacy
`newtuple` SpaceOperation; args route through `operand_value_id`
+ `lookup_operand` so legacy Hlvalue identities match checkgraph's
defined-var set, result through `resolve_result_hlvalue`.

lib 2808/0; check.py dynasm 39/39. Skip 64 -> 65: one
`baseobjspace.is_none` event closes (no longer wraps in
`continue_with_unknown` cascade), +2 Cat 2.1 cross-block body Input
from new NewTuple-result threading across blocks.

* register.rs: add extract_static_decls helper

Pure-extract helper walking syn::File items for Item::Static
(pub and non-pub) and projecting each to
(Vec<String> segments, ValueType) — segments are [prefix?, ident],
ValueType is derived from the declared syn::Type via
classify_fn_arg_ty.

Bare primitives (i8..i64 / u8..u64 / bool / f32 / f64) map to
Int / Unsigned / Bool / Float; compound types (PyType, LazyLock<...>,
custom structs) fall through to Ref.

7 unit tests cover empty input, const-vs-static, primitive
projection, empty-prefix bare-name segments, compound types,
non-pub statics, and other-item filtering. No production callers
yet.

* CallControl: add static_decls carrier + populate from parsed files

Field `static_decls: Vec<(Vec<String>, ValueType)>` added to
CallControl, initialized in `CallControl::new()` to an empty Vec.

`lib.rs::analyze_pipeline_from_parsed` walks each parsed source
file and calls `extract_static_decls(file, module_path)`, extending
`call_control.static_decls` with the result. Sits alongside the
existing `unsafe_fn_stubs` population.

No consumer wired yet — the catalogue is data-only. Lib 2815/0.

* Add OpKind::LoadStatic variant + 8 match-arm stubs

LoadStatic { segments, ty } carries a single-segment Expr::Path that
resolves to a crate-local static declaration. Compile-only addition;
no emit site yet, no semantic change.

- model.rs: variant declaration after NewTuple.
- call.rs: RaiseClass::No (static read cannot raise).
- assembler.rs: opkind_tag "LoadStatic"; flowspace opname "same_as".
- jtransform.rs: remap arm passes through (no Variable operands).
- inline.rs: remap_op_kind clones; op_variable_refs returns []; is_pure_op true.
- legacy_annotator.rs: result ValueType from the ty field.

* Wire KNOWN_STATICS thread_local + LoadStatic adapter translate arm

Front-end thread_local catalogue populated from extract_static_decls
before semantic build runs.  lower_expr Expr::Path consults but
defers OpKind::LoadStatic emission to a follow-up slice (see body
comment around the catalogue lookup at front/ast.rs).

- front/ast.rs: KNOWN_STATICS thread_local + populate_known_statics fn.
- lib.rs: extract_static_decls moved to pre-semantic-build entry, reused
  for both KNOWN_STATICS publish and CallControl.static_decls.
- flowspace_adapter.rs: translate_op arm for OpKind::LoadStatic emits
  same_as(Constant(UniStr(segments_joined))).  No emit site at present;
  arm is wired for the deferred Slice 3c.

* jtransform Ref ordering: restrict to eq/ne only (PyPy ptr_eq/ne parity)

Restore PyPy parity for the Ref-tainted integer comparison recovery
arm.  RPython jtransform (jtransform.py:1243-1255) has ptr_eq / ptr_ne
rewrites only; pyre's prior arm widened the recovery to lt | le | gt |
ge | eq | ne, which silently masked the upstream cast_ptr_to_int-
elision producer bug for strict orderings.  Narrowing the match guard
to eq | ne surfaces the real bug as an unwired int_le/r* opname at the
codewriter snapshot.

The default_bh_builder_unwired_set_matches_task_85_snapshot expected
list documents the new int_le/r* drift alongside the prior newtuple/*
drift (Z2.5 NewTuple slice 4666d12ea8).  Both retire when the
underlying optimizer / cast-preservation pass lands.

call.rs::getcalldescr keeps the PYRE_STRICT_GETCALLDESCR-gated
leniency.  The PyPy-strict version of the RESULT != FUNC.RESULT check
requires closing the dispatch-arm fn_return_types gap first —
restoring strict-mode without the gap closure surfaces strict failures
at jtransform.rs (outside the dual_gate catch_unwind) and makes the
build unbuildable.  Comment expanded to document the dependency.

* front/ast.rs: route multi-segment paths through LoadStatic lookup

Z2.5 Cat 2.1 Slice 3f follow-on.  `populate_known_statics`
(front/ast.rs:3499) already publishes each catalogue entry twice — once
under the bare leaf segment and once under the joined `::`-path — so
the multi-segment lookup is a single-line gate change: drop
`path.path.segments.len() == 1` from the LoadStatic emit predicate.

OpKind::LoadStatic.segments now carries the full path component list
(was a single-element Vec previously), preserving the segments shape
that flowspace_adapter::translate_op joins via `segments.join("::")`
when materialising the Constant(UniStr(...)) operand.

Multi-segment reads that resolve to a catalogued crate-local static
(e.g. `pyre_object::PY_NULL`) now emit LoadStatic instead of the body-
`OpKind::Input` fallthrough, removing them from the Cat 2.1 Skip
family.  Closes pyre_object::PY_NULL (5) and similar multi-segment
crate-local statics at the build-time Skip trace.

* pyre_call_registry: reject PascalCase pre-leaf candidates in lookup_with_leaf_match when query is free-fn shape

`lookup_with_leaf_match` previously collected every entry sharing the
leaf identifier and the `is_user_function()` flag; when the registry
held both a free-fn `pyre_object::is_none` and an unrelated impl-method
`OpRef::is_none` (or any other `Type::method` collision), the
multi-match `all_same` check on `host_object` Arc identity returned
None because the two candidates had distinct Arc identities.

Add a shape-aware filter: when the query's pre-leaf segments are all
snake_case (free-fn shape), reject candidates whose immediate pre-leaf
segment is PascalCase (impl-target type by Rust naming convention).
Caller queries spelled `module::fn_name` no longer latch onto a
`Type::method` registration sharing the leaf identifier.

Two new tests cover the disambiguator + the negative case (no false
latch when only an impl-method is registered).

* bookkeeper + builtin: BuiltinCallable fallback in immutablevalue_hostobject + std.ptr.eq analyzer

`immutablevalue_hostobject` had no arm for `BuiltinCallable` HostObjects
whose qualname is absent from `BUILTIN_ANALYZERS`; the registered-
analyzer branch declined and every later arm rejected too, surfacing
"Don't know how to represent HostObject(<host std.ptr.eq>)" panics on
`baseobjspace::is_w` / `baseobjspace::is_` (which lower the Python
identity check via `std::ptr::eq(w_one, w_two)`).

Add a callable-fallback arm after `is_user_function` that returns
`SomeBuiltin` with `analyzer = None`.  Register a `std_ptr_eq`
analyzer (returns `SomeBool`) so the call-site dispatch on
`std.ptr.eq` resolves cleanly without falling through to the
"no analyser registered" error.

* flowspace/model + builtin: add std.mem.size_of HOST_ENV stub + analyzer

`lltype::malloc_typed` / `object_array::items_block_layout` call
`std::mem::size_of::<T>()` which emits OpKind::Call::FunctionPath
{ segments: ["std", "mem", "size_of"] }.  Branch 3b in
flowspace_adapter::translate_op resolves multi-segment paths via
HOST_ENV.import_module(joined).module_get(leaf); registering "size_of"
on the existing std.mem module closes the registry-Skip path.

Add std_mem_size_of analyzer (returns SomeInteger, the lattice
projection of usize) so SomeBuiltin.call dispatch resolves cleanly.

* flowspace/model + builtin: add majit_metainterp HOST_ENV stubs + bool flag analyzer

External crate `majit_metainterp` exposes two `pub fn -> bool` flag
helpers called from pyre source: `majit_log_enabled` (logging gate)
and `jit::we_are_jitted` (JIT-context probe).  Their call sites emit
`["majit_metainterp", "majit_log_enabled"]` (2 segs) and
`["majit_metainterp", "jit", "we_are_jitted"]` (3 segs); neither
resolves through PyreCallRegistry (extract_unsafe_fn_stubs only walks
parsed pyre-object/pyre-interpreter sources, not external crates).

Add HOST_ENV modules `majit_metainterp` + `majit_metainterp.jit` with
each helper as a BuiltinCallable, plus a shared
`majit_metainterp_bool_flag` analyzer returning `SomeBool` so
`SomeBuiltin.call` dispatch resolves.

Closes the 4 unregistered-FunctionPath skips on these paths.  4
downstream undefined-operand skips surface as legitimate fail-loud in
the same graphs (pre-existing producer-side gaps previously masked).
lib 2819/2819 + check.py 39/39+39/39+2/2 PASS.

* flowspace/model + builtin: add Rust primitive type conversion HOST_ENV stubs (u32/i64/usize From + TryFrom)

Callsites in pyre source emit `["u32", "from"]` / `["i64", "from"]` /
`["i64", "try_from"]` / `["usize", "try_from"]` 2-segment paths for
Rust primitive type conversion impls.  Branch 3b in
flowspace_adapter::translate_op resolves these via
HOST_ENV.import_module("u32").module_get("from") etc., so register
each primitive as a HOST_ENV module with the conversion methods as
BuiltinCallables.

Add `primitive_integer_conversion` analyzer (returns SomeInteger,
shared across all conversion qualnames).

Closes 6 unregistered FunctionPath events.  11 downstream events
surface as legitimate fail-loud (cross-block / undefined-operand /
compute_at_fixpoint) in graphs that previously hit these primitive
calls first; check.py 39/39+39/39+2/2 PASS confirms functional
behaviour unchanged.

* cargo fmt: collapse single-arg call sites in flowspace/model, rust_source/register, front/ast, pyre_call_registry tests

* jtransform int-cmp Ref coerce: extend to lt/le/gt/ge

The earlier eq/ne-only restriction left strict orderings (lt/le/gt/ge)
with a Ref-typed operand reaching the assembler unchanged, producing
unwired int_le/ri>i and int_le/rr>i blackhole opnames.  Re-extending
the coerce arm to all six comparison ops makes both operands pass
through cast_ptr_to_int before the int_<cmp> binop, so the resulting
JITCode opname is int_<cmp>/ii>i which the blackhole interp wires.

jitcode_runtime snapshot expected set: int_le/ri>i and int_le/rr>i
removed; same_as/>i and same_as/>r now documented as pending Slice C
closure (OpKind::LoadStatic emits a same_as sentinel that survives to
JITCode; the resolved-constant rewrite at flowspace_adapter retires
both the variant and the unwired entries together).

* extract_static_decls: literal RHS → ConstValue + ConstX emit at lower_expr

extract_static_decls now folds Rust `bool` / signed-integer / float /
string / byte-string literal initializers — plus the
`thread_local! { static X: T = const { LIT }; }` wrapper — into
`Option<ConstValue>` alongside the existing `(segments, ValueType)`
pair.  KNOWN_STATICS, populate_known_statics, CallControl.static_decls,
and OpKind::LoadStatic carry the resolved value through.

front/ast.rs `lower_expr`'s LoadStatic emit site now bypasses
OpKind::LoadStatic entirely when the resolved (ValueType, ConstValue)
pair matches one of the dedicated primitive op kinds:

  (ValueType::Bool, ConstValue::Bool(b)) → OpKind::ConstBool(b)
  (ValueType::Int,  ConstValue::Int(i))  → OpKind::ConstInt(i)
  (ValueType::Float, ConstValue::Float(b)) → OpKind::ConstFloat(b)

Non-primitive ConstValue variants (`UniStr`, `ByteStr`), mismatched
declared types (e.g. `ValueType::Unsigned`), and unresolved RHS keep
OpKind::LoadStatic { segments, ty, value }.

flowspace_adapter `LoadStatic` translate arm: when the LoadStatic
carries `value: Some(v)`, the emitted `same_as` SpaceOperation operand
is `Constant(v)`; the `UniStr(joined_path)` sentinel remains only when
value resolution failed.

* register_unsafe_fn_stubs: document annotator-only layering + pin invariant test

The synthetic stub pygraph built by `build_stub_pygraph_for_unsafe_fn`
returns a single default-`Constant` link.  Reviewer round 2026-05-24
item #5 flagged the concern that if this graph reaches `make_jitcodes`
as an executable JITCode, the default-Constant body would run instead
of the actual Rust unsafe-fn helper.

Audit result: the stub is annotator-only and cannot reach the
codewriter.  `register_unsafe_fn_stubs` writes to `PyreCallRegistry`
only — `CallControl::function_graphs` is populated exclusively by
safe-fn `build_flow` output (`build_flow.rs:215` rejects unsafe
bodies).  `CallControl::find_all_graphs`' BFS walks
`function_graphs.keys()` and resolves each call target via
`target_to_path_and_graph` (`jit_codewriter/call.rs:2601`); a path
absent from `function_graphs` returns `None` and the BFS `continue`s
past the call, so the unsafe stub never enters `candidate_graphs`
and never reaches `transform_graph_to_jitcode`.

This commit:
- extends the doc on `register_unsafe_fn_stubs` with the layering
  argument that closes the audit;
- adds
  `register_unsafe_fn_stubs_does_not_populate_callcontrol_function_graphs`
  which mirrors the production call shape (registry +
  `callcontrol.unsafe_fn_stubs`) and asserts the path is reachable
  via the registry but absent from `function_graphs` /
  `candidate_graphs`.

* opcode dispatch arms: thread ProgramMetadata.fn_return_types

`lower_expr_into_graph_with_signature` (front/ast.rs:1608) gains a
`fn_return_types: &HashMap<String, String>` parameter that
`GraphBuildContext::new` consumes instead of the empty map it
constructed locally.  The map is threaded from
`build_canonical_opcode_dispatch` (lib.rs:1093) → `extract_opcode_dispatch_arms`
(parse.rs:714) → `extract_match_arms` (parse.rs:789) →
`lower_expr_into_graph_with_signature`, pointing at
`program.fn_return_types` produced by
`front::build_semantic_program_from_parsed_files`.

With the side-table populated for arm-body callsites, the unconditional
strict mismatch check at `jit_codewriter/call.rs:4418-4466` no longer
needs the `PYRE_STRICT_GETCALLDESCR` env-var override; the env-var gate
and the `result_type_unresolved` narrow are removed.

* struct field attrs: qualify registry key under walker prefix

Add WALKER_MODULE_PREFIX thread_local + WalkerModulePrefixGuard RAII
guard in `flowspace/rust_source/register.rs`, parallel to
WalkerTypeAliasGuard and WalkerStructPtrsGuard. The
`register_items_into_namespace` inline-mod arm pushes the nested mod
ident onto the prefix while walking inner items, so a `mod outer { mod
inner { struct Foo { ... } } }` keys its field-attr stub under
`"outer::inner::Foo"` instead of the bare `"Foo"`.

`build_host_class_from_struct` reads the prefix and registers
`REGISTERED_STRUCT_FIELD_ATTRS` under the qualified key.
`struct_field_attrs_snapshot` no longer falls back from
`"a::b::Foo"` to the bare `"Foo"`; lookups must match the
registered key exactly. The prior bare-leaf fallback test is replaced
by two structural tests:

- `struct_field_attrs_snapshot_qualified_key_no_bare_fallback`
- `struct_field_attrs_snapshot_nested_mod_qualified_key`

Bundled fmt fixes in inline.rs / parse.rs / cutover.rs collapse
single-arg call sites and OpKind::LoadStatic destructure shapes.

* translate: cross-file type aliases, Ordering, wrapping_* lowering

register.rs: WalkerTypeAliasGuard::enter mirrors entering aliases into
a new process-wide PROCESS_WIDE_TYPE_ALIASES registry. Each entry is
serialised as a token-stream string at write time (syn::Type is
!Send/!Sync) and re-parsed via syn::parse_str on first read; a
thread-local PROCESS_WIDE_ALIAS_PARSED_CACHE memoises the parsed
result per (thread, alias) so syn::parse_str runs at most once per
spelling. entry().or_insert_with keeps the mirror first-writer-wins.
walker_type_alias_lookup consults the thread-local map first
(inline-mod shadowing), then the per-thread parsed cache, then the
global serialised table. Adds a cross-file alias resolution test.

front/ast.rs: populate_known_statics registers
Ordering::{Relaxed,Acquire,Release,AcqRel,SeqCst} as ConstInt
placeholders via a new register_stdlib_known_statics helper.

front/ast.rs lower_expr MethodCall arm emits OpKind::BinOp for
wrapping_{add,sub,mul} (2 args) and OpKind::UnaryOp("abs") for
wrapping_abs (1 arg) before the polymorphic-receiver branch.
Removes the now-unreachable wrapping_* entries from
transparent_option_method_result_type and primitive_method_result_type.

* jitcode_runtime: overlay wellknown_bh_insns + pyre_extension_insns

Replace the pyre_extension_insns-only overlay over `INSNS_OPNAME_TO_BYTE`
with a shared `overlay_insns` helper that applies BOTH
`wellknown_bh_insns()` and `pyre_extension_insns()` on top of the build-
time `pipeline.insns` snapshot.

Build-time `pipeline.insns` only records opnames the assembler actually
emitted; canonical opnames the analyzed source set did not exercise
(e.g. `ref_guard_value/r`, `newtuple/rr>r`) were absent from the runtime
table. Overlaying `wellknown_bh_insns` restores the closed key universe
`BlackholeInterpBuilder` wires up via `wire_bhimpl_handlers`
(`blackhole.py:152-179`), so opname coverage becomes a property of the
codebase rather than of which paths the build happened to observe.

* call.rs target_to_path: leaf-keyed indices + drain profile

CallControl gains two leaf-name indices maintained by a single mutation
point `insert_function_graph_indexed`:

- `free_fn_leaf_index: HashMap<String, Vec<CallPath>>` — free fns
  (`owner_root.is_none()`) keyed by their path's last segment.
- `impl_method_leaf_index: HashMap<String, Vec<CallPath>>` — impl
  methods (`owner_root.is_some()`) keyed by method-name leaf.

`target_to_path`'s two cross-module fallbacks
(FunctionPath bare-callsite leaf-match and Method receiver-leaf
fallback) previously iterated the full `function_graphs` HashMap on
every direct-call op. They now lookup the same-leaf bucket and apply
the suffix/receiver-leaf filter only against that narrowed set.

Add env-gated profiling infrastructure used to localize the hotspot:

- `jit_codewriter::transform_profile` — per-phase wall-time accumulator
  with a `PhaseScope` RAII helper.
- `PYRE_PROFILE_DRAIN=1` — wraps each `transform_graph_to_jitcode` sub-
  step (`dual_gate_publish_concretetypes`, `lower_indirect_calls`,
  `jtransform.transform`, `regalloc`, `flatten`, `assemble`) and prints
  per-drain totals plus a sorted phase summary.
- `PYRE_PROFILE_PIPELINE=1` — phase markers around
  `analyze_pipeline_from_parsed`'s top-level steps.

Disabled by default; zero cost when the env var is unset.

Measured impact on `test_codegen_output`: 1108.61s → 12.59s. Full
majit-translate lib test suite 2827/2827 PASS in 19.15s. `pyre/check.py`
39/39 + 39/39 + 2/2 ALL PASSED.

* jitcode_runtime: drop newtuple/rr>r from Task #85 unwired snapshot

The current build's `pipeline.insns` no longer contains `newtuple/rr>r`
— no analyzed source path constructs a `(Ref, Ref)` 2-tuple — so the
runtime `INSNS_OPNAME_TO_BYTE` table omits it, and
`build_default_bh_builder_with_unwired_report` no longer surfaces it.
Remove the entry from the Task #85 expected list to match observed
reality; trim the matching doc-comment reference.

Unwired-set transition direction is from-emitted-but-unhandled toward
nothing-emitted (`wire_bhimpl_handlers` deliberately does not wire
`newtuple/*` — see the test comment), so this is progress, not a
regression.

* unsafe-fn stub pygraph: pre-annotated Variable instead of default Constant

build_stub_pygraph_for_unsafe_fn now emits a Variable carrying
lltype_to_annotation(return_lltype) on the return Link, replacing the
prior Constant(default_value) form. default_constvalue_for_lltype
replaced by default_someshell_for_lltype returning a SomeValue shell
with no const_box.

Closes the path where Bookkeeper::immutableconstant lifted the stub's
default Constant to SomeXXX { const_box = Some(...) }, leaking a
fold-eligible "known false / known 0" annotation into the rtyper at
every unsafe-fn callsite.

Container-type returns and Address still surface None (SomeAddress is
not yet ported to SomeValue).

* pyre-jit-trace build.rs: sort source files + rename runtime_ops jit-callable helper

build.rs collect_rs_files now calls WalkDir::sort_by_file_name so the
analyzer sees source files in the same lexicographic order on every
platform. Filesystem-native readdir orders (APFS / ext4 / NTFS)
differ; the build script's analysis was order-sensitive and PR 91 CI
failed on Ubuntu/Windows while macOS happened to read files in an
order that worked.

pyre-interpreter/src/runtime_ops.rs's `call_user_function_with_args`
is renamed to `jit_call_user_function_with_args` to remove the
free-fn leaf collision with pyre-interpreter/src/call.rs's same-named
`fn call_user_function_with_args(PyObjectRef, &[PyObjectRef]) -> PyObjectRef`.
The two functions had different signatures (PyObjectRef vs i64) and
the cross-module leaf-match disambiguator's path resolution depended
on registration order; sorted analysis tripped getcalldescr's
"return type Ref but actual return type Int" assertion at
call::call_user_function_with_args callsites.

Local verification (sorted order on macOS): cargo test --all
--no-default-features --features cranelift PASS, --features dynasm
PASS, pyre/check.py PASS 39/39 + 39/39.

* jitcode_runtime task #85 snapshot: accept newtuple/rr>r alongside same_as/>i

The unwired-opname tripwire previously asserted exact equality against a
five-entry list pinned to macOS APFS analyzer behaviour. Linux ext4 and
Windows NTFS traverse the analyzer's file-order-sensitive paths
differently and produce `newtuple/rr>r` where macOS produces `same_as/>i`.

Switch the assertion to a subset check against an allow-list that covers
both shapes. New entries outside the allow-list still trip the test.
Underlying order-sensitivity remains a multi-session Z2.5 deep fix.

Also drop the duplicated `newtuple/*` comment block.

* extract_unsafe_fn: qualify impl-method key under module prefix; cargo fmt

extract_unsafe_fn_signatures now prepends the file's module prefix when
the impl target was written as a bare ident (`impl PyFrame {...}` in
`pyframe.rs` → `["pyframe", "PyFrame", method]`), so the registered key
matches the module-qualified `CallPath::for_impl_method` shape that
`register_function_graph` registers from `method_info.self_ty_root`.
Impl targets written with their own qualifier (`impl pyframe::PyFrame
{...}`) keep the syntactic spelling verbatim.

lookup_return_type_for_signature mirrors the same prefix-prepend
behaviour so the return-type re-walk still matches the registered key.

Tests cover bare-impl + non-empty prefix, qualified-impl ignoring
prefix, and the trait-impl bare-target + empty-prefix degenerate case.

Also applies cargo fmt on three files touched by the rebase / Task
#85 snapshot tolerance commit (call.rs, jitcode_runtime.rs).

* pyre_call_registry::lookup_with_leaf_match: consult caller use_imports before global scan

When the query is a two-segment `[caller_module, leaf]` path, look up
`leaf` in the caller's `use_imports` table first and try the resolved
fully-qualified `module::leaf` path via the literal `lookup`.  This
mirrors PyPy `flowcontext.py:845-866 LOAD_GLOBAL` consulting
`frame.globals[name]` before any fallback.

The cross-module leaf-match safety net remains as the final fallback
for callsites whose import aggregation has not captured the alias yet
(e.g. crate-level `pub use` re-exports threaded through indirect
namespaces).  The convergence-on-same-host_object check keeps that
safety net unambiguous; the new use_imports priority means we no
longer rely on iteration-order convergence when a same-leaf collision
arises across two distinct hosts in two different modules.

New test `lookup_with_leaf_match_uses_caller_use_imports_before_global_scan`
proves the priority.

* jtransform: emit ptr_eq/ptr_ne for ref-ref eq/ne; keep mixed/ordered cast bridge

PyPy `rpython/rtyper/rptr.py:167-184` registers `rtype_eq` / `rtype_ne`
on `pairtype(PtrRepr, Repr)` and emits `ptr_eq` / `ptr_ne` after
coercing both operands through `inputargs(r_ptr, r_ptr)`.  Pyre's
blackhole has `bhimpl_ptr_eq` / `bhimpl_ptr_ne` wired at
`bh_binop_r_to_i`, so the resulting `ptr_eq/rr>i` opname dispatches
without going through `cast_ptr_to_int`.

Add a new jtransform arm for `eq`/`ne` with both operands ref-kind that
emits the orthodox `ptr_eq` / `ptr_ne` directly.  The existing
cast-to-int bridge stays in place for the cases PyPy itself does not
support (mixed ref+int eq/ne, ordered lt/le/gt/ge with any ref
operand) — those source patterns are pyre-specific and need either
source-level cleanup or rtyper-side cast emission to fully retire the
bridge (Task #146 deferred).

* type-alias registry: IndexMap for deterministic iteration, drop negative cache

Replace `StdHashMap` with `indexmap::IndexMap` in `collect_type_aliases`,
`WALKER_TYPE_ALIASES` thread-local, `PROCESS_WIDE_TYPE_ALIASES` global,
and `PROCESS_WIDE_ALIAS_PARSED_CACHE` per-thread cache.  The mirror loop
inside `WalkerTypeAliasGuard::enter` previously iterated the per-file
aliases via HashMap iteration order; with random hasher seeds differing
across hosts (Linux ext4 vs macOS APFS vs Windows NTFS rustc builds),
first-writer-wins outcomes could diverge by platform.  Source-order
iteration keeps the floor reproducible.

Drop the negative-miss cache.  The global table grows monotonically as
later files register their aliases, so caching a miss made any alias
registered after the cache write permanently invisible to the writing
thread.  Recomputing on each miss is a single `IndexMap` lookup; the
expensive `syn::parse_str` round-trip remains cached for positive hits.

Module-keyed lookup (reviewer 2.2 follow-up — `(module_prefix, alias)`
instead of bare alias) is deferred to a multi-session refactor.

* register tests: update assertions for ValueType::Ref(Option<String>) tuple variant

Origin/main widened ValueType::Ref to Ref(Option<String>) carrying the
syn::Type root ident (e7cc6951110 / 85e9078ebf upstream). Our pre-rebase
tests asserted the bare unit-variant pattern. Update to match the new
projection shape: PyType, String, LazyLock all carry their leaf ident.

* OpKind::Input.class_root structural carry (M2.5g.2.a + .2.b)

Add 'class_root: Option<String>' to OpKind::Input.  Front-end populates
from the leaf segment of type_root_ident at every construction site
where the ValueType is Ref(Some(root)); non-Ref params and legacy/test
fixtures carry None.

derive_subject_inputcells consumes the carried root via the Input op's
field, calling host_class_for_root + getuniqueclassdef and gating on
attrs_populated to match the existing self-narrowing path.  Falls back
to legacy.owner_root for the receiver inputarg when class_root is None
(legacy/test-fixture compatibility).

86 construction sites + 7 pattern-match sites updated.  Test fixtures
that build synthetic Inputs default to class_root: None.

Closes M2.5g.2.a (IR shape) and M2.5g.2.b (derive_subject_inputcells
narrowing).  M2.5g.2.c (method-lookup gap via canonical_inherent_methods
fallback) remains for follow-up.

* Z2.5 slices #147-#157: HOST_ENV stubs, glob fallbacks, adapter branches

Cargo.toml: add stacker workspace dep.

front/ast.rs:
- lower_expr wrapped in stacker::maybe_grow (256KB/4MB) to absorb deep
  syn::Expr recursion on eval_loop_jit;
- USE_GLOBS_BY_SOURCE thread_local + populate_use_globs_by_source;
- Expr::Path multi-segment crate-strip via PYRE_INTERNAL_CRATES; single-
  segment glob fallback against this source's `use <path>::*` roots;
- is_synthetic_unit_variant_path extended with StepResult::Return/Yield/
  CloseLoop and JitAction::ContinueRunningNormally;
- hint_promote_or_string lowering returns same_as fold instead of
  routing to OpKind::Call.

flowspace/model.rs: HOST_ENV new_module entries for std.slice
(from_raw_parts/_mut), BigInt.from, majit_metainterp/jit (bool flag),
u32/i64/usize primitive conversions, Box/Vec/String (new + with_capacity)
/IndexMap/HashMap/FrameDebugData/RootScope/IntArray (new + from_vec).

flowspace/rust_source/register.rs: pre_register_struct_fields_from_file
pre-pass driven from analyze_pipeline_from_parsed populates
REGISTERED_STRUCT_FIELD_ATTRS before annotator runs.

translator/rtyper/flowspace_adapter.rs: Branch 3c at the FunctionPath
arm resolves `["simple_call", ExcClass]` via HOST_ENV.lookup_builtin
(front/raise.rs::lower_exc_from_raise reconstruction shape);
hint_promote_or_string single-segment passthrough.

lib.rs:
- portal CallPath tie-break orders by (len, is_crate_alias, segs) to
  deflake eval_loop_jit's portal selection;
- free_function_alias_paths: require strictly-longer name segments
  before treating as already-prefixed; emit secondary loop fanning
  out aliases through pub_use_globs roots;
- test_recognition_report switched to read_all_pyre_sources_with_modules
  + analyze_multiple_pipeline_with_modules so registry alias generation
  sees each source's module_path;
- populate_use_globs_by_source wiring from each ParsedInterpreter's
  {module_path, use_globs}.

parse.rs: pub_use_globs collected from `pub use <path>::*` (consumed by
free_function_alias_paths fan-out); use_globs collected from plain
`use <path>::*` (consumed by Expr::Path single-segment lookup). Shared
walker walk_pub_use_for_globs + strip_glob_root.

Verification: lib 2839/2839 + workspace cargo test --features dynasm
--all exit 0. PYRE_RTYPER_VERBOSE Skip event count 852 (unchanged
session-net); unique unregistered FunctionPath paths 48 -> 0;
cross-block body Input bucket 371 -> 349 (TUPLE/FLOAT/STR/INT_TYPE
statics resolved via glob fallback).

* baseobjspace: split setitem/getitem per-container helpers

Extract setitem_list/setitem_list_slice/setitem_bytearray/setitem_instance
and getitem_list/getitem_tuple/getitem_str/getitem_bytes_like/getitem_type/
getitem_instance/getitem_range_iter. Each helper is #[inline(never)] so the
dispatcher frame holds only the dispatch locals (obj/index/value) rather
than the union of every branch's locals.

The original setitem (~130 lines) and getitem (~270 lines) megafunctions
allocated a debug-mode frame sized to the worst-case branch. The dict
branch's setitem/getitem call chain (w_dict_store_checked → switch_to_
correct_strategy → object_key_for_checked) added on top of that frame
exhausted the 2 MB Linux test-thread stack at argument::tests::
topacked_round_trips. macOS frames are tighter and pass without the
split.

* annotator/classdesc: merge struct field projection into FORCE_ATTRIBUTES_INTO_CLASSES

Retire REGISTERED_STRUCT_FIELD_ATTRS (separate process-global Mutex
keyed by qualname → IndexMap<String, ValueType>) in favour of writing
directly into FORCE_ATTRIBUTES_INTO_CLASSES (rpython/annotator/
classdesc.py:957-968).  PyPy has two writers to that one dict — the
literal EnvironmentError block at :957-961 and the conditional
WindowsError assignment at :963-968.  The Rust port now has the same
shape with three writers: the hand-coded EnvironmentError block
(thread_local init), plus register_struct_fields and the diagnostic
pre_register_struct_fields_from_file pass writing struct field
projections.

FORCE_ATTRIBUTES_INTO_CLASSES becomes thread_local because SomeValue
carries Rc<...> and is not Send.  This matches RPython single-thread
annotator semantics; the walker pre-pass and _init_classdef consumer
run on the same thread.

register_struct_fields converts ValueType -> SomeValue at write time
via valuetype_to_someshell, so the stored value matches PyPy's dict
shape (SomeInteger() etc.) rather than a pyre-only intermediate.
ClassDesc::_init_classdef now has a single read site for both hand-
coded and walker-derived entries.

Test fixtures previously asserted ValueType equality; updated to
match the SomeValue variants (SomeValue::Integer / ::Instance /
::Bool) through forced_attributes_for accessor.

* rust_source/register: retire WALKER_MODULE_PREFIX thread_local; pass module_prefix as explicit arg

register_items_into_namespace and build_host_class_from_struct now take
module_prefix: &str directly. Nested-mod recursion composes the prefix
by concatenation at the call site instead of pushing onto a thread-local
RAII guard. Removes WalkerModulePrefixGuard, walker_module_prefix, and
qualify_under_walker_prefix.

* front/ast: retire KNOWN_STATICS thread_local; introduce KnownStaticsCatalogue threaded through GraphBuildContext

KnownStaticsCatalogue (IndexMap-backed) is constructed once per build
in build_semantic_program_*_with_options via from_parsed_files (which
walks each parsed file's extract_static_decls + registers the stdlib
Ordering variants). build_function_graph takes &KnownStaticsCatalogue
and attaches it to GraphBuildContext via with_known_statics; the
Expr::Path arm's primary + glob fallback lookups read through
ctx.known_statics. Removes populate_known_statics, the thread_local
KNOWN_STATICS cell, and the HashMap shape on register_stdlib_known_
statics. lib.rs no longer pre-populates a process-wide cell; the
codewriter still consumes early_static_decls directly.

* rust_source/register: retire PROCESS_WIDE_TYPE_ALIASES + alias parsed cache; merge-semantic WalkerTypeAliasGuard + WalkerAliasFloorGuard

WalkerTypeAliasGuard::enter now clones the prior thread_local content
for restore and extends the per-thread map in place, so inline-mod
scopes inherit parent-scope aliases by merge rather than via the
removed PROCESS_WIDE fallback. The cross-file alias floor moves to a
new WalkerAliasFloorGuard installed at the top of analyze_pipeline_
from_parsed, seeded with the union of every parsed file's top-level
`type T = U;` declarations. Removes PROCESS_WIDE_TYPE_ALIASES and
PROCESS_WIDE_ALIAS_PARSED_CACHE (the latter was a perf workaround
for the Mutex<IndexMap<String, String>> serialisation round-trip
that is no longer needed). walker_type_alias_lookup reads only the
thread_local. Rewrites the cross-file alias test to install the
floor explicitly.

* front/ast: expand use ::* globs into use_imports at semantic build time; retire USE_GLOBS_BY_SOURCE thread_local

build_semantic_program_from_parsed_files_with_options now walks each
parsed file's use_globs and adds (leaf → glob_root::leaf) entries to a
cloned use_imports map for that file by iterating
KnownStaticsCatalogue::keys_with_prefix. The expanded map flows
through build_graphs_from_items / build_function_graph to
GraphBuildContext.use_imports, so the front-end Expr::Path arm's
primary lookup resolves glob-imported bare names through the same
path as explicit `use X as Y;` aliases. Removes USE_GLOBS_BY_SOURCE
thread_local, populate_use_globs_by_source, and the lower_expr glob
fallback.

* rust_source/register: retire PROCESS_WIDE_STRUCT_PTRS; merge-semantic WalkerStructPtrsGuard + WalkerStructPtrsFloorGuard

WalkerStructPtrsGuard::enter switches from REPLACE to MERGE
(clone-on-enter, restore-on-drop), mirroring WalkerTypeAliasGuard.
walker_struct_ptr_register / walker_struct_ptr_lookup no longer
read or write the process-wide LazyLock<Mutex>; the static is
deleted. Cross-file struct-Ptr identity is now seeded by
WalkerStructPtrsFloorGuard::install, which runs the same
fixed-point preseed_struct_ptrs minting over the union of every
parsed file's top-level Item::Struct. The cross-file embedding
test installs the floor guard explicitly instead of relying on
the process-wide fallback.

* jitcode_dispatch: bail vable get/setfield on None box

getfield_vable_via_metainterp and setfield_vable_via_metainterp read
the box operand from a Ref register that an inlined callee frame may
leave unseeded (OpRef::None). Both route through
is_nonstandard_virtualizable -> heapcache.nonstandard_virtualizables_now_known,
whose dense Vec<u32> flag store is indexed by OpRef::raw(). OpRef::None
has raw() == u32::MAX and is_constant() == false, so it slips past the
is_constant() guard and resizes the vector to u32::MAX + 1 = 2^32 u32 =
16 GiB (list_setslice TIMEOUT, ~17 GiB peak RSS).

Add DispatchError::VableBoxNotSeeded and bail both handlers when the box
is None, surfacing a trace abort so production falls back to trait
dispatch instead of allocating. list_setslice: TIMEOUT -> 0.28s,
~305 MB; check.py 39/39 dynasm + cranelift.

* annotator/builtin: size_of result as non-negative integer

std_mem_size_of returned SomeInteger::default() (nonneg=false). The
value is a usize byte size and is always non-negative, so model it as
SomeInteger(nonneg=True) — the rffi.sizeof / len lattice. This is
strictly more precise than the default and safe under join, avoiding
weakened downstream reasoning that depends on SomeInteger.nonneg.

* front/ast: drop redundant split/join in use_imports lookup

full.split("::").collect::<Vec<_>>().join("::") reconstructs the same
string for any path; replace with full.clone(). Also folds in rustfmt
of adjacent lines (KnownStaticsCatalogue / register_stdlib_known_statics
/ use_imports entry).

* baseobjspace: validate bytearray setitem value type and range

setitem_bytearray cast the assigned value with `w_int_get_value(value)
as u8`, silently wrapping out-of-range ints (256 -> 0) and skipping the
non-int check. Coerce via space.index (honoring __index__) and enforce
0 <= v < 256, raising ValueError("byte must be in range(0, 256)") and
TypeError as bytearrayobject.py _getbytevalue does. Index bounds are
still checked first, matching bytearray_ass_subscript.

* annotator/classdesc, rust_source/register: rustfmt

* Expand CPython builtin type/method/error compatibility

typedef.rs / type_methods.rs:
- bytes/bytearray method suites (search, transform, split/join,
  case, pad, is*, fromhex, translate, mutators) and bytearray
  vec-mut accessor
- operator-slot dunders on list/tuple/int/float/str/bytes/bytearray
  (container, arithmetic, unary/conversion, rich comparison)
- str.format field subscript/attribute access and nested/auto-vs-
  manual numbering; format() routed through the shared spec parser
  with %, comma/underscore grouping, and the 'c' presentation type
- set/frozenset __or__/__and__/__sub__/__xor__ return NotImplemented
  for non-set operands
- str.encode / bytes.decode utf-16 / utf-32 codecs; str.encode raises
  structured UnicodeEncodeError; str.__add__/__mul__ return
  NotImplemented instead of recursing
- slice type: indices(length), __repr__, __eq__/__ne__, __reduce__
- dict.popitem empty message; list.remove/index not-found messages

builtins.rs:
- ascii(), int.as_integer_ratio, float.hex, round(ndigits) half-even,
  sum() Neumaier compensated summation, int.bit_length bigint path,
  float() underscore separators, reversed(range)
- int() argument TypeError formats the operand type; abs() type suffix;
  chr() range message

descroperation.rs:
- compare() TypeError uses the operator symbol; zero-division unified
  to "division by zero"; pow zero/negative message; set binary-op
  guards require both operands to be sets

error.rs: PyError::unicode_encode_error structured constructor;
KeyError.__str__ reprs a single arg (display.rs)

majit-translate: std.mem.align_of analyzer; zero-division constfold
messages unified to match the interpreter

sliceobject.rs / rangeobject.rs / baseobjspace.rs: indices3 helper,
range-iter field accessor, bytearray setitem validation + unicode
error fields

* operation/descroperation: float-specific zero-division messages

Float div/floordiv/mod and divmod raise float-specific messages
(float division by zero / float floor division by zero / float modulo)
rather than the integer-unified "division by zero". The all-integer
truediv path keeps the unified message. Constfold and runtime sites
match.

* eval/unpack_ex: accept any iterable in starred assignment

UNPACK_EX (a, *b, c = X) only handled list and tuple, raising
"cannot unpack non-sequence" for any other iterable. Materialise the
value via collect_iterable for the non-list/tuple case, matching
unpack_sequence_exact's iteration-protocol fallback, so range,
generators, str, map and other iterables unpack correctly.

* typedef/eval: maketrans is a non-binding static method

str/bytes/bytearray maketrans were registered as ordinary methods, so an
instance call like b''.maketrans(b'a', b'b') passed the receiver as the
first argument, raising the length error or building the wrong table.

Wrap the three registrations in a staticmethod descriptor (make_maketrans_descr),
and honor staticmethod/classmethod in load_method's builtin-type-instance
binding branch so the receiver is not prepended (getattr already unwrapped
the descriptor).

* typedef: dict.fromkeys is a classmethod

fromkeys was a plain builtin function reading args[0] as the iterable, so
an instance call {}.fromkeys(it, v) treated the receiver as the iterable
and returned an empty dict. Register it via w_classmethod_new and read the
bound cls at args[0], so dict.fromkeys(it, v), {}.fromkeys(it, v) and a
bound reference all pass the iterable/value at args[1]/args[2].

* builtins/eval: vars() on a type; iterate mappingproxy

vars() rejected type objects (its has_dict gate omitted is_type), so
vars(SomeClass) raised instead of returning the class __dict__. Add
is_type to the gate.

Iterating the returned mappingproxy then raised "not iterable" because
ensure_iter_value (GET_ITER) had no dict_proxy case. Unwrap the proxy to
its backing dict at the top so the existing dict branch yields a key
iterator, matching dictproxyobject.py descr_iter.

* typedef: int.from_bytes honors signed and byteorder kwargs

from_bytes ignored the signed argument (both positional and keyword) and
mistook the trailing __pyre_kw__ dict for byteorder, so
int.from_bytes(b, 'big', signed=True) returned the unsigned value. Split
the kwargs dict via split_builtin_kwargs, locate the bytes/str data (skips
a bound receiver), read byteorder/signed from positionals or keywords, and
apply two's-complement for signed values up to 8 bytes.

* eval_loop: advance last_instr past EXTENDED_ARG prefix

The plain interpreter loop set last_instr to the dispatch pc before
decoding, but decode_instruction_for_dispatch absorbs EXTENDED_ARG
prefix units and returns the trailing opcode_pc. A falling-through
handler then computed next_instr() as last_instr + 1, landing back on
the opcode unit and re-dispatching it.

UNPACK_EX with targets after the star encodes after in the oparg high
byte, so its oparg is >= 256 and the compiler emits an EXTENDED_ARG
prefix. Module-level exec/eval ran UNPACK_EX twice, overflowing the
value stack. Re-point last_instr at opcode_pc after decoding, matching
the JIT dispatch loop.

* fromhex: skip all ASCII whitespace between byte pairs

parse_hex_string skipped only the literal space byte, so valid inputs
containing tabs, newlines, vertical tabs, form feeds, or carriage
returns (e.g. bytes.fromhex("\nab\tcd")) raised ValueError. Skip the
full Py_ISSPACE set between byte pairs, matching _PyBytes_FromHex.
Whitespace within a byte pair still raises the positional error.

* format: dispatch to user-defined __format__

format(), the FormatSimple/FormatWithSpec f-string opcodes, and
str.format field rendering applied the builtin spec parser (or str())
directly, ignoring a __format__ defined on a class instance. Add
type_methods::format_value_dispatch: when the value is an instance whose
type provides __format__, call it with the spec (result must be str,
else TypeError) and route all four surfaces through it. Builtin types
keep the shared spec parser; empty spec still collapses to str(value).

* str.split/rsplit: accept sep and maxsplit keyword args

str_method_split/rsplit read sep and maxsplit from args[1]/args[2]
only, so str.split(maxsplit=1) or str.split(sep='-') passed the
trailing __pyre_kw__ kwargs dict into parse_split_sep and raised
TypeError. Add resolve_split_args: strip the kwargs dict, take each
argument from its positional slot after the receiver, else the keyword.

* str.encode: honor errors handler for ascii/latin-1

str_method_encode ignored the errors argument and always raised
UnicodeEncodeError on an unencodable character. Parse encoding/errors
positionally or by keyword and route ascii/latin-1 through a new
encode_narrow helper implementing strict, ignore, replace,
backslashreplace, and xmlcharrefreplace. The handler is consulted
lazily, so an all-encodable string with an unknown handler name does
not raise; an unencodable one raises LookupError.

* display: cycle-guard recursive container repr

py_repr recursed unbounded on a reference cycle (a list holding
itself, a self-valued dict, a tuple reachable through a contained
list), overflowing the stack. Add a thread-local set of object
pointers currently being repr'd (Py_ReprEnter/Py_ReprLeave) and emit
the placeholder form on re-entry: [...] for list, {...} for dict,
(...) for tuple, set(...)/frozenset(...) for set.

* typedef: validate bytes split maxsplit / replace count via __index__

bytes.split/rsplit silently ignored a non-integer maxsplit and
bytes.replace silently ignored a non-integer count, defaulting to
unlimited. Route both through space_index_w so a non-integer
(including None) raises TypeError 'X object cannot be interpreted as
an integer', matching __index__ semantics; bool is accepted.

split/rsplit now also resolve sep and maxsplit from keyword arguments
via split_builtin_kwargs. replace is positional-only and rejects any
keyword argument with '<type>.replace() takes no keyword arguments'.

* type_methods: reject None maxsplit in str split/rsplit

parse_split_maxsplit treated an explicit None as the absent default
(-1), so str.split(sep, None) returned the full split instead of
raising. Only a null (absent) argument now defaults to -1; a present
None routes through space_index_w (__index__) and raises TypeError
'NoneType object cannot be interpreted as an integer'.

* cargo fmt: wrap encode_narrow ascii arm and bytes replace call

* typedef: validate bytes/bytearray width and index args via __index__

bytes.ljust/rjust/center/zfill/expandtabs and bytearray.pop/insert
read their width/tabsize/index argument directly as W_IntObject; a
non-integer (e.g. b'x'.ljust('4'), bytearray(b'ab').pop('0')) misread
the object layout, producing bogus sizes or hanging on a huge resize.
Route each through space_index_w so a non-integer raises TypeError
'X object cannot be interpreted as an integer'; bool is accepted.
expandtabs now also resolves tabsize from the tabsize= keyword.

bytes_partition: on a miss with a bytearray receiver, the result
tuple aliased the mutable receiver; return a fresh new_bytes_like
copy so mutating the bytearray no longer mutates the tuple element.

* flowspace_adapter: classify OpKind::Abort as skipped in translate_op_is_skipped

translate_op maps OpKind::Abort to Ok(Vec::new()), but
translate_op_is_skipped omitted it from its match. The canraise gate
that consults translate_op_is_skipped therefore treated an Abort tail
op as a surviving raising op and closed the block as canraise, leaving
checkgraph to panic with "canraise block must end with a raising
operation" on the eval::eval_loop_jit graph. Add OpKind::Abort to the
match so the two stay in sync.

* dispatch: fold OpCode::IntIsTrue in eval_unary_i

trace_unary_i dispatches int_is_true through eval_unary_i, which only
handled IntNeg and IntInvert and panicked on any other unary opcode.
With constant operands inlined into OpRef, int_is_true now reaches the
fold path and tripped "unsupported jitcode integer unary op IntIsTrue"
on list_slicing / list_setslice. Add the arm: int_is_true(x) = (x != 0).

* rtyper: resolve impl-method receiver class via annotation

derive_subject_inputcells eager-seeded the receiver inputarg from
OpKind::Input.class_root via getuniqueclassdef_for_struct_root.  That
minted a struct-root ClassDef whose identity differed from the
call-propagated one, so the annotation fixpoint depended on graph
(HashMap) iteration order: the two virtualizable-rewrite tests died
nondeterministically with "annotator.annotated[block] is False" on
ubuntu/windows and passed on macOS only by chance.  Drop the seed;
the receiver's ClassDef is resolved by call-propagation during
annotation (description.py:283-305 FunctionDesc.pycall).

Strengthen both tripwire tests to assert the inlined load_fast body
carries VableFieldRead (self.next_instr) and VableArrayRead
(self.locals_w[idx]), so a dropped vable rewrite fails instead of
passing the prior insns.len() > 0 check.

rustfmt the callable_host dispatch ladder.

* builtins: sum() plain left-fold, reject str/bytes/bytearray start

Replace the Neumaier compensated float accumulation with the plain
`last = last + x` left-fold of app_functional.py _regular_sum.  Float
operands now accumulate with ordinary left-to-right IEEE rounding
(sum([0.1, 0.2, 0.3]) is 0.6000000000000001).  Reject a str / bytes /
bytearray start up front with the app-level messages.

* annotator/builtin: analyzer for String::new / String::with_capacity

The String.new / String.with_capacity HOST_ENV stubs resolved to a
builtin callable but had no analyzer, so SomeBuiltin.call() would error
with "no analyser registered" if a graph annotated those callsites
(the formatting helpers in type_methods.rs use them).  Register an
analyzer returning a mutable SomeString, mirroring the std.mem.align_of
stub-coverage analyzer.

* jit_codewriter/call: canonicalize leaf-match alias selection

When the leaf-match fallback collapses several aliases of one source
graph (all_same by FunctionGraph.name), it returned the bucket's
arbitrary first entry.  Return the lexicographically smallest segments
instead, so the resolved CallPath is deterministic across runs and
lines up consistently with the path-keyed registries
(function_fnaddrs / return_types / builtin_targets / portal_targets).

* register: fold null-pointer static RHS to LLAddress(Nu…
youknowone added a commit that referenced this pull request Jun 20, 2026
…215)

mark_object: collect a traced object's child refs into a reused scratch
buffer (`IncrementalMarkState.mark_scratch`, mem::take'd per call) instead
of allocating a fresh `Vec<GcRef>` on the custom_trace path and cloning
`type_info.gc_ptr_offsets` on the plain path. Both paths now collect into
the buffer while the immutable `self.types` borrow is live, then grey the
children after the borrow ends. Removes the per-marked-object allocation
(#215 fix #5).

Nursery sizing: port env.py's estimate_best_nursery_size /
best_nursery_size_for_L2cache / get_L2cache_darwin (env.py:413-456). When
PYPY_GC_NURSERY is unset, size the nursery to half the L2+L3 cache when
that exceeds 8MB, else the 4MB unknown-cache fallback. Replaces the
"estimator not ported" stopgap in default_nursery_size (#215 fix #3).
macOS reads hw.l2cachesize/hw.l3cachesize via sysctl (libc); other
platforms keep the -1 (4MB) fallback until their probe is ported. No-op on
8MB-L2 machines (returns 4MB) but parity-correct on larger-cache hosts.

check.py 127/127 both backends; majit-gc 160 tests green.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Jun 21, 2026
…215)

mark_object: collect a traced object's child refs into a reused scratch
buffer (`IncrementalMarkState.mark_scratch`, mem::take'd per call) instead
of allocating a fresh `Vec<GcRef>` on the custom_trace path and cloning
`type_info.gc_ptr_offsets` on the plain path. Both paths now collect into
the buffer while the immutable `self.types` borrow is live, then grey the
children after the borrow ends. Removes the per-marked-object allocation
(#215 fix #5).

Nursery sizing: port env.py's estimate_best_nursery_size /
best_nursery_size_for_L2cache / get_L2cache_darwin (env.py:413-456). When
PYPY_GC_NURSERY is unset, size the nursery to half the L2+L3 cache when
that exceeds 8MB, else the 4MB unknown-cache fallback. Replaces the
"estimator not ported" stopgap in default_nursery_size (#215 fix #3).
macOS reads hw.l2cachesize/hw.l3cachesize via sysctl (libc); other
platforms keep the -1 (4MB) fallback until their probe is ported. No-op on
8MB-L2 machines (returns 4MB) but parity-correct on larger-cache hosts.

check.py 127/127 both backends; majit-gc 160 tests green.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Jun 21, 2026
…215)

mark_object: collect a traced object's child refs into a reused scratch
buffer (`IncrementalMarkState.mark_scratch`, mem::take'd per call) instead
of allocating a fresh `Vec<GcRef>` on the custom_trace path and cloning
`type_info.gc_ptr_offsets` on the plain path. Both paths now collect into
the buffer while the immutable `self.types` borrow is live, then grey the
children after the borrow ends. Removes the per-marked-object allocation
(#215 fix #5).

Nursery sizing: port env.py's estimate_best_nursery_size /
best_nursery_size_for_L2cache / get_L2cache_darwin (env.py:413-456). When
PYPY_GC_NURSERY is unset, size the nursery to half the L2+L3 cache when
that exceeds 8MB, else the 4MB unknown-cache fallback. Replaces the
"estimator not ported" stopgap in default_nursery_size (#215 fix #3).
macOS reads hw.l2cachesize/hw.l3cachesize via sysctl (libc); other
platforms keep the -1 (4MB) fallback until their probe is ported. No-op on
8MB-L2 machines (returns 4MB) but parity-correct on larger-cache hosts.

check.py 127/127 both backends; majit-gc 160 tests green.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Jun 21, 2026
…ship (#215); opt-in walker vframe inline (default off) (#219)

* majit-gc: port incminimark major-collection threshold; O(1) oldgen membership (#215)

Replace the `oldgen.total_bytes() > last_major_bytes * 1.82` major-cycle
gating (no lower floor) with incminimark's threshold model: add
`major_collection_threshold`, `growth_rate_max`, `min_heap_size`,
`max_heap_size`, `max_delta`, `next_major_collection_initial` and
`next_major_collection_threshold` fields, plus `get_total_memory_used`,
`threshold_reached` and `set_major_threshold_from` (incminimark.py:304-310,
562-594, 1264-1290, 2566-2577). `min_heap_size` defaults to
`max(PYPY_GC_MIN or nursery*8, nursery*major_collection_threshold)`, so a
new major cycle no longer starts off a near-zero surviving baseline.

This stops fib_recursive (tiny live set, huge allocation volume) from
thrashing major cycles: ~3653 major collections for fib(34) drop to a
handful, and `seed_major_roots` (was ~50% of non-idle CPU) leaves the
profile.

Deferring majors lets the old gen grow, which exposed `oldgen.contains`
as a hot O(n) linear scan (called per jitframe root in minor-collection
`walk_jf_roots` and per traced field in `is_managed_heap_object`),
turning the win into an O(n^2) regression. Restore O(1) membership with a
`payloads` address index on `OldGen`, standing in for the arena page-bounds
check incminimark's ArenaCollection provides (pyre's OldGen allocates each
object through the system allocator, so there is no range to test).

fib_recursive (dynasm): 1.36s -> 0.88s. check.py 127/127 both backends;
majit-gc 159 tests + dynasm backend 56+12 green.

`max_delta` defaults to the incminimark pre-setup sentinel (unbounded)
until `env.get_total_memory()` is ported (#215 fix #3); the
`total*ratio` term governs on any heap small relative to total memory.

Assisted-by: Claude

* majit-gc: reuse mark scratch buffer; port estimate_best_nursery_size (#215)

mark_object: collect a traced object's child refs into a reused scratch
buffer (`IncrementalMarkState.mark_scratch`, mem::take'd per call) instead
of allocating a fresh `Vec<GcRef>` on the custom_trace path and cloning
`type_info.gc_ptr_offsets` on the plain path. Both paths now collect into
the buffer while the immutable `self.types` borrow is live, then grey the
children after the borrow ends. Removes the per-marked-object allocation
(#215 fix #5).

Nursery sizing: port env.py's estimate_best_nursery_size /
best_nursery_size_for_L2cache / get_L2cache_darwin (env.py:413-456). When
PYPY_GC_NURSERY is unset, size the nursery to half the L2+L3 cache when
that exceeds 8MB, else the 4MB unknown-cache fallback. Replaces the
"estimator not ported" stopgap in default_nursery_size (#215 fix #3).
macOS reads hw.l2cachesize/hw.l3cachesize via sysctl (libc); other
platforms keep the -1 (4MB) fallback until their probe is ported. No-op on
8MB-L2 machines (returns 4MB) but parity-correct on larger-cache hosts.

check.py 127/127 both backends; majit-gc 160 tests green.

Assisted-by: Claude

* majit-gc: port get_total_memory; max_delta defaults to 0.125*total (#215)

Close the one parity deviation left by the threshold-model port: max_delta's
unset default was incminimark.py:310's pre-setup sentinel float(r_uint(-1))
instead of the setup value 0.125 * env.get_total_memory() (incminimark.py:498).

Port env.get_total_memory (env.py:100-127): macOS reads hw.memsize via sysctl,
clamped/fallen-back to the addressable size by get_total_memory_darwin
(env.py:100-110); other platforms return the addressable size (env.py:126-127;
the Linux /proc/meminfo probe is not yet ported). Hoist get_darwin_sysctl_signed
(env.py:387-411) out of get_l2cache so both probes share it. with_config now
sets max_delta = 0.125 * get_total_memory() when PYPY_GC_MAX_DELTA is unset.

The finish_incremental_cycle cap min(total*major_collection_threshold,
total+max_delta) now binds at ~0.125*RAM as in production, instead of never
(the sentinel dwarfed every realistic heap). Byte-identical for heaps small
relative to RAM (fib_recursive unchanged).

check.py 127/127 both backends; majit-gc 160 tests green.

Assisted-by: Claude

* majit-gc: share read_float_and_factor_from_env across the env readers (#215)

Extract env.py:17-36 `_read_float_and_factor_from_env` as a shared helper and
rebuild the readers on it (env.py:38-50):
- read_uint_from_env (renamed from read_size_from_env) = value*factor as a
  positive byte count, used for PYPY_GC_NURSERY/MIN/MAX/MAX_DELTA.
- read_float_from_env now returns the value only when no size factor was given
  (factor != 1 -> unset), matching env.py:46-50; previously it did a plain
  parse and silently accepted a suffixed value like "1.5g".

Document at the finish_incremental_cycle threshold update that the `bounded`
result of set_major_threshold_from is intentionally dropped: incminimark.py
:2603-2615 raises MemoryError on `bounded and threshold_reached`, but pyre has
no GC out-of-memory path (PYPY_GC_MAX OOM policy unported).

check.py 127/127 both backends; majit-gc 160 tests green.

Assisted-by: Claude

* multiframe inline: collect callee snapshot boxes at the carried jitcode_pc liveness

`collect_callee_active_boxes` queried the callee frame's live register banks
via `frame_liveness_reg_indices_by_bank_at(callee_py_pc)`, which resolves the
JitCode coordinate through the lossy `pc_map`. The resume decoder consumes the
frame's section per the liveness at the carried `jitcode_pc`
(`setposition` -> `get_current_position_info`), so the two coordinates could
resolve to different liveness windows.

For a forward-branch callee inlined under the multi-frame path, a branch guard
stashes its own JitCode offset in `BRANCH_GUARD_JITCODE_PC`; when that offset
mapped to a different liveness window than `pc_map(callee_py_pc)`, the encoder
wrote the box banks for one window while the decoder read section sizes for the
other. A callee that int-specializes a param then put a Ref where the int
section expected a value, panicking at resume with
`getvirtual_int: not a raw virtual`.

Compute `callee_jitcode_pc` before the box collection and pass it to
`collect_callee_active_boxes`, which now calls
`frame_liveness_reg_indices_by_bank_at_with_jitcode_pc` with the same carried
word the snapshot carries and the decoders (`collect_outer_active_boxes`,
`setup_bridge_sym`, `rebuild_inline_callee`) already use.

check.py dynasm 131/131, cranelift 131/131.

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.

2 participants