Skip to main content

ab_riscv_interpreter/
basic.rs

1//! Basic implementations of various interpreter traits
2
3#[cfg(feature = "alloc")]
4mod eager_instruction_fetcher;
5#[cfg(test)]
6mod tests;
7
8#[cfg(feature = "alloc")]
9pub use crate::basic::eager_instruction_fetcher::{
10    BasicEagerInstructionFetcher, BasicEagerInstructions,
11};
12use crate::zawrs::WrsHandler;
13use crate::zifencei::FenceIHandler;
14use crate::{
15    Address, BasicInt, ExecutableInstruction, ExecutionError, ExecutionResult,
16    FetchInstructionResult, InstructionFetcher, PackedAddress, ProgramCounter, RegisterFile,
17    Rs1Rs2OperandValues, Rs1Rs2Operands, SystemInstructionHandler, VirtualMemory,
18    VirtualMemoryError,
19};
20use ab_riscv_primitives::prelude::*;
21#[cfg(feature = "alloc")]
22use alloc::boxed::Box;
23use core::hint::cold_path;
24use core::mem;
25use core::ops::ControlFlow;
26use replace_with::replace_with_or_abort_and_return;
27
28/// Basic general purpose register to be used with [`BasicRegisters`]
29///
30/// # Safety
31/// `Self::offset()` must return values in `0..Self::N` range. `Self::from_bits()` must return
32/// `Some()` for `0..=31` if `Self::RVE = false` and `0..=15` if `Self::RVE = true`.
33pub const unsafe trait BasicRegister
34where
35    Self: [const] Register,
36{
37    /// The number of general purpose registers.
38    ///
39    /// Canonically 32 unless E extension is used, in which case 16.
40    const N: usize;
41
42    /// Offset in a set of registers
43    fn offset(self) -> u8;
44}
45
46// SAFETY: `Self::offset()` returns values within `0..Self::N` range
47const unsafe impl<Type> BasicRegister for EReg<Type>
48where
49    Self: [const] Register,
50{
51    const N: usize = 16;
52
53    #[inline(always)]
54    fn offset(self) -> u8 {
55        // SAFETY: Enum is `#[repr(u8)]` and doesn't have any fields
56        unsafe { mem::transmute::<Self, u8>(self) }
57    }
58}
59
60// SAFETY: `Self::offset()` returns values within `0..Self::N` range
61const unsafe impl<Type> BasicRegister for Reg<Type>
62where
63    Self: [const] Register,
64{
65    const N: usize = 32;
66
67    #[inline(always)]
68    fn offset(self) -> u8 {
69        // SAFETY: Enum is `#[repr(u8)]` and doesn't have any fields
70        unsafe { mem::transmute::<Self, u8>(self) }
71    }
72}
73
74/// A basic set of RISC-V GPRs (General Purpose Registers).
75///
76/// `ZEROSTORE` generic determines whether to zero `x0` register on write instead of checking
77/// register index on read. `match` loop usually performs better with branching, while threaded
78/// execution benefits from zeroing.
79#[derive(Debug, Clone, Copy)]
80#[repr(align(16))]
81pub struct BasicRegisters<Reg, const ZEROSTORE: bool = false>
82where
83    Reg: BasicRegister,
84{
85    regs: [Reg::Type; Reg::N],
86}
87
88impl<Reg, const ZEROSTORE: bool> Default for BasicRegisters<Reg, ZEROSTORE>
89where
90    Reg: BasicRegister,
91{
92    #[inline(always)]
93    fn default() -> Self {
94        Self {
95            regs: [Reg::Type::default(); _],
96        }
97    }
98}
99
100const impl<Reg, const ZEROSTORE: bool> RegisterFile<Reg> for BasicRegisters<Reg, ZEROSTORE>
101where
102    Reg: [const] BasicRegister,
103{
104    #[inline(always)]
105    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic(const))]
106    fn read(&self, reg: Reg) -> Reg::Type {
107        if reg == Reg::ZERO && !ZEROSTORE {
108            // Always zero
109            return Reg::Type::default();
110        }
111
112        // SAFETY: register offset is always within bounds
113        *unsafe { self.regs.get_unchecked(usize::from(reg.offset())) }
114    }
115
116    #[inline(always)]
117    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic(const))]
118    fn write(&mut self, reg: Reg, value: Reg::Type) {
119        // SAFETY: register offset is always within bounds
120        *unsafe { self.regs.get_unchecked_mut(usize::from(reg.offset())) } = value;
121        if ZEROSTORE {
122            // SAFETY: The register file always has at least one slot
123            *unsafe { self.regs.get_unchecked_mut(0) } = Reg::Type::default();
124        }
125    }
126}
127
128/// Basic interpreter state.
129///
130/// This is a simple container, which is not required to be used, is helpful for storing the whole
131/// state related to the interpreter together.
132#[derive(Debug)]
133pub struct BasicInterpreterState<Regs, Env, Memory, IF> {
134    /// General purpose registers
135    pub regs: Regs,
136    /// Execution environment.
137    ///
138    /// Extensions might use this to place additional constraints on `Env` to require additional
139    /// registers, handlers (like [`SystemInstructionHandler`]) or other resources. If no such
140    /// extension is used, `()` can be used as a placeholder.
141    pub env: Env,
142    /// Memory
143    pub memory: Memory,
144    /// Instruction fetcher
145    pub instruction_fetcher: IF,
146}
147
148impl<Regs, Env, Memory, IF> BasicInterpreterState<Regs, Env, Memory, IF> {
149    /// Execute the program with a given basic interpreter state.
150    ///
151    /// The implementation is designed to be efficient with little left to optimize further. Though
152    /// it is still possible to improve performance by applying additional constraints on the
153    /// program.
154    // TODO: It might be impractical to support `no-panic` here directly in a general case, but it
155    //  should be possible to do so for small extensions to verify the workflow
156    //
157    // Make sure each handler starts on a cache line boundary
158    #[rustc_align(64)]
159    pub fn execute<I>(&mut self) -> Result<(), ExecutionError<Address<I>>>
160    where
161        Regs: RegisterFile<<I as Instruction>::Reg>,
162        I: ExecutableInstruction<Regs, Env, Memory, IF>,
163        Memory: VirtualMemory,
164        IF: InstructionFetcher<I, Memory> + ProgramCounter<Address<I>, Memory>,
165    {
166        replace_with_or_abort_and_return(
167            &mut self.instruction_fetcher,
168            #[inline(always)]
169            |mut instruction_fetcher| {
170                loop {
171                    let instruction = match instruction_fetcher.fetch_instruction(&self.memory) {
172                        FetchInstructionResult::Instruction(instruction) => instruction,
173                        FetchInstructionResult::Continue => {
174                            cold_path();
175                            continue;
176                        }
177                        FetchInstructionResult::Break => {
178                            cold_path();
179                            break;
180                        }
181                        FetchInstructionResult::Err(error) => {
182                            cold_path();
183                            return (Err(error), instruction_fetcher);
184                        }
185                    };
186
187                    let Rs1Rs2Operands { rs1, rs2 } = instruction.get_rs1_rs2_operands();
188                    let rs1rs2_values = Rs1Rs2OperandValues {
189                        rs1_value: self.regs.read(rs1),
190                        rs2_value: self.regs.read(rs2),
191                    };
192
193                    let outcome = instruction.execute(
194                        rs1rs2_values,
195                        &mut self.regs,
196                        &mut self.env,
197                        &mut self.memory,
198                        &mut instruction_fetcher,
199                    );
200
201                    let control_flow =
202                        match outcome {
203                            ExecutionResult::Continue { rd, value } => {
204                                self.regs.write(rd, value);
205                                continue;
206                            }
207                            ExecutionResult::ContinueNoWrite => {
208                                continue;
209                            }
210                            ExecutionResult::Branch { offset } => instruction_fetcher
211                                .set_pc_relative(&self.memory, instruction.size(), offset),
212                            ExecutionResult::Jump { target } => {
213                                instruction_fetcher.set_pc(&self.memory, target)
214                            }
215                            ExecutionResult::Break => {
216                                cold_path();
217                                break;
218                            }
219                            ExecutionResult::Err(error) => {
220                                cold_path();
221                                return (Err(error), instruction_fetcher);
222                            }
223                        };
224
225                    match control_flow {
226                        Ok(ControlFlow::Continue(())) => {}
227                        Ok(ControlFlow::Break(())) => {
228                            cold_path();
229                            break;
230                        }
231                        Err(error) => {
232                            cold_path();
233                            return (Err(error), instruction_fetcher);
234                        }
235                    }
236                }
237
238                (Ok(()), instruction_fetcher)
239            },
240        )
241    }
242}
243
244/// Basic memory implementation.
245///
246/// Flat structure, no rwx protections, no alignment requirements. It uses stack, so for larger
247/// allocation it'll need to be boxed (zero-initialized is fine) or a custom implementation to be
248/// used.
249///
250/// `BASE_ADDR + SIZE` must fit into `u64`, meaning the memory region can't wrap around or end at
251/// the very end of the address space, which is checked at compile time. This is what allows an
252/// address below `BASE_ADDR` to be recognized by the bounds check that follows the subtraction of
253/// the base address rather than by a check of its own.
254///
255/// This implementation is intentionally basic and correct, but not the most performant. It is
256/// possible to have a more efficient implementation that skips certain checks by placing additional
257/// constraints on the program.
258///
259/// This works for simpler cases, while a more sophisticated implementation might prevent certain
260/// memory from being writable, supporting actual virtual memory with dynamically allocated memory
261/// pages, etc.
262#[derive(Debug, Copy, Clone)]
263#[repr(align(16))]
264pub struct BasicMemory<const BASE_ADDR: u64, const SIZE: usize> {
265    data: [u8; SIZE],
266}
267
268const impl<const BASE_ADDR: u64, const SIZE: usize> VirtualMemory for BasicMemory<BASE_ADDR, SIZE> {
269    #[inline(always)]
270    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic(const))]
271    fn read<T>(&self, address: u64) -> Result<T, VirtualMemoryError>
272    where
273        T: BasicInt,
274    {
275        let offset = Self::offset(address);
276
277        if offset.saturating_add(size_of::<T>() as u64) > self.data.len() as u64 {
278            cold_path();
279            return Err(VirtualMemoryError::OutOfBoundsRead { address });
280        }
281
282        // SAFETY: Only reading basic integers from initialized memory
283        unsafe {
284            Ok(self
285                .data
286                .as_ptr()
287                .cast::<T>()
288                .byte_add(offset as usize)
289                .read_unaligned())
290        }
291    }
292
293    #[inline(always)]
294    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic(const))]
295    unsafe fn read_unchecked<T>(&self, address: u64) -> T
296    where
297        T: BasicInt,
298    {
299        // SAFETY: Guaranteed by function contract
300        unsafe {
301            let offset = address.unchecked_sub(BASE_ADDR) as usize;
302            self.data
303                .as_ptr()
304                .cast::<T>()
305                .byte_add(offset)
306                .read_unaligned()
307        }
308    }
309
310    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic(const))]
311    fn read_slice(&self, address: u64, len: u32) -> Result<&[u8], VirtualMemoryError> {
312        let offset = Self::offset(address);
313
314        if offset > self.data.len() as u64 {
315            cold_path();
316            return Err(VirtualMemoryError::OutOfBoundsRead { address });
317        }
318
319        self.data
320            .get(offset as usize..)
321            .and_then(const |data| data.get(..len as usize))
322            .ok_or(VirtualMemoryError::OutOfBoundsRead { address })
323    }
324
325    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic(const))]
326    fn read_slice_up_to(&self, address: u64, len: u32) -> &[u8] {
327        let offset = Self::offset(address);
328
329        if offset > self.data.len() as u64 {
330            cold_path();
331            return &[];
332        }
333
334        let remaining = self.data.get(offset as usize..).unwrap_or_default();
335        remaining.get(..len as usize).unwrap_or(remaining)
336    }
337
338    #[inline(always)]
339    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic(const))]
340    fn write<T>(&mut self, address: u64, value: T) -> Result<(), VirtualMemoryError>
341    where
342        T: BasicInt,
343    {
344        let offset = Self::offset(address);
345
346        if offset.saturating_add(size_of::<T>() as u64) > self.data.len() as u64 {
347            cold_path();
348            return Err(VirtualMemoryError::OutOfBoundsWrite { address });
349        }
350
351        // SAFETY: Only writing basic integers to initialized memory
352        unsafe {
353            self.data
354                .as_mut_ptr()
355                .cast::<T>()
356                .byte_add(offset as usize)
357                .write_unaligned(value);
358        }
359
360        Ok(())
361    }
362
363    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic(const))]
364    fn write_slice(&mut self, address: u64, data: &[u8]) -> Result<(), VirtualMemoryError> {
365        let offset = Self::offset(address);
366
367        if offset > self.data.len() as u64 {
368            cold_path();
369            return Err(VirtualMemoryError::OutOfBoundsWrite { address });
370        }
371
372        let len = data.len();
373        let Some(target_data) = self
374            .data
375            .get_mut(offset as usize..)
376            .and_then(const |data| data.get_mut(..len))
377        else {
378            cold_path();
379            return Err(VirtualMemoryError::OutOfBoundsWrite { address });
380        };
381
382        target_data.copy_from_slice(data);
383
384        Ok(())
385    }
386}
387
388impl<const BASE_ADDR: u64, const SIZE: usize> Default for BasicMemory<BASE_ADDR, SIZE> {
389    #[inline(always)]
390    fn default() -> Self {
391        Self { data: [0; _] }
392    }
393}
394
395impl<const BASE_ADDR: u64, const SIZE: usize> BasicMemory<BASE_ADDR, SIZE> {
396    /// Offset of `address` within this memory region.
397    ///
398    /// Subtraction wraps rather than being checked: an address below `BASE_ADDR` produces an offset
399    /// so large that the bounds check every caller does next rejects it, which is one comparison
400    /// instead of two on every memory access.
401    #[inline(always)]
402    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
403    const fn offset(address: u64) -> u64 {
404        const {
405            assert!(
406                BASE_ADDR.checked_add(SIZE as u64).is_some(),
407                "`BasicMemory` must not wrap around the end of the address space, or an address \
408                below its base address could wrap into it"
409            );
410        }
411
412        address.wrapping_sub(BASE_ADDR)
413    }
414
415    #[cfg(feature = "alloc")]
416    pub fn new_boxed() -> Box<Self> {
417        // SAFETY: Zeroed memory is a valid invariant
418        unsafe { Box::<Self>::new_zeroed().assume_init() }
419    }
420
421    /// Get a mutable slice of memory.
422    ///
423    /// This is primarily useful for setting up the program and should not be used beyond that.
424    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
425    pub const fn get_mut_bytes(
426        &mut self,
427        address: u64,
428        size: usize,
429    ) -> Result<&mut [u8], VirtualMemoryError> {
430        let Some(offset) = address.checked_sub(BASE_ADDR) else {
431            cold_path();
432            return Err(VirtualMemoryError::OutOfBoundsRead { address });
433        };
434        let offset = offset as usize;
435
436        let Some(slice) = self
437            .data
438            .get_mut(offset..)
439            .and_then(const |data| data.get_mut(..size))
440        else {
441            cold_path();
442            return Err(VirtualMemoryError::OutOfBoundsRead { address });
443        };
444
445        Ok(slice)
446    }
447}
448
449/// Basic instruction fetcher implementation.
450///
451/// This implementation is intentionally basic and correct, but not the most performant. It is
452/// possible to have a more efficient implementation that skips certain checks by placing additional
453/// constraints on the constructor.
454///
455/// Note that it loads instructions from anywhere in memory. This works, but it is likely that you
456/// want to restrict this to a specific executable region of memory.
457#[derive(Debug, Copy, Clone)]
458pub struct BasicInstructionFetcher<I>
459where
460    I: Instruction,
461{
462    return_trap_address: Address<I>,
463    pc: Address<I>,
464}
465
466const impl<I, Memory> ProgramCounter<Address<I>, Memory> for BasicInstructionFetcher<I>
467where
468    I: [const] Instruction,
469    Memory: [const] VirtualMemory,
470{
471    #[inline(always)]
472    fn get_pc(&self) -> Address<I> {
473        self.pc
474    }
475
476    #[inline(always)]
477    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic(const))]
478    unsafe fn try_set_pc_relative(&mut self, instruction_size: u8, offset: i32) -> bool {
479        let old_pc = <Self as ProgramCounter<_, Memory>>::old_pc(self, instruction_size);
480        let pc = old_pc.wrapping_add_signed(offset);
481        // Stored either way: on the way out it is what `failed_branch()` reports on, and until then
482        // nothing else is allowed to look at it
483        self.pc = pc;
484
485        pc != self.return_trap_address && pc.as_u64().is_multiple_of(u64::from(I::ALIGNMENT))
486    }
487
488    #[cold]
489    #[inline(never)]
490    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic(const))]
491    unsafe fn failed_branch(
492        &mut self,
493        memory: &Memory,
494    ) -> Result<ControlFlow<()>, ExecutionError<Address<I>>> {
495        // The program counter holds the refused target, and `set_pc()` is what says what is wrong
496        // with it
497        self.set_pc(memory, self.pc)
498    }
499
500    #[inline]
501    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic(const))]
502    fn set_pc(
503        &mut self,
504        _memory: &Memory,
505        pc: Address<I>,
506    ) -> Result<ControlFlow<()>, ExecutionError<Address<I>>> {
507        if pc == self.return_trap_address {
508            cold_path();
509            return Ok(ControlFlow::Break(()));
510        }
511
512        if !pc.as_u64().is_multiple_of(u64::from(I::ALIGNMENT)) {
513            cold_path();
514            return Err(ExecutionError::UnalignedInstruction {
515                address: PackedAddress::new(pc),
516            });
517        }
518
519        self.pc = pc;
520
521        Ok(ControlFlow::Continue(()))
522    }
523}
524
525const impl<I, Memory> InstructionFetcher<I, Memory> for BasicInstructionFetcher<I>
526where
527    I: [const] Instruction,
528    Memory: [const] VirtualMemory,
529{
530    type Peeked = I;
531
532    #[inline(always)]
533    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic(const))]
534    fn peeked_instruction<'a>(&'a self, peeked: &'a I) -> &'a I {
535        peeked
536    }
537
538    #[inline]
539    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic(const))]
540    fn peek_instruction(&mut self, memory: &Memory) -> FetchInstructionResult<I> {
541        let instruction = match memory.read(self.pc.as_u64()).or_else(const |error| {
542            cold_path();
543            // Attempt to read a 16-bit compressed instruction
544            if let Ok(instruction) = memory.read::<u16>(self.pc.as_u64())
545                && (instruction & 0b11) != 0b11
546            {
547                return Ok(u32::from(instruction));
548            }
549            Err(error)
550        }) {
551            Ok(instruction) => instruction,
552            Err(error) => {
553                cold_path();
554                return FetchInstructionResult::Err(ExecutionError::from(error));
555            }
556        };
557
558        let Some(instruction) = I::try_decode(instruction) else {
559            cold_path();
560            return FetchInstructionResult::Err(ExecutionError::IllegalInstruction {
561                address: PackedAddress::new(self.pc),
562            });
563        };
564        FetchInstructionResult::Instruction(instruction)
565    }
566
567    #[inline(always)]
568    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic(const))]
569    unsafe fn advance(&mut self, instruction_size: u8) {
570        self.pc = self.pc.wrapping_add_signed(i32::from(instruction_size));
571    }
572
573    #[inline]
574    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic(const))]
575    fn fetch_instruction(&mut self, memory: &Memory) -> FetchInstructionResult<I> {
576        let result = InstructionFetcher::<I, Memory>::peek_instruction(self, memory);
577
578        if let FetchInstructionResult::Instruction(instruction) = result {
579            // SAFETY: The instruction was just peeked successfully, and this is the only place
580            // that moves past it
581            unsafe {
582                InstructionFetcher::<I, Memory>::advance(self, instruction.size());
583            }
584        }
585
586        result
587    }
588}
589
590impl<I> BasicInstructionFetcher<I>
591where
592    I: Instruction,
593{
594    /// Create a new instance.
595    ///
596    /// `return_trap_address` is the address at which the interpreter will stop execution
597    /// (gracefully).
598    #[inline(always)]
599    pub const fn new(return_trap_address: Address<I>, pc: Address<I>) -> Self {
600        Self {
601            return_trap_address,
602            pc,
603        }
604    }
605}
606
607/// System instruction handler that results in illegal instruction for all system calls and does
608/// nothing for other system instructions.
609///
610/// Being stateless, it can be used as the whole execution environment of a configuration that
611/// needs nothing else from it.
612#[derive(Debug, Default, Clone, Copy)]
613pub struct IllegalEcallSystemInstructionHandler;
614
615const impl<Reg, Regs, Memory, PC> SystemInstructionHandler<Reg, Regs, Memory, PC>
616    for IllegalEcallSystemInstructionHandler
617where
618    Reg: [const] Register,
619    PC: [const] ProgramCounter<Reg::Type, Memory>,
620{
621    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic(const))]
622    fn handle_ecall(
623        &mut self,
624        _regs: &mut Regs,
625        _memory: &mut Memory,
626        program_counter: &mut PC,
627    ) -> Result<ControlFlow<()>, ExecutionError<Reg::Type>> {
628        Err(ExecutionError::IllegalInstruction {
629            address: PackedAddress::new(program_counter.old_pc(size_of::<u32>() as u8)),
630        })
631    }
632}
633
634const impl WrsHandler for IllegalEcallSystemInstructionHandler {}
635
636const impl FenceIHandler for IllegalEcallSystemInstructionHandler {}