Skip to main content

ab_riscv_interpreter/
basic.rs

1//! Basic implementations of various interpreter traits
2
3#[cfg(test)]
4mod tests;
5
6use crate::{
7    Address, BasicInt, CustomErrorPlaceholder, ExecutableInstruction, ExecutionError,
8    FetchInstructionResult, InstructionFetcher, ProgramCounter, ProgramCounterError, RegisterFile,
9    Rs1Rs2OperandValues, Rs1Rs2Operands, SystemInstructionHandler, VirtualMemory,
10    VirtualMemoryError,
11};
12use ab_riscv_primitives::prelude::*;
13use core::hint::cold_path;
14use core::marker::PhantomData;
15use core::ops::ControlFlow;
16use replace_with::replace_with_or_abort_and_return;
17
18/// Basic general purpose register to be used with [`BasicRegisters`]
19///
20/// # Safety
21/// `Self::offset()` must return values in `0..Self::N` range. `Self::from_bits()` must return
22/// `Some()` for `0..=31` if `Self::RVE = false` and `0..=15` if `Self::RVE = true`.
23pub const unsafe trait BasicRegister
24where
25    Self: [const] Register,
26{
27    /// The number of general purpose registers.
28    ///
29    /// Canonically 32 unless E extension is used, in which case 16.
30    const N: usize;
31
32    /// Offset in a set of registers
33    fn offset(self) -> u8;
34}
35
36// SAFETY: `Self::offset()` returns values within `0..Self::N` range
37const unsafe impl<Type> BasicRegister for EReg<Type>
38where
39    Self: [const] Register,
40{
41    const N: usize = 16;
42
43    #[inline(always)]
44    fn offset(self) -> u8 {
45        // SAFETY: Enum is `#[repr(u8)]` and doesn't have any fields
46        unsafe { core::mem::transmute::<Self, u8>(self) }
47    }
48}
49
50// SAFETY: `Self::offset()` returns values within `0..Self::N` range
51const unsafe impl<Type> BasicRegister for Reg<Type>
52where
53    Self: [const] Register,
54{
55    const N: usize = 32;
56
57    #[inline(always)]
58    fn offset(self) -> u8 {
59        // SAFETY: Enum is `#[repr(u8)]` and doesn't have any fields
60        unsafe { core::mem::transmute::<Self, u8>(self) }
61    }
62}
63
64/// A basic set of RISC-V GPRs (General Purpose Registers)
65#[derive(Debug, Clone, Copy)]
66#[repr(align(16))]
67pub struct BasicRegisters<Reg>
68where
69    Reg: BasicRegister,
70{
71    regs: [Reg::Type; Reg::N],
72}
73
74impl<Reg> Default for BasicRegisters<Reg>
75where
76    Reg: BasicRegister,
77{
78    #[inline(always)]
79    fn default() -> Self {
80        Self {
81            regs: [Reg::Type::default(); _],
82        }
83    }
84}
85
86const impl<Reg> RegisterFile<Reg> for BasicRegisters<Reg>
87where
88    Reg: [const] BasicRegister,
89{
90    #[inline(always)]
91    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic(const))]
92    fn read(&self, reg: Reg) -> Reg::Type {
93        if reg == Reg::ZERO {
94            // Always zero
95            return Reg::Type::default();
96        }
97
98        // SAFETY: register offset is always within bounds
99        *unsafe { self.regs.get_unchecked(usize::from(reg.offset())) }
100    }
101
102    #[inline(always)]
103    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic(const))]
104    fn write(&mut self, reg: Reg, value: Reg::Type) {
105        // SAFETY: register offset is always within bounds
106        *unsafe { self.regs.get_unchecked_mut(usize::from(reg.offset())) } = value;
107    }
108}
109
110/// Basic interpreter state.
111///
112/// This is a simple container, which is not required to be used, is helpful for storing the whole
113/// state related to the interpreter together.
114#[derive(Debug)]
115pub struct BasicInterpreterState<Regs, ExtState, Memory, IF, InstructionHandler> {
116    /// General purpose registers
117    pub regs: Regs,
118    /// Extended state.
119    ///
120    /// Extensions might use this to place additional constraints on `ExtState` to require
121    /// additional registers or other resources. If no such extension is used, `()` can be used as
122    /// a placeholder.
123    pub ext_state: ExtState,
124    /// Memory
125    pub memory: Memory,
126    /// Instruction fetcher
127    pub instruction_fetcher: IF,
128    /// System instruction handler
129    pub system_instruction_handler: InstructionHandler,
130}
131
132impl<Regs, ExtState, Memory, IF, InstructionHandler>
133    BasicInterpreterState<Regs, ExtState, Memory, IF, InstructionHandler>
134{
135    /// Execute the program with a given basic interpreter state.
136    ///
137    /// The implementation is designed to be efficient with little left to optimize further. Though
138    /// it is still possible to improve performance by applying additional constraints on the
139    /// program.
140    // TODO: It might be impractical to support `no-panic` here directly in a general case, but it
141    //  should be possible to do so for small extensions to verify the workflow
142    pub fn execute<I>(&mut self) -> Result<(), ExecutionError<Address<I>>>
143    where
144        Regs: RegisterFile<<I as Instruction>::Reg>,
145        I: ExecutableInstruction<Regs, ExtState, Memory, IF, InstructionHandler>,
146        Memory: VirtualMemory,
147        IF: InstructionFetcher<I, Memory> + ProgramCounter<Address<I>, Memory>,
148    {
149        replace_with_or_abort_and_return(
150            &mut self.instruction_fetcher,
151            #[inline(always)]
152            |mut instruction_fetcher| {
153                loop {
154                    let instruction = match instruction_fetcher.fetch_instruction(&self.memory) {
155                        Ok(FetchInstructionResult::Instruction(instruction)) => instruction,
156                        Ok(FetchInstructionResult::ControlFlow(ControlFlow::Continue(()))) => {
157                            cold_path();
158                            continue;
159                        }
160                        Ok(FetchInstructionResult::ControlFlow(ControlFlow::Break(()))) => {
161                            cold_path();
162                            break;
163                        }
164                        Err(error) => {
165                            cold_path();
166                            return (Err(error), instruction_fetcher);
167                        }
168                    };
169
170                    let Rs1Rs2Operands { rs1, rs2 } = instruction.get_rs1_rs2_operands();
171                    let rs1rs2_values = Rs1Rs2OperandValues {
172                        rs1_value: self.regs.read(rs1),
173                        rs2_value: self.regs.read(rs2),
174                    };
175
176                    match instruction.execute(
177                        rs1rs2_values,
178                        &mut self.regs,
179                        &mut self.ext_state,
180                        &mut self.memory,
181                        &mut instruction_fetcher,
182                        &mut self.system_instruction_handler,
183                    ) {
184                        Ok(ControlFlow::Continue((rd, rd_value))) => {
185                            self.regs.write(rd, rd_value);
186                        }
187                        Ok(ControlFlow::Break(())) => {
188                            cold_path();
189                            break;
190                        }
191                        Err(error) => {
192                            cold_path();
193                            return (Err(error), instruction_fetcher);
194                        }
195                    }
196                }
197
198                (Ok(()), instruction_fetcher)
199            },
200        )
201    }
202}
203
204/// Basic memory implementation.
205///
206/// Flat structure, no rwx protections, no alignment requirements. It uses stack, so for larger
207/// allocation it'll need to be boxed (zero-initialized is fine) or a custom implementation to be
208/// used.
209///
210/// This implementation is intentionally basic and correct, but not the most performant. It is
211/// possible to have a more efficient implementation that skips certain checks by placing additional
212/// constraints on the program.
213///
214/// This works for simpler cases, while a more sophisticated implementation might prevent certain
215/// memory from being writable, supporting actual virtual memory with dynamically allocated memory
216/// pages, etc.
217#[derive(Debug, Copy, Clone)]
218#[repr(align(16))]
219pub struct BasicMemory<const BASE_ADDR: u64, const SIZE: usize> {
220    data: [u8; SIZE],
221}
222
223impl<const BASE_ADDR: u64, const SIZE: usize> VirtualMemory for BasicMemory<BASE_ADDR, SIZE> {
224    #[inline(always)]
225    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
226    fn read<T>(&self, address: u64) -> Result<T, VirtualMemoryError>
227    where
228        T: BasicInt,
229    {
230        let Some(offset) = address.checked_sub(BASE_ADDR) else {
231            cold_path();
232            return Err(VirtualMemoryError::OutOfBoundsRead { address });
233        };
234
235        if offset.saturating_add(size_of::<T>() as u64) > self.data.len() as u64 {
236            cold_path();
237            return Err(VirtualMemoryError::OutOfBoundsRead { address });
238        }
239
240        // SAFETY: Only reading basic integers from initialized memory
241        unsafe {
242            Ok(self
243                .data
244                .as_ptr()
245                .cast::<T>()
246                .byte_add(offset as usize)
247                .read_unaligned())
248        }
249    }
250
251    #[inline(always)]
252    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
253    unsafe fn read_unchecked<T>(&self, address: u64) -> T
254    where
255        T: BasicInt,
256    {
257        // SAFETY: Guaranteed by function contract
258        unsafe {
259            let offset = address.unchecked_sub(BASE_ADDR) as usize;
260            self.data
261                .as_ptr()
262                .cast::<T>()
263                .byte_add(offset)
264                .read_unaligned()
265        }
266    }
267
268    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
269    fn read_slice(&self, address: u64, len: u32) -> Result<&[u8], VirtualMemoryError> {
270        let Some(offset) = address.checked_sub(BASE_ADDR) else {
271            cold_path();
272            return Err(VirtualMemoryError::OutOfBoundsRead { address });
273        };
274
275        if offset > self.data.len() as u64 {
276            cold_path();
277            return Err(VirtualMemoryError::OutOfBoundsRead { address });
278        }
279
280        self.data
281            .get(offset as usize..)
282            .and_then(|data| data.get(..len as usize))
283            .ok_or(VirtualMemoryError::OutOfBoundsRead { address })
284    }
285
286    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
287    fn read_slice_up_to(&self, address: u64, len: u32) -> &[u8] {
288        let Some(offset) = address.checked_sub(BASE_ADDR) else {
289            cold_path();
290            return &[];
291        };
292
293        if offset > self.data.len() as u64 {
294            cold_path();
295            return &[];
296        }
297
298        let remaining = self.data.get(offset as usize..).unwrap_or_default();
299        remaining.get(..len as usize).unwrap_or(remaining)
300    }
301
302    #[inline(always)]
303    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
304    fn write<T>(&mut self, address: u64, value: T) -> Result<(), VirtualMemoryError>
305    where
306        T: BasicInt,
307    {
308        let Some(offset) = address.checked_sub(BASE_ADDR) else {
309            cold_path();
310            return Err(VirtualMemoryError::OutOfBoundsWrite { address });
311        };
312
313        if offset.saturating_add(size_of::<T>() as u64) > self.data.len() as u64 {
314            cold_path();
315            return Err(VirtualMemoryError::OutOfBoundsWrite { address });
316        }
317
318        // SAFETY: Only writing basic integers to initialized memory
319        unsafe {
320            self.data
321                .as_mut_ptr()
322                .cast::<T>()
323                .byte_add(offset as usize)
324                .write_unaligned(value);
325        }
326
327        Ok(())
328    }
329
330    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
331    fn write_slice(&mut self, address: u64, data: &[u8]) -> Result<(), VirtualMemoryError> {
332        let Some(offset) = address.checked_sub(BASE_ADDR) else {
333            cold_path();
334            return Err(VirtualMemoryError::OutOfBoundsWrite { address });
335        };
336
337        if offset > self.data.len() as u64 {
338            cold_path();
339            return Err(VirtualMemoryError::OutOfBoundsWrite { address });
340        }
341
342        let len = data.len();
343        let Some(target_data) = self
344            .data
345            .get_mut(offset as usize..)
346            .and_then(|data| data.get_mut(..len))
347        else {
348            cold_path();
349            return Err(VirtualMemoryError::OutOfBoundsWrite { address });
350        };
351
352        target_data.copy_from_slice(data);
353
354        Ok(())
355    }
356}
357
358impl<const BASE_ADDR: u64, const SIZE: usize> Default for BasicMemory<BASE_ADDR, SIZE> {
359    #[inline(always)]
360    fn default() -> Self {
361        Self { data: [0; _] }
362    }
363}
364
365impl<const BASE_ADDR: u64, const SIZE: usize> BasicMemory<BASE_ADDR, SIZE> {
366    /// Get a mutable slice of memory.
367    ///
368    /// This is primarily useful for setting up the program and should not be used beyond that.
369    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
370    pub fn get_mut_bytes(
371        &mut self,
372        address: u64,
373        size: usize,
374    ) -> Result<&mut [u8], VirtualMemoryError> {
375        let Some(offset) = address.checked_sub(BASE_ADDR) else {
376            cold_path();
377            return Err(VirtualMemoryError::OutOfBoundsRead { address });
378        };
379        let offset = offset as usize;
380
381        let Some(slice) = self
382            .data
383            .get_mut(offset..)
384            .and_then(|data| data.get_mut(..size))
385        else {
386            cold_path();
387            return Err(VirtualMemoryError::OutOfBoundsRead { address });
388        };
389
390        Ok(slice)
391    }
392}
393
394/// Basic instruction fetcher implementation.
395///
396/// This implementation is intentionally basic and correct, but not the most performant. It is
397/// possible to have a more efficient implementation that skips certain checks by placing additional
398/// constraints on the constructor.
399///
400/// Note that it loads instructions from anywhere in memory. This works, but it is likely that you
401/// want to restrict this to a specific executable region of memory.
402#[derive(Debug, Copy, Clone)]
403pub struct BasicInstructionFetcher<I, CustomError = CustomErrorPlaceholder>
404where
405    I: Instruction,
406{
407    return_trap_address: Address<I>,
408    pc: Address<I>,
409    _phantom: PhantomData<CustomError>,
410}
411
412impl<I, Memory, CustomError> ProgramCounter<Address<I>, Memory, CustomError>
413    for BasicInstructionFetcher<I, CustomError>
414where
415    I: Instruction,
416    Memory: VirtualMemory,
417{
418    #[inline(always)]
419    fn get_pc(&self) -> Address<I> {
420        self.pc
421    }
422
423    #[inline]
424    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
425    fn set_pc(
426        &mut self,
427        memory: &Memory,
428        pc: Address<I>,
429    ) -> Result<ControlFlow<()>, ProgramCounterError<Address<I>, CustomError>> {
430        // TODO: Workaround for https://github.com/rust-lang/rust-clippy/issues/17430
431        let _: &Memory = memory;
432        if pc == self.return_trap_address {
433            cold_path();
434            return Ok(ControlFlow::Break(()));
435        }
436
437        if !pc.as_u64().is_multiple_of(u64::from(I::alignment())) {
438            cold_path();
439            return Err(ProgramCounterError::UnalignedInstruction { address: pc });
440        }
441
442        self.pc = pc;
443
444        Ok(ControlFlow::Continue(()))
445    }
446}
447
448impl<I, Memory, CustomError> InstructionFetcher<I, Memory, CustomError>
449    for BasicInstructionFetcher<I, CustomError>
450where
451    I: Instruction,
452    Memory: VirtualMemory,
453{
454    #[inline]
455    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
456    fn fetch_instruction(
457        &mut self,
458        memory: &Memory,
459    ) -> Result<FetchInstructionResult<I>, ExecutionError<Address<I>, CustomError>> {
460        let instruction = match memory.read(self.pc.as_u64()).or_else(|error| {
461            cold_path();
462            // Attempt to read a 16-bit compressed instruction
463            if let Ok(instruction) = memory.read::<u16>(self.pc.as_u64())
464                && (instruction & 0b11) != 0b11
465            {
466                return Ok(u32::from(instruction));
467            }
468            Err(error)
469        }) {
470            Ok(instruction) => instruction,
471            Err(error) => {
472                cold_path();
473                return Err(ExecutionError::MemoryAccess(error));
474            }
475        };
476
477        let Some(instruction) = I::try_decode(instruction) else {
478            cold_path();
479            return Err(ExecutionError::IllegalInstruction { address: self.pc });
480        };
481        self.pc += instruction.size().into();
482
483        Ok(FetchInstructionResult::Instruction(instruction))
484    }
485}
486
487impl<I, CustomError> BasicInstructionFetcher<I, CustomError>
488where
489    I: Instruction,
490{
491    /// Create a new instance.
492    ///
493    /// `return_trap_address` is the address at which the interpreter will stop execution
494    /// (gracefully).
495    #[inline(always)]
496    pub fn new(return_trap_address: Address<I>, pc: Address<I>) -> Self {
497        Self {
498            return_trap_address,
499            pc,
500            _phantom: PhantomData,
501        }
502    }
503}
504
505/// System instruction handler that results in illegal instruction for all system calls and does
506/// nothing for other system instructions
507#[derive(Debug, Default, Clone, Copy)]
508pub struct IllegalEcallSystemInstructionHandler;
509
510impl<Reg, Regs, Memory, PC, CustomError>
511    SystemInstructionHandler<Reg, Regs, Memory, PC, CustomError>
512    for IllegalEcallSystemInstructionHandler
513where
514    Reg: Register,
515    Regs: RegisterFile<Reg>,
516    PC: ProgramCounter<Reg::Type, Memory, CustomError>,
517{
518    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
519    fn handle_ecall(
520        &mut self,
521        regs: &mut Regs,
522        memory: &mut Memory,
523        program_counter: &mut PC,
524    ) -> Result<ControlFlow<()>, ExecutionError<Reg::Type, CustomError>> {
525        // TODO: Workaround for https://github.com/rust-lang/rust-clippy/issues/17430
526        let _: &Regs = regs;
527        let _: &Memory = memory;
528        Err(ExecutionError::IllegalInstruction {
529            address: program_counter.old_pc(size_of::<u32>() as u8),
530        })
531    }
532}