Skip to content

jit: decline to fold float-result pure calls in the walker; majit-ir clippy allows; record 6 missing jit-stats baselines - #963

Merged
youknowone merged 3 commits into
mainfrom
rewrite-tracer
Aug 2, 2026
Merged

jit: decline to fold float-result pure calls in the walker; majit-ir clippy allows; record 6 missing jit-stats baselines#963
youknowone merged 3 commits into
mainfrom
rewrite-tracer

Conversation

@youknowone

@youknowone youknowone commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Two independent changes.

1. Decline to fold float-result pure calls in the walker

try_fold_pure_call_via_executor routed every constant-funcbox CallPure*
through executor::execute_pure_call, whose Float arm calls
call_int_function and reinterprets the integer return register via
f64::from_bits.

That convention is a caller contract, not a property of the descr. Two emitters
bake operand 0 of a residual call, with opposite float conventions:

emitter funcbox float return
majit-translate codewriter (LLBC path) the callee's own address real f64 register
runtime *_canonical_via_target (jitcode/assembler.rs:3343) JitCallTarget::concrete_ptr i64 with f64::to_bits pre-packed

Both land in the same walker handlers and dispatch on descr.result_type()
alone. CallDescr records the signature, not which emitter produced the
pointer, so no descr-based dispatch can be right for both — feeding a
codewriter funcbox to the i64-bits arm reads whatever was left in the integer
return register.

The fix returns early on result_type() == Type::Float and leaves the recorded
CallPure* for the backend, which calls the same pointer with the descr's real
signature (majit-backend-wasm/src/codegen.rs:909 residual_call_float_sig
builds an f64-returning call_indirect from it). execute_pure_call's
Float arm keeps the i64-bits convention, now documented as a caller
contract.

Adding a bh_call_f instead would only flip which channel is broken. The
discriminator does exist in the data — JitCode::call_descr_to_call_target
(jitcode/mod.rs:367) maps a descr slot back to its JitCallTarget — but
nothing in pyre/ reads it today, so folding these is left to a future change.

Evidence. New regression test walker_declines_to_fold_a_float_result_pure_call
drives a real WalkContext over an f64-returning callee plus an Int
control. Without the decline the float box folds to Float(3.5e-323) =
f64::from_bits(7) — the argument still sitting in the integer register —
while the CallPureI sibling in the same fixture folds to 42, so the fixture
is not the problem. A standalone probe against jit_bigint_to_f64_or_inf
mismatches 7/7 (std::hint::black_box is required, or LLVM sees through the
transmute and re-emits a correctly-ABI'd direct call, which is why the
pre-existing float_result_routes_through_call_int_function_with_bits_packing
test passed).

Latent, not live. Instrumenting the fold for result_type() == Float and
sweeping all ~1400 files of pyre/bench + pyre/bench/synth (check.py's
corpus) found zero occurrences.

2. Allow three deny-by-default clippy lints in majit-ir

cargo clippy -p majit-ir failed to compile on three deny-by-default lints, so
the crate could not be linted at all. Each is a false positive against
deliberate structure and gets a scoped #[allow] plus a comment stating why:

  • ineffective_bit_mask on the eval-breaker flag-width static assertion — every
    operand is a constant, so there is no runtime value to compare.
  • absurd_extreme_comparisons on OpCode::is_finalFINAL_FIRST is
    OpCode::Jump as u16 == 0 today; the bound is kept symbolic to match the
    sibling classifiers and to stay correct if the category stops leading the enum.
  • mut_from_ref on SharedConstPool::as_mut_vec_for_gc — the pool is
    UnsafeCell-backed so the GC root walker can mutate through a shared
    reference, and the existing # Safety clause carries the contract.

No behavior change; the crate now reports 0 errors and 41 warnings.

Verification

3. Record the six missing synthetic jit-stats baselines

hot_loop_exit_then_class_stmt (#890) and five fixtures from #934/#948 were
added without a committed .jitstats, so check.py failed them on both backends
with "no committed jit-stats baseline". Recorded one fixture at a time with
--snapshot --synthetic-pattern <name>.py, so no existing baseline is
rewritten; 350 synth scripts now have 350 dynasm and 350 cranelift baselines.

Two record a weak floor worth revisiting: pypy_dict_primitives_nonbinding
compiles no loop at all (loops_compiled=0), and raise_reg_unbound_jitstress
enters at loops_aborted=10.

CI status — the remaining 8 failures are pre-existing on main

main at f3bddb31f28 fails pyre/check.py on all three platforms with
exactly the same 14 fixtures and the same numbers as this branch did before
commit 3 (compare run 30707884899 against 30713388334). This PR introduces no
check.py regression; the 6 baseline recordings above take it from 14 to 8
failed / 357 passed
per backend.

The 8 that remain are jit-stats floor regressions that entered main between
#947 (which recorded the baselines) and the current tip. Measured root split —
one build with an env-gated early return true in
optimizeopt/heap.rs quasiimmut_field_still_valid, all 8 fixtures re-run both
ways:

fixture with #956's backstop backstop disabled
exception_reraise_tb_depth_jitstress aborted 0→1198, compiled 805→305 passes
the other 7 as reported unchanged

So exactly one is #956's quasi-immutable revalidation. Reverting #956 is not
the fix: PyPy aborts 2100× on this same script (abort: force quasi-immut), so
firing here is faithful — what pyre lacks is upstream's primary mechanism,
opimpl_jit_force_quasi_immut (pyjitpl.py:1104-1118), which aborts early at
the write instead of letting every event fall through to the optimizer
backstop. ABORT_FORCE_QUASIIMMUT is a dead counter in pyre today
(pyjitpl.rs:15634, no site raises it). That port is a separate slice.

The other 7 (closure_freevar_branch_resume, exception_args_virtual,
exception_multi_handler_warmup, list_length_hint_validate,
sre_pattern_methods, sre_wasm_min, sre_wasm_min1) have a different,
still-unbisected root in the same window.

authored by Claude

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of pure calls that return floating-point values, preventing incorrect result folding and ensuring they execute with the appropriate calling convention.
    • Corrected validation for global-loading calls that legitimately omit a namespace value.
    • Preserved error reporting for other calls that provide an invalid null namespace argument.
  • Tests

    • Added regression coverage for floating-point and integer call results, along with valid and invalid namespace handling.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a30409a7-6241-49df-9821-8646eaf65cff

📥 Commits

Reviewing files that changed from the base of the PR and between 76555da and a80e8d6.

📒 Files selected for processing (6)
  • majit/majit-ir/src/eval_breaker_word.rs
  • majit/majit-ir/src/resoperation.rs
  • majit/majit-ir/src/value.rs
  • majit/majit-metainterp/src/executor.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs

Walkthrough

The change documents intentional Clippy suppressions, clarifies float-call ABI constraints, prevents executor folding for float pure calls, and adds regression coverage for pure-call results and nullable LoadGlobal namespace arguments.

Changes

JIT dispatch behavior

Layer / File(s) Summary
Float-call ABI contract
majit/majit-metainterp/src/executor.rs
The documentation defines packed-i64 and direct-f64 return conventions.
Float-call folding guard
pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
Float-returning pure calls remain recorded for backend execution.
Pure-call ABI regression coverage
pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs
Tests verify symbolic CallPureF results and folded integer results.
May-force null-argument coverage
pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs
Tests cover nullable LoadGlobal namespace arguments and the existing error for untagged calls.

Intentional Clippy annotations

Layer / File(s) Summary
Targeted Clippy annotations
majit/majit-ir/src/eval_breaker_word.rs, majit/majit-ir/src/resoperation.rs, majit/majit-ir/src/value.rs
Comments and targeted allowances document intentional bit-mask, comparison, and mutable-reference patterns.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Poem

A rabbit sees CallPureF pause,
While integers fold to 42 without loss.
Null globals pass when tagged just right,
Clippy’s sharp warnings now sit tight.
ABI notes guide each backend run—
Hop, hop, the review is done!

🚥 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 clearly summarizes the main float-call folding change and accurately mentions the related Clippy allowances and jit-stats baselines.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch rewrite-tracer

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 75a17be098

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1577 to +1578
if call_descr.result_type() == majit_ir::Type::Float {
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve concrete execution for pure float calls

When a CallPureF result feeds a float comparison and conditional, this early return leaves the call's OpRef without a concrete value; record_float_cmp consequently cannot stamp its result, and fused_goto_if_not_float raises GotoIfNotValueNotConcrete, aborting tracing for that otherwise supported program. RPython's execute_and_record_varargs executes every residual call before recording it, so retain an emitter/ABI discriminator and invoke the matching float-return route rather than skipping execution entirely.

AGENTS.md reference: AGENTS.md:L194-L196

Useful? React with 👍 / 👎.

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit a80e8d6).
Updated: 2026-08-02T13:45:42.614Z

Files in the reviewed diff
majit/majit-ir/src/eval_breaker_word.rs
majit/majit-ir/src/resoperation.rs
majit/majit-ir/src/value.rs
majit/majit-metainterp/src/executor.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs

1. Regressions to PyPy parity introduced by this patch

None.

2. Other mismatches introduced by this patch

None.

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

  • majit/majit-metainterp/src/executor.rs:824 ↔ rpython/jit/metainterp/executor.py:66-72 — Float calls use call_int_function, whereas PyPy dispatches them through cpu.bh_call_f; a direct extern "C" fn(...) -> f64 pointer is therefore read from the wrong return register. This pre-dates the patch; the patch only documents it.

4. Structural adaptations

  • pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs:1620 ↔ rpython/jit/metainterp/pyjitpl.py:2119-2121 — Pyre now deliberately leaves CallPureF symbolic instead of concrete-executing and stamping its result as PyPy does. This is a Rust ABI adaptation: the descriptor does not distinguish a direct f64 callee pointer from Pyre’s i64-bits wrapper pointer, so executing it here cannot be made reliable.

@youknowone youknowone changed the title jit: decline to fold float-result pure calls in the walker; majit-ir clippy allows jit: decline to fold float-result pure calls in the walker; majit-ir clippy allows; record 6 missing jit-stats baselines Aug 2, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 01b0288e24

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1577 to +1578
if call_descr.result_type() == majit_ir::Type::Float {
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep runtime float wrappers off typed backend calls

When a runtime-emitted call_pure_float_canonical_via_target survives into a completed trace, this return leaves its CallPureF symbolic even though operand 0 is JitCallTarget::concrete_ptr, whose generated ABI is extern "C" fn(...) -> i64. The native backends subsequently derive an f64 return ABI from the CallDescr and invoke that same pointer as an f64 function, reading the floating-point return register instead of the wrapper's integer result and producing a stale/garbage compiled value. Use the call-target side table to execute the wrapper through the integer-bits route, or record the target's f64-returning trace_ptr, rather than universally deferring float calls to the backend.

AGENTS.md reference: AGENTS.md:L14-L19

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@pyre/bench/synth/inline_freevar_after_mayforce.wasm.jitstats`:
- Around line 1-5: Regenerate the Wasm jitstats baselines using the complete
descriptor-counter snapshot emitted by the Wasm runner and recorded by check.py.
Add the recorded descr_set_resolved entry to
pyre/bench/synth/inline_freevar_after_mayforce.wasm.jitstats,
pyre/bench/synth/math_log_trig_hot.wasm.jitstats, and
pyre/bench/synth/pypy_dict_primitives_nonbinding.wasm.jitstats, preserving the
existing counters and generated ordering.
🪄 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 Plus

Run ID: b83334aa-5371-4fa6-b645-65161b352c5a

📥 Commits

Reviewing files that changed from the base of the PR and between 75a17be and 76555da.

📒 Files selected for processing (24)
  • majit/majit-ir/src/eval_breaker_word.rs
  • majit/majit-ir/src/resoperation.rs
  • majit/majit-ir/src/value.rs
  • majit/majit-metainterp/src/executor.rs
  • pyre/bench/synth/hot_loop_exit_then_class_stmt.cranelift.jitstats
  • pyre/bench/synth/hot_loop_exit_then_class_stmt.dynasm.jitstats
  • pyre/bench/synth/hot_loop_exit_then_class_stmt.wasm.jitstats
  • pyre/bench/synth/inline_freevar_after_mayforce.cranelift.jitstats
  • pyre/bench/synth/inline_freevar_after_mayforce.dynasm.jitstats
  • pyre/bench/synth/inline_freevar_after_mayforce.wasm.jitstats
  • pyre/bench/synth/math_log_trig_hot.cranelift.jitstats
  • pyre/bench/synth/math_log_trig_hot.dynasm.jitstats
  • pyre/bench/synth/math_log_trig_hot.wasm.jitstats
  • pyre/bench/synth/pypy_dict_primitives_nonbinding.cranelift.jitstats
  • pyre/bench/synth/pypy_dict_primitives_nonbinding.dynasm.jitstats
  • pyre/bench/synth/pypy_dict_primitives_nonbinding.wasm.jitstats
  • pyre/bench/synth/raise_reg_unbound_jitstress.cranelift.jitstats
  • pyre/bench/synth/raise_reg_unbound_jitstress.dynasm.jitstats
  • pyre/bench/synth/raise_reg_unbound_jitstress.wasm.jitstats
  • pyre/bench/synth/tuple_unpack_array_backed_hot.cranelift.jitstats
  • pyre/bench/synth/tuple_unpack_array_backed_hot.dynasm.jitstats
  • pyre/bench/synth/tuple_unpack_array_backed_hot.wasm.jitstats
  • pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs

Comment on lines +1 to +5
descr_set_absent=0
descr_set_ambiguous=0
descr_set_stale_absent=0
internal_compile_panics=0
loops_aborted=0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Regenerate all Wasm baselines with the complete descriptor-counter contract.

pyre/pyre-wasm-runner/src/main.rs:652-696 emits descr_set_resolved, and pyre/check.py:981-1033 records the complete snapshot. These baselines omit that counter.

  • pyre/bench/synth/inline_freevar_after_mayforce.wasm.jitstats#L1-L5: add the recorded descr_set_resolved value.
  • pyre/bench/synth/math_log_trig_hot.wasm.jitstats#L1-L5: add the recorded descr_set_resolved value.
  • pyre/bench/synth/pypy_dict_primitives_nonbinding.wasm.jitstats#L1-L5: add the recorded descr_set_resolved value.
📍 Affects 3 files
  • pyre/bench/synth/inline_freevar_after_mayforce.wasm.jitstats#L1-L5 (this comment)
  • pyre/bench/synth/math_log_trig_hot.wasm.jitstats#L1-L5
  • pyre/bench/synth/pypy_dict_primitives_nonbinding.wasm.jitstats#L1-L5
🤖 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/bench/synth/inline_freevar_after_mayforce.wasm.jitstats` around lines 1
- 5, Regenerate the Wasm jitstats baselines using the complete
descriptor-counter snapshot emitted by the Wasm runner and recorded by check.py.
Add the recorded descr_set_resolved entry to
pyre/bench/synth/inline_freevar_after_mayforce.wasm.jitstats,
pyre/bench/synth/math_log_trig_hot.wasm.jitstats, and
pyre/bench/synth/pypy_dict_primitives_nonbinding.wasm.jitstats, preserving the
existing counters and generated ordering.

`try_fold_pure_call_via_executor` routed every constant-funcbox
`CallPure*` through `executor::execute_pure_call`, whose Float arm calls
`call_int_function` and reinterprets the integer return register via
`f64::from_bits`.

That convention holds only for funcboxes baked by the runtime emitter's
`*_canonical_via_target` family, which store `JitCallTarget::concrete_ptr`
(`jitcode/assembler.rs:3343`) — an `extern "C" fn(...) -> i64` wrapper
that pre-packs the `f64`. The codewriter's residual bakes the callee's own
address instead, so the `f64` comes back in the floating-point return
register. `CallDescr` records the signature, not which emitter produced
the pointer, so the arm cannot distinguish them.

Return early on `result_type() == Type::Float` and leave the recorded
`CallPure*` for the backend, which calls the same pointer with the descr's
real signature. `execute_pure_call`'s Float arm keeps the i64-bits
convention, now documented as a caller contract.

Adds `walker_declines_to_fold_a_float_result_pure_call`, which drives a
real `WalkContext` over an `f64`-returning callee plus an `Int` control;
without the decline the float box folds to `Float(3.5e-323)` =
`f64::from_bits(7)`, the argument still in the integer register.

Assisted-by: Claude
`cargo clippy -p majit-ir` failed to compile on three deny-by-default lints,
so the crate could not be linted at all:

- `ineffective_bit_mask` on the eval-breaker flag-width static assertion;
  every operand is a constant, so there is no runtime value to compare.
- `absurd_extreme_comparisons` on `OpCode::is_final`; `FINAL_FIRST` is
  `OpCode::Jump as u16` == 0 today, and the bound is kept symbolic to match
  the sibling classifiers.
- `mut_from_ref` on `SharedConstPool::as_mut_vec_for_gc`; the pool is
  `UnsafeCell`-backed so the GC root walker can mutate through a shared
  reference, and the existing `# Safety` clause carries the contract.

Each site gets a scoped `#[allow]` and a comment stating why. No behavior
change; the crate now reports 0 errors and 41 warnings.

Assisted-by: Claude
…g refusals

`bh_load_global_fn(namespace, w_code, frame, namei)` never reads `namespace`
(`call_jit.rs` `let _ = namespace_ptr`): it resolves the globals from the
executing frame when that frame owns `w_code`, and from `w_code`'s own live
`w_globals` otherwise. The codewriter bakes the operand from
`w_code_get_w_globals`, which is `PY_NULL` until a frame for that code object
stamps it, so a jitcode built before its callee's first frame carries a
concrete NULL there and `walker_abort_if_mayforce_null_ref_arg` aborted the
walk. The walker executes residuals eagerly and a non-committed exit replays
the walked region from its start, so every residual the walk had already run
executed a second time.

Exempt arg 0 in both refusals — the walker abort and the executor's decline,
which the code requires to stay in step — alongside the existing checked-
`PY_NULL` sentinel exemptions for `CallFn`, `CallKw`, `CallFunctionEx`,
`RaiseVarargs` and `StoreDeref`.

`pickle_terminal_raise_resume` crashed `IndexError: pop from empty list` on
all three backends and now prints the interpreter's `checksum = 216`; its
`loops_aborted` goes 1 -> 0 and `loops_compiled` 11 -> 33 (dynasm) / 34
(cranelift). `raise_reg_unbound_jitstress` moves `loops_aborted` 10 -> 2 and
`loops_compiled` 3 -> 5 on both native backends.

Assisted-by: Claude
@youknowone
youknowone merged commit 8fa1f2f into main Aug 2, 2026
16 of 19 checks passed
@youknowone
youknowone deleted the rewrite-tracer branch August 2, 2026 15:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant