Skip to main content

ab_riscv_interpreter/v/zvexx/arith/
zvexx_arith_helpers.rs

1//! Opaque helpers for ZveXx extension
2
3use crate::v::vector_config::BoundedVl;
4use crate::v::vector_registers::{VRegGroup, VectorRegisterFile, VectorRegistersExt};
5use crate::v::zvexx::load::zvexx_load_helpers::{mask_bit, snapshot_mask};
6use crate::v::zvexx::zvexx_helpers::INSTRUCTION_SIZE;
7use crate::{ExecutionError, PackedAddress, ProgramCounter};
8use ab_riscv_primitives::prelude::*;
9use core::hint::cold_path;
10
11/// Effective element width of a register operand at `SEW`
12const SEW_EEW<const SEW: Vsew>: Eew = SEW.as_eew();
13
14/// Check mask-destination / source overlap constraint for compare/carry instructions.
15///
16/// Per RVV §5.2's narrowing-destination overlap rule (a mask destination has EEW=1, narrower
17/// than any source EEW), `vd` may overlap a multi-register source group only in the
18/// lowest-numbered register of that group. Overlapping any other register in the group is
19/// reserved: `execute_compare_op`/`execute_carry_*_mask` process elements in increasing index
20/// order and write one mask bit per element into `vd`. When `vd` is the group's base register,
21/// every mask byte written during the processing of register `base_reg` targets bytes that hold
22/// data from elements at or before the one just read (byte `b` can only be touched while
23/// processing elements `>= 8*b`, and it stores raw data for element `b / sew_bytes <= 8*b`, which
24/// is always read first). When `vd` is any later register in the group, that guarantee no longer
25/// holds: early elements from the group's *first* register write mask bits into byte 0 of `vd`,
26/// which is where the not-yet-read data of a later element (stored in `vd` itself) lives,
27/// corrupting it before it is read.
28#[inline(always)]
29#[doc(hidden)]
30#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
31pub fn check_mask_dest_overlap<Reg, Env, Memory, PC>(
32    program_counter: &PC,
33    vd: VReg,
34    src: VRegGroup<Env::Hart>,
35) -> Result<(), ExecutionError<Reg::Type>>
36where
37    Reg: Register,
38    Env: VectorRegistersExt<Hart: HartConfig<Reg = Reg>>,
39    PC: ProgramCounter<Reg::Type, Memory>,
40{
41    if src.contains(vd) && vd != src.base() {
42        cold_path();
43        return Err(ExecutionError::IllegalInstruction {
44            address: PackedAddress::new(program_counter.old_pc(INSTRUCTION_SIZE)),
45        });
46    }
47    Ok(())
48}
49
50/// Write one mask bit (the comparison result for element `elem_i`) into register `vd`.
51///
52/// Bits are stored LSB-first: element `i` lives at byte `i / 8`, bit `i % 8`.
53/// Only the target bit is modified; all other bits are undisturbed (tail-undisturbed semantics
54/// required for mask destinations per spec §5.3).
55///
56/// Returns `None` if `elem_i` is not below `VLEN`, which never happens for `elem_i < vl`.
57#[inline(always)]
58#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
59pub(in super::super) fn write_mask_bit<Hart>(
60    vregs: &mut VectorRegisterFile<Hart>,
61    vd: VReg,
62    elem_i: u16,
63    result: bool,
64) -> Option<()>
65where
66    Hart: VectorHartConfig,
67{
68    let byte_idx = usize::from(elem_i / u8::BITS as u16);
69    let bit_idx = elem_i % u8::BITS as u16;
70    let byte = vregs.get_mut(vd).get_mut(byte_idx)?;
71    if result {
72        *byte |= 1 << bit_idx;
73    } else {
74        *byte &= !(1 << bit_idx);
75    }
76    Some(())
77}
78
79/// Operand source
80#[derive(Debug)]
81#[doc(hidden)]
82pub enum OpSrc<V> {
83    /// Vector-vector: source register group
84    Vreg(V),
85    /// Vector-scalar: scalar value (sign- or zero-extended to u64)
86    Scalar(u64),
87}
88
89impl<Hart> OpSrc<VRegGroup<Hart>>
90where
91    Hart: VectorHartConfig,
92{
93    /// The same source if a vector register group source has `vl` of `vl`, `None` otherwise, see
94    /// [`VRegGroup::with_same_vl()`]
95    #[inline(always)]
96    #[doc(hidden)]
97    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
98    pub fn with_same_vl(self, vl: BoundedVl<Hart>) -> Option<SameVlOpSrc<Hart>> {
99        match self {
100            Self::Vreg(group) => Some(SameVlOpSrc {
101                vreg: Some(group.with_same_vl(vl)?),
102                scalar: 0,
103            }),
104            Self::Scalar(scalar) => Some(SameVlOpSrc { vreg: None, scalar }),
105        }
106    }
107}
108
109/// [`OpSrc`] with a vector register group source of a known `vl`, see [`OpSrc::with_same_vl()`].
110///
111/// Unlike an enum, the group doesn't share storage with the scalar, which would otherwise hide
112/// from the optimizer that the group's `vl` is the one it was rebound to.
113#[derive(Debug, Clone, Copy)]
114#[doc(hidden)]
115pub struct SameVlOpSrc<Hart>
116where
117    Hart: VectorHartConfig,
118{
119    /// Vector-vector: source register group
120    pub vreg: Option<VRegGroup<Hart>>,
121    /// Vector-scalar: scalar value (sign- or zero-extended to u64), used when `vreg` is `None`
122    pub scalar: u64,
123}
124
125/// Execute a single-width element-wise arithmetic operation over `0..vl`.
126///
127/// `op` receives `(vs2_elem: u64, src_elem: u64, sew: Vsew)` and returns the `u64` result (only the
128/// low `sew.bits_width()` are written back).
129///
130/// All register groups must have element width `sew` and the same `vl`, which holds for groups
131/// created from the same configuration, nothing is written otherwise.
132#[inline(always)]
133#[doc(hidden)]
134#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
135pub fn execute_arith_op<Reg, Env, F>(
136    env: &mut Env,
137    vd: VRegGroup<Env::Hart>,
138    vs2: VRegGroup<Env::Hart>,
139    src: OpSrc<VRegGroup<Env::Hart>>,
140    vm: bool,
141    sew: Vsew,
142    op: F,
143) where
144    Reg: Register,
145    Env: VectorRegistersExt<Hart: HartConfig<Reg = Reg>>,
146    F: Fn(u64, u64, Vsew) -> u64,
147{
148    // Dispatch on the element width once, so that the loop below is compiled for each width
149    // separately, with element loads and stores of a constant size
150    match sew {
151        Vsew::E8 => {
152            execute_arith_op_const::<{ Vsew::E8 }, _, _, _>(env, vd, vs2, src, vm, op);
153        }
154        Vsew::E16 => {
155            execute_arith_op_const::<{ Vsew::E16 }, _, _, _>(env, vd, vs2, src, vm, op);
156        }
157        Vsew::E32 => {
158            execute_arith_op_const::<{ Vsew::E32 }, _, _, _>(env, vd, vs2, src, vm, op);
159        }
160        Vsew::E64 => {
161            execute_arith_op_const::<{ Vsew::E64 }, _, _, _>(env, vd, vs2, src, vm, op);
162        }
163    }
164}
165
166/// [`execute_arith_op()`] with the element width known at compile time
167#[inline(always)]
168#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
169fn execute_arith_op_const<const SEW: Vsew, Reg, Env, F>(
170    env: &mut Env,
171    vd: VRegGroup<Env::Hart>,
172    vs2: VRegGroup<Env::Hart>,
173    src: OpSrc<VRegGroup<Env::Hart>>,
174    vm: bool,
175    op: F,
176) where
177    Reg: Register,
178    Env: VectorRegistersExt<Hart: HartConfig<Reg = Reg>>,
179    F: Fn(u64, u64, Vsew) -> u64,
180{
181    let vl = vd.vl();
182    let src_eew_matches = match src {
183        OpSrc::Vreg(vs1) => vs1.eew() == SEW_EEW::<SEW>,
184        OpSrc::Scalar(_) => true,
185    };
186    if vd.eew() != SEW_EEW::<SEW> || vs2.eew() != SEW_EEW::<SEW> || !src_eew_matches {
187        cold_path();
188        return;
189    }
190    let (Some(vs2), Some(src)) = (vs2.with_same_vl(vl), src.with_same_vl(vl)) else {
191        cold_path();
192        return;
193    };
194
195    let vregs = env.write_vregs();
196
197    for i in vl.indices() {
198        // The decoder rejects masked `vd == v0`, so the mask can be read in place rather than
199        // snapshotted, no write below can modify it
200        if !vm && !mask_bit(vregs.get(VReg::V0), i) {
201            continue;
202        }
203
204        let a = vregs
205            .read_const::<{ SEW_EEW::<SEW> }>(vs2, i)
206            .expect("`vs2` has element width `SEW` and `vl` checked above; qed");
207
208        let b = match src.vreg {
209            Some(vs1) => vregs
210                .read_const::<{ SEW_EEW::<SEW> }>(vs1, i)
211                .expect("`vs1` has element width `SEW` and `vl` checked above; qed"),
212            None => src.scalar,
213        };
214
215        let result = op(a, b, SEW);
216
217        vregs
218            .write_const::<{ SEW_EEW::<SEW> }>(vd, i, result)
219            .expect("`vd` has element width `SEW` and `vl` checked above; qed");
220    }
221
222    env.mark_vs_dirty();
223}
224
225/// Execute a single-width element-wise integer compare over `0..vl`, writing one result
226/// bit per element into the mask register `vd`.
227///
228/// `op` receives `(vs2_elem: u64, src_elem: u64, sew: Vsew) -> bool`.
229///
230/// Mask destination tail bits (indices `>= vl`) are always left undisturbed per spec §5.3,
231/// regardless of `vta`. Only bits in `0..vl` are written.
232///
233/// Source register groups must have the same `vl`, which holds for groups created from the same
234/// configuration, nothing is written otherwise.
235#[inline(always)]
236#[doc(hidden)]
237#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
238pub fn execute_compare_op<Reg, Env, F>(
239    env: &mut Env,
240    vd: VReg,
241    vs2: VRegGroup<Env::Hart>,
242    src: OpSrc<VRegGroup<Env::Hart>>,
243    vm: bool,
244    sew: Vsew,
245    op: F,
246) where
247    Reg: Register,
248    Env: VectorRegistersExt<Hart: HartConfig<Reg = Reg>>,
249    F: Fn(u64, u64, Vsew) -> bool,
250{
251    let vl = vs2.vl();
252    let Some(src) = src.with_same_vl(vl) else {
253        cold_path();
254        return;
255    };
256
257    let mask_buf = snapshot_mask(env.read_vregs(), vm);
258
259    for i in vl.indices() {
260        // When masked, inactive elements in the destination mask register are left undisturbed
261        // (spec §12.8: "mask register results follow mask-undisturbed policy")
262        if !mask_bit(&mask_buf, i) {
263            continue;
264        }
265
266        let a = env
267            .read_vregs()
268            .read(vs2, i)
269            .expect("`i < vl` of `vs2`; qed");
270
271        let b = match src.vreg {
272            Some(vs1) => env
273                .read_vregs()
274                .read(vs1, i)
275                .expect("`vs1` has the same `vl` as `vs2`, checked above; qed"),
276            None => src.scalar,
277        };
278
279        let result = op(a, b, sew);
280
281        write_mask_bit(env.write_vregs(), vd, i, result)
282            .expect("`i < vl <= VLEN`, so the mask bit is within `vd`; qed");
283    }
284
285    env.mark_vs_dirty();
286}
287
288/// Sign-extend the low `sew.bits_width()` of `val` to a full `i64`
289#[inline(always)]
290#[doc(hidden)]
291#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
292pub fn sign_extend(val: u64, sew: Vsew) -> i64 {
293    let shift = u64::BITS - u32::from(sew.bits_width());
294    (val.cast_signed() << shift) >> shift
295}
296
297/// Mask off the upper bits of a `u64` to leave only the low `sew.bits_width()`.
298///
299/// Used for unsigned arithmetic and comparisons where only the SEW-wide portion is significant. For
300/// SEW = 64 this is a no-op (all bits are significant).
301#[inline(always)]
302#[doc(hidden)]
303#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
304pub fn sew_mask(sew: Vsew) -> u64 {
305    if u32::from(sew.bits_width()) == u64::BITS {
306        u64::MAX
307    } else {
308        (1u64 << sew.bits_width()) - 1
309    }
310}