From 8270e66811f911b53b9017f33cc19eed606a988c Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Sun, 3 May 2026 15:41:20 +0200 Subject: [PATCH] =?UTF-8?q?feat(riscv):=20RV32IMAC=20backend=20skeleton=20?= =?UTF-8?q?=E2=80=94=20encoder,=20ELF=20builder,=20PMP=20allocator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- Cargo.lock | 13 + Cargo.toml | 1 + crates/BUILD.bazel | 20 +- crates/synth-backend-riscv/Cargo.toml | 18 + crates/synth-backend-riscv/src/backend.rs | 346 +++++++++ crates/synth-backend-riscv/src/elf_builder.rs | 600 +++++++++++++++ crates/synth-backend-riscv/src/encoder.rs | 718 ++++++++++++++++++ crates/synth-backend-riscv/src/lib.rs | 31 + crates/synth-backend-riscv/src/pmp.rs | 414 ++++++++++ crates/synth-backend-riscv/src/register.rs | 241 ++++++ crates/synth-backend-riscv/src/riscv_op.rs | 228 ++++++ crates/synth-backend-riscv/src/selector.rs | 355 +++++++++ crates/synth-cli/Cargo.toml | 4 +- crates/synth-cli/src/main.rs | 122 ++- 14 files changed, 3107 insertions(+), 4 deletions(-) create mode 100644 crates/synth-backend-riscv/Cargo.toml create mode 100644 crates/synth-backend-riscv/src/backend.rs create mode 100644 crates/synth-backend-riscv/src/elf_builder.rs create mode 100644 crates/synth-backend-riscv/src/encoder.rs create mode 100644 crates/synth-backend-riscv/src/lib.rs create mode 100644 crates/synth-backend-riscv/src/pmp.rs create mode 100644 crates/synth-backend-riscv/src/register.rs create mode 100644 crates/synth-backend-riscv/src/riscv_op.rs create mode 100644 crates/synth-backend-riscv/src/selector.rs diff --git a/Cargo.lock b/Cargo.lock index ec146ed..01cba8d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1912,6 +1912,18 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "synth-backend-riscv" +version = "0.1.0" +dependencies = [ + "anyhow", + "proptest", + "synth-core", + "synth-synthesis", + "thiserror 1.0.69", + "tracing", +] + [[package]] name = "synth-backend-wasker" version = "0.1.0" @@ -1934,6 +1946,7 @@ dependencies = [ "serde_json", "synth-backend", "synth-backend-awsm", + "synth-backend-riscv", "synth-backend-wasker", "synth-core", "synth-frontend", diff --git a/Cargo.toml b/Cargo.toml index 1904bf1..1cea2f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "crates/synth-memory", "crates/synth-backend-awsm", "crates/synth-backend-wasker", + "crates/synth-backend-riscv", ] resolver = "2" diff --git a/crates/BUILD.bazel b/crates/BUILD.bazel index c8c96a2..3dcd650 100644 --- a/crates/BUILD.bazel +++ b/crates/BUILD.bazel @@ -208,17 +208,35 @@ rust_library( ], ) -# Main CLI binary +# RISC-V backend (RV32IMAC encoder, ELF builder, PMP allocator) +rust_library( + name = "synth-backend-riscv", + srcs = glob(["synth-backend-riscv/src/**/*.rs"]), + crate_root = "synth-backend-riscv/src/lib.rs", + edition = "2024", + deps = [ + ":synth-core", + ":synth-synthesis", + "@crates//:anyhow", + "@crates//:thiserror", + "@crates//:tracing", + ], +) + +# Main CLI binary — `riscv` feature wires synth-backend-riscv in (matches +# Cargo's default = ["riscv"]). rust_binary( name = "synth", srcs = glob(["synth-cli/src/**/*.rs"]), crate_root = "synth-cli/src/main.rs", edition = "2024", + rustc_flags = ["--cfg", "feature=\"riscv\""], deps = [ ":synth-core", ":synth-frontend", ":synth-synthesis", ":synth-backend", + ":synth-backend-riscv", "@crates//:anyhow", "@crates//:clap", "@crates//:serde_json", diff --git a/crates/synth-backend-riscv/Cargo.toml b/crates/synth-backend-riscv/Cargo.toml new file mode 100644 index 0000000..3a91fc5 --- /dev/null +++ b/crates/synth-backend-riscv/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "synth-backend-riscv" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +description = "RISC-V encoder, ELF builder, PMP allocator, and bare-metal startup for synth" + +[dependencies] +synth-core = { path = "../synth-core" } +synth-synthesis = { path = "../synth-synthesis" } +anyhow.workspace = true +thiserror.workspace = true +tracing.workspace = true + +[dev-dependencies] +proptest.workspace = true diff --git a/crates/synth-backend-riscv/src/backend.rs b/crates/synth-backend-riscv/src/backend.rs new file mode 100644 index 0000000..9a8569a --- /dev/null +++ b/crates/synth-backend-riscv/src/backend.rs @@ -0,0 +1,346 @@ +//! RISC-V `Backend` trait implementation — plumbs the selector + encoder + ELF +//! builder behind the same interface as `synth_backend::ArmBackend`. +//! +//! The skeleton supports a narrow subset of WASM (i32 arithmetic, params, +//! `local.get`/`local.set`). Full feature parity with the ARM backend is the +//! Track B2/B3/B4 deliverable. + +use crate::elf_builder::{RiscVElfBuilder, RiscVElfFunction}; +use crate::selector::select_simple; +use synth_core::backend::{ + Backend, BackendCapabilities, BackendError, CompilationResult, CompileConfig, CompiledFunction, +}; +use synth_core::target::{ArchFamily, IsaVariant, TargetSpec}; +use synth_core::wasm_decoder::DecodedModule; +use synth_core::wasm_op::WasmOp; + +pub struct RiscVBackend; + +impl RiscVBackend { + pub fn new() -> Self { + Self + } +} + +impl Default for RiscVBackend { + fn default() -> Self { + Self::new() + } +} + +impl Backend for RiscVBackend { + fn name(&self) -> &str { + "riscv" + } + + fn capabilities(&self) -> BackendCapabilities { + BackendCapabilities { + produces_elf: true, + supports_rule_verification: false, // not yet — coming with B2 selector port + supports_binary_verification: true, + is_external: false, + } + } + + fn supported_targets(&self) -> Vec { + vec![TargetSpec::riscv32imac()] + } + + fn compile_module( + &self, + module: &DecodedModule, + config: &CompileConfig, + ) -> Result { + ensure_supported_target(&config.target)?; + + let exports: Vec<_> = module + .functions + .iter() + .filter(|f| f.export_name.is_some()) + .collect(); + + if exports.is_empty() { + return Err(BackendError::CompilationFailed( + "no exported functions found".into(), + )); + } + + let mut functions = Vec::new(); + let mut elf_funcs = Vec::new(); + for func in &exports { + let name = func.export_name.clone().unwrap(); + let compiled = self.compile_function(&name, &func.ops, config)?; + elf_funcs.push(RiscVElfFunction { + name: compiled.name.clone(), + ops: compile_to_riscv_ops(&func.ops, &compiled)?, + }); + functions.push(compiled); + } + + let builder = RiscVElfBuilder::new_relocatable(); + let elf = builder + .build(&elf_funcs) + .map_err(|e| BackendError::CompilationFailed(format!("RISC-V ELF emit: {e}")))?; + + Ok(CompilationResult { + functions, + elf: Some(elf), + backend_name: self.name().to_string(), + }) + } + + fn compile_function( + &self, + name: &str, + ops: &[WasmOp], + config: &CompileConfig, + ) -> Result { + ensure_supported_target(&config.target)?; + + let num_params = count_params(ops); + let selection = select_simple(ops, num_params) + .map_err(|e| BackendError::CompilationFailed(format!("RISC-V selector: {e}")))?; + + // Encode the function via the ELF builder's per-function pipeline so + // we benefit from label resolution. We discard the ELF and keep the + // raw bytes — that's what `CompiledFunction` carries. + let elf_func = RiscVElfFunction { + name: name.to_string(), + ops: selection.ops, + }; + let bytes = encode_function_bytes(&elf_func)?; + + Ok(CompiledFunction { + name: name.to_string(), + code: bytes, + wasm_ops: ops.to_vec(), + relocations: Vec::new(), + }) + } + + fn is_available(&self) -> bool { + true + } +} + +fn ensure_supported_target(target: &TargetSpec) -> Result<(), BackendError> { + if !matches!(target.family, ArchFamily::RiscV) { + return Err(BackendError::UnsupportedConfig(format!( + "RISC-V backend cannot compile for {:?}", + target.family + ))); + } + if !matches!( + target.isa, + IsaVariant::RiscV32 { .. } | IsaVariant::RiscV64 { .. } + ) { + return Err(BackendError::UnsupportedConfig(format!( + "RISC-V backend requires RiscV32/RiscV64 ISA, got {:?}", + target.isa + ))); + } + Ok(()) +} + +/// Mirrors the ARM backend's parameter inference: walk the wasm and find +/// the highest LocalGet index that is read before being assigned. +fn count_params(wasm_ops: &[WasmOp]) -> u32 { + let mut first_access: std::collections::HashMap = std::collections::HashMap::new(); + for op in wasm_ops { + match op { + WasmOp::LocalGet(idx) => { + first_access.entry(*idx).or_insert(true); + } + WasmOp::LocalSet(idx) | WasmOp::LocalTee(idx) => { + first_access.entry(*idx).or_insert(false); + } + _ => {} + } + } + first_access + .iter() + .filter_map(|(&idx, &is_read_first)| if is_read_first { Some(idx + 1) } else { None }) + .max() + .unwrap_or(0) +} + +/// Re-encode the selector's output to flat bytes — the same pipeline the ELF +/// builder uses, but exposed for `compile_function` (which doesn't return ELF). +fn encode_function_bytes(f: &RiscVElfFunction) -> Result, BackendError> { + let builder = RiscVElfBuilder::new_relocatable(); + let elf = builder + .build(std::slice::from_ref(f)) + .map_err(|e| BackendError::CompilationFailed(format!("RISC-V function emit: {e}")))?; + + // .text starts at offset 52 (ELF header). We need the bytes between header + // and the next section. The simplest robust path: re-parse our own ELF. + if elf.len() < 52 { + return Err(BackendError::CompilationFailed( + "ELF too small to contain header".into(), + )); + } + // shoff is at bytes [32..36] + let shoff = u32::from_le_bytes([elf[32], elf[33], elf[34], elf[35]]) as usize; + // The first section header (after the null one) is .text at shoff + 40. + // Pull sh_offset and sh_size for index 1. + let text_shdr = shoff + 40; + if elf.len() < text_shdr + 40 { + return Err(BackendError::CompilationFailed( + "section header table truncated".into(), + )); + } + let sh_offset = u32::from_le_bytes([ + elf[text_shdr + 16], + elf[text_shdr + 17], + elf[text_shdr + 18], + elf[text_shdr + 19], + ]) as usize; + let sh_size = u32::from_le_bytes([ + elf[text_shdr + 20], + elf[text_shdr + 21], + elf[text_shdr + 22], + elf[text_shdr + 23], + ]) as usize; + Ok(elf[sh_offset..sh_offset + sh_size].to_vec()) +} + +/// Re-run the selector for a function whose `CompiledFunction` we already have. +/// We don't store the `RiscVOp` sequence in `CompiledFunction` to keep it +/// arch-neutral, so this re-derives it on demand for ELF emission. +fn compile_to_riscv_ops( + ops: &[WasmOp], + _compiled: &CompiledFunction, +) -> Result, BackendError> { + let num_params = count_params(ops); + let selection = select_simple(ops, num_params) + .map_err(|e| BackendError::CompilationFailed(format!("RISC-V selector: {e}")))?; + Ok(selection.ops) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::register::Reg; + use crate::riscv_op::RiscVOp; + + #[test] + fn backend_name_and_caps() { + let b = RiscVBackend::new(); + assert_eq!(b.name(), "riscv"); + let caps = b.capabilities(); + assert!(caps.produces_elf); + assert!(!caps.is_external); + } + + #[test] + fn supported_targets_include_riscv32imac() { + let b = RiscVBackend::new(); + let targets = b.supported_targets(); + assert!( + targets + .iter() + .any(|t| matches!(t.family, ArchFamily::RiscV)) + ); + } + + #[test] + fn rejects_arm_target() { + let b = RiscVBackend::new(); + let cfg = CompileConfig { + target: TargetSpec::cortex_m4f(), + ..Default::default() + }; + let r = b.compile_function("test", &[], &cfg); + assert!(matches!(r, Err(BackendError::UnsupportedConfig(_)))); + } + + #[test] + fn compiles_simple_add_to_bytes() { + let b = RiscVBackend::new(); + let cfg = CompileConfig { + target: TargetSpec::riscv32imac(), + ..Default::default() + }; + let ops = vec![ + WasmOp::LocalGet(0), + WasmOp::LocalGet(1), + WasmOp::I32Add, + WasmOp::End, + ]; + let f = b.compile_function("add", &ops, &cfg).unwrap(); + assert_eq!(f.name, "add"); + // Should have at least 5 instructions × 4 bytes = 20 bytes + // (mv t0,a0; mv t1,a1; add t0,t0,t1; mv a0,t0; ret) + assert!(f.code.len() >= 20, "code was {} bytes", f.code.len()); + // Last 4 bytes are the `jalr zero, 0(ra)` ret = 0x00008067 + let last = &f.code[f.code.len() - 4..]; + assert_eq!( + u32::from_le_bytes([last[0], last[1], last[2], last[3]]), + 0x00008067 + ); + } + + #[test] + fn count_params_basic() { + let ops = vec![ + WasmOp::LocalGet(0), + WasmOp::LocalGet(1), + WasmOp::LocalGet(2), + WasmOp::I32Add, + WasmOp::I32Add, + ]; + assert_eq!(count_params(&ops), 3); + } + + #[test] + fn count_params_local_set_first_excludes() { + let ops = vec![ + WasmOp::LocalGet(0), // param + WasmOp::LocalSet(1), // local var (set first) + WasmOp::LocalGet(1), // re-read + ]; + assert_eq!(count_params(&ops), 1); + } + + #[test] + fn manual_riscv_op_round_trip() { + // Exercise compile_to_riscv_ops directly. + let ops = vec![ + WasmOp::LocalGet(0), + WasmOp::LocalGet(1), + WasmOp::I32Add, + WasmOp::End, + ]; + let dummy = CompiledFunction { + name: "x".into(), + code: Vec::new(), + wasm_ops: ops.clone(), + relocations: Vec::new(), + }; + let rv = compile_to_riscv_ops(&ops, &dummy).unwrap(); + assert!(matches!( + rv[0], + RiscVOp::Addi { + rd: Reg::T0, + rs1: Reg::A0, + .. + } + )); + assert!(matches!( + rv[1], + RiscVOp::Addi { + rd: Reg::T1, + rs1: Reg::A1, + .. + } + )); + assert!(matches!( + rv[2], + RiscVOp::Add { + rd: Reg::T0, + rs1: Reg::T0, + rs2: Reg::T1 + } + )); + } +} diff --git a/crates/synth-backend-riscv/src/elf_builder.rs b/crates/synth-backend-riscv/src/elf_builder.rs new file mode 100644 index 0000000..cbf5e9a --- /dev/null +++ b/crates/synth-backend-riscv/src/elf_builder.rs @@ -0,0 +1,600 @@ +//! Minimal RISC-V ELF builder — emits ET_REL or ET_EXEC for RV32IMAC. +//! +//! Mirrors `synth-backend::elf_builder` (ARM) but targets EM_RISCV (0xF3) +//! and writes RISC-V-flavored e_flags. The skeleton only handles the +//! mechanics of producing a well-formed ELF; the instruction selection +//! and code-byte production happen upstream in `synth-synthesis::riscv`. +//! +//! What this skeleton does *now*: +//! - Construct a 32-bit little-endian ELF header +//! - Write `.text` containing concatenated function bytes +//! - Optionally emit a `.symtab` + `.strtab` with one symbol per function +//! - Resolve `Jal`/`Branch`/`Call` ops to byte offsets after layout +//! +//! What it leaves to follow-ups (B3): +//! - Vector tables / mtvec setup +//! - PMP init code linkage +//! - Linker script generation +//! - Multiple sections (.rodata, .bss, .data init copies) + +use crate::encoder::{RiscVEncoder, RiscVEncodingError}; +use crate::register::Reg; +use crate::riscv_op::RiscVOp; +use std::collections::HashMap; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ElfBuildError { + #[error("encoding error: {0}")] + Encoding(#[from] RiscVEncodingError), + + #[error("undefined label `{0}`")] + UndefinedLabel(String), + + #[error("function `{0}` is empty")] + EmptyFunction(String), + + #[error("unsupported in skeleton: {0}")] + Unsupported(&'static str), +} + +/// One compiled function — name + a sequence of RISC-V ops (with embedded +/// `Label { name }` markers to anchor branch targets). +#[derive(Debug, Clone)] +pub struct RiscVElfFunction { + pub name: String, + pub ops: Vec, +} + +/// Output mode — forces the ELF file type. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ElfMode { + /// `ET_REL` — relocatable object, suitable for `ld` / linker. + Relocatable, + /// `ET_EXEC` — fully linked, statically positioned executable. + Executable, +} + +pub struct RiscVElfBuilder { + pub xlen: u8, + pub mode: ElfMode, + /// Entry point virtual address (only used for `Executable` mode). + pub entry_addr: u32, + /// Base virtual address of `.text` (only used for `Executable` mode). + pub text_base: u32, +} + +impl RiscVElfBuilder { + pub fn new_relocatable() -> Self { + Self { + xlen: 32, + mode: ElfMode::Relocatable, + entry_addr: 0, + text_base: 0, + } + } + + pub fn new_executable(entry_addr: u32, text_base: u32) -> Self { + Self { + xlen: 32, + mode: ElfMode::Executable, + entry_addr, + text_base, + } + } + + /// Build the full ELF blob. Functions are concatenated in order; + /// each function is independently resolved (no cross-function branch + /// labels — those will need a second pass once we add `Call`). + pub fn build(&self, functions: &[RiscVElfFunction]) -> Result, ElfBuildError> { + let encoder = RiscVEncoder::new_rv32(); + + // 1. Resolve labels per-function, accumulate code bytes & symbols. + let mut text: Vec = Vec::new(); + let mut symbols: Vec<(String, u32, u32)> = Vec::new(); // (name, st_value, st_size) + + for f in functions { + if f.ops.is_empty() { + return Err(ElfBuildError::EmptyFunction(f.name.clone())); + } + let function_offset = text.len() as u32; + let bytes = self.assemble_function(&encoder, f)?; + let function_size = bytes.len() as u32; + text.extend_from_slice(&bytes); + symbols.push((f.name.clone(), function_offset, function_size)); + } + + // 2. Section ordering: + // [0] null + // [1] .text (PROGBITS, AX) + // [2] .symtab (SYMTAB) + // [3] .strtab (STRTAB) — symbol names + // [4] .shstrtab (STRTAB) — section names + let mut elf = Vec::new(); + let ehsize = 52usize; + let shentsize = 40usize; + let phentsize = 32usize; + + elf.resize(ehsize, 0); + + // .text + let text_offset = elf.len(); + elf.extend_from_slice(&text); + + // Pad to 4-byte alignment for the symbol table. + while elf.len() % 4 != 0 { + elf.push(0); + } + + // .strtab — built first so we know offsets for .symtab. + let mut strtab = vec![0u8]; // ELF requires a leading NUL. + let mut name_offsets: Vec = Vec::with_capacity(symbols.len()); + for (name, _, _) in &symbols { + name_offsets.push(strtab.len() as u32); + strtab.extend_from_slice(name.as_bytes()); + strtab.push(0); + } + + // .symtab — entry 0 is reserved (all zero). + let symtab_offset = elf.len(); + elf.extend_from_slice(&[0u8; 16]); // null symbol + for (i, (_, value, size)) in symbols.iter().enumerate() { + let st_name = name_offsets[i]; + let st_value = if self.mode == ElfMode::Executable { + self.text_base + *value + } else { + *value + }; + let st_info = (1u8 << 4) | 2; // STB_GLOBAL << 4 | STT_FUNC + let st_other = 0u8; + let st_shndx: u16 = 1; // .text + let mut entry = [0u8; 16]; + entry[0..4].copy_from_slice(&st_name.to_le_bytes()); + entry[4..8].copy_from_slice(&st_value.to_le_bytes()); + entry[8..12].copy_from_slice(&size.to_le_bytes()); + entry[12] = st_info; + entry[13] = st_other; + entry[14..16].copy_from_slice(&st_shndx.to_le_bytes()); + elf.extend_from_slice(&entry); + } + let symtab_size = (symbols.len() + 1) * 16; + + // .strtab + let strtab_offset = elf.len(); + elf.extend_from_slice(&strtab); + + // .shstrtab — fixed contents + let shstrtab_offset = elf.len(); + let shstrtab_data = build_shstrtab(); + elf.extend_from_slice(&shstrtab_data.bytes); + + // Pad to 4-byte for the section header table. + while elf.len() % 4 != 0 { + elf.push(0); + } + + let shoff = elf.len(); + + // Section headers + let text_size = text.len() as u32; + let symtab_link = 3u32; // index of .strtab + let shdrs = vec![ + // [0] null + ShEntry::null(), + // [1] .text + ShEntry { + sh_name: shstrtab_data.text_off, + sh_type: 1, // SHT_PROGBITS + sh_flags: 0x6, // SHF_ALLOC | SHF_EXECINSTR + sh_addr: if self.mode == ElfMode::Executable { + self.text_base + } else { + 0 + }, + sh_offset: text_offset as u32, + sh_size: text_size, + sh_link: 0, + sh_info: 0, + sh_addralign: 4, + sh_entsize: 0, + }, + // [2] .symtab + ShEntry { + sh_name: shstrtab_data.symtab_off, + sh_type: 2, // SHT_SYMTAB + sh_flags: 0, + sh_addr: 0, + sh_offset: symtab_offset as u32, + sh_size: symtab_size as u32, + sh_link: symtab_link, + sh_info: 1, // index of first global symbol + sh_addralign: 4, + sh_entsize: 16, + }, + // [3] .strtab + ShEntry { + sh_name: shstrtab_data.strtab_off, + sh_type: 3, // SHT_STRTAB + sh_flags: 0, + sh_addr: 0, + sh_offset: strtab_offset as u32, + sh_size: strtab.len() as u32, + sh_link: 0, + sh_info: 0, + sh_addralign: 1, + sh_entsize: 0, + }, + // [4] .shstrtab + ShEntry { + sh_name: shstrtab_data.shstrtab_off, + sh_type: 3, // SHT_STRTAB + sh_flags: 0, + sh_addr: 0, + sh_offset: shstrtab_offset as u32, + sh_size: shstrtab_data.bytes.len() as u32, + sh_link: 0, + sh_info: 0, + sh_addralign: 1, + sh_entsize: 0, + }, + ]; + + for sh in &shdrs { + sh.write_into(&mut elf); + } + + // Now patch up the ELF header at offset 0. + write_elf_header( + &mut elf, + self.xlen, + self.mode, + self.entry_addr, + shoff as u32, + shdrs.len() as u16, + shentsize as u16, + ehsize as u16, + phentsize as u16, + 4u16, // shstrtab index + ); + + Ok(elf) + } + + fn assemble_function( + &self, + encoder: &RiscVEncoder, + f: &RiscVElfFunction, + ) -> Result, ElfBuildError> { + // Pass 1: compute byte offset of each label. + let mut byte_offsets: Vec = Vec::with_capacity(f.ops.len() + 1); + let mut labels: HashMap = HashMap::new(); + let mut cursor: u32 = 0; + for op in &f.ops { + byte_offsets.push(cursor); + match op { + RiscVOp::Label { name } => { + labels.insert(name.clone(), cursor); + } + RiscVOp::Call { .. } => cursor += 8, // auipc + jalr pair + _ => cursor += 4, + } + } + byte_offsets.push(cursor); + + // Pass 2: emit bytes, resolving Jal/Branch/Call with the offsets we just collected. + let mut bytes: Vec = Vec::with_capacity(cursor as usize); + for (i, op) in f.ops.iter().enumerate() { + let here = byte_offsets[i] as i32; + match op { + RiscVOp::Label { .. } => {} + RiscVOp::Jal { rd, label } => { + let target = *labels + .get(label) + .ok_or_else(|| ElfBuildError::UndefinedLabel(label.clone()))? + as i32; + let inst = encoder.encode_jal(rd.num(), target - here)?; + bytes.extend_from_slice(&inst.to_le_bytes()); + } + RiscVOp::Branch { + cond, + rs1, + rs2, + label, + } => { + let target = *labels + .get(label) + .ok_or_else(|| ElfBuildError::UndefinedLabel(label.clone()))? + as i32; + let inst = encoder.encode_branch(*cond, rs1.num(), rs2.num(), target - here)?; + bytes.extend_from_slice(&inst.to_le_bytes()); + } + RiscVOp::Call { label } => { + // Skeleton emits a placeholder auipc + jalr ra, 0(ra) — the + // ELF builder for executables will need PC-relative + // relocations; for now we emit a self-resolving local call + // if the label is local, else error. + if let Some(&target) = labels.get(label) { + let rel = target as i32 - here; + // auipc t1, rel[31:12] + carry + let hi = (rel + 0x800) >> 12; + let lo = rel - (hi << 12); + let auipc = RiscVOp::Auipc { + rd: Reg::T1, + imm20: (hi as u32) & 0xFFFFF, + }; + bytes.extend_from_slice(&encoder.encode(&auipc)?.to_le_bytes()); + let jalr = RiscVOp::Jalr { + rd: Reg::RA, + rs1: Reg::T1, + imm: lo, + }; + bytes.extend_from_slice(&encoder.encode(&jalr)?.to_le_bytes()); + } else { + return Err(ElfBuildError::Unsupported( + "external call without relocation table", + )); + } + } + _ => { + let inst = encoder.encode(op)?; + bytes.extend_from_slice(&inst.to_le_bytes()); + } + } + // Sanity: the byte cursor in pass-1 must match what we actually wrote. + debug_assert_eq!(bytes.len() as u32, byte_offsets[i + 1]); + } + Ok(bytes) + } +} + +// ──────────────────────────────────────────────────────────────────── +// ELF plumbing +// ──────────────────────────────────────────────────────────────────── + +#[derive(Debug, Clone, Copy)] +struct ShEntry { + sh_name: u32, + sh_type: u32, + sh_flags: u32, + sh_addr: u32, + sh_offset: u32, + sh_size: u32, + sh_link: u32, + sh_info: u32, + sh_addralign: u32, + sh_entsize: u32, +} + +impl ShEntry { + fn null() -> Self { + Self { + sh_name: 0, + sh_type: 0, + sh_flags: 0, + sh_addr: 0, + sh_offset: 0, + sh_size: 0, + sh_link: 0, + sh_info: 0, + sh_addralign: 0, + sh_entsize: 0, + } + } + + fn write_into(&self, out: &mut Vec) { + out.extend_from_slice(&self.sh_name.to_le_bytes()); + out.extend_from_slice(&self.sh_type.to_le_bytes()); + out.extend_from_slice(&self.sh_flags.to_le_bytes()); + out.extend_from_slice(&self.sh_addr.to_le_bytes()); + out.extend_from_slice(&self.sh_offset.to_le_bytes()); + out.extend_from_slice(&self.sh_size.to_le_bytes()); + out.extend_from_slice(&self.sh_link.to_le_bytes()); + out.extend_from_slice(&self.sh_info.to_le_bytes()); + out.extend_from_slice(&self.sh_addralign.to_le_bytes()); + out.extend_from_slice(&self.sh_entsize.to_le_bytes()); + } +} + +struct ShstrtabData { + bytes: Vec, + text_off: u32, + symtab_off: u32, + strtab_off: u32, + shstrtab_off: u32, +} + +fn build_shstrtab() -> ShstrtabData { + let mut bytes = vec![0u8]; + let text_off = bytes.len() as u32; + bytes.extend_from_slice(b".text\0"); + let symtab_off = bytes.len() as u32; + bytes.extend_from_slice(b".symtab\0"); + let strtab_off = bytes.len() as u32; + bytes.extend_from_slice(b".strtab\0"); + let shstrtab_off = bytes.len() as u32; + bytes.extend_from_slice(b".shstrtab\0"); + ShstrtabData { + bytes, + text_off, + symtab_off, + strtab_off, + shstrtab_off, + } +} + +#[allow(clippy::too_many_arguments)] +fn write_elf_header( + out: &mut [u8], + xlen: u8, + mode: ElfMode, + entry: u32, + shoff: u32, + shnum: u16, + shentsize: u16, + ehsize: u16, + _phentsize: u16, + shstrndx: u16, +) { + // e_ident[0..4] — magic + out[0..4].copy_from_slice(&[0x7F, b'E', b'L', b'F']); + // EI_CLASS — 1 = 32-bit, 2 = 64-bit + out[4] = if xlen == 32 { 1 } else { 2 }; + // EI_DATA — 1 = little endian + out[5] = 1; + // EI_VERSION + out[6] = 1; + // EI_OSABI = 0 (System V) + out[7] = 0; + // EI_ABIVERSION = 0 + out[8] = 0; + // padding 9..15 already zero + + // e_type + let e_type: u16 = match mode { + ElfMode::Relocatable => 1, // ET_REL + ElfMode::Executable => 2, // ET_EXEC + }; + out[16..18].copy_from_slice(&e_type.to_le_bytes()); + // e_machine = 0xF3 (EM_RISCV) + let e_machine: u16 = 0xF3; + out[18..20].copy_from_slice(&e_machine.to_le_bytes()); + // e_version = 1 + out[20..24].copy_from_slice(&1u32.to_le_bytes()); + // e_entry + out[24..28].copy_from_slice(&entry.to_le_bytes()); + // e_phoff = 0 (no program headers in this skeleton) + out[28..32].copy_from_slice(&0u32.to_le_bytes()); + // e_shoff + out[32..36].copy_from_slice(&shoff.to_le_bytes()); + // e_flags — RVC + soft float ABI + let e_flags: u32 = 0x1; // RVC + out[36..40].copy_from_slice(&e_flags.to_le_bytes()); + // e_ehsize + out[40..42].copy_from_slice(&ehsize.to_le_bytes()); + // e_phentsize, e_phnum + out[42..44].copy_from_slice(&0u16.to_le_bytes()); + out[44..46].copy_from_slice(&0u16.to_le_bytes()); + // e_shentsize + out[46..48].copy_from_slice(&shentsize.to_le_bytes()); + // e_shnum + out[48..50].copy_from_slice(&shnum.to_le_bytes()); + // e_shstrndx + out[50..52].copy_from_slice(&shstrndx.to_le_bytes()); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::register::Reg; + + fn nop_op() -> RiscVOp { + RiscVOp::Addi { + rd: Reg::ZERO, + rs1: Reg::ZERO, + imm: 0, + } + } + + #[test] + fn build_minimal_elf() { + let builder = RiscVElfBuilder::new_relocatable(); + let f = RiscVElfFunction { + name: "add".into(), + ops: vec![ + RiscVOp::Add { + rd: Reg::A0, + rs1: Reg::A0, + rs2: Reg::A1, + }, + RiscVOp::Jalr { + rd: Reg::ZERO, + rs1: Reg::RA, + imm: 0, + }, // ret + ], + }; + let elf = builder.build(&[f]).unwrap(); + // Sanity-check the magic bytes and machine type. + assert_eq!(&elf[0..4], &[0x7F, b'E', b'L', b'F']); + assert_eq!(elf[4], 1, "EI_CLASS = 32-bit"); + assert_eq!(elf[5], 1, "EI_DATA = little endian"); + // EM_RISCV = 0xF3 + assert_eq!(u16::from_le_bytes([elf[18], elf[19]]), 0xF3); + // ET_REL + assert_eq!(u16::from_le_bytes([elf[16], elf[17]]), 1); + } + + #[test] + fn jal_with_label_resolution() { + let builder = RiscVElfBuilder::new_relocatable(); + let f = RiscVElfFunction { + name: "loop".into(), + ops: vec![ + RiscVOp::Label { name: "top".into() }, + nop_op(), + RiscVOp::Jal { + rd: Reg::ZERO, + label: "top".into(), + }, + ], + }; + let bytes = builder.build(&[f]).unwrap(); + // .text starts at 52 (ELF header). First instruction is the nop (4 bytes). + // The JAL is at offset 52+4 = 56 and targets offset 52 → rel = -4 + // jal zero, -4 encodes to 0xFFDFF06F (rd=0, imm=-4) + let jal = u32::from_le_bytes([bytes[56], bytes[57], bytes[58], bytes[59]]); + assert_eq!(jal, 0xFFDFF06F); + } + + #[test] + fn empty_function_rejected() { + let builder = RiscVElfBuilder::new_relocatable(); + let f = RiscVElfFunction { + name: "empty".into(), + ops: vec![], + }; + assert!(matches!( + builder.build(&[f]), + Err(ElfBuildError::EmptyFunction(_)) + )); + } + + #[test] + fn undefined_label_rejected() { + let builder = RiscVElfBuilder::new_relocatable(); + let f = RiscVElfFunction { + name: "broken".into(), + ops: vec![RiscVOp::Jal { + rd: Reg::ZERO, + label: "missing".into(), + }], + }; + assert!(matches!( + builder.build(&[f]), + Err(ElfBuildError::UndefinedLabel(_)) + )); + } + + #[test] + fn executable_mode_writes_text_base_in_symbols() { + let builder = RiscVElfBuilder::new_executable(0x80000000, 0x80000000); + let f = RiscVElfFunction { + name: "main".into(), + ops: vec![ + nop_op(), + RiscVOp::Jalr { + rd: Reg::ZERO, + rs1: Reg::RA, + imm: 0, + }, + ], + }; + let elf = builder.build(&[f]).unwrap(); + assert_eq!(u16::from_le_bytes([elf[16], elf[17]]), 2, "ET_EXEC"); + assert_eq!( + u32::from_le_bytes([elf[24], elf[25], elf[26], elf[27]]), + 0x80000000, + "e_entry" + ); + } +} diff --git a/crates/synth-backend-riscv/src/encoder.rs b/crates/synth-backend-riscv/src/encoder.rs new file mode 100644 index 0000000..f1c5182 --- /dev/null +++ b/crates/synth-backend-riscv/src/encoder.rs @@ -0,0 +1,718 @@ +//! RV32 instruction encoder — translates `RiscVOp` to 32-bit machine words. +//! +//! Reference: RISC-V Unprivileged ISA, version 20191213 (Volume I), § 2.2-2.6. +//! All encodings below are little-endian 32-bit instructions. +//! +//! The encoder is intentionally thin: branches/jumps with labels are NOT +//! resolved here — the ELF builder fills those after layout. This module +//! handles the mechanical bit-packing. + +use crate::riscv_op::{Branch, Csr, RiscVOp}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum RiscVEncodingError { + #[error("immediate {value} out of range for {what} (allowed: {min}..={max})")] + ImmediateOutOfRange { + value: i64, + what: &'static str, + min: i64, + max: i64, + }, + + #[error("shift amount {0} out of range (must be 0..32)")] + ShiftAmountOutOfRange(u8), + + #[error("CSR address 0x{0:03X} out of range (must be 12-bit)")] + CsrOutOfRange(u16), + + #[error("instruction `{0}` requires label resolution and cannot be encoded standalone")] + UnresolvedLabel(&'static str), +} + +pub type EncodingResult = Result; + +/// RV32IMAC encoder. +/// +/// `xlen` selects between RV32 (32) and RV64 (64). Currently only RV32 is +/// fully exercised; RV64-specific instructions (LD, SD, ADDIW, etc.) will be +/// added in a follow-up. Storing it here makes that a localized change. +pub struct RiscVEncoder { + pub xlen: u8, +} + +impl RiscVEncoder { + pub fn new_rv32() -> Self { + Self { xlen: 32 } + } + + pub fn new_rv64() -> Self { + Self { xlen: 64 } + } + + /// Encode a single op. Variants requiring label resolution + /// (`Jal`, `Branch`, `Call`, `Label`) return `UnresolvedLabel` — + /// the ELF builder calls back through the lower-level helpers + /// once it knows byte offsets. + pub fn encode(&self, op: &RiscVOp) -> EncodingResult { + use RiscVOp::*; + match op { + Lui { rd, imm20 } => Ok(encode_u(0b0110111, rd.num(), *imm20)), + Auipc { rd, imm20 } => Ok(encode_u(0b0010111, rd.num(), *imm20)), + + Addi { rd, rs1, imm } => encode_i_signed(0b0010011, 0b000, rd.num(), rs1.num(), *imm), + Slti { rd, rs1, imm } => encode_i_signed(0b0010011, 0b010, rd.num(), rs1.num(), *imm), + Sltiu { rd, rs1, imm } => encode_i_signed(0b0010011, 0b011, rd.num(), rs1.num(), *imm), + Xori { rd, rs1, imm } => encode_i_signed(0b0010011, 0b100, rd.num(), rs1.num(), *imm), + Ori { rd, rs1, imm } => encode_i_signed(0b0010011, 0b110, rd.num(), rs1.num(), *imm), + Andi { rd, rs1, imm } => encode_i_signed(0b0010011, 0b111, rd.num(), rs1.num(), *imm), + + Slli { rd, rs1, shamt } => { + encode_shift(0b0010011, 0b001, 0b0000000, rd.num(), rs1.num(), *shamt) + } + Srli { rd, rs1, shamt } => { + encode_shift(0b0010011, 0b101, 0b0000000, rd.num(), rs1.num(), *shamt) + } + Srai { rd, rs1, shamt } => { + encode_shift(0b0010011, 0b101, 0b0100000, rd.num(), rs1.num(), *shamt) + } + + Add { rd, rs1, rs2 } => Ok(encode_r( + 0b0110011, + 0b000, + 0b0000000, + rd.num(), + rs1.num(), + rs2.num(), + )), + Sub { rd, rs1, rs2 } => Ok(encode_r( + 0b0110011, + 0b000, + 0b0100000, + rd.num(), + rs1.num(), + rs2.num(), + )), + Sll { rd, rs1, rs2 } => Ok(encode_r( + 0b0110011, + 0b001, + 0b0000000, + rd.num(), + rs1.num(), + rs2.num(), + )), + Slt { rd, rs1, rs2 } => Ok(encode_r( + 0b0110011, + 0b010, + 0b0000000, + rd.num(), + rs1.num(), + rs2.num(), + )), + Sltu { rd, rs1, rs2 } => Ok(encode_r( + 0b0110011, + 0b011, + 0b0000000, + rd.num(), + rs1.num(), + rs2.num(), + )), + Xor { rd, rs1, rs2 } => Ok(encode_r( + 0b0110011, + 0b100, + 0b0000000, + rd.num(), + rs1.num(), + rs2.num(), + )), + Srl { rd, rs1, rs2 } => Ok(encode_r( + 0b0110011, + 0b101, + 0b0000000, + rd.num(), + rs1.num(), + rs2.num(), + )), + Sra { rd, rs1, rs2 } => Ok(encode_r( + 0b0110011, + 0b101, + 0b0100000, + rd.num(), + rs1.num(), + rs2.num(), + )), + Or { rd, rs1, rs2 } => Ok(encode_r( + 0b0110011, + 0b110, + 0b0000000, + rd.num(), + rs1.num(), + rs2.num(), + )), + And { rd, rs1, rs2 } => Ok(encode_r( + 0b0110011, + 0b111, + 0b0000000, + rd.num(), + rs1.num(), + rs2.num(), + )), + + // Loads (I-type, opcode 0b0000011) + Lb { rd, rs1, imm } => encode_i_signed(0b0000011, 0b000, rd.num(), rs1.num(), *imm), + Lh { rd, rs1, imm } => encode_i_signed(0b0000011, 0b001, rd.num(), rs1.num(), *imm), + Lw { rd, rs1, imm } => encode_i_signed(0b0000011, 0b010, rd.num(), rs1.num(), *imm), + Lbu { rd, rs1, imm } => encode_i_signed(0b0000011, 0b100, rd.num(), rs1.num(), *imm), + Lhu { rd, rs1, imm } => encode_i_signed(0b0000011, 0b101, rd.num(), rs1.num(), *imm), + + // Stores (S-type, opcode 0b0100011) + Sb { rs1, rs2, imm } => encode_s(0b0100011, 0b000, rs1.num(), rs2.num(), *imm), + Sh { rs1, rs2, imm } => encode_s(0b0100011, 0b001, rs1.num(), rs2.num(), *imm), + Sw { rs1, rs2, imm } => encode_s(0b0100011, 0b010, rs1.num(), rs2.num(), *imm), + + // Jumps + Jalr { rd, rs1, imm } => encode_i_signed(0b1100111, 0b000, rd.num(), rs1.num(), *imm), + + // System ops with fixed funct12 / funct7 encodings + Ecall => Ok(encode_i_unsigned(0b1110011, 0b000, 0, 0, 0)), + Ebreak => Ok(encode_i_unsigned(0b1110011, 0b000, 0, 0, 1)), + Mret => Ok(encode_i_unsigned(0b1110011, 0b000, 0, 0, 0x302)), + Wfi => Ok(encode_i_unsigned(0b1110011, 0b000, 0, 0, 0x105)), + Fence => Ok(0x0FF0000F), // fence iorw, iorw + + // CSR ops + Csrrw { rd, csr, rs1 } => encode_csr(0b001, rd.num(), rs1.num(), *csr), + Csrrs { rd, csr, rs1 } => encode_csr(0b010, rd.num(), rs1.num(), *csr), + Csrrc { rd, csr, rs1 } => encode_csr(0b011, rd.num(), rs1.num(), *csr), + Csrrwi { rd, csr, uimm5 } => encode_csr(0b101, rd.num(), *uimm5, *csr), + Csrrsi { rd, csr, uimm5 } => encode_csr(0b110, rd.num(), *uimm5, *csr), + Csrrci { rd, csr, uimm5 } => encode_csr(0b111, rd.num(), *uimm5, *csr), + + // M-extension + Mul { rd, rs1, rs2 } => Ok(encode_r( + 0b0110011, + 0b000, + 0b0000001, + rd.num(), + rs1.num(), + rs2.num(), + )), + Mulh { rd, rs1, rs2 } => Ok(encode_r( + 0b0110011, + 0b001, + 0b0000001, + rd.num(), + rs1.num(), + rs2.num(), + )), + Mulhsu { rd, rs1, rs2 } => Ok(encode_r( + 0b0110011, + 0b010, + 0b0000001, + rd.num(), + rs1.num(), + rs2.num(), + )), + Mulhu { rd, rs1, rs2 } => Ok(encode_r( + 0b0110011, + 0b011, + 0b0000001, + rd.num(), + rs1.num(), + rs2.num(), + )), + Div { rd, rs1, rs2 } => Ok(encode_r( + 0b0110011, + 0b100, + 0b0000001, + rd.num(), + rs1.num(), + rs2.num(), + )), + Divu { rd, rs1, rs2 } => Ok(encode_r( + 0b0110011, + 0b101, + 0b0000001, + rd.num(), + rs1.num(), + rs2.num(), + )), + Rem { rd, rs1, rs2 } => Ok(encode_r( + 0b0110011, + 0b110, + 0b0000001, + rd.num(), + rs1.num(), + rs2.num(), + )), + Remu { rd, rs1, rs2 } => Ok(encode_r( + 0b0110011, + 0b111, + 0b0000001, + rd.num(), + rs1.num(), + rs2.num(), + )), + + // Label-relative ops — must be resolved by the ELF builder. + Jal { .. } => Err(RiscVEncodingError::UnresolvedLabel("jal")), + Branch { .. } => Err(RiscVEncodingError::UnresolvedLabel("branch")), + Call { .. } => Err(RiscVEncodingError::UnresolvedLabel("call")), + Label { .. } => Err(RiscVEncodingError::UnresolvedLabel("label")), + } + } + + /// Encode a `jal rd, label` once the byte offset is known. + /// `rel_offset` is the byte distance from the JAL instruction to the target. + /// Must be even and in the range [-1 MiB, +1 MiB). + pub fn encode_jal(&self, rd: u8, rel_offset: i32) -> EncodingResult { + encode_j(0b1101111, rd, rel_offset) + } + + /// Encode a `b{cond} rs1, rs2, label` once the byte offset is known. + /// `rel_offset` is the byte distance from the branch to the target. + /// Must be even and in the range [-4 KiB, +4 KiB). + pub fn encode_branch(&self, cond: Branch, rs1: u8, rs2: u8, rel_offset: i32) -> EncodingResult { + encode_b(0b1100011, cond.funct3() as u32, rs1, rs2, rel_offset) + } +} + +impl Default for RiscVEncoder { + fn default() -> Self { + Self::new_rv32() + } +} + +// ──────────────────────────────────────────────────────────────────────── +// Bit-packing helpers — one per RV32 instruction format. +// ──────────────────────────────────────────────────────────────────────── + +#[inline] +fn encode_r(opcode: u32, funct3: u32, funct7: u32, rd: u8, rs1: u8, rs2: u8) -> u32 { + opcode + | ((rd as u32 & 0x1F) << 7) + | (funct3 << 12) + | ((rs1 as u32 & 0x1F) << 15) + | ((rs2 as u32 & 0x1F) << 20) + | (funct7 << 25) +} + +#[inline] +fn encode_i_unsigned(opcode: u32, funct3: u32, rd: u8, rs1: u8, imm12: u32) -> u32 { + opcode + | ((rd as u32 & 0x1F) << 7) + | (funct3 << 12) + | ((rs1 as u32 & 0x1F) << 15) + | ((imm12 & 0xFFF) << 20) +} + +fn encode_i_signed(opcode: u32, funct3: u32, rd: u8, rs1: u8, imm: i32) -> EncodingResult { + if !(-2048..=2047).contains(&imm) { + return Err(RiscVEncodingError::ImmediateOutOfRange { + value: imm as i64, + what: "I-type imm12", + min: -2048, + max: 2047, + }); + } + let imm12 = (imm as u32) & 0xFFF; + Ok(encode_i_unsigned(opcode, funct3, rd, rs1, imm12)) +} + +fn encode_s(opcode: u32, funct3: u32, rs1: u8, rs2: u8, imm: i32) -> EncodingResult { + if !(-2048..=2047).contains(&imm) { + return Err(RiscVEncodingError::ImmediateOutOfRange { + value: imm as i64, + what: "S-type imm12", + min: -2048, + max: 2047, + }); + } + let imm12 = (imm as u32) & 0xFFF; + let imm_low = imm12 & 0x1F; // imm[4:0] + let imm_high = (imm12 >> 5) & 0x7F; // imm[11:5] + Ok(opcode + | (imm_low << 7) + | (funct3 << 12) + | ((rs1 as u32 & 0x1F) << 15) + | ((rs2 as u32 & 0x1F) << 20) + | (imm_high << 25)) +} + +fn encode_b(opcode: u32, funct3: u32, rs1: u8, rs2: u8, imm: i32) -> EncodingResult { + if !(-4096..=4094).contains(&imm) || (imm & 1) != 0 { + return Err(RiscVEncodingError::ImmediateOutOfRange { + value: imm as i64, + what: "B-type rel offset", + min: -4096, + max: 4094, + }); + } + let i = imm as u32 & 0x1FFE; // 13-bit signed, low bit always 0 + // Field layout for B-type imm: + // inst[31] = imm[12] + // inst[7] = imm[11] + // inst[30:25]= imm[10:5] + // inst[11:8] = imm[4:1] + let bit12 = (i >> 12) & 0x1; + let bit11 = (i >> 11) & 0x1; + let bits10_5 = (i >> 5) & 0x3F; + let bits4_1 = (i >> 1) & 0xF; + Ok(opcode + | (bit11 << 7) + | (bits4_1 << 8) + | (funct3 << 12) + | ((rs1 as u32 & 0x1F) << 15) + | ((rs2 as u32 & 0x1F) << 20) + | (bits10_5 << 25) + | (bit12 << 31)) +} + +#[inline] +fn encode_u(opcode: u32, rd: u8, imm20: u32) -> u32 { + opcode | ((rd as u32 & 0x1F) << 7) | ((imm20 & 0xFFFFF) << 12) +} + +fn encode_j(opcode: u32, rd: u8, imm: i32) -> EncodingResult { + if !(-(1 << 20)..(1 << 20)).contains(&imm) || (imm & 1) != 0 { + return Err(RiscVEncodingError::ImmediateOutOfRange { + value: imm as i64, + what: "J-type rel offset", + min: -(1 << 20), + max: (1 << 20) - 1, + }); + } + let i = imm as u32 & 0x1FFFFE; // 21-bit signed, low bit always 0 + // Field layout: + // inst[31] = imm[20] + // inst[30:21] = imm[10:1] + // inst[20] = imm[11] + // inst[19:12] = imm[19:12] + let bit20 = (i >> 20) & 0x1; + let bits10_1 = (i >> 1) & 0x3FF; + let bit11 = (i >> 11) & 0x1; + let bits19_12 = (i >> 12) & 0xFF; + Ok(opcode + | ((rd as u32 & 0x1F) << 7) + | (bits19_12 << 12) + | (bit11 << 20) + | (bits10_1 << 21) + | (bit20 << 31)) +} + +fn encode_shift( + opcode: u32, + funct3: u32, + funct7: u32, + rd: u8, + rs1: u8, + shamt: u8, +) -> EncodingResult { + // RV32: shamt is 5 bits (0..31). + if shamt >= 32 { + return Err(RiscVEncodingError::ShiftAmountOutOfRange(shamt)); + } + Ok(opcode + | ((rd as u32 & 0x1F) << 7) + | (funct3 << 12) + | ((rs1 as u32 & 0x1F) << 15) + | ((shamt as u32 & 0x1F) << 20) + | (funct7 << 25)) +} + +fn encode_csr(funct3: u32, rd: u8, rs1_or_uimm: u8, csr: Csr) -> EncodingResult { + if csr.0 >= 0x1000 { + return Err(RiscVEncodingError::CsrOutOfRange(csr.0)); + } + Ok(0b1110011 + | ((rd as u32 & 0x1F) << 7) + | (funct3 << 12) + | ((rs1_or_uimm as u32 & 0x1F) << 15) + | ((csr.0 as u32 & 0xFFF) << 20)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::register::Reg; + + fn enc(op: RiscVOp) -> u32 { + RiscVEncoder::new_rv32().encode(&op).unwrap() + } + + // Reference encodings cross-checked with the RISC-V official spec + // and the SiFive `riscv-opcodes` table. These aren't tautologies — + // they're literal hex values that any toolchain (GAS, LLVM) must produce. + + #[test] + fn addi_a0_a0_1() { + // addi a0, a0, 1 → 00150513 + let inst = enc(RiscVOp::Addi { + rd: Reg::A0, + rs1: Reg::A0, + imm: 1, + }); + assert_eq!(inst, 0x00150513); + } + + #[test] + fn addi_zero_zero_neg1() { + // addi zero, zero, -1 → fff00013 + let inst = enc(RiscVOp::Addi { + rd: Reg::ZERO, + rs1: Reg::ZERO, + imm: -1, + }); + assert_eq!(inst, 0xFFF00013); + } + + #[test] + fn add_a0_a0_a1() { + // add a0, a0, a1 → 00b50533 + let inst = enc(RiscVOp::Add { + rd: Reg::A0, + rs1: Reg::A0, + rs2: Reg::A1, + }); + assert_eq!(inst, 0x00B50533); + } + + #[test] + fn sub_a0_a0_a1() { + // sub a0, a0, a1 → 40b50533 + let inst = enc(RiscVOp::Sub { + rd: Reg::A0, + rs1: Reg::A0, + rs2: Reg::A1, + }); + assert_eq!(inst, 0x40B50533); + } + + #[test] + fn lui_a0_0x12345() { + // lui a0, 0x12345 → 12345537 + let inst = enc(RiscVOp::Lui { + rd: Reg::A0, + imm20: 0x12345, + }); + assert_eq!(inst, 0x12345537); + } + + #[test] + fn auipc_ra_0() { + // auipc ra, 0 → 00000097 + let inst = enc(RiscVOp::Auipc { + rd: Reg::RA, + imm20: 0, + }); + assert_eq!(inst, 0x00000097); + } + + #[test] + fn jalr_ra_0_ra() { + // jalr ra, 0(ra) → 000080e7 + let inst = enc(RiscVOp::Jalr { + rd: Reg::RA, + rs1: Reg::RA, + imm: 0, + }); + assert_eq!(inst, 0x000080E7); + } + + #[test] + fn ret_jalr_zero_0_ra() { + // ret = jalr zero, 0(ra) → 00008067 + let inst = enc(RiscVOp::Jalr { + rd: Reg::ZERO, + rs1: Reg::RA, + imm: 0, + }); + assert_eq!(inst, 0x00008067); + } + + #[test] + fn lw_a0_4_a0() { + // lw a0, 4(a0) → 00452503 + let inst = enc(RiscVOp::Lw { + rd: Reg::A0, + rs1: Reg::A0, + imm: 4, + }); + assert_eq!(inst, 0x00452503); + } + + #[test] + fn sw_a1_4_a0() { + // sw a1, 4(a0) → 00b52223 + let inst = enc(RiscVOp::Sw { + rs1: Reg::A0, + rs2: Reg::A1, + imm: 4, + }); + assert_eq!(inst, 0x00B52223); + } + + #[test] + fn slli_a0_a0_3() { + // slli a0, a0, 3 → 00351513 + let inst = enc(RiscVOp::Slli { + rd: Reg::A0, + rs1: Reg::A0, + shamt: 3, + }); + assert_eq!(inst, 0x00351513); + } + + #[test] + fn srai_a0_a0_31() { + // srai a0, a0, 31 → 41f55513 + let inst = enc(RiscVOp::Srai { + rd: Reg::A0, + rs1: Reg::A0, + shamt: 31, + }); + assert_eq!(inst, 0x41F55513); + } + + #[test] + fn ecall() { + assert_eq!(enc(RiscVOp::Ecall), 0x00000073); + } + + #[test] + fn ebreak() { + assert_eq!(enc(RiscVOp::Ebreak), 0x00100073); + } + + #[test] + fn mret() { + assert_eq!(enc(RiscVOp::Mret), 0x30200073); + } + + #[test] + fn wfi() { + assert_eq!(enc(RiscVOp::Wfi), 0x10500073); + } + + #[test] + fn fence_iorw() { + assert_eq!(enc(RiscVOp::Fence), 0x0FF0000F); + } + + #[test] + fn csrrw_mtvec() { + // csrrw zero, mtvec, a0 → 30551073 + let inst = enc(RiscVOp::Csrrw { + rd: Reg::ZERO, + csr: Csr::MTVEC, + rs1: Reg::A0, + }); + assert_eq!(inst, 0x30551073); + } + + #[test] + fn mul_a0_a0_a1() { + // mul a0, a0, a1 → 02b50533 + let inst = enc(RiscVOp::Mul { + rd: Reg::A0, + rs1: Reg::A0, + rs2: Reg::A1, + }); + assert_eq!(inst, 0x02B50533); + } + + #[test] + fn divu_a0_a0_a1() { + // divu a0, a0, a1 → 02b55533 + let inst = enc(RiscVOp::Divu { + rd: Reg::A0, + rs1: Reg::A0, + rs2: Reg::A1, + }); + assert_eq!(inst, 0x02B55533); + } + + #[test] + fn imm_out_of_range() { + // addi a0, a0, 4096 → overflow (max is 2047) + let result = RiscVEncoder::new_rv32().encode(&RiscVOp::Addi { + rd: Reg::A0, + rs1: Reg::A0, + imm: 4096, + }); + assert!(matches!( + result, + Err(RiscVEncodingError::ImmediateOutOfRange { .. }) + )); + } + + #[test] + fn shamt_out_of_range() { + let result = RiscVEncoder::new_rv32().encode(&RiscVOp::Slli { + rd: Reg::A0, + rs1: Reg::A0, + shamt: 32, + }); + assert!(matches!( + result, + Err(RiscVEncodingError::ShiftAmountOutOfRange(32)) + )); + } + + #[test] + fn jal_unresolved() { + let result = RiscVEncoder::new_rv32().encode(&RiscVOp::Jal { + rd: Reg::RA, + label: "foo".into(), + }); + assert!(matches!( + result, + Err(RiscVEncodingError::UnresolvedLabel("jal")) + )); + } + + #[test] + fn jal_resolved_forward_4() { + // jal ra, +4 (next instruction) → 004000ef + let inst = RiscVEncoder::new_rv32() + .encode_jal(Reg::RA.num(), 4) + .unwrap(); + assert_eq!(inst, 0x004000EF); + } + + #[test] + fn jal_resolved_backward_4() { + // jal ra, -4 (loop to self at offset -4) → ffdff0ef + let inst = RiscVEncoder::new_rv32() + .encode_jal(Reg::RA.num(), -4) + .unwrap(); + assert_eq!(inst, 0xFFDFF0EF); + } + + #[test] + fn beq_a0_a1_8() { + // beq a0, a1, +8 → 00b50463 + let inst = RiscVEncoder::new_rv32() + .encode_branch(Branch::Eq, Reg::A0.num(), Reg::A1.num(), 8) + .unwrap(); + assert_eq!(inst, 0x00B50463); + } + + #[test] + fn bne_a0_zero_neg4() { + // bne a0, zero, -4 (busy-wait until zero) → fe051ee3 + let inst = RiscVEncoder::new_rv32() + .encode_branch(Branch::Ne, Reg::A0.num(), Reg::ZERO.num(), -4) + .unwrap(); + assert_eq!(inst, 0xFE051EE3); + } + + #[test] + fn branch_odd_offset_rejected() { + let r = RiscVEncoder::new_rv32().encode_branch(Branch::Eq, 0, 0, 3); + assert!(matches!( + r, + Err(RiscVEncodingError::ImmediateOutOfRange { .. }) + )); + } +} diff --git a/crates/synth-backend-riscv/src/lib.rs b/crates/synth-backend-riscv/src/lib.rs new file mode 100644 index 0000000..0273a9f --- /dev/null +++ b/crates/synth-backend-riscv/src/lib.rs @@ -0,0 +1,31 @@ +//! RISC-V backend for synth — RV32IMAC encoder, ELF builder, PMP allocator. +//! +//! Mirrors the structure of `synth-backend` (ARM Cortex-M) but emits RISC-V +//! machine code. The instruction selector lives in `synth-synthesis::riscv`; +//! this crate contains the binary encoder, ELF emission, linker script +//! generation, and Physical Memory Protection (PMP) allocator. +//! +//! Supported variants (per `TargetSpec::IsaVariant::RiscV32 { extensions }`): +//! - RV32I — base 32-bit integer +//! - RV32IM — adds multiply/divide +//! - RV32IMA — adds atomics +//! - RV32IMAC — adds compressed (16-bit) instructions +//! +//! The encoder always emits 32-bit instructions in this skeleton. The 16-bit +//! C-extension encoding will be a peephole pass in a follow-up. + +pub mod backend; +pub mod elf_builder; +pub mod encoder; +pub mod pmp; +pub mod register; +pub mod riscv_op; +pub mod selector; + +pub use backend::RiscVBackend; +pub use elf_builder::{ElfMode, RiscVElfBuilder, RiscVElfFunction}; +pub use encoder::{RiscVEncoder, RiscVEncodingError}; +pub use pmp::{PMPAllocator, PMPEntry, PMPError, PMPMode, PMPPermissions}; +pub use register::{Reg, RegClass}; +pub use riscv_op::{Branch, Csr, RiscVOp}; +pub use selector::{RiscVSelection, SelectorError, select_simple}; diff --git a/crates/synth-backend-riscv/src/pmp.rs b/crates/synth-backend-riscv/src/pmp.rs new file mode 100644 index 0000000..333776a --- /dev/null +++ b/crates/synth-backend-riscv/src/pmp.rs @@ -0,0 +1,414 @@ +//! Physical Memory Protection (PMP) allocator. +//! +//! PMP is RISC-V's analogue of ARM's MPU — a small set of memory regions +//! (8 or 16 entries on RV32) checked by hardware on every load/store/fetch. +//! Unlike the ARM MPU, PMP supports both *naturally aligned power-of-two* +//! (NAPOT) and *top-of-range* (TOR) modes, which makes arbitrary-size +//! regions cheaper than on Cortex-M. +//! +//! Reference: RISC-V Privileged Architecture, version 20211203, +//! Chapter 3.7 (Physical Memory Protection). + +use synth_core::{HardwareCapabilities, Memory}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum PMPError { + #[error("no PMP entries available (max: {max})")] + OutOfEntries { max: u8 }, + + #[error("region size {0} is too small for NAPOT (minimum 4 bytes)")] + NapotTooSmall(u64), + + #[error("region [{base:#X}..{end:#X}] overlaps with existing PMP entry {existing}")] + RegionOverlap { base: u64, end: u64, existing: u8 }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct PMPPermissions { + pub read: bool, + pub write: bool, + pub execute: bool, +} + +impl PMPPermissions { + pub const fn rwx() -> Self { + Self { + read: true, + write: true, + execute: true, + } + } + pub const fn rw() -> Self { + Self { + read: true, + write: true, + execute: false, + } + } + pub const fn ro() -> Self { + Self { + read: true, + write: false, + execute: false, + } + } + pub const fn rx() -> Self { + Self { + read: true, + write: false, + execute: true, + } + } +} + +/// Address-matching mode encoded in `pmpcfg.A` (2 bits). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PMPMode { + /// Region disabled (A = 0b00) + Off, + /// Top-of-range — entry `i` matches `[pmpaddr[i-1], pmpaddr[i])` + Tor, + /// Naturally-aligned 4-byte region (A = 0b10) — rarely used + Na4, + /// Naturally-aligned power-of-2 (A = 0b11) — most common + Napot, +} + +impl PMPMode { + pub fn bits(self) -> u8 { + match self { + PMPMode::Off => 0b00, + PMPMode::Tor => 0b01, + PMPMode::Na4 => 0b10, + PMPMode::Napot => 0b11, + } + } +} + +/// One PMP entry: a region with permissions and matching mode. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PMPEntry { + pub index: u8, + pub mode: PMPMode, + pub perms: PMPPermissions, + pub base: u64, + pub size: u64, + pub locked: bool, +} + +impl PMPEntry { + /// Compute the value to write into the corresponding `pmpaddr` CSR. + /// PMP addresses are *byte-aligned*, but the CSR holds them shifted right + /// by 2 (so each LSB represents 4 bytes). + pub fn pmpaddr_value(&self) -> u64 { + match self.mode { + PMPMode::Napot => { + // For NAPOT: the address encodes both base and size. + // The lowest (log2(size) - 3) bits are 1, the rest hold the base. + if self.size < 8 { + // 4-byte regions use NA4, not NAPOT — but still encode shifted. + self.base >> 2 + } else { + let trailing_ones = (self.size.trailing_zeros() as u64).saturating_sub(3); + let mask = (1u64 << trailing_ones) - 1; + (self.base >> 2) | mask + } + } + PMPMode::Tor => self.base >> 2, + PMPMode::Na4 => self.base >> 2, + PMPMode::Off => 0, + } + } + + /// Compute the byte to OR into `pmpcfg` for this entry. + pub fn pmpcfg_byte(&self) -> u8 { + let mut b = 0u8; + if self.perms.read { + b |= 0b001; + } + if self.perms.write { + b |= 0b010; + } + if self.perms.execute { + b |= 0b100; + } + b |= self.mode.bits() << 3; + if self.locked { + b |= 0b1000_0000; + } + b + } +} + +/// Allocator for PMP entries — picks `Napot` mode when the region is a +/// power of two, falls back to `Tor` (which costs 2 entries) otherwise. +pub struct PMPAllocator { + hw_caps: HardwareCapabilities, + entries: Vec, +} + +impl PMPAllocator { + pub fn new(hw_caps: HardwareCapabilities) -> Self { + Self { + hw_caps, + entries: Vec::new(), + } + } + + pub fn entries(&self) -> &[PMPEntry] { + &self.entries + } + + pub fn available_entries(&self) -> u8 { + self.hw_caps + .pmp_entries + .saturating_sub(self.entries.len() as u8) + } + + /// Add a region described by a wasm `Memory` plus permissions. + pub fn allocate_for_memory( + &mut self, + memory: Memory, + base: u64, + perms: PMPPermissions, + ) -> Result<&PMPEntry, PMPError> { + let size_bytes = memory.initial as u64 * 65536; + self.allocate_region(base, size_bytes, perms) + } + + /// Add a flat byte-range region. + pub fn allocate_region( + &mut self, + base: u64, + size: u64, + perms: PMPPermissions, + ) -> Result<&PMPEntry, PMPError> { + if self.available_entries() == 0 { + return Err(PMPError::OutOfEntries { + max: self.hw_caps.pmp_entries, + }); + } + if size < 4 { + return Err(PMPError::NapotTooSmall(size)); + } + + for existing in &self.entries { + let e_start = existing.base; + let e_end = existing.base + existing.size; + let n_end = base + size; + if !(n_end <= e_start || e_end <= base) { + return Err(PMPError::RegionOverlap { + base, + end: n_end, + existing: existing.index, + }); + } + } + + let mode = if size.is_power_of_two() && size >= 8 && (base & (size - 1)) == 0 { + PMPMode::Napot + } else if size == 4 && (base & 3) == 0 { + PMPMode::Na4 + } else { + // TOR consumes two entries; check we have room. + if self.available_entries() < 2 { + return Err(PMPError::OutOfEntries { + max: self.hw_caps.pmp_entries, + }); + } + PMPMode::Tor + }; + + if mode == PMPMode::Tor { + // Entry N: base address (mode = OFF, addr = base) + // Entry N+1: TOR mode, addr = base+size + let lo_idx = self.entries.len() as u8; + self.entries.push(PMPEntry { + index: lo_idx, + mode: PMPMode::Off, + perms: PMPPermissions::default(), + base, + size: 0, + locked: false, + }); + let hi_idx = self.entries.len() as u8; + self.entries.push(PMPEntry { + index: hi_idx, + mode: PMPMode::Tor, + perms, + base: base + size, + size, + locked: false, + }); + return Ok(self.entries.last().unwrap()); + } + + let idx = self.entries.len() as u8; + self.entries.push(PMPEntry { + index: idx, + mode, + perms, + base, + size, + locked: false, + }); + Ok(self.entries.last().unwrap()) + } + + /// Generate C-like initialization code (mirrors the ARM MPU emitter). + pub fn generate_init_code(&self) -> String { + let mut out = String::new(); + out.push_str("/* PMP Initialization Code */\n"); + out.push_str("/* Generated by Synth WebAssembly Component Synthesizer */\n\n"); + out.push_str("#include \n\n"); + out.push_str("static inline void csrw_pmpaddr(int i, unsigned long v) {\n"); + out.push_str(" switch(i) {\n"); + for i in 0..self.hw_caps.pmp_entries { + out.push_str(&format!( + " case {0}: __asm__ volatile(\"csrw pmpaddr{0}, %0\" :: \"r\"(v)); break;\n", + i + )); + } + out.push_str(" }\n}\n\n"); + + out.push_str("void pmp_init(void) {\n"); + for entry in &self.entries { + out.push_str(&format!( + " /* Entry {}: base=0x{:X} size=0x{:X} mode={:?} */\n", + entry.index, entry.base, entry.size, entry.mode + )); + out.push_str(&format!( + " csrw_pmpaddr({}, 0x{:X}UL);\n", + entry.index, + entry.pmpaddr_value() + )); + } + // Aggregate pmpcfg writes — pack 4 entries per pmpcfg CSR on RV32. + let mut cfg_word: [u8; 16] = [0; 16]; + for entry in &self.entries { + cfg_word[entry.index as usize] = entry.pmpcfg_byte(); + } + for chunk in 0..(self.hw_caps.pmp_entries.div_ceil(4) as usize) { + let base = chunk * 4; + let mut w: u32 = 0; + for (i, b) in cfg_word.iter().skip(base).take(4).enumerate() { + w |= (*b as u32) << (i * 8); + } + out.push_str(&format!( + " __asm__ volatile(\"csrw pmpcfg{0}, %0\" :: \"r\"(0x{1:08X}UL));\n", + chunk, w + )); + } + out.push_str("}\n"); + out + } +} + +#[cfg(test)] +mod tests { + use super::*; + use synth_core::{HardwareCapabilities, RISCVVariant, TargetArch}; + + fn riscv32_caps() -> HardwareCapabilities { + HardwareCapabilities { + arch: TargetArch::RISCV(RISCVVariant::RV32IMAC), + has_mpu: false, + mpu_regions: 0, + has_pmp: true, + pmp_entries: 16, + has_fpu: false, + fpu_precision: None, + has_simd: false, + simd_level: None, + xip_capable: true, + flash_size: 4 * 1024 * 1024, + ram_size: 256 * 1024, + } + } + + #[test] + fn napot_mode_for_power_of_two() { + let mut alloc = PMPAllocator::new(riscv32_caps()); + let entry = alloc + .allocate_region(0x80000000, 0x10000, PMPPermissions::rw()) + .unwrap(); + assert_eq!(entry.mode, PMPMode::Napot); + } + + #[test] + fn tor_mode_for_non_power_of_two_consumes_two_entries() { + let mut alloc = PMPAllocator::new(riscv32_caps()); + let _ = alloc + .allocate_region(0x80000000, 12288, PMPPermissions::rw()) // 12 KB — not pow2 + .unwrap(); + assert_eq!(alloc.entries().len(), 2); + assert_eq!(alloc.entries()[0].mode, PMPMode::Off); + assert_eq!(alloc.entries()[1].mode, PMPMode::Tor); + } + + #[test] + fn napot_addr_encoding_64kb() { + // 64 KB region @ 0x80000000: + // pmpaddr = (base >> 2) | mask where mask = 0x3FFF (14 ones) + // 64 KB = 2^16, so trailing_zeros = 16, trailing_ones = 16 - 3 = 13 + // base >> 2 = 0x20000000 + // mask = 0x1FFF + // result = 0x20001FFF + let entry = PMPEntry { + index: 0, + mode: PMPMode::Napot, + perms: PMPPermissions::rw(), + base: 0x80000000, + size: 0x10000, + locked: false, + }; + assert_eq!(entry.pmpaddr_value(), 0x20001FFF); + } + + #[test] + fn pmpcfg_byte_rwx() { + let entry = PMPEntry { + index: 0, + mode: PMPMode::Napot, + perms: PMPPermissions::rwx(), + base: 0, + size: 16, + locked: false, + }; + // R | W | X | mode 0b11<<3 + assert_eq!(entry.pmpcfg_byte(), 0b0001_1111); + } + + #[test] + fn out_of_entries() { + let mut caps = riscv32_caps(); + caps.pmp_entries = 1; + let mut alloc = PMPAllocator::new(caps); + alloc.allocate_region(0, 16, PMPPermissions::rw()).unwrap(); + let r = alloc.allocate_region(0x1000, 16, PMPPermissions::rw()); + assert!(matches!(r, Err(PMPError::OutOfEntries { .. }))); + } + + #[test] + fn overlap_detected() { + let mut alloc = PMPAllocator::new(riscv32_caps()); + alloc + .allocate_region(0x80000000, 0x10000, PMPPermissions::rw()) + .unwrap(); + let r = alloc.allocate_region(0x80008000, 0x10000, PMPPermissions::rw()); + assert!(matches!(r, Err(PMPError::RegionOverlap { .. }))); + } + + #[test] + fn init_code_contains_csr_writes() { + let mut alloc = PMPAllocator::new(riscv32_caps()); + alloc + .allocate_region(0x80000000, 0x10000, PMPPermissions::rw()) + .unwrap(); + let code = alloc.generate_init_code(); + assert!(code.contains("pmp_init")); + assert!(code.contains("csrw pmpaddr0")); + assert!(code.contains("csrw pmpcfg0")); + } +} diff --git a/crates/synth-backend-riscv/src/register.rs b/crates/synth-backend-riscv/src/register.rs new file mode 100644 index 0000000..5e857dd --- /dev/null +++ b/crates/synth-backend-riscv/src/register.rs @@ -0,0 +1,241 @@ +//! RISC-V register file (RV32 / RV64 share the same 32 GPR namespace). +//! +//! ABI names follow the RISC-V psABI: +//! - `x0` = zero (hardwired) +//! - `x1` = ra (return address) +//! - `x2` = sp (stack pointer) +//! - `x3` = gp (global pointer) +//! - `x4` = tp (thread pointer) +//! - `x5..7`, `x28..31` = t0..t6 (temporaries, caller-saved) +//! - `x8..9`, `x18..27` = s0..s11 (saved, callee-saved) +//! - `x10..17` = a0..a7 (argument / return) +//! +//! For floating-point variants (F/D extensions) we'd add f0..f31 in a separate +//! `FReg` enum; this skeleton focuses on integer registers. + +use std::fmt; + +/// RISC-V general-purpose register (x0..x31). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[repr(u8)] +pub enum Reg { + X0 = 0, // zero + X1 = 1, // ra + X2 = 2, // sp + X3 = 3, // gp + X4 = 4, // tp + X5 = 5, // t0 + X6 = 6, // t1 + X7 = 7, // t2 + X8 = 8, // s0/fp + X9 = 9, // s1 + X10 = 10, // a0 + X11 = 11, // a1 + X12 = 12, // a2 + X13 = 13, // a3 + X14 = 14, // a4 + X15 = 15, // a5 + X16 = 16, // a6 + X17 = 17, // a7 + X18 = 18, // s2 + X19 = 19, // s3 + X20 = 20, // s4 + X21 = 21, // s5 + X22 = 22, // s6 + X23 = 23, // s7 + X24 = 24, // s8 + X25 = 25, // s9 + X26 = 26, // s10 + X27 = 27, // s11 + X28 = 28, // t3 + X29 = 29, // t4 + X30 = 30, // t5 + X31 = 31, // t6 +} + +impl Reg { + /// Numeric encoding (5-bit field). + pub fn num(self) -> u8 { + self as u8 + } + + /// Build from the numeric encoding. Returns `None` for values >= 32. + pub fn from_num(n: u8) -> Option { + if n >= 32 { + return None; + } + // SAFETY: `Reg` is `#[repr(u8)]` with a contiguous 0..32 range. + unsafe { Some(std::mem::transmute::(n)) } + } + + /// ABI name (zero, ra, sp, ...). + pub fn abi_name(self) -> &'static str { + match self { + Reg::X0 => "zero", + Reg::X1 => "ra", + Reg::X2 => "sp", + Reg::X3 => "gp", + Reg::X4 => "tp", + Reg::X5 => "t0", + Reg::X6 => "t1", + Reg::X7 => "t2", + Reg::X8 => "s0", + Reg::X9 => "s1", + Reg::X10 => "a0", + Reg::X11 => "a1", + Reg::X12 => "a2", + Reg::X13 => "a3", + Reg::X14 => "a4", + Reg::X15 => "a5", + Reg::X16 => "a6", + Reg::X17 => "a7", + Reg::X18 => "s2", + Reg::X19 => "s3", + Reg::X20 => "s4", + Reg::X21 => "s5", + Reg::X22 => "s6", + Reg::X23 => "s7", + Reg::X24 => "s8", + Reg::X25 => "s9", + Reg::X26 => "s10", + Reg::X27 => "s11", + Reg::X28 => "t3", + Reg::X29 => "t4", + Reg::X30 => "t5", + Reg::X31 => "t6", + } + } + + /// Constants for the most-used ABI registers — improves call sites. + pub const ZERO: Reg = Reg::X0; + pub const RA: Reg = Reg::X1; + pub const SP: Reg = Reg::X2; + pub const GP: Reg = Reg::X3; + pub const TP: Reg = Reg::X4; + pub const FP: Reg = Reg::X8; // s0 = fp + pub const A0: Reg = Reg::X10; + pub const A1: Reg = Reg::X11; + pub const A2: Reg = Reg::X12; + pub const A3: Reg = Reg::X13; + pub const A4: Reg = Reg::X14; + pub const A5: Reg = Reg::X15; + pub const A6: Reg = Reg::X16; + pub const A7: Reg = Reg::X17; + pub const T0: Reg = Reg::X5; + pub const T1: Reg = Reg::X6; + pub const T2: Reg = Reg::X7; + pub const T3: Reg = Reg::X28; + pub const T4: Reg = Reg::X29; + pub const T5: Reg = Reg::X30; + pub const T6: Reg = Reg::X31; + pub const S0: Reg = Reg::X8; // also fp + pub const S1: Reg = Reg::X9; + pub const S2: Reg = Reg::X18; + pub const S3: Reg = Reg::X19; + pub const S4: Reg = Reg::X20; + pub const S5: Reg = Reg::X21; + pub const S6: Reg = Reg::X22; + pub const S7: Reg = Reg::X23; + pub const S8: Reg = Reg::X24; + pub const S9: Reg = Reg::X25; + pub const S10: Reg = Reg::X26; + pub const S11: Reg = Reg::X27; + + /// Whether this register is callee-saved per the standard RV psABI. + /// Callee-saved: sp, gp, tp, s0..s11. + pub fn is_callee_saved(self) -> bool { + matches!( + self, + Reg::X2 + | Reg::X3 + | Reg::X4 + | Reg::X8 + | Reg::X9 + | Reg::X18 + | Reg::X19 + | Reg::X20 + | Reg::X21 + | Reg::X22 + | Reg::X23 + | Reg::X24 + | Reg::X25 + | Reg::X26 + | Reg::X27 + ) + } + + /// First N argument registers (a0..a(n-1)). Capped at 8 (a0..a7). + pub fn arg_regs(n: usize) -> Vec { + let cap = n.min(8); + (0..cap) + .map(|i| Reg::from_num((10 + i) as u8).unwrap()) + .collect() + } +} + +impl fmt::Display for Reg { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.abi_name()) + } +} + +/// Coarse register class for vreg-allocator hints. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RegClass { + /// 32-bit GPR (the only class implemented in this skeleton). + Gpr, + /// 64-bit GPR — present on RV64 only. + Gpr64, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn round_trip_num() { + for i in 0..32u8 { + let r = Reg::from_num(i).unwrap(); + assert_eq!(r.num(), i); + } + } + + #[test] + fn out_of_range_num() { + assert!(Reg::from_num(32).is_none()); + assert!(Reg::from_num(255).is_none()); + } + + #[test] + fn abi_aliases() { + assert_eq!(Reg::ZERO.abi_name(), "zero"); + assert_eq!(Reg::RA.abi_name(), "ra"); + assert_eq!(Reg::SP.abi_name(), "sp"); + assert_eq!(Reg::A0.abi_name(), "a0"); + assert_eq!(Reg::A7.abi_name(), "a7"); + assert_eq!(Reg::S0.abi_name(), "s0"); + } + + #[test] + fn callee_saved_set() { + // sp + gp + tp + s0..s11 = 15 registers + let count = (0..32u8) + .filter(|&i| Reg::from_num(i).unwrap().is_callee_saved()) + .count(); + assert_eq!(count, 15); + } + + #[test] + fn arg_regs_capped_at_8() { + let r = Reg::arg_regs(20); + assert_eq!(r.len(), 8); + assert_eq!(r[0], Reg::A0); + assert_eq!(r[7], Reg::A7); + } + + #[test] + fn arg_regs_partial() { + let r = Reg::arg_regs(3); + assert_eq!(r, vec![Reg::A0, Reg::A1, Reg::A2]); + } +} diff --git a/crates/synth-backend-riscv/src/riscv_op.rs b/crates/synth-backend-riscv/src/riscv_op.rs new file mode 100644 index 0000000..61c8195 --- /dev/null +++ b/crates/synth-backend-riscv/src/riscv_op.rs @@ -0,0 +1,228 @@ +//! RISC-V instruction representation. +//! +//! These are *post-instruction-selection* enum values — the equivalent of +//! `synth_synthesis::ArmOp` for ARM. The instruction selector emits these; +//! the encoder translates them to 32-bit machine words. +//! +//! Each variant maps to a single base RV32I/M/A instruction. Pseudo-ops +//! (e.g. `li`, `mv`, `nop`) are expressed via existing variants — for +//! example `mv rd, rs` → `Addi { rd, rs1: rs, imm: 0 }`. + +use crate::register::Reg; + +/// Branch condition for the `Branch` instruction (BEQ/BNE/BLT/BGE/BLTU/BGEU). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Branch { + /// Branch if equal (`beq`) + Eq, + /// Branch if not equal (`bne`) + Ne, + /// Branch if less-than, signed (`blt`) + Lt, + /// Branch if greater-or-equal, signed (`bge`) + Ge, + /// Branch if less-than, unsigned (`bltu`) + Ltu, + /// Branch if greater-or-equal, unsigned (`bgeu`) + Geu, +} + +impl Branch { + /// funct3 field for the branch instruction. + pub fn funct3(self) -> u8 { + match self { + Branch::Eq => 0b000, + Branch::Ne => 0b001, + Branch::Lt => 0b100, + Branch::Ge => 0b101, + Branch::Ltu => 0b110, + Branch::Geu => 0b111, + } + } +} + +/// CSR (Control and Status Register) — used for trap setup, mtvec, mstatus, etc. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Csr(pub u16); + +impl Csr { + pub const MSTATUS: Csr = Csr(0x300); + pub const MISA: Csr = Csr(0x301); + pub const MIE: Csr = Csr(0x304); + pub const MTVEC: Csr = Csr(0x305); + pub const MEPC: Csr = Csr(0x341); + pub const MCAUSE: Csr = Csr(0x342); + pub const MTVAL: Csr = Csr(0x343); + pub const MIP: Csr = Csr(0x344); +} + +/// A single RISC-V instruction (post-selection, pre-encoding). +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RiscVOp { + // ────────────────────────────────────────────────────────────────── + // RV32I — base integer + // ────────────────────────────────────────────────────────────────── + + // U-type — load upper immediate + /// `lui rd, imm20` — load (imm20 << 12) into rd + Lui { rd: Reg, imm20: u32 }, + /// `auipc rd, imm20` — pc + (imm20 << 12) + Auipc { rd: Reg, imm20: u32 }, + + // J-type / I-type — jumps + /// `jal rd, label` — pc-relative; `rd` gets pc+4 as link + Jal { rd: Reg, label: String }, + /// `jalr rd, imm(rs1)` — register-indirect; `rd` gets pc+4 as link + Jalr { rd: Reg, rs1: Reg, imm: i32 }, + + // B-type — branches + /// Branch if `cond(rs1, rs2)` — pc-relative to label + Branch { + cond: Branch, + rs1: Reg, + rs2: Reg, + label: String, + }, + + // I-type — register/immediate ops + /// `addi rd, rs1, imm` — rd = rs1 + sign_extend(imm12) + Addi { rd: Reg, rs1: Reg, imm: i32 }, + /// `slti rd, rs1, imm` — rd = (rs1 , +} + +/// Lower a flat sequence of WASM ops to RV32 ops using a tiny stack-based +/// selector. This handles only: +/// - `local.get N` (params 0..3 map to a0..a3) +/// - `local.set N` for the same +/// - `i32.const` +/// - `i32.add`, `i32.sub`, `i32.mul` +/// - `i32.and`, `i32.or`, `i32.xor` +/// - `i32.eqz` +/// - `return` +/// +/// Anything else returns `SelectorError::Unsupported`. The full selector +/// (with control flow, loads/stores, comparisons, division, i64) is B2. +pub fn select_simple( + wasm_ops: &[WasmOp], + num_params: u32, +) -> Result { + let mut out = Vec::new(); + + // Virtual stack: each entry is the RV32 register holding that wasm value. + // We use a0..a7 as both arg-in and short-lived temporaries here. + // (Real selector will use a vreg allocator.) + let mut vstack: Vec = Vec::new(); + + // Argument registers a0..a7 (capped at 8 — anything beyond would spill). + let arg_regs: Vec = Reg::arg_regs(num_params as usize); + + // Tracks the next free temporary register. For the skeleton we do + // round-robin among t0..t6 — the real allocator will be smarter. + let temps = [Reg::T0, Reg::T1, Reg::T2, Reg::T3, Reg::T4, Reg::T5]; + let mut next_temp = 0usize; + let mut alloc_temp = || { + let r = temps[next_temp % temps.len()]; + next_temp += 1; + r + }; + + for op in wasm_ops { + match op { + WasmOp::LocalGet(idx) => { + let src = if (*idx as usize) < arg_regs.len() { + arg_regs[*idx as usize] + } else { + return Err(SelectorError::Unsupported(op.clone())); + }; + let dst = alloc_temp(); + // mv dst, src (= addi dst, src, 0) + out.push(RiscVOp::Addi { + rd: dst, + rs1: src, + imm: 0, + }); + vstack.push(dst); + } + WasmOp::LocalSet(idx) => { + let src = vstack + .pop() + .ok_or_else(|| SelectorError::StackUnderflow(op.clone()))?; + let dst = if (*idx as usize) < arg_regs.len() { + arg_regs[*idx as usize] + } else { + return Err(SelectorError::Unsupported(op.clone())); + }; + out.push(RiscVOp::Addi { + rd: dst, + rs1: src, + imm: 0, + }); + } + WasmOp::I32Const(v) => { + let dst = alloc_temp(); + emit_load_imm(&mut out, dst, *v); + vstack.push(dst); + } + WasmOp::I32Add => emit_binop(&mut out, &mut vstack, op, |rd, rs1, rs2| RiscVOp::Add { + rd, + rs1, + rs2, + })?, + WasmOp::I32Sub => emit_binop(&mut out, &mut vstack, op, |rd, rs1, rs2| RiscVOp::Sub { + rd, + rs1, + rs2, + })?, + WasmOp::I32Mul => emit_binop(&mut out, &mut vstack, op, |rd, rs1, rs2| RiscVOp::Mul { + rd, + rs1, + rs2, + })?, + WasmOp::I32And => emit_binop(&mut out, &mut vstack, op, |rd, rs1, rs2| RiscVOp::And { + rd, + rs1, + rs2, + })?, + WasmOp::I32Or => emit_binop(&mut out, &mut vstack, op, |rd, rs1, rs2| RiscVOp::Or { + rd, + rs1, + rs2, + })?, + WasmOp::I32Xor => emit_binop(&mut out, &mut vstack, op, |rd, rs1, rs2| RiscVOp::Xor { + rd, + rs1, + rs2, + })?, + WasmOp::I32Eqz => { + // sltiu rd, src, 1 → rd = (src == 0) ? 1 : 0 + let src = vstack + .pop() + .ok_or_else(|| SelectorError::StackUnderflow(op.clone()))?; + let dst = alloc_temp(); + out.push(RiscVOp::Sltiu { + rd: dst, + rs1: src, + imm: 1, + }); + vstack.push(dst); + } + WasmOp::Return | WasmOp::End => { + // Move top-of-stack to a0 if it isn't already, then return. + if let Some(&top) = vstack.last() + && top != Reg::A0 + { + out.push(RiscVOp::Addi { + rd: Reg::A0, + rs1: top, + imm: 0, + }); + } + out.push(RiscVOp::Jalr { + rd: Reg::ZERO, + rs1: Reg::RA, + imm: 0, + }); + return Ok(RiscVSelection { ops: out }); + } + other => return Err(SelectorError::Unsupported(other.clone())), + } + } + + // No explicit Return/End → emit one + if let Some(&top) = vstack.last() + && top != Reg::A0 + { + out.push(RiscVOp::Addi { + rd: Reg::A0, + rs1: top, + imm: 0, + }); + } + out.push(RiscVOp::Jalr { + rd: Reg::ZERO, + rs1: Reg::RA, + imm: 0, + }); + Ok(RiscVSelection { ops: out }) +} + +fn emit_binop( + out: &mut Vec, + vstack: &mut Vec, + op: &WasmOp, + build: F, +) -> Result<(), SelectorError> +where + F: FnOnce(Reg, Reg, Reg) -> RiscVOp, +{ + let rs2 = vstack + .pop() + .ok_or_else(|| SelectorError::StackUnderflow(op.clone()))?; + let rs1 = vstack + .pop() + .ok_or_else(|| SelectorError::StackUnderflow(op.clone()))?; + let rd = rs1; // overwrite the first source — temporary aliasing is fine for skeleton + out.push(build(rd, rs1, rs2)); + vstack.push(rd); + Ok(()) +} + +/// Materialize a 32-bit immediate into `rd` using `lui + addi` when needed. +fn emit_load_imm(out: &mut Vec, rd: Reg, value: i32) { + if (-2048..=2047).contains(&value) { + // Single addi from zero. + out.push(RiscVOp::Addi { + rd, + rs1: Reg::ZERO, + imm: value, + }); + return; + } + // lui rd, hi20; addi rd, rd, lo12 (with carry from lo12 sign extension) + let value_u = value as u32; + let lo12 = (value_u & 0xFFF) as i32; + // If lo12 sign-extends negative, bump hi20 by 1 to compensate. + let lo12_signed = if lo12 >= 0x800 { lo12 - 0x1000 } else { lo12 }; + let hi20 = (value_u.wrapping_sub(lo12_signed as u32)) >> 12; + out.push(RiscVOp::Lui { + rd, + imm20: hi20 & 0xFFFFF, + }); + if lo12_signed != 0 { + out.push(RiscVOp::Addi { + rd, + rs1: rd, + imm: lo12_signed, + }); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn add_two_params() { + // (param i32 i32) → local.get 0, local.get 1, i32.add, end + let ops = vec![ + WasmOp::LocalGet(0), + WasmOp::LocalGet(1), + WasmOp::I32Add, + WasmOp::End, + ]; + let sel = select_simple(&ops, 2).unwrap(); + + // Expect: + // addi t0, a0, 0 (LocalGet 0) + // addi t1, a1, 0 (LocalGet 1) + // add t0, t0, t1 (I32Add) + // addi a0, t0, 0 (End: move result to a0) + // jalr zero, 0(ra) (return) + assert!(matches!( + sel.ops[0], + RiscVOp::Addi { + rd: Reg::T0, + rs1: Reg::A0, + imm: 0 + } + )); + assert!(matches!( + sel.ops[1], + RiscVOp::Addi { + rd: Reg::T1, + rs1: Reg::A1, + imm: 0 + } + )); + assert!(matches!( + sel.ops[2], + RiscVOp::Add { + rd: Reg::T0, + rs1: Reg::T0, + rs2: Reg::T1 + } + )); + // Last op is the return. + assert!(matches!( + sel.ops.last().unwrap(), + RiscVOp::Jalr { + rd: Reg::ZERO, + rs1: Reg::RA, + imm: 0 + } + )); + } + + #[test] + fn const_then_eqz() { + // i32.const 0, i32.eqz, end → returns 1 + let ops = vec![WasmOp::I32Const(0), WasmOp::I32Eqz, WasmOp::End]; + let sel = select_simple(&ops, 0).unwrap(); + // First instruction is `addi rd, zero, 0` (small immediate → no LUI) + assert!(matches!( + sel.ops[0], + RiscVOp::Addi { + rs1: Reg::ZERO, + imm: 0, + .. + } + )); + // The Eqz is sltiu rd, src, 1 + assert!(matches!(sel.ops[1], RiscVOp::Sltiu { imm: 1, .. })); + } + + #[test] + fn large_constant_uses_lui_addi() { + // i32.const 0x12345678 + let ops = vec![WasmOp::I32Const(0x12345678), WasmOp::End]; + let sel = select_simple(&ops, 0).unwrap(); + // First should be LUI (high 20 bits = 0x12345 since lo12=0x678 doesn't sign-extend) + match &sel.ops[0] { + RiscVOp::Lui { imm20, .. } => assert_eq!(*imm20, 0x12345), + other => panic!("expected Lui, got {:?}", other), + } + match &sel.ops[1] { + RiscVOp::Addi { imm, .. } => assert_eq!(*imm, 0x678), + other => panic!("expected Addi, got {:?}", other), + } + } + + #[test] + fn negative_lo12_carry() { + // i32.const 0x12345FFF — lo12 sign-extends as -1, so hi20 must bump by 1 + let ops = vec![WasmOp::I32Const(0x12345FFFu32 as i32), WasmOp::End]; + let sel = select_simple(&ops, 0).unwrap(); + match &sel.ops[0] { + RiscVOp::Lui { imm20, .. } => assert_eq!(*imm20, 0x12346, "hi20 must compensate"), + other => panic!("expected Lui, got {:?}", other), + } + match &sel.ops[1] { + RiscVOp::Addi { imm, .. } => assert_eq!(*imm, -1), + other => panic!("expected Addi -1, got {:?}", other), + } + } + + #[test] + fn unsupported_op_errors() { + let ops = vec![WasmOp::F32Const(1.0), WasmOp::End]; + let r = select_simple(&ops, 0); + assert!(matches!(r, Err(SelectorError::Unsupported(_)))); + } +} diff --git a/crates/synth-cli/Cargo.toml b/crates/synth-cli/Cargo.toml index 40aa557..bd3fbcb 100644 --- a/crates/synth-cli/Cargo.toml +++ b/crates/synth-cli/Cargo.toml @@ -12,9 +12,10 @@ name = "synth" path = "src/main.rs" [features] -default = [] +default = ["riscv"] awsm = ["synth-backend-awsm"] wasker = ["synth-backend-wasker"] +riscv = ["synth-backend-riscv"] verify = ["synth-verify"] # Uncomment when loom crate is available: # loom = ["dep:loom-opt"] @@ -28,6 +29,7 @@ synth-backend = { path = "../synth-backend" } # Optional external backends synth-backend-awsm = { path = "../synth-backend-awsm", optional = true } synth-backend-wasker = { path = "../synth-backend-wasker", optional = true } +synth-backend-riscv = { path = "../synth-backend-riscv", optional = true } # Optional verification (requires z3) synth-verify = { path = "../synth-verify", optional = true, features = ["z3-solver", "arm"] } diff --git a/crates/synth-cli/src/main.rs b/crates/synth-cli/src/main.rs index b78ea11..409ef33 100644 --- a/crates/synth-cli/src/main.rs +++ b/crates/synth-cli/src/main.rs @@ -492,6 +492,10 @@ fn build_backend_registry() -> BackendRegistry { #[cfg(feature = "wasker")] registry.register(Box::new(synth_backend_wasker::WaskerBackend::new())); + // Register RISC-V backend if compiled with feature + #[cfg(feature = "riscv")] + registry.register(Box::new(synth_backend_riscv::RiscVBackend::new())); + registry } @@ -729,7 +733,9 @@ fn compile_command( let code = compiled.code; info!("Encoded {} bytes of machine code", code.len()); - let elf_data = if cortex_m { + let elf_data = if matches!(target_spec.family, synth_core::target::ArchFamily::RiscV) { + build_riscv_elf(&code, &func_name)? + } else if cortex_m { build_cortex_m_elf(&code, &func_name, target_spec)? } else { build_simple_elf(&code, &func_name)? @@ -1453,7 +1459,11 @@ fn compile_all_exports( // (which provides __meld_dispatch_import and __meld_get_memory_base). // The --relocatable flag forces ET_REL output even when the wasm has no // imports, for linking into a host build system (e.g. Zephyr). - let elf_data = if has_relocations || relocatable { + let is_riscv = matches!(target_spec.family, synth_core::target::ArchFamily::RiscV); + let elf_data = if is_riscv { + info!("Building RISC-V multi-function relocatable object (EM_RISCV)"); + build_multi_func_riscv_elf(&compiled_funcs)? + } else if has_relocations || relocatable { let total_relocs: usize = compiled_funcs.iter().map(|f| f.relocations.len()).sum(); if has_relocations { info!( @@ -2082,6 +2092,114 @@ fn verify_command(wasm_input: PathBuf, elf_input: PathBuf, backend_name: &str) - Ok(()) } +/// Build a RISC-V relocatable ELF wrapping the bytes the RV backend produced. +/// +/// Re-runs the RISC-V backend's `RiscVElfBuilder` so the output is a real +/// RV32 object file (EM_RISCV, RVC e_flags) rather than the generic ARM +/// ELF that `build_simple_elf` emits. +#[cfg(feature = "riscv")] +fn build_riscv_elf(code: &[u8], func_name: &str) -> Result> { + use synth_backend_riscv::{Reg, RiscVElfBuilder, RiscVElfFunction, RiscVOp}; + + // The RISC-V ELF builder operates on `Vec` to support label + // resolution. The CLI path doesn't have ops at this layer (only bytes), + // so we wrap each 4-byte word as an opaque "raw" instruction by treating + // the bytes as already encoded. We materialize them as `Addi`-shaped + // sentinels and then post-process the ELF body to overwrite with our + // actual code. Cleaner: use a future raw-bytes API on the builder. + // + // For the skeleton, the simpler path: wrap as one Addi per word — wrong + // bits, but the ELF builder writes the section table correctly. We then + // patch .text bytes back. This avoids leaking the encoder back through + // the CLI and is fine until we drop ARM-style byte-handoff entirely. + let n_instrs = code.len().div_ceil(4); + let placeholder_ops: Vec = (0..n_instrs) + .map(|_| RiscVOp::Addi { + rd: Reg::ZERO, + rs1: Reg::ZERO, + imm: 0, + }) + .collect(); + let f = RiscVElfFunction { + name: func_name.to_string(), + ops: placeholder_ops, + }; + + let builder = RiscVElfBuilder::new_relocatable(); + let mut elf = builder + .build(&[f]) + .context("RISC-V ELF generation failed")?; + + // .text starts at offset 52 (ELF header). Overwrite the placeholder bytes + // with the actual code we got from the backend. + let text_offset = 52; + if elf.len() < text_offset + code.len() { + anyhow::bail!("RISC-V ELF is shorter than embedded code"); + } + elf[text_offset..text_offset + code.len()].copy_from_slice(code); + Ok(elf) +} + +#[cfg(not(feature = "riscv"))] +fn build_riscv_elf(_code: &[u8], _func_name: &str) -> Result> { + anyhow::bail!("RISC-V backend was not compiled in (rebuild with --features riscv)") +} + +/// Build a multi-function RISC-V relocatable ELF. +#[cfg(feature = "riscv")] +fn build_multi_func_riscv_elf(funcs: &[ElfFunction]) -> Result> { + use synth_backend_riscv::{Reg, RiscVElfBuilder, RiscVElfFunction, RiscVOp}; + + // Same placeholder-then-overwrite approach as build_riscv_elf. + // We accumulate a single .text spanning all functions and patch the + // bytes back in after the ELF builder finishes layout. + let mut all_code: Vec = Vec::new(); + let mut func_byte_ranges: Vec<(usize, usize)> = Vec::new(); + let mut placeholder_funcs: Vec = Vec::new(); + + for func in funcs { + // Align each function to 4 bytes (RISC-V requires 4-byte instruction alignment). + while !all_code.len().is_multiple_of(4) { + all_code.push(0); + } + let start = all_code.len(); + all_code.extend_from_slice(&func.code); + let end = all_code.len(); + func_byte_ranges.push((start, end)); + + let n_instrs = (end - start).div_ceil(4); + let placeholder_ops: Vec = (0..n_instrs) + .map(|_| RiscVOp::Addi { + rd: Reg::ZERO, + rs1: Reg::ZERO, + imm: 0, + }) + .collect(); + placeholder_funcs.push(RiscVElfFunction { + name: func.name.clone(), + ops: placeholder_ops, + }); + } + + let builder = RiscVElfBuilder::new_relocatable(); + let mut elf = builder + .build(&placeholder_funcs) + .context("RISC-V multi-function ELF generation failed")?; + + // .text starts immediately after the 52-byte ELF header. + let text_offset = 52usize; + if elf.len() < text_offset + all_code.len() { + anyhow::bail!("RISC-V ELF too small to embed code"); + } + elf[text_offset..text_offset + all_code.len()].copy_from_slice(&all_code); + Ok(elf) +} + +#[cfg(not(feature = "riscv"))] +fn build_multi_func_riscv_elf(_funcs: &[ElfFunction]) -> Result> { + anyhow::bail!("RISC-V backend was not compiled in (rebuild with --features riscv)") +} + /// Build a simple ELF with just the code section (for quick testing) fn build_simple_elf(code: &[u8], func_name: &str) -> Result> { let mut elf_builder = ElfBuilder::new_arm32().with_entry(0x8000);