Skip to content

feat(riscv): WasmOp::Call lowering — leaf-call subset#116

Open
avrabe wants to merge 1 commit into
mainfrom
feat/riscv-call-selector
Open

feat(riscv): WasmOp::Call lowering — leaf-call subset#116
avrabe wants to merge 1 commit into
mainfrom
feat/riscv-call-selector

Conversation

@avrabe
Copy link
Copy Markdown
Contributor

@avrabe avrabe commented May 15, 2026

Summary

Minimum-viable cross-function call support in the RISC-V instruction selector. WasmOp::Call(idx) now lowers to a label-based RiscVOp::Call instead of returning SelectorError::Unsupported. The ELF builder already resolves call labels via PC-relative auipc + jalr, so the only missing piece was the selector arm — this PR adds it.

This is the third Track A item for v0.3.1 after PRs #113, #114, #115. RV32 i64 lowering is the next one.

What it does

  • New lower_call(func_idx, op) method in synth-backend-riscv/src/selector.rs.
  • Drains up to 8 vstack values into argument registers `a0..a7` (RV psABI).
  • Emits `RiscVOp::Call { label: format!("synth_func_{idx}") }`.
  • Pushes a fresh `a0` vreg as the return value.

What's deferred (deliberately, with tracking tests)

The v0.3.1 cut is the leaf-call subset. The following are known gaps, each carrying a comment and (where applicable) an `#[ignore]`'d test:

Gap Why deferred Next milestone
Function-signature plumbing Selector doesn't yet receive `FuncSig` from the decoder, so it over-pops the vstack on back-to-back calls with surviving results. v0.4 — pipe `FuncSig` through.
Args > 8 RV psABI spills to stack at fixed offsets. v0.4
Caller-side a0..a7 invalidation Callers wanting to survive a call must currently `drop`/`local.tee` their live values explicitly. v0.4
Multi-result returns (wasm 2.0) Spec-wise out of scope for the leaf subset. v0.5
Cross-`.text` relocs (multi-unit linking) Single-unit only for now. v0.5+

The ignored test `recursive_self_call_emits_two_call_ops` is the canary that v0.4's plumbing work will flip back on.

Tests

  • `call_emits_label_and_argument_marshalling` — single-arg call, label is `synth_func_{idx}`.
  • `call_two_args_marshals_to_a0_a1` — two-arg call from `i32.const` sequence.
  • `recursive_self_call_emits_two_call_ops` — `#[ignore]`'d, documents the back-to-back-calls gap.

Local run before push:

  • `cargo test --package synth-backend-riscv` → 100 passed; 0 failed; 1 ignored.
  • `cargo clippy --package synth-backend-riscv -- -D warnings` — clean.
  • `cargo fmt --check` — clean.

Test plan

  • CI green (Test, Clippy, Format, Z3 Verification, Kani Verification, Bazel Build & Proofs)
  • Fuzz smoke: gating harnesses (wasm_ops_lower_or_error, wasm_to_ir_roundtrip_op_coverage) green
  • Visual check: no regression in existing `compiles_simple_add_to_bytes` byte sequence

🤖 Generated with Claude Code

v0.3.1 minimum-viable cross-function call support in the RISC-V
selector. WasmOp::Call(idx) no longer errors with `Unsupported` for
leaf-call shapes — it now lowers to a label-based RiscVOp::Call that
the ELF builder resolves to a PC-relative `auipc + jalr` when the
callee is in the same compilation unit.

Behavior:
  * Move top N vstack values (capped at 8) into a0..a(N-1).
  * Emit `RiscVOp::Call { label: format!("synth_func_{idx}") }`.
  * Push a fresh `a0` vreg as the return value.

What's deliberately deferred (documented in the lower_call doc + the
#[ignore]-marked `recursive_self_call_emits_two_call_ops` test):
  * Function-signature plumbing from the decoder. Without it, the
    selector can't know how many args to pop, so the v0.3.1 cut
    over-consumes the vstack on back-to-back calls with surviving
    results. v0.4 will pipe `FuncSig` through and lift this restriction.
  * Args beyond 8 (RV psABI says spill to stack at fixed offsets — not
    implemented).
  * Caller-side a0..a7 invalidation across the BL — callers wanting to
    survive a call should `drop` or `local.tee` their live values
    explicitly until v0.4 models this properly.
  * Multi-result returns (wasm 2.0).
  * Cross-`.text` relocations for multi-unit linking.

Tests:
  * `call_emits_label_and_argument_marshalling` — single-arg call, label
    encodes `synth_func_{idx}`.
  * `call_two_args_marshals_to_a0_a1` — two-arg call from i32.const seq.
  * `recursive_self_call_emits_two_call_ops` — #[ignore]'d documentation
    of the back-to-back-calls gap, to be flipped when v0.4 plumbing
    lands.

Total: 100 passing tests in synth-backend-riscv (was 99); 1 ignored
that documents the next milestone.
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