From c9913654cfabb9ddd62d12e7f8b33fdbeacaa391 Mon Sep 17 00:00:00 2001 From: Paul Govereau Date: Fri, 26 Jan 2024 11:18:23 -0500 Subject: [PATCH] migrate traces to NVM --- Cargo.lock | 1 + Cargo.toml | 1 + nvm/Cargo.toml | 5 +- nvm/src/ark_serde.rs | 57 ++++++++ nvm/src/error.rs | 2 +- nvm/src/eval.rs | 100 ++++++++++--- nvm/src/lib.rs | 2 + nvm/src/memory.rs | 6 + nvm/src/memory/cacheline.rs | 11 ++ nvm/src/riscv.rs | 135 ++++++++++++----- nvm/src/trace.rs | 285 ++++++++++++++++++++++++++++++++++++ riscv/Cargo.toml | 2 +- riscv/src/lib.rs | 5 +- riscv/src/vm/memory.rs | 2 +- 14 files changed, 543 insertions(+), 71 deletions(-) create mode 100644 nvm/src/ark_serde.rs create mode 100644 nvm/src/trace.rs diff --git a/Cargo.lock b/Cargo.lock index 254af5bd..597e5e9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1621,6 +1621,7 @@ dependencies = [ "nexus-riscv", "num-derive", "num-traits", + "serde", "thiserror", ] diff --git a/Cargo.toml b/Cargo.toml index f2b7e076..b8a743eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ publish = false clap = { version = "4.3", features = ["derive"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +elf = { version = "0.7", default-features = false, features = ["std"] } ark-crypto-primitives = { version = "0.4.0", features = ["r1cs", "sponge", "crh", "merkle_tree"] } ark-std = "0.4.0" diff --git a/nvm/Cargo.toml b/nvm/Cargo.toml index 149636f4..40049164 100644 --- a/nvm/Cargo.toml +++ b/nvm/Cargo.toml @@ -6,9 +6,8 @@ publish = false [dependencies] thiserror = "1.0" - -# temporary dependency needed for riscv module -elf = { version = "0.7", default-features = false, features = ["std"] } +elf.workspace = true +serde.workspace = true nexus-riscv = { path = "../riscv" } diff --git a/nvm/src/ark_serde.rs b/nvm/src/ark_serde.rs new file mode 100644 index 00000000..2d2713dc --- /dev/null +++ b/nvm/src/ark_serde.rs @@ -0,0 +1,57 @@ +use std::fmt; +use serde::{Serializer, Deserializer, de::Visitor}; +use ark_serialize::{CanonicalSerialize, CanonicalDeserialize}; + +pub fn serialize(t: &T, s: S) -> Result +where + S: Serializer, + T: CanonicalSerialize, +{ + let mut v = Vec::new(); + t.serialize_uncompressed(&mut v) + .map_err(|_| serde::ser::Error::custom("ark error"))?; + s.serialize_bytes(&v) +} + +pub fn deserialize<'a, D, T>(d: D) -> Result +where + D: Deserializer<'a>, + T: CanonicalDeserialize, +{ + let v = d.deserialize_bytes(BV)?; + let t = T::deserialize_uncompressed(v.as_slice()) + .map_err(|_| serde::de::Error::custom("ark Error"))?; + Ok(t) +} + +struct BV; + +impl<'a> Visitor<'a> for BV { + type Value = Vec; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a byte sequence") + } + + fn visit_bytes(self, v: &[u8]) -> Result { + Ok(v.to_vec()) + } + + fn visit_byte_buf(self, v: Vec) -> Result { + Ok(v) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'a>, + { + let mut v = Vec::new(); + loop { + match seq.next_element() { + Ok(Some(x)) => v.push(x), + Ok(None) => return Ok(v), + Err(e) => return Err(e), + } + } + } +} diff --git a/nvm/src/error.rs b/nvm/src/error.rs index 960b4200..6a4cc976 100644 --- a/nvm/src/error.rs +++ b/nvm/src/error.rs @@ -5,7 +5,7 @@ use thiserror::Error; pub enum NVMError { /// Invalid instruction format, could not parse #[error("invalid instruction {1} at {0}")] - InvalidInstruction(u32, u32), + InvalidInstruction(u64, u32), /// Unknown ECALL number #[error("unknown ecall {1} at {0}")] diff --git a/nvm/src/eval.rs b/nvm/src/eval.rs index 9de4ca9c..4ce2690a 100644 --- a/nvm/src/eval.rs +++ b/nvm/src/eval.rs @@ -1,14 +1,38 @@ -use crate::error::Result; +//! Evaluation for Nexus VM programs. + +use num_traits::FromPrimitive; + +use crate::error::{Result, NVMError::InvalidInstruction}; use crate::instructions::{Inst, Opcode, Opcode::*, Width}; use crate::memory::{Memory, path::Path}; +/// State of a running Nexus VM program. +#[derive(Default)] pub struct NVM { + /// Current program counter. pub pc: u32, + /// Register file. pub regs: [u32; 32], + /// Most recent instruction. + pub inst: Inst, + /// Result of most recent instruction. + pub Z: u32, + /// Machine memory. pub memory: Memory, + /// Merkle proof for current instruction at pc pub pc_path: Path, - pub read_path: Path, - pub write_path: Path, + /// Merkle proof for load/store instructions. + pub read_path: Option, + /// Merkle proof for store instructions. + pub write_path: Option, +} + +/// Generate a trivial VM with a single HALT instruction. +pub fn halt_vm() -> NVM { + let mut vm = NVM { pc: 0x1000, ..NVM::default() }; + let inst = Inst { opcode: HALT, ..Inst::default() }; + vm.memory.write_inst(vm.pc, inst.into()).unwrap(); + vm } #[inline] @@ -21,6 +45,7 @@ fn sub32(x: u32, y: u32) -> u32 { x.overflowing_sub(y).0 } +// Evaluator for branch conditions. fn brcc(opcode: Opcode, x: u32, y: u32) -> bool { match opcode { BEQ => x == y, @@ -33,8 +58,16 @@ fn brcc(opcode: Opcode, x: u32, y: u32) -> bool { } } -pub fn step(vm: &mut NVM) -> Result<()> { - let inst = Inst::default(); +/// Execute one step of a running Nexus VM. +/// This function will load the next instruction at the address +/// located at the program counter, execute the instruction, +/// and update the register file, program counter, and merkle +/// proofs. +pub fn eval_step(vm: &mut NVM) -> Result<()> { + let (dword, path) = vm.memory.read_inst(vm.pc)?; + let Some(inst) = Inst::from_u64(dword) else { + return Err(InvalidInstruction(dword, vm.pc)); + }; let I = inst.imm; let X = vm.regs[inst.rs1 as usize]; @@ -43,9 +76,14 @@ pub fn step(vm: &mut NVM) -> Result<()> { let YI = add32(Y, I); let shamt = YI & 0x1f; - let mut Z = 0u32; let mut PC = 0u32; + vm.inst = inst; + vm.Z = 0; + vm.pc_path = path; + vm.read_path = None; + vm.write_path = None; + match inst.opcode { NOP => {} HALT => { @@ -59,7 +97,7 @@ pub fn step(vm: &mut NVM) -> Result<()> { } JAL => { - Z = add32(vm.pc, 8); + vm.Z = add32(vm.pc, 8); PC = add32(X, I); } BEQ | BNE | BLT | BGE | BLTU | BGEU => { @@ -73,39 +111,55 @@ pub fn step(vm: &mut NVM) -> Result<()> { let width = Width::try_from(inst.opcode).unwrap(); let addr = add32(X, I); let (val, path) = vm.memory.load(width, addr)?; - vm.read_path = path; - Z = val; + vm.read_path = Some(path); + vm.Z = val; } SB | SH | SW => { // Note: unwrap cannot fail let width = Width::try_from(inst.opcode).unwrap(); let addr = add32(X, I); let (_, path) = vm.memory.load(width, addr)?; - vm.read_path = path; - vm.write_path = vm.memory.store(width, addr, Y)?; + vm.read_path = Some(path); + vm.write_path = Some(vm.memory.store(width, addr, Y)?); } - ADD => Z = add32(X, YI), - SUB => Z = sub32(X, YI), - SLT => Z = ((X as i32) < (YI as i32)) as u32, - SLTU => Z = (X < Y) as u32, - SLL => Z = X << shamt, - SRL => Z = X >> shamt, - SRA => Z = ((X as i32) >> shamt) as u32, - AND => Z = X & YI, - OR => Z = X | YI, - XOR => Z = X ^ YI, + ADD => vm.Z = add32(X, YI), + SUB => vm.Z = sub32(X, YI), + SLT => vm.Z = ((X as i32) < (YI as i32)) as u32, + SLTU => vm.Z = (X < YI) as u32, + SLL => vm.Z = X << shamt, + SRL => vm.Z = X >> shamt, + SRA => vm.Z = ((X as i32) >> shamt) as u32, + AND => vm.Z = X & YI, + OR => vm.Z = X | YI, + XOR => vm.Z = X ^ YI, } if inst.rd > 0 { - vm.regs[inst.rd as usize] = Z; + vm.regs[inst.rd as usize] = vm.Z; } if PC == 0 { - vm.pc = add32(PC, 8); + vm.pc = add32(vm.pc, 8); } else { vm.pc = PC; } Ok(()) } + +/// Run a VM to completion. The VM will stop when it encounters +/// a HALT instruction. +pub fn eval(vm: &mut NVM, verbose: bool) -> Result<()> { + loop { + let pc = vm.pc; + eval_step(vm)?; + if verbose { + println!("{:x} {:?}", pc, vm.inst); + } + if vm.inst.opcode == HALT { + break; + } + } + Ok(()) +} diff --git a/nvm/src/lib.rs b/nvm/src/lib.rs index 1da389c7..56c00792 100644 --- a/nvm/src/lib.rs +++ b/nvm/src/lib.rs @@ -5,5 +5,7 @@ pub mod error; pub mod instructions; mod memory; pub mod eval; +pub mod trace; +mod ark_serde; pub mod riscv; diff --git a/nvm/src/memory.rs b/nvm/src/memory.rs index 806c699c..7239cc30 100644 --- a/nvm/src/memory.rs +++ b/nvm/src/memory.rs @@ -30,6 +30,12 @@ impl Memory { Ok((cl.ldw(addr)?, path)) } + /// write instruction at address + pub fn write_inst(&mut self, addr: u32, val: u64) -> Result<()> { + let _ = self.trie.update(addr, |cl| cl.sdw(addr, val))?; + Ok(()) + } + /// perform load according to `width` pub fn load(&self, width: Width, addr: u32) -> Result<(u32, Path)> { let (cl, path) = self.trie.query(addr); diff --git a/nvm/src/memory/cacheline.rs b/nvm/src/memory/cacheline.rs index fcf0f977..bffd453c 100644 --- a/nvm/src/memory/cacheline.rs +++ b/nvm/src/memory/cacheline.rs @@ -180,6 +180,17 @@ impl CacheLine { } Ok(()) } + + /// store 64-bit value at addr + pub fn sdw(&mut self, addr: u32, val: u64) -> Result<()> { + if (addr & 7) != 0 { + return Err(Misaligned(addr)); + } + unsafe { + self.dwords[((addr >> 3) & 3) as usize] = val; + } + Ok(()) + } } #[cfg(test)] diff --git a/nvm/src/riscv.rs b/nvm/src/riscv.rs index 8ca44229..6baaf222 100644 --- a/nvm/src/riscv.rs +++ b/nvm/src/riscv.rs @@ -1,7 +1,4 @@ -// Note: this module will be migrated to the riscv crate -// so that the NVM crate does not depend on riscv. -// It is here for now to avoid disturbing the riscv crate -// until the final PR. +//! Translation of RISC-V ro NVM. use std::path::Path; use std::fs::read; @@ -14,10 +11,11 @@ use elf::{ ElfBytes, }; -use nexus_riscv::rv32::{RV32, Inst as RVInst, parse::parse_buf}; +use nexus_riscv::rv32::{RV32, Inst as RVInst, parse::parse_inst}; -use crate::error::Result; -use crate::instructions::{Inst, Opcode, Opcode::*}; +use crate::error::{Result, NVMError::ELFFormat}; +use crate::instructions::{Inst, Opcode, Opcode::*, Width::BU}; +use crate::eval::NVM; #[inline] fn add32(a: u32, b: u32) -> u32 { @@ -41,6 +39,8 @@ fn mul32(a: u32, b: u32) -> u32 { fn translate_inst(start: u32, end: u32, rv: RVInst) -> Inst { let RVInst { pc, len: _, word: _, inst: rv } = rv; + let npc = start + (pc - start) * 2; + let mut inst = Inst::default(); match rv { RV32::LUI { rd, imm } => { @@ -54,7 +54,7 @@ fn translate_inst(start: u32, end: u32, rv: RVInst) -> Inst { let res = add32(pc, imm); if res >= start && res < end { // assume address of label and adjust - inst.imm = add32(pc, mul32(imm, 2)); + inst.imm = add32(npc, mul32(imm, 2)); } else { inst.imm = res; } @@ -62,7 +62,7 @@ fn translate_inst(start: u32, end: u32, rv: RVInst) -> Inst { RV32::JAL { rd, imm } => { inst.opcode = JAL; inst.rd = rd as u8; - inst.imm = add32(pc, mul32(imm, 2)); + inst.imm = add32(npc, mul32(imm, 2)); } RV32::JALR { rd, rs1, imm } => { inst.opcode = JAL; @@ -113,27 +113,17 @@ fn translate_inst(start: u32, end: u32, rv: RVInst) -> Inst { inst } -// Translate a code segment from RV32 to NVM. The result -// is a vector of NVM encoded instructions. - -fn translate(pc: u32, bytes: &[u8]) -> Result> { - let end = pc + bytes.len() as u32; - let insts = parse_buf(pc, bytes)?; - let mut output: Vec = Vec::with_capacity(insts.len()); - for i in insts { - output.push(translate_inst(pc, end, i).into()); - } - Ok(output) -} - /// Translate a RiscV ELF file to NVM. - -// Note: no result is constructed at this point. - -pub fn translate_elf(path: &Path) -> Result<()> { +#[allow(clippy::needless_range_loop)] +#[allow(clippy::field_reassign_with_default)] +pub fn translate_elf(path: &Path) -> Result { let file_data = read(path)?; let bytes = file_data.as_slice(); - let file = ElfBytes::::minimal_parse(bytes).unwrap(); + let file = ElfBytes::::minimal_parse(bytes)?; + + if file.ehdr.e_entry != 0x1000 { + return Err(ELFFormat("invalid start address")); + } let segments: Vec = file .segments() @@ -143,37 +133,106 @@ pub fn translate_elf(path: &Path) -> Result<()> { .collect(); if segments.len() != 2 { - panic!("ELF format: expected 2 loadable segments"); + return Err(ELFFormat("expected 2 loadable segments")); } let code = segments[0]; let data = segments[1]; if code.p_flags & PF_X != PF_X { - panic!("ELF format: expecting one code segment in low memory"); + return Err(ELFFormat("expecting one code segment in low memory")); } if data.p_flags & PF_X != 0 { - panic!("ELF format: expecting one code segment in low memory"); + return Err(ELFFormat("expecting one data segment in high memory")); + } + + if code.p_offset + code.p_filesz * 2 >= data.p_offset { + return Err(ELFFormat("not enough room to expand code to NVM")); } - println!("Code {code:?}"); - println!("Data {data:?}"); + let mut vm = NVM::default(); + vm.pc = 0x1000; - let s = code.p_offset as usize; - let e = (code.p_offset + code.p_filesz) as usize; + // write code segment + let s = code.p_offset as u32; + let e = (code.p_offset + code.p_filesz) as u32; + for i in s..e { + let inst = parse_inst(i, &bytes[i as usize..])?; + let pc = s + (i - s) * 2; + let inst = translate_inst(s, e, inst); + vm.memory.write_inst(pc, inst.into())?; + } - translate(s as u32, &bytes[s..e]).unwrap(); + // write data segment + let s = data.p_offset as usize; + let e = (data.p_offset + data.p_filesz) as usize; + for i in s..e { + let b = bytes[i]; + vm.memory.store(BU, (s + i) as u32, b as u32)?; + } - Ok(()) + Ok(vm) } #[cfg(test)] -mod test { +pub mod test { use super::*; + use crate::eval::eval; use nexus_riscv::rv32::{BOP, LOP, SOP, AOP}; + use nexus_riscv::machines::MACHINES; + + // this function is used by other test crates + #[allow(clippy::field_reassign_with_default)] + pub fn test_machines() -> Vec<(&'static str, NVM)> { + MACHINES + .iter() + .map(|(name, f_vm, _)| { + let rvm = f_vm(); + let mut nvm = NVM::default(); + nvm.pc = rvm.regs.pc; + let mut i = 0; + loop { + let rpc = nvm.pc + i * 4; + let (word, _) = rvm.mem.read_slice(rpc).unwrap(); + let inst = match parse_inst(rpc, word) { + Err(_) => break, + Ok(inst) => inst, + }; + + let inst = translate_inst(nvm.pc, nvm.pc + 0x1000, inst); + let dword = u64::from(inst); + let npc = nvm.pc + i * 8; + nvm.memory.write_inst(npc, dword).unwrap(); + + i += 1; + } + (*name, nvm) + }) + .collect() + } + + #[test] + fn compare_test_machines() { + let tests = MACHINES.iter().zip(test_machines()); + for ((name, _, f_regs), (_, mut nvm)) in tests { + println!("Checking machine {name}"); + let regs = f_regs(); + + eval(&mut nvm, false).unwrap(); + + let npc = 0x1000 + (regs.pc - 0x1000) * 2; + assert_eq!(nvm.pc, npc); + + // code addresses will not match, so register checks + // for tests with jal are skipped + if name != &"loop10" && name != &"jump" { + assert_eq!(nvm.regs, regs.x); + } + } + } // these tests check that the invariants assumed by translate - // are satisfied: the try_from's will never fail. + // are satisfied: the from_u8's will never fail. macro_rules! inv { ($base:ident, $enum:ident, $op:ident) => { assert_eq!( diff --git a/nvm/src/trace.rs b/nvm/src/trace.rs new file mode 100644 index 00000000..59ffef98 --- /dev/null +++ b/nvm/src/trace.rs @@ -0,0 +1,285 @@ +//! Nexus VM program traces +//! +//! A trace is generated by running a Nexus VM program. +//! A `Trace` holds a number of `Blocks`, and each `Block` holds +//! a number of `Steps`. The number of steps in each block +//! (also referred to as `k`), corresponds to the instructions +//! per folding step. Thus, each block corresponds to one +//! folding step. +//! +//! A `Trace` can be divided at block boundaries and each subtrace +//! proved independently of the others (when using PCD). Each `Block` +//! contains enough information to reconstruct the `Witness` for each +//! step contained in the block. The witnesses can be reconstructed +//! by iterating over the steps in the block. + +use num_traits::FromPrimitive; + +use crate::error::Result; +use crate::instructions::{Inst, Opcode::HALT}; +use crate::eval::{NVM, eval_step}; +use crate::memory::path::Path; + +use ark_bn254::Fr as F; + +use serde::{Serialize, Deserialize}; +use ark_serialize::{CanonicalSerialize, CanonicalDeserialize}; + +/// Represents a program trace. +#[derive(Default, Clone, Serialize, Deserialize, CanonicalSerialize, CanonicalDeserialize)] +pub struct Trace { + /// Steps per fold/block. + pub k: usize, + /// First block in this (sub)trace. + pub start: usize, + /// The blocks contained in this trace. + pub blocks: Vec, +} + +/// A seqeunce of program steps. +#[derive(Default, Clone, Serialize, Deserialize, CanonicalSerialize, CanonicalDeserialize)] +pub struct Block { + /// Starting program counter for this block. + pub pc: u32, + /// Starting register file for this block. + pub regs: [u32; 32], + /// Sequence of `k` steps contained in this block. + pub steps: Vec, +} + +/// A program step. +#[derive(Default, Clone, Serialize, Deserialize, CanonicalSerialize, CanonicalDeserialize)] +pub struct Step { + /// Encoded NVM instruction. + pub inst: u64, + /// Result of instruction evaluation. + pub Z: u32, + /// Next program counter, for jump and branch instructions. + pub PC: Option, + /// Merkle proof for instruction at pc. + #[serde(with = "crate::ark_serde")] + pub pc_path: Path, + /// Merkle proof for read instructions. + #[serde(with = "crate::ark_serde")] + pub read_path: Option, + /// Merkle proof for write instructions. + #[serde(with = "crate::ark_serde")] + pub write_path: Option, +} + +impl Trace { + /// Split a trace into subtraces with `n` blocks each. Note, the + /// final subtrace may contain fewer than `n` blocks. + pub fn split_by(&self, n: usize) -> impl Iterator + '_ { + let mut index = 0; + self.blocks.chunks(n).map(move |bs| { + let start = index; + index += n; + Trace { k: self.k, start, blocks: bs.to_vec() } + }) + } + + /// Return block with index `n`, if it is contained in this (sub)trace. + pub fn block(&self, n: usize) -> Option<&Block> { + if self.start < n || self.start + self.blocks.len() >= n { + return None; + } + Some(&self.blocks[n - self.start]) + } + + /// Create a subtrace containing only block `n`. + pub fn get(&self, n: usize) -> Option { + Some(Trace { + k: self.k, + start: n, + blocks: vec![self.block(n)?.clone()], + }) + } + + /// Return the circuit input for block at index `n`. + /// This vector is compatible with the NVM step circuit. + pub fn input(&self, n: usize) -> Option> { + let b = self.block(n)?; + let mut v = Vec::new(); + v.push(F::from(b.pc)); + for x in b.regs { + v.push(F::from(x)); + } + v.push(b.steps[0].pc_path.root); + Some(v) + } + + /// Estimate the size, in bytes, of this trace. + pub fn estimate_size(&self) -> usize { + use std::mem::size_of_val as sizeof; + sizeof(self) + + self.blocks.len() + * (sizeof(&self.blocks[0]) + + self.blocks[0].steps.len() * sizeof(&self.blocks[0].steps[0])) + } +} + +// Generate a `Step` by evaluating the next instruction of `vm`. +fn step(vm: &mut NVM) -> Result { + let pc = vm.pc; + eval_step(vm)?; + let step = Step { + inst: vm.inst.into(), + Z: vm.Z, + PC: if vm.pc == pc + 8 { None } else { Some(vm.pc) }, + pc_path: vm.pc_path.clone(), + read_path: vm.read_path.clone(), + write_path: vm.write_path.clone(), + }; + Ok(step) +} + +// Generate a `Block` by evaluating `k` steps of `vm`. +fn k_step(vm: &mut NVM, k: usize) -> Result { + let mut block = Block { + pc: vm.pc, + regs: vm.regs, + steps: Vec::new(), + }; + + for _ in 0..k { + block.steps.push(step(vm)?); + } + + Ok(block) +} + +/// Generate a program trace by evaluating `vm`, using `k` steps +/// per block. If `pow` is true, the total number of steps will +/// be rounded up to the nearest power of two by inserting HALT +/// instructions. +pub fn trace(vm: &mut NVM, k: usize, pow: bool) -> Result { + let mut trace = Trace { k, start: 0, blocks: Vec::new() }; + + loop { + let block = k_step(vm, k)?; + trace.blocks.push(block); + + if vm.inst.opcode == HALT { + if pow { + let count = trace.blocks.len(); + if count.next_power_of_two() == count { + break; + } + } else { + break; + } + } + } + Ok(trace) +} + +/// Witness for a single VM step. +#[derive(Default, Debug)] +pub struct Witness { + /// Initial program counter. + pub pc: u32, + /// Initial register file. + pub regs: [u32; 32], + /// Instruction being executed. + pub inst: Inst, + /// First argument value. + pub X: u32, + /// Second argument value. + pub Y: u32, + /// Result of instuction. + pub Z: u32, + /// Next program counter. + pub PC: u32, + /// Merkle proof for reading instruction at pc. + pub pc_path: Path, + /// Merkle proof for load instructions. + pub read_path: Path, + /// Merkle proof for store instructions. + pub write_path: Path, +} + +impl Block { + pub fn iter(&self) -> BlockIter<'_> { + BlockIter::new(self) + } +} + +impl<'a> IntoIterator for &'a Block { + type Item = Witness; + type IntoIter = BlockIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +pub struct BlockIter<'a> { + pc: u32, + regs: [u32; 32], + block: &'a Block, + index: usize, +} + +impl BlockIter<'_> { + fn new(b: &Block) -> BlockIter<'_> { + BlockIter { + pc: b.pc, + regs: b.regs, + block: b, + index: 0, + } + } +} + +impl Iterator for BlockIter<'_> { + type Item = Witness; + + fn next(&mut self) -> Option { + if self.index >= self.block.steps.len() { + return None; + } + + let s = &self.block.steps[self.index]; + let inst = Inst::from_u64(s.inst)?; + let w = Witness { + pc: self.pc, + regs: self.regs, + inst, + X: self.regs[inst.rs1 as usize], + Y: self.regs[inst.rs2 as usize], + Z: s.Z, + PC: if let Some(pc) = s.PC { pc } else { self.pc + 8 }, + pc_path: s.pc_path.clone(), + read_path: s.read_path.as_ref().unwrap_or(&s.pc_path).clone(), + write_path: s.write_path.as_ref().unwrap_or(&s.pc_path).clone(), + }; + + self.pc = w.PC; + self.regs[w.inst.rd as usize] = w.Z; + self.index += 1; + Some(w) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::riscv::test::test_machines; + + // basic check that tracing and iteration succeeds + #[test] + fn trace_test_machines() { + for (name, mut nvm) in test_machines() { + println!("tracing machine {name}"); + let tr = trace(&mut nvm, 1, false).unwrap(); + let mut pc = 0u32; + for b in tr.blocks { + for w in b.iter() { + pc = w.pc; + } + } + assert_eq!(nvm.pc, pc); + } + } +} diff --git a/riscv/Cargo.toml b/riscv/Cargo.toml index b02b8c05..28ee733f 100644 --- a/riscv/Cargo.toml +++ b/riscv/Cargo.toml @@ -12,7 +12,7 @@ categories = { workspace = true } [dependencies] clap.workspace = true serde.workspace = true -elf = { version = "0.7", default-features = false, features = ["std"] } +elf.workspace = true ark-ff.workspace = true ark-relations.workspace = true diff --git a/riscv/src/lib.rs b/riscv/src/lib.rs index 200c6f12..0e38e5e9 100644 --- a/riscv/src/lib.rs +++ b/riscv/src/lib.rs @@ -119,12 +119,9 @@ pub fn eval(vm: &mut VM, show: bool) -> Result<()> { } loop { - if show { - print!("{:50} ", vm.inst); - } eval_inst(vm)?; if show { - println!("{:8x} {:8x}", vm.Z, vm.regs.pc); + println!("{:50} {:8x} {:8x}", vm.inst, vm.Z, vm.regs.pc); } if vm.inst.inst == RV32::UNIMP { break; diff --git a/riscv/src/vm/memory.rs b/riscv/src/vm/memory.rs index 0ecc40cc..a203c006 100644 --- a/riscv/src/vm/memory.rs +++ b/riscv/src/vm/memory.rs @@ -38,7 +38,7 @@ impl Memory { } /// return memory at address - pub(crate) fn read_slice(&self, addr: u32) -> Result<(&[u8], Option)> { + pub fn read_slice(&self, addr: u32) -> Result<(&[u8], Option)> { let (cl, path) = self.trie.query(addr); Ok((cl.bytes(addr), path)) }