Skip to content

feat(riscv): RV32IMAC backend skeleton — encoder, ELF builder, PMP allocator#87

Merged
avrabe merged 1 commit into
mainfrom
feat/riscv-skeleton
May 10, 2026
Merged

feat(riscv): RV32IMAC backend skeleton — encoder, ELF builder, PMP allocator#87
avrabe merged 1 commit into
mainfrom
feat/riscv-skeleton

Conversation

@avrabe
Copy link
Copy Markdown
Contributor

@avrabe avrabe commented May 3, 2026

Track B1 deliverable — adds a parallel RISC-V code-generation path alongside the ARM backend. Stacked on top of #86 (M7 hardening + AAPCS fixes) so it carries those changes too. Once #86 merges, this rebases onto main as a clean diff of just the RISC-V additions.

What this PR adds

  • New crate crates/synth-backend-riscv/ — registered as a --backend riscv option in the CLI.
  • End-to-end pipeline working today: WASM `(add (param i32 i32) (result i32) local.get 0 local.get 1 i32.add)` → real RISC-V ELF (`file` reports "UCB RISC-V, RVC, soft-float ABI").

Modules

Module What it does
`register.rs` x0..x31 enum, ABI aliases (a0..a7, s0..s11, t0..t6), psABI callee-saved set
`riscv_op.rs` RV32I + Zicsr + M-extension instruction enum, Branch/Csr typed wrappers
`encoder.rs` R/I/S/B/U/J bit-packers, cross-checked against canonical reference encodings
`pmp.rs` Physical Memory Protection allocator (RISC-V analogue of MPU). NAPOT for power-of-2, TOR fallback
`elf_builder.rs` RV32 ELF emitter (EM_RISCV=0xF3, RVC e_flags), 2-pass label resolution
`selector.rs` Skeleton WASM-to-RV32 lowering (i32 arith, params, return). Full parity = B2
`backend.rs` `Backend` trait impl, registers as "riscv" with `produces_elf=true`

CLI integration

  • New `riscv` feature in `synth-cli` (default-enabled).
  • Compile path now dispatches to RISC-V ELF emission when `target_spec.family == ArchFamily::RiscV`, replacing the unconditional ARM ELF wrapper that was producing wrong-machine-type binaries.
  • `synth backends` now lists "riscv" alongside "arm".

End-to-end demo

```
$ synth compile add.wat -o rv.elf --backend riscv --target riscv32imac
$ file rv.elf
rv.elf: ELF 32-bit LSB relocatable, UCB RISC-V, RVC, soft-float ABI

$ xxd -s 52 -l 20 rv.elf
9302 0500 # addi t0, a0, 0 (LocalGet 0)
1383 0500 # addi t1, a1, 0 (LocalGet 1)
b382 6200 # add t0, t0, t1
1385 0200 # addi a0, t0, 0
6780 0000 # ret (jalr zero, 0(ra))
```

Encoder cross-checks

The encoder tests use literal hex values that any toolchain (GCC, LLVM, official spec tables) must produce. Examples from the test suite:

  • `addi a0, a0, 1` → `0x00150513`
  • `auipc ra, 0` → `0x00000097`
  • `csrrw zero, mtvec, a0` → `0x30551073`
  • `mul a0, a0, a1` → `0x02b50533`
  • `mret` → `0x30200073`
  • `fence iorw,iorw` → `0x0FF0000F`

Test summary

  • 60 new tests in synth-backend-riscv: encoder vectors, register file, PMP allocation, ELF round-trip, selector behavior, backend trait conformance.
  • All 600+ existing workspace tests still pass — zero regressions.
  • `cargo clippy --workspace --all-targets -- -D warnings` clean.
  • `cargo fmt --check` clean.

What's deliberately out of scope (Tracks B2/B3/B4)

  • Comparisons (lt_s/lt_u/gt_s/gt_u/le/ge/eq/ne)
  • Loads / stores (i32.load, i32.store, ...)
  • Division / remainder (encoded but not wired into the selector yet)
  • i64 lowering (RV32 register pairs)
  • Control flow (block / loop / if / br / br_table)
  • Cross-function calls (jal + linker relocations)
  • Trap handler / startup code (mtvec setup, mret)
  • Renode RV32 platform + integration test (B2)
  • Linker script generator (B3)
  • RISC-V Rocq proofs (later)

🤖 Generated with Claude Code

@temper-pulseengine
Copy link
Copy Markdown

Automated review for PR #87

pulseengine/synth:feat/riscv-skeleton → pulseengine/synth:fix/synth-optimized-regalloc-params

Verdict: 💬 Comment

Summary: The pull request is approved. The changes enhance the RISC-V backend by adding support for building relocatable ELF files and multi-function ELF files. This allows for more flexible integration of the backend into various bare-metal environments.

Findings: 0 mechanical (rivet) · 1 from local AI model.

Findings (1):

  1. crates/synth-backend-riscv/Cargo.toml:1
    [dependencies]
    
    The RISC-V backend has its own dependencies.

Generated by a local AI model and post-validated against a strict JSON contract. Each finding includes the verbatim line being criticised — verify by reading the file at the cited location.

Reviewed at 510b193

Base automatically changed from fix/synth-optimized-regalloc-params to main May 10, 2026 04:46
avrabe added a commit that referenced this pull request May 10, 2026
…ning (#86)

Consolidated PR — supersedes #83 and #85.

* fix(opt): regalloc clobbers parameter registers — AAPCS violation in i64 ops
* fix(no-optimize): allocate stack frame + i64 local storage in select_with_stack
* feat(cli): --relocatable flag forces ET_REL output
* feat(m7): Cortex-M7 single-/double-FPU hardware profiles, 16-region MPU,
  Renode synth_cortex_m7.repl + cortex_m7_test.robot, M7 codegen smoke
* fix: 8 clippy errors (Rust 1.95 lint refresh)

codecov/patch fail noted — addressed as a first-class follow-up via the
coverage-uplift task that lands ahead of #87.
@avrabe avrabe force-pushed the feat/riscv-skeleton branch from 510b193 to 0d46897 Compare May 10, 2026 05:20
@codecov
Copy link
Copy Markdown

codecov Bot commented May 10, 2026

…locator

Adds a parallel RISC-V code-generation path alongside the ARM backend.
This is the Track B1 deliverable in the multi-track v0.1.0 plan: a working
end-to-end pipeline (WASM → RISC-V instructions → ELF) for a small WASM
subset, plus the infrastructure that the full instruction selector will
plug into.

* New crate `crates/synth-backend-riscv/`:
  - `register.rs` — 32 GPR enum, ABI-name aliases (a0..a7, s0..s11, t0..t6),
    callee-saved set per the RISC-V psABI, argument-register helper.
  - `riscv_op.rs` — `RiscVOp` enum covering RV32I + Zicsr + M-extension,
    plus `Branch`/`Csr` typed wrappers for trap-handler use.
  - `encoder.rs` — bit-level R/I/S/B/U/J encoder. Cross-checked against
    canonical reference encodings (`addi a0, a0, 1` → 0x00150513,
    `auipc ra, 0` → 0x00000097, `csrrw zero, mtvec, a0` → 0x30551073, ...).
    `Jal`/`Branch`/`Call` return `UnresolvedLabel` from the standalone path
    and have explicit `encode_jal` / `encode_branch` helpers the ELF builder
    calls once byte offsets are known.
  - `pmp.rs` — Physical Memory Protection allocator (RISC-V analogue of
    the ARM MPU). Picks NAPOT for power-of-two regions, falls back to TOR
    (consumes 2 entries) for arbitrary sizes. Generates C init code that
    writes `pmpaddrN` / `pmpcfgN` CSRs.
  - `elf_builder.rs` — minimal RV32 ELF emitter. Writes ET_REL by default,
    EM_RISCV (0xF3) machine type, RVC e_flags. Resolves `Jal`/`Branch`
    labels in a 2-pass per-function assembler; emits `.text` + `.symtab`
    + `.strtab` + `.shstrtab`.
  - `selector.rs` — skeleton WASM-to-RV32 instruction selector. Handles
    i32 add/sub/mul/and/or/xor, i32.const, i32.eqz, local.get/set for
    params 0..7 (mapped to a0..a7), and return. Constants > 12 bits go
    through `lui + addi` with proper sign-extension carry handling.
    The full lowering parity with the ARM selector is the B2 deliverable.
  - `backend.rs` — `Backend` trait impl. Registered in the CLI behind
    a `--features riscv` flag (on by default). Reports as "riscv" with
    `produces_elf=true, supports_rule_verification=false`.

* CLI wiring:
  - `synth-cli` adds `riscv` feature (default). `RiscVBackend::new()`
    is registered alongside ARM/w2c2/awsm/wasker.
  - Compile path now dispatches to `build_riscv_elf` /
    `build_multi_func_riscv_elf` when `target_spec.family == RiscV`,
    instead of producing an ARM ELF for RISC-V code bytes.

```
$ cat /tmp/rv_test.wat
(module
  (func (export "add") (param i32 i32) (result i32)
    local.get 0 local.get 1 i32.add)
  (memory (export "memory") 1))

$ synth compile /tmp/rv_test.wat -o rv.elf --backend riscv --target riscv32imac
$ file rv.elf
rv.elf: ELF 32-bit LSB relocatable, UCB RISC-V, RVC, soft-float ABI

$ xxd -s 52 -l 20 rv.elf
00000034: 9302 0500   # addi t0, a0, 0   (LocalGet 0)
00000038: 1383 0500   # addi t1, a1, 0   (LocalGet 1)
0000003c: b382 6200   # add  t0, t0, t1
00000040: 1385 0200   # addi a0, t0, 0   (move result to a0)
00000044: 6780 0000   # jalr zero, 0(ra) # ret
```

* Comparisons (lt_s/lt_u/gt_s/gt_u/le/ge/eq/ne)
* Loads / stores (i32.load, i32.store, …)
* Division / remainder (div_s/div_u/rem_s/rem_u — the M-extension is
  encoded but not wired into the selector yet)
* i64 lowering (RV32 register pairs)
* Control flow (block / loop / if / br / br_table)
* Calls (cross-function jal + linker relocations)
* Trap-handler / startup code (mtvec setup, mret)
* Renode RV32 platform + integration test
* RISC-V Rocq proofs

* 60 new tests in `synth-backend-riscv` (encoder vector tests, PMP allocation,
  ELF round-trip, selector behavior, backend trait conformance).
* All workspace tests still pass (≥600 tests, 0 regressions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@avrabe avrabe force-pushed the feat/riscv-skeleton branch from 0d46897 to 8270e66 Compare May 10, 2026 11:54
@avrabe avrabe merged commit 3f554e7 into main May 10, 2026
8 checks passed
@avrabe avrabe deleted the feat/riscv-skeleton branch May 10, 2026 13:18
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