Skip to main content

ab_riscv_interpreter/v/zvexx/load/
zvexx_load_helpers.rs

1//! Opaque helpers for ZveXx extension
2
3use crate::v::vector_registers::{VLENB_USIZE, VectorRegisterFile, VectorRegistersExt};
4use crate::v::zvexx::zvexx_helpers::INSTRUCTION_SIZE;
5use crate::{ExecutionError, ProgramCounter, VirtualMemory, VirtualMemoryError};
6use ab_riscv_primitives::prelude::*;
7use core::cmp::Ordering;
8use core::fmt;
9use core::hint::cold_path;
10use core::num::NonZeroU8;
11
12/// Return whether mask bit `i` is set in the mask byte slice.
13///
14/// Bits are stored LSB-first within each byte: bit `i` is at byte `i / 8`, position `i % 8`.
15/// Returns `false` for any `i` outside the slice bounds.
16#[inline(always)]
17#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
18pub(crate) fn mask_bit(mask: &[u8], i: u16) -> bool {
19    mask.get(usize::from(i / u8::BITS as u16))
20        .is_some_and(|b| (b >> (i % u8::BITS as u16)) & 1 != 0)
21}
22
23/// Copy the mask bytes needed to cover `vl` elements from `v0` into a stack buffer and return
24/// it. The copy releases the shared borrow on the register file so the caller can immediately
25/// take an exclusive borrow for writes.
26///
27/// When `vm=true` (unmasked), the buffer is filled with `0xff` so that every mask bit reads as `1`.
28/// This means callers can unconditionally call [`mask_bit()`] on the returned buffer without
29/// branching on `vm`. Current callers short-circuit with `!vm &&` before calling [`mask_bit()`] as
30/// a micro-optimization on the common unmasked path, but correctness does not depend on that guard:
31/// if it were removed, the `0xff` fill ensures [`mask_bit()`] would return `true` for every
32/// element, preserving the unmasked semantics.
33///
34/// # Safety
35/// `vl` must be `<= VLEN`, which is always true when `vl` is the current architectural `vl`
36/// (bounded by `VLMAX <= VLEN`).
37#[inline(always)]
38#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
39pub(in super::super) unsafe fn snapshot_mask<const VLEN: Vlen>(
40    vregs: &VectorRegisterFile<VLEN>,
41    vm: bool,
42    vl: Vl,
43) -> [u8; VLENB_USIZE::<VLEN>] {
44    let mut buf = [0u8; _];
45    if vm {
46        // All-ones: every element active
47        buf = [0xffu8; _];
48    } else {
49        let mask_bytes = usize::from(vl.bytes());
50        // SAFETY: `mask_bytes <= VLEN.bytes()` by the caller's precondition
51        unsafe {
52            buf.get_unchecked_mut(..mask_bytes)
53                .copy_from_slice(vregs.get(VReg::V0).get_unchecked(..mask_bytes));
54        }
55    }
56    buf
57}
58
59/// Return whether register groups `[a, a+a_regs)` and `[b, b+b_regs)` overlap.
60#[inline(always)]
61#[doc(hidden)]
62#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
63pub fn groups_overlap(a: VReg, a_regs: NonZeroU8, b: VReg, b_regs: NonZeroU8) -> bool {
64    let (a, b) = (a.to_bits(), b.to_bits());
65    a < b + b_regs.get() && b < a + a_regs.get()
66}
67
68/// Return whether a *non-segment* indexed load's data destination group
69/// `[vd, vd + data_regs)` may legally overlap its index source group `[vs2, vs2 + index_regs)`.
70///
71/// The data EEW equals `sew` (indexed loads take their data width from `vtype.vsew()`) with
72/// `EMUL = LMUL`, whereas the index group has EEW `index_eew` and `EMUL = (index_eew / sew) *
73/// LMUL`. Because the two groups can have different EEW, the general vector register overlap
74/// constraint applies: a destination group may overlap a source group only when one of the
75/// following holds:
76///
77/// - the EEWs are equal (the groups coincide); or
78/// - the destination EEW is smaller and the overlap is in the lowest-numbered part of the source
79///   group, i.e. the destination starts at the source's base register (`vd == vs2`); or
80/// - the destination EEW is larger, the source EMUL is at least one register, and the overlap is in
81///   the highest-numbered part of the destination group, i.e. both groups end at the same register
82///   (`vd + data_regs == vs2 + index_regs`).
83///
84/// Groups that do not overlap at all are always permitted. Any other overlap is reserved.
85///
86/// Unlike indexed *segment* loads (which forbid any `vd`/`vs2` overlap to remain restartable),
87/// these relaxed rules are what allow encodings such as `vluxei32.v v16, (s2), v16` when the data
88/// and index EEW match.
89#[inline(always)]
90#[doc(hidden)]
91#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
92pub fn indexed_load_overlap_allowed(
93    vd: VReg,
94    data_regs: NonZeroU8,
95    vs2: VReg,
96    index_regs: NonZeroU8,
97    index_eew: Eew,
98    sew: Vsew,
99    vlmul: Vlmul,
100) -> bool {
101    if !groups_overlap(vd, data_regs, vs2, index_regs) {
102        return true;
103    }
104
105    match sew.bytes_width().cmp(&index_eew.bytes_width()) {
106        // Equal EEW: the two groups coincide, overlap is permitted.
107        Ordering::Equal => true,
108        // Smaller data EEW: overlap must be in the lowest-numbered part of the index group, which
109        // (given both groups are alignment-checked) means the data group starts at the index base.
110        Ordering::Less => vd == vs2,
111        // Larger data EEW: overlap must be in the highest-numbered part of the data group, and the
112        // index EMUL must be at least one full register. `index_regs` alone cannot distinguish a
113        // whole-register EMUL from a fractional one clamped to a single register, so the EMUL is
114        // recomputed here as `(index_eew / sew) * LMUL >= 1`.
115        Ordering::Greater => {
116            let (lmul_num, lmul_den) = vlmul.as_fraction();
117            let index_emul_at_least_one = u16::from(index_eew.bits_width())
118                * u16::from(lmul_num.get())
119                >= u16::from(sew.bits_width()) * u16::from(lmul_den.get());
120            let (vd, vs2) = (vd.to_bits(), vs2.to_bits());
121            index_emul_at_least_one && vd + data_regs.get() == vs2 + index_regs.get()
122        }
123    }
124}
125
126/// Check that `vd` is aligned to `group_regs` and that the group fits within `[0, 32)`.
127///
128/// Per spec, the base register of every register group must be a multiple of the group size.
129#[inline(always)]
130#[doc(hidden)]
131#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
132pub fn check_register_group_alignment<Reg, Memory, PC, CustomError>(
133    program_counter: &PC,
134    vd: VReg,
135    group_regs: NonZeroU8,
136) -> Result<(), ExecutionError<Reg::Type, CustomError>>
137where
138    Reg: Register,
139    PC: ProgramCounter<Reg::Type, Memory, CustomError>,
140{
141    let group_regs = group_regs.get();
142    let vd = vd.to_bits();
143    if !vd.is_multiple_of(group_regs) || vd + group_regs > 32 {
144        cold_path();
145        return Err(ExecutionError::IllegalInstruction {
146            address: program_counter.old_pc(INSTRUCTION_SIZE),
147        });
148    }
149    Ok(())
150}
151
152/// Validate segment register layout: all `nf` field groups fit within `[0, 32)`, the base
153/// register is group-aligned, and the first field group does not include `v0` when masked.
154///
155/// Field `f` occupies registers `[vd + f * group_regs, vd + f * group_regs + group_regs)`.
156/// On `Ok`, `vd.to_bits() + nf * group_regs <= 32` is guaranteed.
157#[inline(always)]
158#[doc(hidden)]
159#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
160pub fn validate_segment_registers<Reg, Memory, PC, CustomError>(
161    program_counter: &PC,
162    vd: VReg,
163    vm: bool,
164    group_regs: NonZeroU8,
165    nf: Nf,
166) -> Result<(), ExecutionError<Reg::Type, CustomError>>
167where
168    Reg: Register,
169    PC: ProgramCounter<Reg::Type, Memory, CustomError>,
170{
171    let group_regs = u32::from(group_regs.get());
172    let nf = u32::from(nf.fields_per_segment());
173    let vd_idx = u32::from(vd.to_bits());
174    if vd_idx % group_regs != 0 || vd_idx + nf * group_regs > 32 {
175        cold_path();
176        return Err(ExecutionError::IllegalInstruction {
177            address: program_counter.old_pc(INSTRUCTION_SIZE),
178        });
179    }
180    // When masked, no field group may contain v0 (index 0). Since groups are laid out
181    // contiguously from vd and vd is group-aligned, only the first field (f=0) could contain
182    // v0, which happens exactly when vd == 0.
183    if !vm && vd_idx == 0 {
184        cold_path();
185        return Err(ExecutionError::IllegalInstruction {
186            address: program_counter.old_pc(INSTRUCTION_SIZE),
187        });
188    }
189    Ok(())
190}
191
192/// Read element `elem_i` from register group `[base_reg, base_reg + group_regs)` into a
193/// `[u8; Eew::MAX_BYTES]` buffer.
194///
195/// The in-register position of element `elem_i` is:
196///   - register `base_reg + elem_i / (VLEN.bytes() / eew.bytes())`
197///   - byte offset `(elem_i % (VLEN.bytes() / eew.bytes())) * eew.bytes()`
198///
199/// The result is placed in `buf[..eew.bytes()]`; the remaining bytes are zero.
200///
201/// # Safety
202/// `base_reg + elem_i / (VLEN.bytes() / eew.bytes())` must be less than 32, i.e. `elem_i` must be
203/// a valid element index within the register group.
204#[inline(always)]
205#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
206pub(in super::super) unsafe fn read_group_element<const VLEN: Vlen>(
207    vregs: &VectorRegisterFile<VLEN>,
208    base_reg: VReg,
209    elem_i: u16,
210    eew: Eew,
211) -> [u8; const { usize::from(Eew::MAX_BYTES) }] {
212    let elem_bytes = u32::from(eew.bytes_width());
213    let elems_per_reg = VLEN.bytes() / elem_bytes;
214    let reg_off = u32::from(elem_i) / elems_per_reg;
215    let byte_off = (u32::from(elem_i) % elems_per_reg) * elem_bytes;
216    // SAFETY: `base_reg + reg_off < 32` by the caller's precondition
217    let reg = unsafe {
218        vregs.get(VReg::from_bits(base_reg.to_bits() + reg_off as u8).unwrap_unchecked())
219    };
220    // SAFETY: `byte_off + elem_bytes <= VLEN.bytes()`: the maximum `byte_off` is
221    // `(elems_per_reg - 1) * elem_bytes = VLEN.bytes() - elem_bytes`, so
222    // `byte_off + elem_bytes <= VLEN.bytes() - elem_bytes + elem_bytes = VLEN.bytes()`.
223    // `elem_bytes <= Eew::MAX_BYTES`: all `Eew` variants are at most E64.
224    let src = unsafe { reg.get_unchecked(byte_off as usize..(byte_off + elem_bytes) as usize) };
225    let mut buf = [0; _];
226    // SAFETY: `elem_bytes <= Eew::MAX_BYTES` as established above, so `..elem_bytes` is in bounds
227    // for `buf`
228    unsafe { buf.get_unchecked_mut(..elem_bytes as usize) }.copy_from_slice(src);
229    buf
230}
231
232/// Write `eew`-sized data from `buf[..eew.bytes()]` into element `elem_i` of register group
233/// `[base_reg, base_reg + group_regs)`.
234///
235/// The in-register position follows the same layout as [`read_group_element`].
236///
237/// # Safety
238/// `base_reg + elem_i / (VLEN.bytes() / eew.bytes())` must be less than 32, i.e. `elem_i` must be
239/// a valid element index within the register group.
240#[inline(always)]
241#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
242unsafe fn write_group_element<const VLEN: Vlen>(
243    vregs: &mut VectorRegisterFile<VLEN>,
244    base_reg: VReg,
245    elem_i: u16,
246    eew: Eew,
247    buf: [u8; const { usize::from(Eew::MAX_BYTES) }],
248) {
249    let elem_bytes = u32::from(eew.bytes_width());
250    let elems_per_reg = VLEN.bytes() / elem_bytes;
251    let reg_off = u32::from(elem_i) / elems_per_reg;
252    let byte_off = (u32::from(elem_i) % elems_per_reg) * elem_bytes;
253    // SAFETY: `base_reg + reg_off < 32` by the caller's precondition
254    let reg = unsafe {
255        vregs.get_mut(VReg::from_bits(base_reg.to_bits() + reg_off as u8).unwrap_unchecked())
256    };
257    // SAFETY: `byte_off + elem_bytes <= VLEN.bytes()` and `elem_bytes <= Eew::MAX_BYTES`: same
258    // argument as in `read_group_element`
259    let dst = unsafe { reg.get_unchecked_mut(byte_off as usize..(byte_off + elem_bytes) as usize) };
260    // SAFETY: `elem_bytes <= Eew::MAX_BYTES` as established above, so `..elem_bytes` is in bounds
261    // for `buf`
262    dst.copy_from_slice(unsafe { buf.get_unchecked(..elem_bytes as usize) });
263}
264
265/// Read `eew`-sized data from memory at `addr` into a `[u8; Eew::MAX_BYTES]` buffer
266/// (little-endian)
267#[inline(always)]
268#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
269fn read_mem_element(
270    memory: &impl VirtualMemory,
271    addr: u64,
272    eew: Eew,
273) -> Result<[u8; const { usize::from(Eew::MAX_BYTES) }], VirtualMemoryError> {
274    let source = match memory.read_slice(addr, u32::from(eew.bytes_width())) {
275        Ok(source) => source,
276        Err(err) => {
277            cold_path();
278            return Err(err);
279        }
280    };
281    let mut out = [0; _];
282    out[..usize::from(eew.bytes_width())].copy_from_slice(source);
283    Ok(out)
284}
285
286/// Execute a unit-stride or unit-stride segment load (including fault-only-first variants).
287///
288/// Segment stride between elements is `nf * eew.bytes()`. Field `f` for element `i` is at
289/// `base + i * nf * eew.bytes() + f * eew.bytes()`. When `nf == 1` this degenerates to a
290/// plain unit-stride load.
291///
292/// When `fault_only_first` is set: a memory error at element `i > 0` truncates `vl` to `i`
293/// and returns `Ok`. An error at element `0` always propagates.
294///
295/// # Safety
296/// - `vd.to_bits() % group_regs == 0`
297/// - `vd.to_bits() + nf * group_regs <= 32`
298/// - `vl <= group_regs * VLEN.bytes() / eew.bytes()` (all `vl` elements fit within the destination
299///   register group; this holds when `vl` is the architectural `vl` and `group_regs` is the EMUL
300///   register count for the given `eew` and `vtype`)
301/// - When `vm=false`: `vd` does not overlap `v0` (i.e. `vd.to_bits() != 0`)
302#[inline(always)]
303#[expect(clippy::too_many_arguments, reason = "Internal API")]
304#[doc(hidden)]
305// TODO: #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
306pub unsafe fn execute_unit_stride_load<
307    const FAULT_ONLY_FIRST: bool,
308    Reg,
309    ExtState,
310    Memory,
311    CustomError,
312>(
313    ext_state: &mut ExtState,
314    memory: &Memory,
315    vd: VReg,
316    vm: bool,
317    base: u64,
318    eew: Eew,
319    group_regs: NonZeroU8,
320    nf: Nf,
321) -> Result<(), ExecutionError<Reg::Type, CustomError>>
322where
323    Reg: Register,
324    ExtState: VectorRegistersExt<Reg, CustomError>,
325    [(); SUPPORTED_ELEN_VLEN::<{ ExtState::ELEN }, { ExtState::VLEN }>]:,
326    Memory: VirtualMemory,
327    CustomError: fmt::Debug,
328{
329    let group_regs = group_regs.get();
330    let vl = ext_state.vl();
331    let vstart = ext_state.vstart();
332    let elem_bytes = eew.bytes_width();
333    let segment_stride = u64::from(nf.fields_per_segment()) * u64::from(elem_bytes);
334
335    // SAFETY: `vl <= VLMAX <= VLEN`
336    let mask_buf = unsafe { snapshot_mask(ext_state.read_vregs(), vm, vl) };
337
338    for i in vstart.range_to(vl) {
339        if !vm && !mask_bit(&mask_buf, i) {
340            continue;
341        }
342
343        let elem_base = base.wrapping_add(u64::from(i) * segment_stride);
344
345        // Read all nf fields into a stack buffer before writing any of them.
346        // This ensures a fault on field f>0 leaves the destination registers untouched for the
347        // faulting element, so only elements with index new_vl are ever written (fault-only-first
348        // semantics).
349        //
350        // Sized by `Nf::MAX * Eew::MAX_BYTES`: the V spec allows at most 8 fields (nf in 1..=8)
351        // each is at most 8 bytes (E64), giving 64 bytes.
352        let mut field_buf = [[0u8; const { usize::from(Eew::MAX_BYTES) }]; const {
353            usize::from(Nf::MAX.fields_per_segment())
354        }];
355
356        for f in 0..nf.fields_per_segment() {
357            let addr = elem_base.wrapping_add(u64::from(f * elem_bytes));
358            match read_mem_element(memory, addr, eew) {
359                Ok(data) => {
360                    // SAFETY: `f < nf` and the precondition on this function requires
361                    // `nf <= Nf::MAX` (the V spec encodes nf in 3 bits giving 1..=Nf::MAX, and the
362                    // decoder enforces this before constructing the instruction). Therefore, `f as
363                    // usize < nf as usize <= Nf::MAX`, which is exactly the length of `field_buf`.
364                    unsafe {
365                        *field_buf.get_unchecked_mut(f as usize) = data;
366                    }
367                }
368                Err(mem_err) => {
369                    cold_path();
370                    if FAULT_ONLY_FIRST && i > 0 {
371                        ext_state.set_vl(Vl::from(i));
372                        ext_state.mark_vs_dirty();
373                        ext_state.reset_vstart();
374                        return Ok(());
375                    }
376                    if i > u16::from(vstart) {
377                        // Elements [vstart, i) were committed; VS is now dirty.
378                        ext_state.mark_vs_dirty();
379                        // vstart records the faulting element for restartability.
380                        ext_state.set_vstart(Vstart::from(i));
381                    }
382                    return Err(ExecutionError::MemoryAccess(mem_err));
383                }
384            }
385        }
386
387        // All nf fields for element i were read successfully; commit to the register file.
388        for f in 0..nf.fields_per_segment() {
389            // SAFETY: Guaranteed by function contract
390            let field_base_reg =
391                unsafe { VReg::from_bits(vd.to_bits() + f * group_regs).unwrap_unchecked() };
392            // SAFETY: need `field_base_reg + i / (VLEN.bytes() / elem_bytes) < 32`.
393            //
394            // Let `elems_per_reg = VLEN.bytes() / elem_bytes`.
395            // `i < vl <= group_regs * elems_per_reg` (precondition), so
396            // `i / elems_per_reg < group_regs`.
397            //
398            // `field_base_reg = vd.to_bits() + f * group_regs`. Since `f < nf` and the
399            // precondition guarantees `vd.to_bits() + nf * group_regs <= 32`:
400            // `field_base_reg + group_regs <= vd.to_bits() + (f+1) * group_regs
401            //                             <= vd.to_bits() + nf * group_regs <= 32`.
402            //
403            // Therefore, `field_base_reg + i / elems_per_reg
404            //            < field_base_reg + group_regs <= 32`.
405            //
406            // For `field_buf`: `f < nf <= Nf::MAX` (the same argument as in the read loop
407            // above), so `f as usize < Nf::MAX = field_buf.len()`.
408            unsafe {
409                write_group_element(
410                    ext_state.write_vregs(),
411                    field_base_reg,
412                    i,
413                    eew,
414                    *field_buf.get_unchecked(f as usize),
415                );
416            }
417        }
418    }
419
420    ext_state.mark_vs_dirty();
421    ext_state.reset_vstart();
422    Ok(())
423}
424
425/// Execute a strided or strided segment load.
426///
427/// `addr[i] = base + i * stride` where `stride` is a signed XLEN-wide value. Field `f` of
428/// element `i` is at `addr[i] + f * eew.bytes()`.
429///
430/// # Safety
431/// - `vd.to_bits() % group_regs == 0`
432/// - `vd.to_bits() + nf * group_regs <= 32`
433/// - `vl <= group_regs * VLEN.bytes() / eew.bytes()`
434/// - When `vm=false`: `vd` does not overlap `v0` (i.e. `vd.to_bits() != 0`)
435#[inline(always)]
436#[expect(clippy::too_many_arguments, reason = "Internal API")]
437#[doc(hidden)]
438// TODO: #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
439pub unsafe fn execute_strided_load<Reg, ExtState, Memory, CustomError>(
440    ext_state: &mut ExtState,
441    memory: &Memory,
442    vd: VReg,
443    vm: bool,
444    base: u64,
445    stride: i64,
446    eew: Eew,
447    group_regs: NonZeroU8,
448    nf: Nf,
449) -> Result<(), ExecutionError<Reg::Type, CustomError>>
450where
451    Reg: Register,
452    ExtState: VectorRegistersExt<Reg, CustomError>,
453    [(); SUPPORTED_ELEN_VLEN::<{ ExtState::ELEN }, { ExtState::VLEN }>]:,
454    Memory: VirtualMemory,
455    CustomError: fmt::Debug,
456{
457    let group_regs = group_regs.get();
458    let vl = ext_state.vl();
459    let vstart = ext_state.vstart();
460    let elem_bytes = eew.bytes_width();
461
462    // SAFETY: `vl <= VLMAX <= VLEN` (precondition)
463    let mask_buf = unsafe { snapshot_mask(ext_state.read_vregs(), vm, vl) };
464
465    for i in vstart.range_to(vl) {
466        if !vm && !mask_bit(&mask_buf, i) {
467            continue;
468        }
469
470        let elem_base = base.wrapping_add(i64::from(i).wrapping_mul(stride).cast_unsigned());
471
472        for f in 0..nf.fields_per_segment() {
473            let addr = elem_base.wrapping_add(u64::from(f * elem_bytes));
474            let data = match read_mem_element(memory, addr, eew) {
475                Ok(data) => data,
476                Err(mem_err) => {
477                    cold_path();
478                    if f > 0 || i > u16::from(vstart) {
479                        ext_state.mark_vs_dirty();
480                        ext_state.set_vstart(Vstart::from(i));
481                    }
482                    return Err(ExecutionError::MemoryAccess(mem_err));
483                }
484            };
485            // SAFETY: Guaranteed by function contract
486            let field_base_reg =
487                unsafe { VReg::from_bits(vd.to_bits() + f * group_regs).unwrap_unchecked() };
488            // SAFETY: need `field_base_reg + i / (VLEN.bytes() / elem_bytes) < 32`.
489            //
490            // Let `elems_per_reg = VLEN.bytes() / elem_bytes`.
491            // `i < vl <= group_regs * elems_per_reg` (precondition), so
492            // `i / elems_per_reg < group_regs`.
493            //
494            // `field_base_reg = vd.to_bits() + f * group_regs`. Since `f < nf` and
495            // `vd.to_bits() + nf * group_regs <= 32` (precondition):
496            // `field_base_reg + group_regs <= vd.to_bits() + (f+1) * group_regs
497            //                             <= vd.to_bits() + nf * group_regs <= 32`.
498            //
499            // Therefore, `field_base_reg + i / elems_per_reg < field_base_reg + group_regs <= 32`.
500            unsafe {
501                write_group_element(ext_state.write_vregs(), field_base_reg, i, eew, data);
502            }
503        }
504    }
505
506    ext_state.mark_vs_dirty();
507    ext_state.reset_vstart();
508    Ok(())
509}
510
511/// Execute an indexed (unordered or ordered) or indexed segment load.
512///
513/// For element `i`, reads `index_eew`-sized bytes from register group `vs2` at element `i`
514/// to obtain a zero-extended byte offset, then loads `nf` data fields from
515/// `base + offset + f * data_eew.bytes()`. Unordered vs ordered is functionally identical in
516/// a software interpreter.
517///
518/// # Safety
519/// - `vd.to_bits() % data_group_regs == 0`
520/// - `vd.to_bits() + nf * data_group_regs <= 32`
521/// - `vs2.to_bits() + (vl - 1) / (VLEN.bytes() / index_eew.bytes()) < 32` (all `vl` index elements
522///   fit within the register file; satisfied when `vs2` is alignment-checked against `EMUL_index`
523///   and `vl` is the architectural `vl` bounded by `VLMAX`)
524/// - `vl <= data_group_regs * VLEN.bytes() / data_eew.bytes()` (all `vl` elements fit in a data
525///   group)
526/// - When `vm=false`: `vd` does not overlap `v0` (i.e. `vd.to_bits() != 0`)
527#[inline(always)]
528#[expect(clippy::too_many_arguments, reason = "Internal API")]
529#[doc(hidden)]
530// TODO: #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
531pub unsafe fn execute_indexed_load<Reg, ExtState, Memory, CustomError>(
532    ext_state: &mut ExtState,
533    memory: &Memory,
534    vd: VReg,
535    vs2: VReg,
536    vm: bool,
537    base: u64,
538    data_eew: Eew,
539    index_eew: Eew,
540    data_group_regs: NonZeroU8,
541    nf: Nf,
542) -> Result<(), ExecutionError<Reg::Type, CustomError>>
543where
544    Reg: Register,
545    ExtState: VectorRegistersExt<Reg, CustomError>,
546    [(); SUPPORTED_ELEN_VLEN::<{ ExtState::ELEN }, { ExtState::VLEN }>]:,
547    Memory: VirtualMemory,
548    CustomError: fmt::Debug,
549{
550    let data_group_regs = data_group_regs.get();
551    let vl = ext_state.vl();
552    let vstart = ext_state.vstart();
553    let index_base_reg = vs2;
554
555    // SAFETY: `vl <= VLMAX <= VLEN` (precondition)
556    let mask_buf = unsafe { snapshot_mask(ext_state.read_vregs(), vm, vl) };
557
558    for i in vstart.range_to(vl) {
559        if !vm && !mask_bit(&mask_buf, i) {
560            continue;
561        }
562
563        // SAFETY: need `index_base_reg + i / (VLEN.bytes() / index_eew.bytes()) < 32`.
564        //
565        // The caller verified `vs2` is aligned to `EMUL_index` registers and that
566        // `vs2.to_bits() + EMUL_index <= 32`. `EMUL_index` is defined so that
567        // `EMUL_index * (VLEN.bytes() / index_eew.bytes()) = VLMAX`. Since `i < vl <= VLMAX`,
568        // `i / (VLEN.bytes() / index_eew.bytes()) < EMUL_index`, and therefore
569        // `index_base_reg + i / (VLEN.bytes() / index_eew.bytes()) < index_base_reg + EMUL_index <=
570        // 32`.
571        let index_buf =
572            unsafe { read_group_element(ext_state.read_vregs(), index_base_reg, i, index_eew) };
573        let offset = u64::from_le_bytes(index_buf);
574        let elem_addr = base.wrapping_add(offset);
575
576        let data_elem_bytes = data_eew.bytes_width();
577        for f in 0..nf.fields_per_segment() {
578            let addr = elem_addr.wrapping_add(u64::from(f) * u64::from(data_elem_bytes));
579            let data = match read_mem_element(memory, addr, data_eew) {
580                Ok(data) => data,
581                Err(mem_err) => {
582                    cold_path();
583                    if f > 0 || i > u16::from(vstart) {
584                        ext_state.mark_vs_dirty();
585                        ext_state.set_vstart(Vstart::from(i));
586                    }
587                    return Err(ExecutionError::MemoryAccess(mem_err));
588                }
589            };
590            // SAFETY: Guaranteed by function contract
591            let field_base_reg =
592                unsafe { VReg::from_bits(vd.to_bits() + f * data_group_regs).unwrap_unchecked() };
593            // SAFETY: need `field_base_reg + i / (VLEN.bytes() / data_eew.bytes()) < 32`.
594            //
595            // Let `data_elems_per_reg = VLEN.bytes() / data_eew.bytes()`.
596            // `i < vl <= data_group_regs * data_elems_per_reg` (precondition), so
597            // `i / data_elems_per_reg < data_group_regs`.
598            //
599            // `field_base_reg = vd.to_bits() + f * data_group_regs`. Since `f < nf` and
600            // `vd.to_bits() + nf * data_group_regs <= 32` (precondition):
601            // `field_base_reg + data_group_regs <= vd.to_bits() + (f+1) * data_group_regs
602            //                                  <= vd.to_bits() + nf * data_group_regs <= 32`.
603            //
604            // Therefore,
605            // `field_base_reg + i / data_elems_per_reg < field_base_reg + data_group_regs <= 32`.
606            unsafe {
607                write_group_element(ext_state.write_vregs(), field_base_reg, i, data_eew, data);
608            }
609        }
610    }
611
612    ext_state.mark_vs_dirty();
613    ext_state.reset_vstart();
614    Ok(())
615}