Skip to content

wasm32 JIT: self-recursive CALL_ASSEMBLER on GC JitFrames + bridge-chaining groundwork - #312

Merged
youknowone merged 11 commits into
mainfrom
wasm-jit
Jul 1, 2026
Merged

wasm32 JIT: self-recursive CALL_ASSEMBLER on GC JitFrames + bridge-chaining groundwork#312
youknowone merged 11 commits into
mainfrom
wasm-jit

Conversation

@youknowone

@youknowone youknowone commented Jun 30, 2026

Copy link
Copy Markdown
Owner

#262

Summary

wasm32 JIT backend work layered on main (1b45bbd712); 8 commits, all wasm backend / runner. Headline is the self-recursive CALL_ASSEMBLER fast path on GC-managed JitFrames; the rest is inter-trace bridge-chaining groundwork (default-off) plus runner diagnostics.

Self-recursive CALL_ASSEMBLER on GC-managed JitFrames (PYRE_WASM_CA, default-off)

  • Lower a self-recursive CallAssemblerR into an in-module call_indirect into the source loop instead of a host round-trip.
  • Each callee frame is a GC-managed old-gen JitFrame (wasm_jit_ca_alloc_frame / wasm_jit_ca_pop_frame trampolines, push_jf-rooted, traced by a per-frame gcmap over its input + home Ref slots); the source loop runs on frame_base + FIRST_ITEM_OFFSET, so its local-0-relative slot accesses are unchanged.
  • The wasm host entry frame F0 and the non-CA loops also run on real JitFrames (jitframe GC type id pushed into the wasm backend from eval.rs, the sole registration authority); F0 is sized for the larger of the loop's and any chained CA bridge's ref homes.
  • A guard deopt hands the callee frame to wasm_ca_resume_deopt, which blackhole-resumes it on the host.
  • Verified: flag-OFF byte-identical; flag-ON fib(20..30) correct, and fib(24) under a tiny nursery (PYPY_GC_NURSERY=131072, forcing mid-recursion collections) correct.

Inter-trace bridge-chaining groundwork (bridge tracer default-off via PYRE_WASM_ENABLE_BRIDGES)

  • Runtime toggle for the dormant wasm bridge tracer (pyre_jit_set_enable_bridges export — export, not host import, to keep the baked function-index space stable).
  • Resume-at-LABEL preamble-skip extended from single-label to multi-label peeled loops (accept a loop-closing bridge that re-enters at the source's last LABEL).
  • Direct call_indirect for eligible residual Int/Ref calls.
  • Decline a loop-closing bridge whose terminal JUMP carries no induction-advancing arithmetic: such a bridge re-presents byte-identical loop state, so the exit guard never flips and the loop⇄bridge cycle livelocks (observed on fannkuch). It falls back to blackhole resume and is remembered so the metainterp stops re-tracing it.

Runner / diagnostics

  • PYRE_WASM_FUEL=N instruction-budget interrupt so a diagnostic livelock traps after N units instead of hanging; the PYRE_WASM_JIT_STATS readout now runs on trap as well as on clean return.
  • Static-counter diagnostics for the bridge-trace gate, read via guest exports.

Self-review

This patch is AI-assisted (every commit carries an Assisted-by: Claude trailer).

A local Codex parity review (gpt-5.5, the prompt below) was run, but note the template's caveat that review should be done in a separate session — an independent review pass is still recommended before merge. For reference, that run reported section 1 (parity regressions introduced by this patch) = None; the only flagged mismatches were in model_ssa.rs, which belongs to the base (#309, already on main) and is outside this PR's diff.

  • I fully resolved all reasonable code review comments from Codex and CodeRabbit.
    • Auto-review section 1 is clear. This check is mandatory.
    • Auto-review section 2 is clear. If this is not checked, please add a comment explaining why.
  • I did not use AI to write the code of this patch.
    • If this is not checked, commits must include Assisted-by

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added wasm bridge and metainterp diagnostics with exported counters, plus runtime toggles to enable/disable bridges and wasm CALL_ASSEMBLER.
    • Extended wasm execution with CA deoptimization resume support and improved in-module bridging for compatible traces.
    • Improved support for resumable peeled loops, including correct re-entry at the last label.
  • Bug Fixes

    • Strengthened generated WASM loop/control-flow validation and re-entry behavior for peeled loops.
    • Fixed wasm bridge deoptimization/frame recovery paths and refined handling of unsupported bridge declines.
  • Tests

    • Added coverage for peeled-loop validation, resumable label behavior, and registration-loop label stamping.

@coderabbitai

coderabbitai Bot commented Jun 30, 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

Run ID: a234c932-7885-4949-88ed-f8f64f775ded

📥 Commits

Reviewing files that changed from the base of the PR and between 6dcbd52 and 8c1273a.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • majit/majit-backend-wasm/src/codegen.rs
  • majit/majit-backend-wasm/src/failguard.rs
  • majit/majit-backend-wasm/src/lib.rs
  • majit/majit-backend-wasm/tests/codegen_test.rs
  • majit/majit-backend/src/lib.rs
  • majit/majit-metainterp/src/lib.rs
  • majit/majit-metainterp/src/pyjitpl.rs
  • pyre/pyre-jit/src/call_jit.rs
  • pyre/pyre-jit/src/eval.rs
  • pyre/pyre-wasm-runner/src/main.rs
  • pyre/pyre-wasm/Cargo.toml
  • pyre/pyre-wasm/src/lib.rs

Walkthrough

Adds CA-aware wasm code generation and bridge execution support, expands loop/bridge metadata and diagnostics, and wires wasm host/runtime entry points plus runner toggles for bridges, CA, fuel, and stats.

Changes

Wasm CA In-Module Recursive Codegen and Bridge Infra

Layer / File(s) Summary
Frame layout constants, CaParams struct, CompiledWasmLoop fields, loop predicates
majit/majit-backend-wasm/src/codegen.rs, majit/majit-backend-wasm/src/failguard.rs
Exports frame-layout constants, adds CA parameter/state types, ownership-aware bridge cell handling, peeled-loop predicates, and loop metadata for label targeting and CA ref-home tracking.
Backend trait terminal-decline, metainterp MC_DIAG, declined_bridge_guards change
majit/majit-backend/src/lib.rs, majit/majit-metainterp/src/lib.rs, majit/majit-metainterp/src/pyjitpl.rs
Adds terminal-decline trait support, metainterp diagnostic counters, exit-layout resolution, guard/retrace diagnostics, and terminal-only storage for unsupported bridge declines.
build_wasm_module CA integration: signature, dispatch, type section, residual calls
majit/majit-backend-wasm/src/codegen.rs
Extends module construction with CA parameters, owned bridge-cell return values, CA-aware dispatch gating, and direct residual-call/type planning.
build_function peeled-loop dispatch and CA/CALL opcode lowering
majit/majit-backend-wasm/src/codegen.rs
Adds peeled-loop resume dispatch, dispatch-key writes, CallAssemblerR lowering, and direct in-module residual call_indirect lowering.
compile_loop/compile_bridge CA wiring, bridge hashing, execute_token
majit/majit-backend-wasm/src/lib.rs
Updates loop/bridge metadata, CA bridge eligibility and validation, bridge hashing, and CA-aware token execution with JitFrame allocation and GC tracing.
Host wiring, wasm_ca_resume_deopt, pyre-wasm exports, runner env toggles
pyre/pyre-jit/src/eval.rs, pyre/pyre-jit/src/call_jit.rs, pyre/pyre-wasm/src/lib.rs, pyre/pyre-wasm/Cargo.toml, pyre/pyre-wasm-runner/src/main.rs
Adds wasm init hooks, bridge tracing toggles, CA deopt entrypoint, wasm exports, dependency wiring, and runner env/diagnostic plumbing.
Tests: 5-tuple updates and new peeled-loop/label-block-id tests
majit/majit-backend-wasm/tests/codegen_test.rs
Updates call sites to the new module return shape and adds peeled-loop and label-block-id tests.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • youknowone/pyre#75: Overlaps on the wasm backend interface surface, including build_wasm_module and related plumbing.
  • youknowone/pyre#133: Also changes bridge tracing control flow in pyre/pyre-jit/src/call_jit.rs.
  • youknowone/pyre#257: Touches wasm backend frame layout and Ref-home handling in the same area.

Poem

I’m a rabbit in the wasm-lit glade,
Hopping through loops that self-remade.
A key, a frame, a deopt sigh,
Then back I bounce with a cautious eye.
🐰

🚥 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 summarizes the main change: a wasm32 self-recursive CALL_ASSEMBLER fast path on GC JitFrames plus bridge-chaining support.
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 wasm-jit

Warning

Tools execution failed with the following error:

Failed to run tools: 13 INTERNAL: Received RST_STREAM with code 2 (Internal server error)


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

❤️ Share

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

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown

🤖 Codex parity review

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

1. Regressions to PyPy parity introduced by this patch

None.

2. Other mismatches introduced by this patch

  • majit/majit-backend-wasm/src/lib.rs:830 ↔ rpython/jit/backend/llsupport/assembler.py:323: wasm CA only accepts the “self-recursive single-int” CallAssemblerR shape with [Ref, Ref] -> Ref; PyPy’s call_assembler handles void, int, ref, and float result kinds (op.type == 'v', INT, REF, FLOAT) and arbitrary target JitCellTokens.

  • majit/majit-backend-wasm/src/lib.rs:1318 ↔ rpython/jit/backend/x86/assembler.py:623: wasm declines loop-closing bridges into peeled multi-label loops unless the terminal JUMP resumes at the last label; PyPy’s backend assembles the bridge normally and patches the original guard (assemble_bridge(...); patch_jump_for_descr(...)) without this last-label-only restriction.

  • majit/majit-backend-wasm/src/lib.rs:1341 ↔ rpython/jit/metainterp/compile.py:492: wasm declines loop-closing bridges whose JUMP args are not produced by an “induction-advancing arithmetic op”; PyPy passes the optimized bridge operations directly to cpu.compile_bridge(...) and has no semantic filter based on producer opcode class.

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

  • pyre/pyre-jit/src/call_jit.rs:2189 ↔ rpython/jit/metainterp/compile.py:704: wasm still keeps bridge tracing disabled by default and returns false; PyPy calls start_compiling() and _trace_and_compile_from_bridge(...) whenever must_compile() fires and the stack is not almost full.

  • majit/majit-backend-wasm/src/lib.rs:1370 ↔ rpython/jit/backend/x86/assembler.py:638: wasm only chains bridges attached directly to a source loop guard; PyPy rebuilds fail locations from the failing descriptor and patches the guard jump, so bridge attachment is descriptor-based rather than restricted to the source loop’s flat guard-cell array.

  • majit/majit-backend-wasm/src/lib.rs:1466 ↔ rpython/jit/backend/x86/assembler.py:641: wasm declines bridges needing more ref-home slots than the source loop reserved; PyPy regalloc prepares the bridge frame/gcmap for the bridge operations and does not require the bridge’s root set to fit the parent loop’s preallocated home region.

  • majit/majit-metainterp/src/pyjitpl.rs:8894 ↔ rpython/jit/metainterp/compile.py:790: wasm structural declines are recorded so must_compile_with_values stops retrying that guard; PyPy only clears ST_BUSY_FLAG in done_compiling(), and the guard counter may fire again after a failed compilation.

4. Structural adaptations

  • majit/majit-backend-wasm/src/codegen.rs:2514 ↔ rpython/jit/backend/x86/assembler.py:689: wasm uses bridge-cell lookup plus return_call_indirect through an imported table; PyPy patches the machine-code jump in place with patch_jump_for_descr(...). This is a WebAssembly immutability/table adaptation.

  • majit/majit-backend-wasm/src/lib.rs:517 ↔ rpython/jit/backend/llsupport/jitframe.py:104: wasm builds explicit jf_gcmap bitmaps for Ref-home slots in a GC-managed JitFrame; PyPy’s jitframe_trace walks jf_gcmap over native jf_frame slots. The layout differs, but the traced-root mechanism is the intended counterpart.

  • majit/majit-backend-wasm/src/codegen.rs:2209 ↔ rpython/jit/backend/llsupport/assembler.py:316: wasm can lower eligible residual calls to call_indirect using the shared wasm function table; PyPy emits a native call to the target address. This is a backend ABI adaptation, not an interpreter semantic mismatch.

@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: 6dcbd52039

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

}];
let constants: majit_ir::VecMap<u32, i64> = majit_ir::VecMap::new();
let (bytes, guards, _, _) = codegen::build_wasm_module(
let (bytes, guards, _, _, _) = codegen::build_wasm_module(

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 Pass CaParams to codegen test calls

This updated call site, and the other direct build_wasm_module calls in this test file, still pass only the old argument list even though the function now requires the trailing ca: CaParams parameter. The tuple destructuring was updated for the new return value, but the missing argument means majit-backend-wasm's test target will fail to compile with an arity error until these calls pass codegen::CaParams::default() or use a wrapper.

Useful? React with 👍 / 👎.

Comment on lines +2218 to +2219
// call_indirect(table_index, type_index): table 0, type for arity n.
sink.call_indirect(0, base + nargs as u32);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep residual calls on the trampoline unless ABI is proven

When a residual target's actual wasm signature is not exactly the synthetic (i64...)->i64 type, this direct call_indirect traps instead of using the existing host trampoline that reflects the target's real ValTypes and catches target traps (pyre-wasm-runner/src/main.rs:788-823). The JIT CallDescr only tells us logical Int/Ref types, not the callee's wasm ABI, so any eligible CallI/CallR helper with an i32 pointer ABI or a trapping residual target can now abort the whole wasm run rather than being marshalled/caught through jit_call.

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

Caution

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

⚠️ Outside diff range comments (1)
majit/majit-backend-wasm/tests/codegen_test.rs (1)

45-58: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Add the trailing codegen::CaParams::default() argument to every build_wasm_module call in this file. build_wasm_module now takes a required final CaParams, so these invocations stop short and won’t type-check as written.

🤖 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-backend-wasm/tests/codegen_test.rs` around lines 45 - 58, The
build_wasm_module call in this test is missing the required trailing CaParams
argument, so update every build_wasm_module invocation in codegen_test.rs to
pass codegen::CaParams::default() as the final parameter. Use the existing
build_wasm_module call site and its surrounding test helpers to locate all
affected invocations and make them match the current function signature exactly.
🤖 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/pyre-jit/src/call_jit.rs`:
- Around line 2742-2745: The guard-exit path in call_jit.rs is using a synthetic
green key via unwrap_or(0), which can pick the wrong compiled-exit layout when a
fail descr has no owning token. Update the logic around green_key and
mi.build_exit_layout_for_descr so it does not default to 0; instead, recover the
key from the owning compiled loop through majit_backend::descr_owning_jct when
possible, or fail loudly if no owning token exists. Keep the fix localized to
the guard exit handling in this section.

In `@pyre/pyre-wasm-runner/src/main.rs`:
- Around line 200-202: The PYRE_WASM_FUEL parsing in main.rs currently treats
any invalid value as None, which silently disables fuel. Update the fuel_limit
setup so invalid values for PYRE_WASM_FUEL are rejected explicitly instead of
falling back to no fuel, and preserve the existing Option<u64> behavior only for
a missing variable. Use the existing fuel_limit handling in main to surface a
clear error for bad input rather than calling parse().ok().
- Around line 242-254: Treat the wasm feature toggles in main as boolean flags
instead of mere presence checks: the PYRE_WASM_ENABLE_BRIDGES and PYRE_WASM_CA
branches should only enable their exports when the environment variable is set
to a non-disabled value, so "0" does not turn them on. Update the logic around
the env checks and the calls to instance.get_typed_func and f.call in main so
both toggles honor disabled values consistently, while still allowing missing
exports to remain no-ops.

---

Outside diff comments:
In `@majit/majit-backend-wasm/tests/codegen_test.rs`:
- Around line 45-58: The build_wasm_module call in this test is missing the
required trailing CaParams argument, so update every build_wasm_module
invocation in codegen_test.rs to pass codegen::CaParams::default() as the final
parameter. Use the existing build_wasm_module call site and its surrounding test
helpers to locate all affected invocations and make them match the current
function signature exactly.
🪄 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: 8fbd4f78-24f0-4c88-8e3f-699f6351cb37

📥 Commits

Reviewing files that changed from the base of the PR and between 1b45bbd and 6dcbd52.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • majit/majit-backend-wasm/src/codegen.rs
  • majit/majit-backend-wasm/src/failguard.rs
  • majit/majit-backend-wasm/src/lib.rs
  • majit/majit-backend-wasm/tests/codegen_test.rs
  • majit/majit-backend/src/lib.rs
  • majit/majit-metainterp/src/lib.rs
  • majit/majit-metainterp/src/pyjitpl.rs
  • pyre/pyre-jit/src/call_jit.rs
  • pyre/pyre-jit/src/eval.rs
  • pyre/pyre-wasm-runner/src/main.rs
  • pyre/pyre-wasm/Cargo.toml
  • pyre/pyre-wasm/src/lib.rs

Comment on lines +2742 to +2745
let green_key = majit_backend::descr_owning_jct(descr)
.map(|jct| jct.green_key)
.unwrap_or(0);
let exit_layout = mi.build_exit_layout_for_descr(green_key, descr);

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

Do not resume CA deopts with a synthetic green key.

unwrap_or(0) can route a guard exit through the wrong compiled-exit layout when the fail descr has no owning token. This path should fail loudly or recover the key from the owning compiled loop instead of blackhole-resuming with key 0.

Suggested fix
-            let green_key = majit_backend::descr_owning_jct(descr)
-                .map(|jct| jct.green_key)
-                .unwrap_or(0);
+            let green_key = majit_backend::descr_owning_jct(descr)
+                .expect("CA deopt: fail descr has no owning JitCellToken")
+                .green_key;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let green_key = majit_backend::descr_owning_jct(descr)
.map(|jct| jct.green_key)
.unwrap_or(0);
let exit_layout = mi.build_exit_layout_for_descr(green_key, descr);
let green_key = majit_backend::descr_owning_jct(descr)
.expect("CA deopt: fail descr has no owning JitCellToken")
.green_key;
let exit_layout = mi.build_exit_layout_for_descr(green_key, descr);
🤖 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/call_jit.rs` around lines 2742 - 2745, The guard-exit path
in call_jit.rs is using a synthetic green key via unwrap_or(0), which can pick
the wrong compiled-exit layout when a fail descr has no owning token. Update the
logic around green_key and mi.build_exit_layout_for_descr so it does not default
to 0; instead, recover the key from the owning compiled loop through
majit_backend::descr_owning_jct when possible, or fail loudly if no owning token
exists. Keep the fix localized to the guard exit handling in this section.

Comment on lines +200 to +202
let fuel_limit: Option<u64> = std::env::var("PYRE_WASM_FUEL")
.ok()
.and_then(|s| s.parse().ok());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reject invalid PYRE_WASM_FUEL instead of disabling fuel silently.

A typo like PYRE_WASM_FUEL=10s currently parses to None, disables fuel, and can hang the diagnostic livelock run this option is meant to stop.

Suggested fix
-    let fuel_limit: Option<u64> = std::env::var("PYRE_WASM_FUEL")
-        .ok()
-        .and_then(|s| s.parse().ok());
+    let fuel_limit: Option<u64> = match std::env::var("PYRE_WASM_FUEL") {
+        Ok(s) => Some(
+            s.parse()
+                .with_context(|| format!("parse PYRE_WASM_FUEL={s:?} as u64"))?,
+        ),
+        Err(std::env::VarError::NotPresent) => None,
+        Err(e) => return Err(e).context("read PYRE_WASM_FUEL"),
+    };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let fuel_limit: Option<u64> = std::env::var("PYRE_WASM_FUEL")
.ok()
.and_then(|s| s.parse().ok());
let fuel_limit: Option<u64> = match std::env::var("PYRE_WASM_FUEL") {
Ok(s) => Some(
s.parse()
.with_context(|| format!("parse PYRE_WASM_FUEL={s:?} as u64"))?,
),
Err(std::env::VarError::NotPresent) => None,
Err(e) => return Err(e).context("read PYRE_WASM_FUEL"),
};
🤖 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-wasm-runner/src/main.rs` around lines 200 - 202, The PYRE_WASM_FUEL
parsing in main.rs currently treats any invalid value as None, which silently
disables fuel. Update the fuel_limit setup so invalid values for PYRE_WASM_FUEL
are rejected explicitly instead of falling back to no fuel, and preserve the
existing Option<u64> behavior only for a missing variable. Use the existing
fuel_limit handling in main to surface a clear error for bad input rather than
calling parse().ok().

Comment on lines +242 to +254
if std::env::var_os("PYRE_WASM_ENABLE_BRIDGES").is_some() {
if let Ok(f) = instance.get_typed_func::<u32, ()>(&mut store, "pyre_jit_set_enable_bridges")
{
f.call(&mut store, 1)?;
}
}

// Enable the self-recursive CALL_ASSEMBLER guest→guest `call_indirect` arm
// (`PYRE_WASM_CA`). The guest has no environment, so the flag is plumbed
// through this export. No-op if the export is absent (older modules).
if std::env::var_os("PYRE_WASM_CA").is_some() {
if let Ok(f) = instance.get_typed_func::<u32, ()>(&mut store, "pyre_jit_set_wasm_ca") {
f.call(&mut store, 1)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Treat "0" as disabled for wasm feature toggles.

var_os(...).is_some() enables bridges/CA even for PYRE_WASM_ENABLE_BRIDGES=0 or PYRE_WASM_CA=0, which can accidentally turn on dormant experimental paths.

Suggested fix
+    let env_flag_enabled = |name: &str| {
+        std::env::var_os(name)
+            .map(|v| {
+                let v = v.to_string_lossy();
+                !v.is_empty() && v != "0"
+            })
+            .unwrap_or(false)
+    };
+
-    if std::env::var_os("PYRE_WASM_ENABLE_BRIDGES").is_some() {
+    if env_flag_enabled("PYRE_WASM_ENABLE_BRIDGES") {
         if let Ok(f) = instance.get_typed_func::<u32, ()>(&mut store, "pyre_jit_set_enable_bridges")
         {
             f.call(&mut store, 1)?;
         }
     }
@@
-    if std::env::var_os("PYRE_WASM_CA").is_some() {
+    if env_flag_enabled("PYRE_WASM_CA") {
         if let Ok(f) = instance.get_typed_func::<u32, ()>(&mut store, "pyre_jit_set_wasm_ca") {
             f.call(&mut store, 1)?;
         }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if std::env::var_os("PYRE_WASM_ENABLE_BRIDGES").is_some() {
if let Ok(f) = instance.get_typed_func::<u32, ()>(&mut store, "pyre_jit_set_enable_bridges")
{
f.call(&mut store, 1)?;
}
}
// Enable the self-recursive CALL_ASSEMBLER guest→guest `call_indirect` arm
// (`PYRE_WASM_CA`). The guest has no environment, so the flag is plumbed
// through this export. No-op if the export is absent (older modules).
if std::env::var_os("PYRE_WASM_CA").is_some() {
if let Ok(f) = instance.get_typed_func::<u32, ()>(&mut store, "pyre_jit_set_wasm_ca") {
f.call(&mut store, 1)?;
let env_flag_enabled = |name: &str| {
std::env::var_os(name)
.map(|v| {
let v = v.to_string_lossy();
!v.is_empty() && v != "0"
})
.unwrap_or(false)
};
if env_flag_enabled("PYRE_WASM_ENABLE_BRIDGES") {
if let Ok(f) = instance.get_typed_func::<u32, ()>(&mut store, "pyre_jit_set_enable_bridges")
{
f.call(&mut store, 1)?;
}
}
// Enable the self-recursive CALL_ASSEMBLER guest→guest `call_indirect` arm
// (`PYRE_WASM_CA`). The guest has no environment, so the flag is plumbed
// through this export. No-op if the export is absent (older modules).
if env_flag_enabled("PYRE_WASM_CA") {
if let Ok(f) = instance.get_typed_func::<u32, ()>(&mut store, "pyre_jit_set_wasm_ca") {
f.call(&mut store, 1)?;
}
}
🤖 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-wasm-runner/src/main.rs` around lines 242 - 254, Treat the wasm
feature toggles in main as boolean flags instead of mere presence checks: the
PYRE_WASM_ENABLE_BRIDGES and PYRE_WASM_CA branches should only enable their
exports when the environment variable is set to a non-disabled value, so "0"
does not turn them on. Update the logic around the env checks and the calls to
instance.get_typed_func and f.call in main so both toggles honor disabled values
consistently, while still allowing missing exports to remain no-ops.

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

ℹ️ 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 +252 to +254
if std::env::var_os("PYRE_WASM_CA").is_some() {
if let Ok(f) = instance.get_typed_func::<u32, ()>(&mut store, "pyre_jit_set_wasm_ca") {
f.call(&mut store, 1)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Enable bridge tracing when PYRE_WASM_CA is set

When a user sets only PYRE_WASM_CA, this block flips the backend CA codegen flag but leaves the wasm bridge tracer disabled; trace_and_compile_from_bridge still returns early while WASM_BRIDGES_ENABLED is false, so compile_bridge never runs and the self-recursive CALL_ASSEMBLER bridge/CA arm is never emitted. Either set pyre_jit_set_enable_bridges as part of this flag path or make the documented CA flag require the bridge flag, otherwise the advertised PYRE_WASM_CA mode remains a no-op unless users know to also set PYRE_WASM_ENABLE_BRIDGES.

Useful? React with 👍 / 👎.

youknowone added 11 commits July 1, 2026 17:07
…e parity

Lower an eligible residual CALL (CallI/R, CallPureI/R, CallMayForceI/R,
CallLoopinvariantI/R whose calldescr arg and result types are all Int/Ref) to a
direct in-module call_indirect into the callee's __indirect_function_table slot
with a static (i64xN)->i64 type, instead of the jit_call host trampoline. The
residual ABI is uniformly i64 for Int/Ref args and result, so args and result
move on the wasm stack with no marshalling and no call-area traffic. Gated by
WASM_DIRECT_RESIDUAL_CALL; the type section declares one (i64xN)->i64 type per
residual arity. call_indirect takes (table_index, type_index): table 0, type
for the arity.

Also carries the bridge-decline parity fixes:
- Backend::bridge_decline_is_terminal() scopes declined_bridge_guards to wasm
  structural declines; native backends keep transient-retry semantics for
  Unsupported.
- wasm bridge guard store_hash; decline bridges exceeding source frame output
  slots; RAII-own bridge cells instead of leaking.

dynasm 160/160, wasm 150/160 (0 wrong; remaining fails are pre-existing heavy
TIMEOUTs and list JIT-PANICs).

Assisted-by: Claude
A loop-closing bridge (terminal JUMP, no LABEL) re-enters the source loop
through its table-slot function entry. For a peeled loop that re-runs the
preamble (the unrolled first iteration) instead of resuming at the LABEL, so the
induction variable never advances; compile_bridge declined such bridges.

Add a resume-at-LABEL preamble-skip dispatch for the single-label peeled shape
(codegen::is_single_label_peeled), so a loop-closing bridge re-enters at the
LABEL in-module instead of declining to a host round-trip:

- A reserved frame slot DISPATCH_KEY_OFS (= MIN_FRAME_BYTES, between the call
  area and the Ref-home region; HOME_SLOT_BASE shifts +8). 0 = preamble/host
  entry (the execute_token frame is zeroed, +1 slot); 1 = resume at LABEL,
  written by the loop-closing JUMP arm before return_call_indirect.
- build_function wraps such a loop in three blocks ($exit / $past_loader /
  $skip_preamble) and an entry br_if on the key: key 0 runs the entry inputarg
  loader + preamble then branches over the resume loader to the loop; key 1
  skips both and runs the resume loader, which loads each label arg from its
  frame slot and refreshes its Ref home. The entry inputarg loader runs on the
  key-0 path only, so a resuming bridge never scatters frame values into the
  function inputargs' home slots. Preamble guards br to depth 2, body to 1.
- CompiledWasmLoop.is_single_label_peeled (shared predicate, set in compile_loop)
  keys both the wrapper and the narrowed decline: compile_bridge now declines a
  loop-closing bridge only into a MULTI-label peeled loop (the br_table form is a
  follow-up).

Also fix codegen_test's stale 4-tuple build_wasm_module destructures (the return
is a 5-tuple) and add test_single_label_peeled_loop_validates, which validates
the new control-flow nesting and br depths with wasmparser.

dynasm 160/160, wasm 151/160 (0 wrong; remaining fails are the pre-existing heavy
TIMEOUTs and list JIT-PANICs). The single-label-peeled shape is not what blocks
the heavy benches' chaining, so their timings are unchanged.

Assisted-by: Claude
Add two static-counter diagnostics, read out via guest exports at
PYRE_WASM_JIT_STATS time, to trace why wasm guard exits do not chain into
in-module bridges. Static counters + exports (not host imports, which would
shift the module's function-index space and break the JIT's baked
`fn as usize` table indices).

- majit-backend-wasm: `BRIDGE_DIAG[8]` bumped at each `compile_bridge` outcome
  (entered / decline-CALL_ASSEMBLER / decline-multi-label-peeled /
  decline-not-direct-guard / decline-ref-home / compiled / loop-closing shape /
  source-has-preamble), read by `bridge_diag`.
- majit-metainterp: `MC_DIAG[8]` bumped along the guard-failure → bridge-trace
  gate in `must_compile_with_values` (entered / declined-short-circuit /
  descr_addr==0 / status-busy / jitcounter-fired) and `start_retrace_from_guard`
  (entered / source-loop-evicted bail), read by `mc_diag`.
- pyre-wasm: `pyre_jit_bridge_diag` / `pyre_jit_mc_diag` exports forward to those
  readers (direct path deps on majit-backend-wasm and majit-metainterp, both
  default-features=false, under the wasm-host feature).
- pyre-wasm-runner: prints `[jit-stats] bridge_diag …` / `mc_diag …`.

These measured that the wasm bridge tracer never reaches the backend:
`must_compile` fires (FIRED>0) but `start_retrace_from_guard` is never entered,
because `trace_and_compile_from_bridge` returns early under
`#[cfg(target_arch = "wasm32")]`.

Assisted-by: Claude
Extend the resume-at-LABEL preamble-skip wrapper from single-label peeled
loops to any peeled loop, and accept a loop-closing bridge into a multi-label
peeled source when it re-enters at the source's last LABEL (the one carrying
the `loop`).

- codegen.rs: add `is_resumable_peeled` (a peeled loop, single- or multi-label)
  and gate the wrapper on it instead of `is_single_label_peeled`; keep
  `is_single_label_peeled` as its single-label subset. The wrapper resumes at
  the last label, so its block nesting and br depths are byte-identical to the
  single-label case.
- lib.rs compile_loop: stamp each LABEL's loop-target descr with its ordinal
  (`set_label_block_id`, skipping a descr without an `AtomicU32` slot) — pure
  metadata, no wasm bytes. Record `last_label_block_id` and `last_label_num_args`
  on `CompiledWasmLoop`; derive `has_preamble` through the shared predicate.
- lib.rs compile_bridge: replace the blanket multi-label decline with a
  last-label check — accept when the closing JUMP's recovered target ordinal
  (read off its descr, shared by Arc identity with the source LABEL) equals the
  source's last label and the arity matches; decline (blackhole fallback)
  otherwise. The bridge_diag tally gains a sub-breakdown of the decline reason
  (stripped descr / non-last label / arity mismatch).
- failguard.rs: `last_label_block_id`, `last_label_num_args` fields; updated
  `has_preamble` / `is_single_label_peeled` docs.
- codegen_test.rs: a multi-label peeled loop validates with the same wrapper,
  and the per-LABEL registration stamps an ordinal readable from a JUMP that
  shares the LABEL's descr.

The wasm bridge tracer remains dormant (call_jit.rs), so this is inert at
runtime until chaining is enabled; with the tracer enabled a raise_catch
loop-closing bridge (last-label target) is accepted, while nbody's bridges
target non-last labels and stay declined pending the br_table form.

Assisted-by: Claude
Replace the unconditional wasm `return false` in trace_and_compile_from_bridge
with a check of a `WASM_BRIDGES_ENABLED` flag (default off, so behaviour is
unchanged), settable at runtime through a new `pyre_jit_set_enable_bridges`
guest export. The runner calls it when PYRE_WASM_ENABLE_BRIDGES is set, so
inter-trace chaining can be enabled and measured without rebuilding the module.

- pyre-jit call_jit.rs: `WASM_BRIDGES_ENABLED` AtomicBool + `set_wasm_bridges_enabled`;
  the dormancy early-return is now gated on the flag.
- pyre-wasm: `pyre_jit_set_enable_bridges(u32)` export forwarding to the setter
  (export, not import, to keep the function-index space stable).
- pyre-wasm-runner: call the export when PYRE_WASM_ENABLE_BRIDGES is set.

Assisted-by: Claude
Add an optional instruction budget (PYRE_WASM_FUEL=N enables consume_fuel and
sets the store fuel) so a diagnostic run that livelocks — e.g. a buggy
loop-closing bridge — traps after N fuel units instead of hanging forever.
Restructure the run so the PYRE_WASM_JIT_STATS readout happens regardless of
whether the guest returned or trapped (the JIT counters are populated at compile
time), refilling the store fuel first so the diagnostic export calls do not
themselves trap on the exhausted budget.

Assisted-by: Claude
…thmetic

A loop-closing bridge re-enters the source loop through its terminal JUMP args
and tail-calls the loop to iterate again. When none of those args is the result
of an induction-advancing arithmetic op — every loop-carried value is a verbatim
input reload, a fresh allocation, or a baked constant — the bridge re-presents
byte-identical loop state on every pass, so the loop's exit guard never flips and
the loop⇄bridge cycle spins at constant stack depth and heap state (a control-
flow livelock; observed on fannkuch with the bridge tracer enabled: flat RSS,
100% CPU, no output).

Add `is_inductive_arith` (the IntAdd/.. and float-arithmetic block, the overflow
variants, and unary IntNeg/IntInvert; excludes copies, casts, comparisons, and
allocations) and, in compile_bridge, decline any loop-closing bridge whose
terminal JUMP has no arg produced by such an op. The guard then falls back to
blackhole resume and declined_bridge_guards stops the metainterp re-tracing it. A
bridge that advances a loop-carried value (an `i += 1` counter feeding a JUMP
arg) still compiles and chains. BRIDGE_DIAG slot 11 (decl_noadvance) tallies the
decline; the runner labels it.

With PYRE_WASM_ENABLE_BRIDGES=1: fannkuch declines 10 such bridges and completes
correctly; raise_catch's advancing bridge is still accepted (BRIDGE_OK=1).

Assisted-by: Claude
Add the PYRE_WASM_CA arm: lower a self-recursive CallAssemblerR into an
in-module call_indirect into the source loop instead of a host round-trip.
Each callee frame is allocated as a GC-managed old-gen JitFrame via the
wasm_jit_ca_alloc_frame / wasm_jit_ca_pop_frame trampolines (push_jf-rooted,
traced by a per-frame gcmap over its input + home Ref slots); the source loop
runs on frame_base + FIRST_ITEM_OFFSET so its local-0-relative slot accesses
are unchanged. A guard deopt hands the callee frame to wasm_ca_resume_deopt,
which blackhole-resumes it on the host.

Run the wasm host entry frame F0 and the non-CA loops on real JitFrames too:
push the jitframe GC type id into the wasm backend from eval.rs
(set_wasm_jitframe_tid, the sole registration authority), size F0 for the
larger of the loop's and any chained CA bridge's ref homes
(CompiledWasmLoop::ca_bridge_ref_homes), and gate the path behind the runtime
PYRE_WASM_CA flag (pyre_jit_set_wasm_ca export, set by the runner).

build_exit_layout_for_descr (metainterp) and wasm_ca_resume_deopt (call_jit)
reconstruct the deopt exit layout for a callee frame an in-guest call_indirect
already ran to a guard exit.

Assisted-by: Claude
… no-op

residual_call_i64_arity: note CallMayForce* is direct-call eligible because the
wasm backend has no force tokens / guard_not_forced — a forceable residual call
needs no pre-call jf_descr (a guard inside it deopts through fail_index; the
caller's live Refs survive via the JitFrame gcmap).

CondCallGcWb arm: note the COND_CALL_GC_WB / COND_CALL_GC_WB_ARRAY ops are
no-ops because the wasm backend emits the write barrier inline at each ref-store
(write_barrier_base + emit_write_barrier) rather than consuming a standalone
barrier op.

Assisted-by: Claude
residual_call_i64_arity no longer accepts CallMayForceI/R, so a forceable
residual call keeps the jit_call trampoline instead of a direct call_indirect.
jf_descr (the force descr guard_not_forced reads) is set immediately before
CALL_MAY_FORCE, and the direct call_indirect does not reproduce that store. The
wasm backend has no force tokens, so the direct lowering would be sound, but the
fast-path stays parity-conservative and covers only the non-forceable residual
calls (Call{,Pure,Loopinvariant}{I,R}).

Assisted-by: Claude
build_wasm_module gained the trailing CaParams argument with the self-recursive
CALL_ASSEMBLER arm; the codegen_test call sites still passed 11 arguments and
failed to compile (E0061). Pass CaParams::default() (emit_ca off) at each call.

Assisted-by: Claude

@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: 8c1273aaf1

ℹ️ 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 +2735 to +2736
if descr.is_finish() {
Outcome::Finished(backend.get_ref_value(&frame, 0).as_usize() as i64)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Propagate exception FINISH exits from CA helper

When a self-recursive wasm CA callee exits via ExitFrameWithExceptionDescrRef, the helper reaches this descr.is_finish() branch and returns slot 0 as an ordinary Ref result without publishing the exception. The normal compiled outcome path treats is_exit_frame_with_exception() specially and raises it, and handle_blackhole_result only gets a chance to stash the exception on the non-finish deopt path; with this shortcut, recursive calls that raise under PYRE_WASM_CA can be observed as a normal object return instead of propagating the Python exception.

Useful? React with 👍 / 👎.

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