Skip to content

Fix merge points in fannkuch loop - #34

Merged
youknowone merged 6 commits into
youknowone:mainfrom
lifthrasiir:fix-fannkuch-merge-points
May 13, 2026
Merged

Fix merge points in fannkuch loop#34
youknowone merged 6 commits into
youknowone:mainfrom
lifthrasiir:fix-fannkuch-merge-points

Conversation

@lifthrasiir

@lifthrasiir lifthrasiir commented May 13, 2026

Copy link
Copy Markdown
Contributor

Summary

This pull request makes several important changes to the JIT and meta-interpreter infrastructure, focusing on improving correctness, Python compatibility, and test coverage. The most notable updates include refining how virtual box types are determined during resume state construction, updating the default retrace limit for better alignment with upstream PyPy behavior, and enhancing test coverage for new and existing behaviors. Additionally, the handling of merge points in JIT code generation is made more robust, and a new field is added to the JIT frame state to support more accurate exception guard handling.

Resume state and virtual box handling:

  • The logic for determining the type of a box during resume state construction in ResumeDataLoopMemo::number is updated to use the replacement box's type, matching PyPy's behavior and preventing incorrect virtual classification. A targeted test is added to verify this behavior.

JIT configuration and merge point handling:

  • The default retrace limit is changed from 5 to 0 in warmstate.rs, aligning with PyPy's default and ensuring immediate retry after the first cancelled unrolled compile. Associated comments are updated for clarity.
  • The merge point offset logic in JitCodeBuilder is relaxed to allow multiple merge points per portal jitcode, preserving only the first offset for schema validation and supporting PyPy's dispatch loop semantics.

Test coverage and state struct enhancements:

  • A new field, suppress_guard_no_exception_for_opcode, is added to MIFrame to track when a Python opcode's exception guard should be suppressed due to primitive lowering. All relevant test instantiations are updated to set this field.

Optimizer and virtual state handling:

  • The optimizer no longer exports the virtual state before flushing and forcing at the end of the preamble, and now passes None for the virtual state in jump handling, simplifying the logic and better matching RPython semantics.

Self-review

Prompt & Model

Model: gpt-5.5

Prompt:

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

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

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

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

Answer

• 1. Cases Where Patch Regressed PyPy Parity Compared To Main

  None found.

  The optimizer change in majit/majit-metainterp/src/optimizeopt/optimizer.rs:3241 restores parity: RPython optimize_bridge() does
  flush(), then force_box_for_end_of_preamble(), then calls jump_to_existing_trace(), whose _jump_to_existing_trace() computes
  get_virtual_state(jump_op.getarglist()) at that point. Passing None for the precomputed virtual state now matches that timing.

  2. Other Mismatches Introduced By Patch

  None found as behavior mismatches.

  The STORE_SUBSCR exception-guard suppression in pyre/pyre-jit-trace/src/trace_opcode.rs:4524 matches the RPython shape: once the
  path is lowered to list primitives, RPython has check_neg_index/setarrayitem style ops, not an exc=True residual call followed by
  generic handle_possible_exception().

  The resume numbering change in majit/majit-metainterp/src/resume.rs:4098 also matches RPython resume.py: it calls
  box.get_box_replacement() before checking box.type.

  3. Mismatches That Already Existed Before This Patch

  No newly relevant pre-existing parity bugs found in the changed areas.

  I did observe existing non-1:1 code around list index normalization: majit/majit-translate/src/codegen.rs:2274 adds bounds guards
  for raw-pointer safety where RPython opimpl_check_resizable_neg_index() only normalizes negative indices. That appears intentional
  and belongs under structural adaptation, not a patch regression.

  4. Structural Adaptations

  - pyre/pyre-jit/src/jit/codewriter.rs:3148: emitting portal jit_merge_point at every detected Python loop header is a Pyre lowering
    adaptation. PyPy has one syntactic pypyjitdriver.jit_merge_point() in PyFrame.dispatch(), executed every bytecode dispatch; Pyre
    materializes loop headers inside generated dispatch jitcode.
  - majit/majit-metainterp/src/jitcode/assembler.rs:1350: keeping the first jit_merge_point_offset is Pyre schema-validation state,
    not an RPython structure.
  - pyre/pyre-jit-trace/src/trace_opcode.rs:6056: suppress_guard_no_exception_for_opcode compensates for Pyre’s bytecode-level may-
    raise classification. RPython attaches exception handling to individual JIT operations during jtransform/pyjitpl execution.
  - majit/majit-metainterp/src/warmstate.rs:272: default retrace_limit = 0 matches rpython/rlib/jit.py production parameters; the old
    5 was only the warmspot.py test harness default.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed exception guard emission for list assignment operations to prevent unnecessary guards.
    • Updated resume data classification to correctly identify virtual values in snapshot slots.
  • Chores

    • Adjusted default retrace limit setting from 5 to 0.
    • Relaxed merge-point validation to allow multiple emissions per dispatch cycle.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

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

You’ve run out of usage credits. Purchase more 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: 9eade0da-7d64-4671-ba1c-952377baa7c5

📥 Commits

Reviewing files that changed from the base of the PR and between ed4e19f and a03950c.

📒 Files selected for processing (3)
  • majit/majit-metainterp/src/resume.rs
  • pyre/pyre-jit-trace/src/state.rs
  • pyre/pyre-jit/src/jit/codewriter.rs

Walkthrough

This PR introduces five coordinated enhancements to the JIT compilation system. Portal jit_merge_point emission is relaxed to allow multiple calls per dispatch instead of asserting on the first call only. Exception-guard suppression for primitive-lowered opcodes is implemented via a new per-opcode flag on MIFrame. Bridge optimization virtual-state handling is simplified by removing pre-force state capture. Resume numbering now correctly uses replacement OpRef types for virtual classification. Default retrace limits are reduced from 5 to 0.

Changes

JIT System Enhancements for Portal Merge-Points, Exception Guards, and State Handling

Layer / File(s) Summary
Portal Merge-Point Relaxation
majit/majit-metainterp/src/jitcode/assembler.rs, pyre/pyre-jit/src/jit/codewriter.rs
jit_merge_point now conditionally records the code offset only on first call, preserving the initial value across subsequent calls. Portal loop-header emission is gated by is_portal rather than the single merge_point_pc hint, allowing multiple merge-point emissions per dispatch body.
Guard Exception Suppression for Primitive-Lowered Opcodes
pyre/pyre-jit-trace/src/state.rs, pyre/pyre-jit-trace/src/trace_opcode.rs, pyre/pyre-jit-trace/src/jitcode_dispatch.rs
New suppress_guard_no_exception_for_opcode boolean field on MIFrame is initialized to false and reset before each opcode execution. When the STORE_SUBSCR list-strategy primitive-lowering path handles an opcode, the flag is set to true, preventing handle_possible_exception from emitting GUARD_NO_EXCEPTION for that step. All test fixtures and dispatch tests updated to initialize the new field.
Bridge Virtual-State Simplification
majit/majit-metainterp/src/optimizeopt/optimizer.rs
Pre-force virtual-state capture (pre_force_vs) is removed from the bridge path. Both try_jump_to_existing_trace calls now pass pre_vs: None instead of the pre-captured state, simplifying jump-trace lookup logic.
Resume Numbering with Replacement OpRef Types
majit/majit-metainterp/src/resume.rs
Resume numbering in _number_boxes now determines virtual classification from the replacement OpRef's intrinsic type rather than the snapshot slot's fallback type, correctly handling slots forwarded via env.replacements. Unit test added to verify a slot typed as Int but forwarded to a Ref virtual is classified as TAGVIRTUAL.
Default Retrace Limit Reduction
majit/majit-metainterp/src/warmstate.rs
DEFAULT_RETRACE_LIMIT constant changed from 5 to 0. Inline documentation in WarmEnterState::new and WarmEnterState::with_jitlog updated to reflect the new default.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • youknowone

Poem

🐰 Portal paths now split and trace in dance,
Guards suppress their cries on SUBSCR's advance,
Types find their truth through replacements' sight,
While limits fall to zero's grace so light.
Hop, code, hop!

🚥 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 PR title clearly identifies the primary change: fixing merge points in the fannkuch loop, which is the main focus of merge-point handling updates across multiple files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

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

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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

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

4468-4604: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Remove the now-unreachable loop_header fallback.

is_portal is derived from portal_jd_index.is_some() on Line 3016, so after Line 4469 the else if let Some(jdindex) = portal_jd_index arm can never run. Leaving it here makes the new contract harder to follow because it still looks like some loop headers may emit loop_header instead of jit_merge_point.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/pyre-jit/src/jit/codewriter.rs` around lines 4468 - 4604, The code
contains a dead "else if let Some(jdindex) = portal_jd_index" branch guarded by
is_portal (which was set as portal_jd_index.is_some()), so remove the
unreachable loop_header fallback: delete the entire else-if arm that builds
loop_header_op and its GraphFlattener.emit_space_operation block, leaving only
the is_portal branch that emits "jit_merge_point"; ensure no other code depends
on the removed jdindex binding and run tests to confirm behavior.
🤖 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/resume.rs`:
- Around line 5096-5124: The test
test_number_boxes_uses_replacement_type_for_virtual_classification currently
uses SimpleBoxEnv whose is_virtual_ref and is_virtual_raw share the same backing
set, hiding regressions; change the test to use a small test-local BoxEnv double
(implementing the same trait used by ResumeDataLoopMemo) that overrides
is_virtual_ref and is_virtual_raw to return true for is_virtual_ref(target) and
false for is_virtual_raw(target), while keeping replacements/virtuals/types
entries as before; this will force _number_boxes (invoked via
ResumeDataLoopMemo::number) to take the ref-virtual branch and make wrong
type-selection observable. Ensure the new env still provides
replacements.insert, virtuals.insert, and types.insert behavior used earlier in
the test.
- Around line 4111-4118: The code currently skips calling opref.ty() when opref
!= raw_opref, risking wrong classification; change the box_type computation to
prefer the replacement OpRef's intrinsic type by always checking opref.ty()
first (falling back to snapshot_box.tp and then env.get_type(opref)) even in the
else branch—i.e., ensure the logic that computes box_type for both the opref ==
raw_opref and opref != raw_opref cases uses
opref.ty().or(snapshot_box.tp).unwrap_or_else(|| env.get_type(opref)) so typed
replacements are honored.

In `@majit/majit-metainterp/src/warmstate.rs`:
- Around line 272-273: The DEFAULT_RETRACE_LIMIT constant is incorrect (set to
0) and its comment misstates PyPy's default; update the constant
DEFAULT_RETRACE_LIMIT to 5 to match PyPy's retrace_limit default and adjust or
remove the comment referencing rlib/jit.py:595 so it accurately reflects the
intent (or note any deliberate deviation if you intend a non-PyPy default).
Ensure you change the value where DEFAULT_RETRACE_LIMIT is defined and update
the adjacent comment text to correctly state the source/default.

In `@pyre/pyre-jit-trace/src/state.rs`:
- Around line 1596-1600: Two MIFrame struct literals are missing the new
required field suppress_guard_no_exception_for_opcode which causes a
missing-field compile error; update each MIFrame construction (the literals
identified in the diff) to include suppress_guard_no_exception_for_opcode: false
so the struct literal matches the MIFrame definition and compiles. Ensure both
MIFrame instances that currently lack this field are updated.

---

Outside diff comments:
In `@pyre/pyre-jit/src/jit/codewriter.rs`:
- Around line 4468-4604: The code contains a dead "else if let Some(jdindex) =
portal_jd_index" branch guarded by is_portal (which was set as
portal_jd_index.is_some()), so remove the unreachable loop_header fallback:
delete the entire else-if arm that builds loop_header_op and its
GraphFlattener.emit_space_operation block, leaving only the is_portal branch
that emits "jit_merge_point"; ensure no other code depends on the removed
jdindex binding and run tests to confirm behavior.
🪄 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: 362bc717-6552-4d2c-8c3f-d73284c7e24e

📥 Commits

Reviewing files that changed from the base of the PR and between 73b218d and ed4e19f.

📒 Files selected for processing (8)
  • majit/majit-metainterp/src/jitcode/assembler.rs
  • majit/majit-metainterp/src/optimizeopt/optimizer.rs
  • majit/majit-metainterp/src/resume.rs
  • majit/majit-metainterp/src/warmstate.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch.rs
  • pyre/pyre-jit-trace/src/state.rs
  • pyre/pyre-jit-trace/src/trace_opcode.rs
  • pyre/pyre-jit/src/jit/codewriter.rs

Comment thread majit/majit-metainterp/src/resume.rs Outdated
Comment thread majit/majit-metainterp/src/resume.rs
Comment thread majit/majit-metainterp/src/warmstate.rs
Comment thread pyre/pyre-jit-trace/src/state.rs

@youknowone youknowone left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

awesome!

@youknowone
youknowone merged commit 7f42780 into youknowone:main May 13, 2026
23 of 25 checks passed
youknowone added a commit that referenced this pull request Jul 18, 2026
Add FBW_APPEND_PROMOTE_JOURNAL recording each list promoted Empty→typed
during a speculative full-body walk, wired parallel to FBW_APPEND_JOURNAL
through reset/commit/rollback and the GC root walker area. Rollback drains
it after the length-rewind and calls w_list_clear to restore the Empty
strategy + drop the typed backing block — the strategy switch + realloc the
length journal alone cannot undo.

No caller yet (fbw_append_promote_journal_push is #[allow(dead_code)] until
the recognize gate admits the empty append); strict no-op with the gate off.
Removable when single-executor tracing lands (gh#73/#34).

Assisted-by: Claude
youknowone added a commit that referenced this pull request Jul 18, 2026
Add FBW_APPEND_PROMOTE_JOURNAL recording each list promoted Empty→typed
during a speculative full-body walk, wired parallel to FBW_APPEND_JOURNAL
through reset/commit/rollback and the GC root walker area. Rollback drains
it after the length-rewind and calls w_list_clear to restore the Empty
strategy + drop the typed backing block — the strategy switch + realloc the
length journal alone cannot undo.

No caller yet (fbw_append_promote_journal_push is #[allow(dead_code)] until
the recognize gate admits the empty append); strict no-op with the gate off.
Removable when single-executor tracing lands (gh#73/#34).

Assisted-by: Claude
youknowone added a commit that referenced this pull request Jul 18, 2026
Add FBW_APPEND_PROMOTE_JOURNAL recording each list promoted Empty→typed
during a speculative full-body walk, wired parallel to FBW_APPEND_JOURNAL
through reset/commit/rollback and the GC root walker area. Rollback drains
it after the length-rewind and calls w_list_clear to restore the Empty
strategy + drop the typed backing block — the strategy switch + realloc the
length journal alone cannot undo.

No caller yet (fbw_append_promote_journal_push is #[allow(dead_code)] until
the recognize gate admits the empty append); strict no-op with the gate off.
Removable when single-executor tracing lands (gh#73/#34).

Assisted-by: Claude
youknowone added a commit that referenced this pull request Jul 18, 2026
* jit: add PYRE_EMPTY_APPEND_VIRT gate scaffold (default-off)

Add `empty_append_virt_enabled()` reading `PYRE_EMPTY_APPEND_VIRT`
(default-off), mirroring `newlist_virt_enabled()`. The gate will admit the
empty-list first append into the orthodox `w_list_append` fold in a later
slice. No caller yet; `#[allow(dead_code)]` until wired.

Assisted-by: Claude

* jit: add dormant append-promote restore journal for FBW rollback

Add FBW_APPEND_PROMOTE_JOURNAL recording each list promoted Empty→typed
during a speculative full-body walk, wired parallel to FBW_APPEND_JOURNAL
through reset/commit/rollback and the GC root walker area. Rollback drains
it after the length-rewind and calls w_list_clear to restore the Empty
strategy + drop the typed backing block — the strategy switch + realloc the
length journal alone cannot undo.

No caller yet (fbw_append_promote_journal_push is #[allow(dead_code)] until
the recognize gate admits the empty append); strict no-op with the gate off.
Removable when single-executor tracing lands (gh#73/#34).

Assisted-by: Claude

* jit: virtualize empty-list first append via Empty->typed promotion (gated)

Admit the Empty-strategy first LIST_APPEND on the FBW walker under the
PYRE_EMPTY_APPEND_VIRT gate (default off). recognize classifies the append
by the value's type in switch_to_correct_strategy's int->float->object order;
commit installs the typed backing block on the concrete receiver, emits the
Empty->typed transition as inline heap IR mutating the existing wrapper, and
journals the rewind to Empty for a non-commit walk.

emit_promote_empty_list_inline returns the transition's NewArray block OpRef.
commit stamps it with the concrete backing-block address (new
w_list_items_block_ptr) so the append body sub-walk's
`list.items_block.capacity` getfield folds via field_sanity_load to the
concrete capacity (1); the spare-capacity `0 < capacity` branch then resolves
instead of aborting with a symbolic GOTO_IF_NOT condition.

The concrete strategy switch runs before the IR emit so the backing block
exists when the block OpRef is stamped.

Assisted-by: Claude

* jit: enable empty-list first-append promotion by default

Flip the PYRE_EMPTY_APPEND_VIRT gate to default-on (set PYRE_EMPTY_APPEND_VIRT=0
to restore the residual abort), matching the newlist_virt_enabled default-on
convention. dynasm and cranelift both pass check.py 194/194 with the gate on.

Assisted-by: Claude

* jit: fold empty-append capacity via heapcache seed, drop concrete-stamp path

Replace the NewArray-block concrete-address stamp with a heapcache capacity
seed. emit_promote_empty_list_inline now seeds the new block's ItemsBlock.capacity
getfield cache with the const (1) on the Integer and Float arms, so the append
body sub-walk's spare-capacity `0 < capacity` check folds to the const instead
of reading a symbolic getfield. This is the same shape the heapcache uses for a
const-length new_array; the capacity is read through a getfield (not arraylen),
so the field-index channel is seeded explicitly.

Removes the emit_promote_empty_list_inline `Option<OpRef>` return, the
commit-site reorder + set_opref_concrete stamp, and the now-unused
w_list_items_block_ptr accessor. Object storage needs no seed (its capacity
read resolves through list.items already).

A/B verified both levers independently compile the empty-append loop; the seed
is the smaller and matches the heapcache const-length model. dynasm+cranelift
194/194, list repros byte-identical under MAJIT_GC_STRESS.

Assisted-by: Claude

* jit: reseed vstack mirror across FOR_ITER-entry permutation; narrow LIST_APPEND admit to call-free bodies

The py3.14 inlined comprehension emits a SWAP/BUILD_LIST/SWAP preamble
before its FOR_ITER, lowered non-monotonically in jitcode: the JitCode-PC
floor pivot maps a later jit_pc back to an earlier py_pc, so the walk
re-visits those preamble py_pcs out of order (a backward py transition,
then a forward re-walk). reconcile_vstack_at_boundary replayed the SWAP
effect and reused a stale vstack_last_ref across that region, dropping the
just-built list from the vstack_boxes mirror; the stack_sync overlay then
copied the corrupt mirror into the resume snapshot, so a deopt from the
comprehension body reconstructed a frame whose FOR_ITER iterator slot held
a duplicated iterator and a dropped list ("not an iterator").

Add WalkContext.vstack_reorder_ceiling: when the previous opcode is a
Swap/Copy and the py transition is backward (a codewriter layout artifact,
never a real branch), reseed the whole mirror from the virtualizable
shadow (VstackOpClass::ShadowReseed) until the walk advances past the
py_pc it backed off from.

Admit LIST_APPEND in for_iter_bodies_all_jit_safe only when its FOR_ITER
body performs no CALL. A per-element user-frame call (a class ctor / user
function) sets the FOR_ITER in-flight body-effect flag, and a mid-body
abort then routes through fbw_foriter_inflight_take, which refuses
delivery to avoid a double-apply and drops the trace-attempt iteration's
item. Call-free comprehension bodies never enter a user frame and stay
admissible; call-bearing bodies decline to interpretation.

Assisted-by: Claude

* jit: reload comprehension LIST_APPEND list receiver from its value-stack slot

The codewriter lowered LIST_APPEND's list operand by cloning the peeked
FlowValue from the compile-time stack. The inlined-comprehension
accumulator lives on the operand stack, which is not loop-carried in
registers (the jit_merge_point reds are [frame, ec]), so the preamble
BUILD_LIST value is not a live register inside the resident loop. The
jit_list_append residual then read an unbound register for its receiver
and aborted ResidualCallArgUnbound, so the comprehension inner loop never
compiled and the #171 empty-append fold was never reached.

Reload the list receiver from its value-stack slot via
getarrayitem_vable_r each iteration, the same per-iteration
re-materialization the FOR_ITER iterator reload already uses. Call-free
comprehension inner loops (`[j for j in range(n)]`) now compile with the
append folded. A comprehension whose body enters a user frame (e.g.
`[C(i) for i in ...]`) still declines via for_iter_bodies_all_jit_safe's
call-free LIST_APPEND gate, since its in-flight FOR_ITER delivery is a
separate concern.

Assisted-by: Claude

* jit: restrict LIST_APPEND FOR_ITER body admit to bare-accumulator shape

A comprehension whose LIST_APPEND lands in a list Object-strategy
(str/None/tuple/list/dict/f-string element) lowers the append to a Void
setarrayitem_gc residual. The FBW body-effect gate marks that Void write
as a body effect, so fbw_foriter_inflight_take refuses delivery of the
in-flight FOR_ITER item on the trace-attempt abort and drops one
iteration (len 999 vs 1000). int/float-strategy appends fold via the pure
length-rewind journal and stay correct.

Replace the body_has_call scan with a body_has_list_append scan: when a
FOR_ITER body contains LIST_APPEND, admit only the bare-accumulator ops
(LoadFast/StoreFast family, ListAppend, stack shuffles, backward jumps,
ExtendedArg/Cache). Any value-producing op declines the body to the
interpreter. Bodies without LIST_APPEND keep the existing predicate.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 19, 2026
This reverts commit 8a3bdd85b89a55552f8046ef4018b18903e3bdd1.

Admitting a call-bearing `LIST_APPEND` body reaches the in-flight delivery
gap often enough to lose an element. `[m.heappop(heap) for i in
range(size)]` aborts with `LoopBearingCalleeInlineUnsupported { pc: 40,
blackhole_required: true }` and the flush declines the multi-frame blackhole
handoff ("no outer caller resume pc"), so the legacy replay drops the
iteration the walk had already consumed: `test.test_heapq` raises
`IndexError: index out of range` out of `test_heapsort`, and
`extra_tests/parity_tests/for_iter_call_bearing_comprehension.py` builds 22
of 23 elements. Both are deterministic here and both pass on
`61dc91a5c1e`, so the widening owns them.

The commit says as much — it widens how often the gap is reached, and the
call scan it deleted was never a boundary the gap stayed behind. Closing
the gap is gh#73/#34; until then the scan is what keeps these shapes on the
interpreter. Declining to trace is semantically safe, so this is a
coverage reduction rather than a parity change.

What the commit bought comes back with it: seven synth fixtures compile
less again and their jitstats baselines return to the recorded values,
`mapdict_frozen_unboxing_fold`'s build phase goes back to 0.612s from
0.330s, and `exception_group_type` / `list_to_tuple_star` compile zero
loops again.

check.py dynasm 440/440.

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