Skip to main content

ab_riscv_interpreter/v/zvexx/fixed_point/
zvexx_fixed_point_helpers.rs

1//! Opaque helpers for ZveXx extension
2
3use crate::v::vector_registers::{VectorRegisterFile, VectorRegistersExt};
4pub use crate::v::zvexx::arith::zvexx_arith_helpers::{
5    OpSrc, check_vreg_group_alignment, sew_mask,
6};
7use crate::v::zvexx::arith::zvexx_arith_helpers::{
8    read_element_u64, sign_extend, write_element_u64,
9};
10use crate::v::zvexx::load::zvexx_load_helpers::{mask_bit, snapshot_mask};
11use crate::v::zvexx::zvexx_helpers::INSTRUCTION_SIZE;
12use crate::{ExecutionError, PackedAddress, ProgramCounter};
13use ab_riscv_primitives::prelude::*;
14use core::hint::cold_path;
15
16/// Compute the rounding increment for a right shift of `val` by `shift` bits.
17///
18/// When `shift == 0` there are no fractional bits so the increment is always zero.
19/// `current_result_lsb` is the LSB of the truncated result, required for `Rne` and `Rod`.
20#[inline(always)]
21#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
22fn round_increment(val: u64, shift: u32, mode: Vxrm, current_result_lsb: u64) -> u64 {
23    if shift == 0 {
24        return 0;
25    }
26    // `d_minus1_bit`: the most-significant discarded bit (bit position `shift - 1`)
27    let d_minus1_bit = (val >> (shift - 1)) & 1;
28    // `sticky`: OR of all bits below position `shift - 1`
29    let sticky = if shift >= 2 {
30        // Any of bits [shift-2 : 0] set?
31        (val & ((1u64 << (shift - 1)).wrapping_sub(1))) != 0
32    } else {
33        false
34    };
35    match mode {
36        // Round nearest up: increment = v[d-1]
37        Vxrm::Rnu => d_minus1_bit,
38        // Round nearest even: increment = v[d-1] & (sticky | result_lsb)
39        Vxrm::Rne => d_minus1_bit & u64::from(sticky || current_result_lsb != 0),
40        // Round down / truncate: never increment
41        Vxrm::Rdn => 0,
42        // Round to odd: set result LSB if any discarded bit was non-zero
43        Vxrm::Rod => u64::from(current_result_lsb == 0 && (d_minus1_bit != 0 || sticky)),
44    }
45}
46
47/// Perform a rounded right shift of `val` by `shift` bits (logical / unsigned).
48///
49/// Returns `(val >> shift) + round_increment`.
50#[inline(always)]
51#[doc(hidden)]
52#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
53pub fn rounded_srl(val: u64, shift: u32, mode: Vxrm) -> u64 {
54    let truncated = val >> shift;
55    let r = round_increment(val, shift, mode, truncated & 1);
56    truncated.wrapping_add(r)
57}
58
59/// Perform a rounded arithmetic right shift of `val` (sign-extended to SEW) by `shift` bits.
60///
61/// Returns the SEW-wide signed result as `u64` (sign bits above SEW are meaningful).
62#[inline(always)]
63#[doc(hidden)]
64#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
65pub fn rounded_sra(val: u64, shift: u32, mode: Vxrm, sew: Vsew) -> u64 {
66    let signed = sign_extend(val, sew);
67    // Treat the raw bits for rounding purposes: rounding uses the unsigned representation of the
68    // SEW-wide value (only bits below `shift` matter, so masking is not needed here since the
69    // discarded bits are the same regardless of sign extension).
70    let truncated_signed = signed >> shift;
71    let r = round_increment(val, shift, mode, truncated_signed.cast_unsigned() & 1);
72    truncated_signed.cast_unsigned().wrapping_add(r)
73}
74
75/// Saturating unsigned add: `vs2 + src`, clamped to `[0, 2^SEW - 1]`.
76///
77/// Sets `vxsat` to `true` on overflow.
78#[inline(always)]
79#[doc(hidden)]
80#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
81pub fn sat_addu(a: u64, b: u64, sew: Vsew, vxsat: &mut bool) -> u64 {
82    let mask = sew_mask(sew);
83    let a_w = a & mask;
84    let b_w = b & mask;
85    let result = a_w.wrapping_add(b_w);
86    if result & mask < a_w {
87        // Overflow: wrapped around
88        *vxsat = true;
89        mask
90    } else {
91        result & mask
92    }
93}
94
95/// Saturating signed add: `vs2 + src`, clamped to `[-(2^(SEW-1)), 2^(SEW-1) - 1]`.
96///
97/// Sets `vxsat` to `true` on overflow.
98#[inline(always)]
99#[doc(hidden)]
100#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
101pub fn sat_add(a: u64, b: u64, sew: Vsew, vxsat: &mut bool) -> u64 {
102    let sa = i128::from(sign_extend(a, sew));
103    let sb = i128::from(sign_extend(b, sew));
104    let result = sa.wrapping_add(sb);
105    let min_val = i128::MIN >> (i128::BITS - u32::from(sew.bits_width()));
106    let max_val = i128::MAX >> (i128::BITS - u32::from(sew.bits_width()));
107    if result < min_val {
108        *vxsat = true;
109        (min_val as i64).cast_unsigned() & sew_mask(sew)
110    } else if result > max_val {
111        *vxsat = true;
112        (max_val as i64).cast_unsigned() & sew_mask(sew)
113    } else {
114        (result as i64).cast_unsigned() & sew_mask(sew)
115    }
116}
117
118/// Saturating unsigned subtract: `vs2 - src`, clamped to `[0, 2^SEW - 1]`.
119///
120/// Sets `vxsat` to `true` on overflow (underflow to negative).
121#[inline(always)]
122#[doc(hidden)]
123#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
124pub fn sat_subu(a: u64, b: u64, sew: Vsew, vxsat: &mut bool) -> u64 {
125    let mask = sew_mask(sew);
126    let a_w = a & mask;
127    let b_w = b & mask;
128    if a_w < b_w {
129        *vxsat = true;
130        0
131    } else {
132        (a_w - b_w) & mask
133    }
134}
135
136/// Saturating signed subtract: `vs2 - src`, clamped to `[-(2^(SEW-1)), 2^(SEW-1) - 1]`.
137///
138/// Sets `vxsat` to `true` on overflow.
139#[inline(always)]
140#[doc(hidden)]
141#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
142pub fn sat_sub(a: u64, b: u64, sew: Vsew, vxsat: &mut bool) -> u64 {
143    let sa = i128::from(sign_extend(a, sew));
144    let sb = i128::from(sign_extend(b, sew));
145    let result = sa.wrapping_sub(sb);
146    let min_val = i128::MIN >> (i128::BITS - u32::from(sew.bits_width()));
147    let max_val = i128::MAX >> (i128::BITS - u32::from(sew.bits_width()));
148    if result < min_val {
149        *vxsat = true;
150        (min_val as i64).cast_unsigned() & sew_mask(sew)
151    } else if result > max_val {
152        *vxsat = true;
153        (max_val as i64).cast_unsigned() & sew_mask(sew)
154    } else {
155        (result as i64).cast_unsigned() & sew_mask(sew)
156    }
157}
158
159/// Averaging unsigned add: `(vs2 + src) >> 1` with rounding per `vxrm`.
160///
161/// Uses a 1-bit wider intermediate to avoid overflow; no saturation, no `vxsat`.
162#[inline(always)]
163#[doc(hidden)]
164#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
165pub fn avg_addu(a: u64, b: u64, sew: Vsew, mode: Vxrm) -> u64 {
166    let mask = sew_mask(sew);
167    let a_w = a & mask;
168    let b_w = b & mask;
169    // Compute full sum in one extra bit by using u128 or by widening trick.
170    // Since SEW <= 64 and both operands are SEW-bit values, the sum fits in SEW+1 bits.
171    // Use wrapping_add: the carry out of bit SEW-1 is the extra bit.
172    let sum = a_w.wrapping_add(b_w);
173    // Carry: set if unsigned sum overflowed SEW bits
174    let carry = u64::from(sum & mask < a_w);
175    // Full (SEW+1)-bit value: `carry` is at bit position SEW, `sum & mask` are low SEW bits.
176    // We need `(carry:sum) >> 1` with rounding.
177    // Bit 0 of `sum & mask` is the rounding bit for the truncated division.
178    let r = round_increment(sum & mask, 1, mode, (sum >> 1u8) & 1);
179    // Shift the (SEW+1)-bit quantity right by 1: result = (carry << (SEW-1)) | ((sum & mask) >> 1)
180    let shifted = (carry << (u32::from(sew.bits_width()) - 1)) | ((sum & mask) >> 1u8);
181    (shifted.wrapping_add(r)) & mask
182}
183
184/// Averaging signed add: `(vs2 + src) >> 1` with rounding per `vxrm`.
185///
186/// No saturation, no `vxsat`.
187#[inline(always)]
188#[doc(hidden)]
189#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
190pub fn avg_add(a: u64, b: u64, sew: Vsew, mode: Vxrm) -> u64 {
191    let sa = sign_extend(a, sew);
192    let sb = sign_extend(b, sew);
193    // Full sum as i128 to avoid overflow
194    let sum = i128::from(sa).wrapping_add(i128::from(sb));
195    // The low bit is the fractional bit for rounding
196    let r = match mode {
197        Vxrm::Rnu => (sum & 1).cast_unsigned() as u64,
198        Vxrm::Rne => {
199            // round-to-nearest-even: increment if fractional bit set AND (result LSB or sticky)
200            // For a single bit shift there are no lower sticky bits, so only check result LSB
201            let result_lsb = ((sum >> 1u8) & 1).cast_unsigned() as u64;
202            ((sum & 1).cast_unsigned() as u64) & result_lsb
203        }
204        Vxrm::Rdn => 0,
205        Vxrm::Rod => {
206            // Set result LSB if it would be 0 and the fractional bit is nonzero
207            let result_lsb = (sum >> 1u8) & 1;
208            u64::from(result_lsb == 0 && (sum & 1) != 0)
209        }
210    };
211    let result = (sum >> 1u8) + i128::from(r);
212    (result as i64).cast_unsigned() & sew_mask(sew)
213}
214
215/// Averaging unsigned subtract: `(vs2 - src) >> 1` with rounding per `vxrm`.
216///
217/// No saturation, no `vxsat`.
218#[inline(always)]
219#[doc(hidden)]
220#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
221pub fn avg_subu(a: u64, b: u64, sew: Vsew, mode: Vxrm) -> u64 {
222    let mask = sew_mask(sew);
223    let a_w = a & mask;
224    let b_w = b & mask;
225    // Compute difference with borrow using wrapping sub; borrow extends to SEW+1 bit.
226    let diff = a_w.wrapping_sub(b_w);
227    // Borrow: set if a < b (unsigned)
228    let borrow = u64::from(a_w < b_w);
229    // Full (SEW+1)-bit two's-complement difference:
230    // If borrow: the SEW-bit `diff` is correct (it wrapped), and the sign extension bit is 1.
231    // Rounding: bit 0 of diff is the fractional bit.
232    let r = round_increment(diff & mask, 1, mode, (diff >> 1u8) & 1);
233    // Arithmetic right shift by 1 of the (SEW+1)-bit signed value.
234    // For unsigned averaging subtract: result = ((SEW+1)-bit diff) / 2 with rounding.
235    // The (SEW+1)-bit value is: borrow is the sign bit. If borrow set, value is negative.
236    // Result = (borrow << SEW | diff) >> 1 (arithmetic) + r
237    // Arithmetic shift: sign bit (`borrow`) propagates.
238    let sign_fill = borrow.wrapping_neg(); // all ones if borrow set, zero otherwise
239    let shifted = (sign_fill << (u32::from(sew.bits_width()) - 1)) | ((diff & mask) >> 1u8);
240    (shifted.wrapping_add(r)) & mask
241}
242
243/// Averaging signed subtract: `(vs2 - src) >> 1` with rounding per `vxrm`.
244///
245/// No saturation, no `vxsat`.
246#[inline(always)]
247#[doc(hidden)]
248#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
249pub fn avg_sub(a: u64, b: u64, sew: Vsew, mode: Vxrm) -> u64 {
250    let sa = sign_extend(a, sew);
251    let sb = sign_extend(b, sew);
252    let diff = i128::from(sa).wrapping_sub(i128::from(sb));
253    let r = match mode {
254        Vxrm::Rnu => (diff & 1).cast_unsigned() as u64,
255        Vxrm::Rne => {
256            let result_lsb = ((diff >> 1u8) & 1).cast_unsigned() as u64;
257            ((diff & 1).cast_unsigned() as u64) & result_lsb
258        }
259        Vxrm::Rdn => 0,
260        Vxrm::Rod => {
261            let result_lsb = (diff >> 1u8) & 1;
262            u64::from(result_lsb == 0 && (diff & 1) != 0)
263        }
264    };
265    let result = (diff >> 1u8) + i128::from(r);
266    (result as i64).cast_unsigned() & sew_mask(sew)
267}
268
269/// Fractional multiply with rounding and saturation: `vsmul`.
270///
271/// Computes `(a * b * 2 + rounding) >> SEW`, saturating at the signed maximum when the
272/// product of two minimum signed values overflows (`INT_MIN * INT_MIN`).
273///
274/// Per spec §12.4: `vd[i] = clip(roundoff_signed(vs2[i] * vs1[i] * 2, SEW))`.
275/// Sets `vxsat` on overflow.
276#[inline(always)]
277#[doc(hidden)]
278#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
279pub fn smul(a: u64, b: u64, sew: Vsew, mode: Vxrm, vxsat: &mut bool) -> u64 {
280    // SEW-wide signed min and max in i64 (valid for all SEW <= 64)
281    let min_sew = i64::MIN >> (i64::BITS - u32::from(sew.bits_width()));
282    let max_sew = i64::MAX >> (i64::BITS - u32::from(sew.bits_width()));
283    let sa = i128::from(sign_extend(a, sew));
284    let sb = i128::from(sign_extend(b, sew));
285    // The only case where `product * 2` overflows a 2*SEW signed result is INT_MIN * INT_MIN.
286    // Detect this before any multiply: for SEW=64 INT64_MIN^2 = 2^126 and <<1 would overflow i128.
287    if sa == i128::from(min_sew) && sb == i128::from(min_sew) {
288        cold_path();
289        *vxsat = true;
290        return max_sew.cast_unsigned() & sew_mask(sew);
291    }
292    // Full 2*SEW-bit product; no overflow possible because at least one operand != INT_MIN,
293    // so |product| < INT_MIN^2 and the value fits in i128 for SEW <= 64.
294    let product = sa * sb;
295    // Left shift by 1 for the Q-format fractional interpretation; safe because
296    // |product| < INT_MIN^2, so after <<1 the result still fits in i128 for SEW <= 64.
297    let doubled = product << 1u8;
298    // Extract the low SEW bits (the discarded portion) for rounding.
299    // Cast to u128 first to avoid sign-extension contaminating the mask.
300    let shift = u32::from(sew.bits_width());
301    let low_bits = (doubled.cast_unsigned() & u128::from(sew_mask(sew))) as u64;
302    // Arithmetic right shift by SEW gives the truncated signed result in SEW-wide range.
303    let truncated = doubled >> shift;
304    let r = round_increment(
305        low_bits,
306        shift.min(64),
307        mode,
308        (truncated.cast_unsigned() as u64) & 1,
309    );
310    // `truncated` fits in i64 after the SEW-bit shift (it is a SEW-wide signed value).
311    let result = (truncated as i64).wrapping_add(r.cast_signed());
312    // Clamp to SEW-wide signed range (only reachable if rounding pushed the value over)
313    if result < min_sew {
314        *vxsat = true;
315        min_sew.cast_unsigned() & sew_mask(sew)
316    } else if result > max_sew {
317        *vxsat = true;
318        max_sew.cast_unsigned() & sew_mask(sew)
319    } else {
320        result.cast_unsigned() & sew_mask(sew)
321    }
322}
323
324/// Narrowing unsigned clip: read a 2*SEW element from `vs2`, shift right by `shamt` with
325/// rounding, saturate to unsigned SEW range, set `vxsat` on clamp.
326///
327/// `vs2_elem` is the 2*SEW-bit element (zero-extended to u64 for SEW <= 32;
328/// for SEW = 64 the doubled width would be 128 bits, but Zve64x only supports SEW up to 64 and
329/// the narrowing destination is at most 64 bits wide, so 2*SEW = 128 - however the spec requires
330/// `ELEN >= 2*SEW` for narrowing instructions. Since `ELEN = 64` in Zve64x, narrowing is only
331/// valid for SEW <= 32 (`2*SEW <= 64`).  The caller must enforce this constraint by checking
332/// `vsew` before invoking narrowing operations.
333///
334/// `vs2_elem` is passed as `u64`; for SEW = 32 it holds a 64-bit (2*SEW) value.
335#[inline(always)]
336#[doc(hidden)]
337#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
338pub fn nclipu(vs2_elem: u64, shamt: u32, sew: Vsew, mode: Vxrm, vxsat: &mut bool) -> u64 {
339    // Shift right with rounding
340    let shifted = rounded_srl(vs2_elem, shamt, mode);
341    // Saturate to destination SEW unsigned range [0, 2^SEW - 1]
342    let max_dst = sew_mask(sew);
343    if shifted > max_dst {
344        *vxsat = true;
345        max_dst
346    } else {
347        shifted & max_dst
348    }
349}
350
351/// Narrowing signed clip: read a 2*SEW signed element from `vs2`, shift right arithmetically
352/// with rounding, saturate to signed SEW range.
353///
354/// Same SEW constraint as [`nclipu`].
355#[inline(always)]
356#[doc(hidden)]
357#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
358pub fn nclip(vs2_elem: u64, shamt: u32, sew: Vsew, mode: Vxrm, vxsat: &mut bool) -> u64 {
359    // Sign-extend vs2_elem to full i64 treating it as a 2*SEW-bit signed value.
360    // For SEW=8 the source is 16-bit, for SEW=16 it is 32-bit, for SEW=32 it is 64-bit.
361    // TODO: Use `sew.double_width()`
362    let double_sew_bits = sew.bits_width() * 2;
363    let shift_amt = i64::BITS - u32::from(double_sew_bits);
364    let signed_wide = (vs2_elem.cast_signed() << shift_amt) >> shift_amt;
365    // Arithmetic right shift with rounding
366    // For rounding we need the raw low bits of the wide value before shifting
367    let low_bits = signed_wide.cast_unsigned()
368        & if double_sew_bits == 64 {
369            u64::MAX
370        } else {
371            (1u64 << double_sew_bits) - 1
372        };
373    let truncated = signed_wide >> shamt;
374    let r = round_increment(low_bits, shamt, mode, (truncated.cast_unsigned()) & 1);
375    let rounded = truncated.wrapping_add(r.cast_signed());
376    // Saturate to signed SEW range
377    let min_dst = i64::MIN >> (i64::BITS - u32::from(sew.bits_width()));
378    let max_dst = i64::MAX >> (i64::BITS - u32::from(sew.bits_width()));
379    if rounded < min_dst {
380        *vxsat = true;
381        min_dst.cast_unsigned() & sew_mask(sew)
382    } else if rounded > max_dst {
383        *vxsat = true;
384        max_dst.cast_unsigned() & sew_mask(sew)
385    } else {
386        rounded.cast_unsigned() & sew_mask(sew)
387    }
388}
389
390/// Read a 2*SEW-wide element as `u64` from the double-width source register group of a narrowing
391/// instruction.
392///
393/// For narrowing instructions `vs2` holds elements of width `2*SEW`. The register group size is
394/// `2 * group_regs`. Element `i` of width `2*SEW` is located in the same way as a SEW-wide
395/// element of width `2*SEW` (i.e., treating `2*SEW` as the element width). For `SEW = 32` this
396/// reads 64-bit elements; for `SEW <= 16` it reads narrower elements but zero-extends to `u64`.
397///
398/// # Safety
399/// - `2*SEW <= 64` (Zve64x constraint: only valid for SEW <= 32; caller must verify)
400/// - `base_reg + elem_i / (VLEN.bytes() / (2*sew_bytes)) < 32`
401#[inline(always)]
402#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
403pub unsafe fn read_wide_element_u64<const VLEN: Vlen>(
404    vregs: &VectorRegisterFile<VLEN>,
405    base_reg: VReg,
406    elem_i: u16,
407    sew: Vsew,
408) -> u64 {
409    let double_sew_bytes = u32::from(sew.bytes_width()) * 2;
410    let elems_per_reg = VLEN.bytes() / double_sew_bytes;
411    let reg_off = u32::from(elem_i) / elems_per_reg;
412    let byte_off = (u32::from(elem_i) % elems_per_reg) * double_sew_bytes;
413    // SAFETY: caller guarantees bounds
414    let reg = unsafe {
415        vregs.get(VReg::from_bits(base_reg.to_bits() + reg_off as u8).unwrap_unchecked())
416    };
417    // SAFETY: `byte_off + double_sew_bytes <= VLEN.bytes()`
418    let src =
419        unsafe { reg.get_unchecked(byte_off as usize..(byte_off + double_sew_bytes) as usize) };
420    let mut buf = [0u8; 8];
421    // SAFETY: `double_sew_bytes <= 8` (SEW <= 32 for Zve64x narrowing)
422    unsafe { buf.get_unchecked_mut(..double_sew_bytes as usize) }.copy_from_slice(src);
423    u64::from_le_bytes(buf)
424}
425
426/// Execute a single-width fixed-point arithmetic operation that may set `vxsat`.
427///
428/// `op` receives `(vs2_elem, src_elem, sew, vxrm)` and returns `(result, saturated)`.
429/// The helper ORs any saturation flag into `vxsat` after the loop.
430///
431/// # Safety
432/// - `vd.to_bits() % group_regs == 0` and `vd.to_bits() + group_regs <= 32` (verified by caller)
433/// - `src` register (when `OpSrc::Vreg`) satisfies the same alignment (verified by caller)
434/// - `vl <= group_regs * VLEN.bytes() / sew_bytes` (all `vl` elements fit within the register
435///   group)
436/// - When `vm=false`: `vd.to_bits() != 0` (vd does not overlap v0)
437#[inline(always)]
438#[doc(hidden)]
439#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
440pub unsafe fn execute_fixed_point_op<Reg, Env, F>(
441    env: &mut Env,
442    vd: VReg,
443    vs2: VReg,
444    src: OpSrc,
445    vm: bool,
446    sew: Vsew,
447    op: F,
448) where
449    Reg: Register,
450    Env: VectorRegistersExt<Reg>,
451    [(); SUPPORTED_ELEN_VLEN::<{ Env::ELEN }, { Env::VLEN }>]:,
452    // op: (vs2_elem, src_elem, sew, vxrm) -> result
453    F: Fn(u64, u64, Vsew, Vxrm, &mut bool) -> u64,
454{
455    let vl = env.vl();
456    let vstart = env.vstart();
457    let vxrm = env.vxrm();
458    // SAFETY: `vl <= VLEN`
459    let mask_buf = unsafe { snapshot_mask(env.read_vregs(), vm, vl) };
460    let mut any_sat = false;
461    for i in vstart.range_to(vl) {
462        if !mask_bit(&mask_buf, i) {
463            continue;
464        }
465        // SAFETY: alignment and bounds checked by caller
466        let a = unsafe { read_element_u64(env.read_vregs(), vs2, i, sew) };
467        let b = match src {
468            OpSrc::Vreg(vs1_base) => {
469                // SAFETY: same argument as vs2
470                unsafe { read_element_u64(env.read_vregs(), vs1_base, i, sew) }
471            }
472            OpSrc::Scalar(val) => val,
473        };
474        let result = op(a, b, sew, vxrm, &mut any_sat);
475        // SAFETY: alignment and bounds checked by caller
476        unsafe {
477            write_element_u64(env.write_vregs(), vd, i, sew, result);
478        }
479    }
480    if any_sat {
481        // vxsat is sticky: OR in the new saturation flag
482        env.set_vxsat(true);
483    }
484    env.mark_vs_dirty();
485    env.reset_vstart();
486}
487
488/// Execute a narrowing fixed-point clip operation.
489///
490/// `vs2` holds a double-width register group (2x `group_regs` registers). `vd` holds the
491/// single-width destination. `src` provides the shift amount (Vreg or Scalar).
492///
493/// For Zve64x narrowing instructions, `SEW` must be at most 32 because `2*SEW` must fit in 64
494/// bits. The caller must verify this constraint before invoking this function.
495///
496/// # Safety
497/// - `sew.bits_width() <= 32` (Zve64x ELEN = 64 constraint for narrowing)
498/// - `vs2.to_bits() % (2 * group_regs) == 0` and `vs2.to_bits() + 2 * group_regs <= 32`
499/// - `vd.to_bits() % group_regs == 0` and `vd.to_bits() + group_regs <= 32`
500/// - `vl <= group_regs * VLEN.bytes() / sew_bytes`
501/// - When `vm=false`: `vd.to_bits() != 0`
502#[inline(always)]
503#[doc(hidden)]
504#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
505pub unsafe fn execute_narrowing_clip_op<Reg, Env, F>(
506    env: &mut Env,
507    vd: VReg,
508    vs2: VReg,
509    src: OpSrc,
510    vm: bool,
511    sew: Vsew,
512    op: F,
513) where
514    Reg: Register,
515    Env: VectorRegistersExt<Reg>,
516    [(); SUPPORTED_ELEN_VLEN::<{ Env::ELEN }, { Env::VLEN }>]:,
517    // op: (vs2_wide_elem, shamt, sew, vxrm, vxsat) -> result
518    F: Fn(u64, u32, Vsew, Vxrm, &mut bool) -> u64,
519{
520    let vl = env.vl();
521    let vstart = env.vstart();
522    let vxrm = env.vxrm();
523    // SAFETY: `vl <= VLEN`
524    let mask_buf = unsafe { snapshot_mask(env.read_vregs(), vm, vl) };
525    let mut any_sat = false;
526    // Mask shift amount to log2(2*SEW) bits per spec §12.11
527    let shamt_mask = u64::from(sew.bits_width() * 2 - 1);
528    for i in vstart.range_to(vl) {
529        if !mask_bit(&mask_buf, i) {
530            continue;
531        }
532        // Read 2*SEW-wide source element
533        // SAFETY: `vs2` double-width alignment checked by caller
534        let wide_a = unsafe { read_wide_element_u64(env.read_vregs(), vs2, i, sew) };
535        let shamt = match src {
536            OpSrc::Vreg(vs1_base) => {
537                // SAFETY: vs1 SEW-wide alignment checked by caller
538                let raw = unsafe { read_element_u64(env.read_vregs(), vs1_base, i, sew) };
539                (raw & shamt_mask) as u32
540            }
541            OpSrc::Scalar(val) => (val & shamt_mask) as u32,
542        };
543        let result = op(wide_a, shamt, sew, vxrm, &mut any_sat);
544        // SAFETY: `vd` alignment checked by caller
545        unsafe {
546            write_element_u64(env.write_vregs(), vd, i, sew, result);
547        }
548    }
549    if any_sat {
550        env.set_vxsat(true);
551    }
552    env.mark_vs_dirty();
553    env.reset_vstart();
554}
555
556/// Verify that the destination SEW is valid for narrowing (must be at most 32 in Zve64x).
557///
558/// Returns `Err(IllegalInstruction)` when `sew.bits_width() > 32`.
559#[inline(always)]
560#[doc(hidden)]
561#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
562pub fn check_narrowing_sew<Reg, Memory, PC>(
563    program_counter: &PC,
564    sew: Vsew,
565) -> Result<(), ExecutionError<Reg::Type>>
566where
567    Reg: Register,
568    PC: ProgramCounter<Reg::Type, Memory>,
569{
570    if sew.bits_width() > 32 {
571        cold_path();
572        return Err(ExecutionError::IllegalInstruction {
573            address: PackedAddress::new(program_counter.old_pc(INSTRUCTION_SIZE)),
574        });
575    }
576    Ok(())
577}
578
579/// Check that the double-width source `vs2` of a narrowing instruction is aligned to its register
580/// group and fits in `[0, 32)`.
581///
582/// The source operand has `EEW = 2*SEW`, so its `EMUL = 2*LMUL`. Per v-spec §5.2 the group must be
583/// aligned to `EMUL` registers, and `EMUL` outside the legal range `[1/8, 8]` (e.g. `LMUL=8`, which
584/// would need `EMUL=16`) is reserved. Unlike `2 * register_count()`, this correctly yields a single
585/// register with no alignment constraint for fractional `LMUL` (where `2*LMUL <= 1`).
586///
587/// `sew` is the destination (narrow) SEW; it must be at most 32 (see [`check_narrowing_sew()`]).
588#[inline(always)]
589#[doc(hidden)]
590#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
591pub fn check_vs2_narrowing_alignment<Reg, Memory, PC>(
592    program_counter: &PC,
593    vs2: VReg,
594    vlmul: Vlmul,
595    sew: Vsew,
596) -> Result<(), ExecutionError<Reg::Type>>
597where
598    Reg: Register,
599    PC: ProgramCounter<Reg::Type, Memory>,
600{
601    // Source EEW is double the destination SEW. SEW=64 is rejected earlier by
602    // `check_narrowing_sew`.
603    let wide_eew = match sew {
604        Vsew::E8 => Eew::E16,
605        Vsew::E16 => Eew::E32,
606        Vsew::E32 => Eew::E64,
607        Vsew::E64 => {
608            cold_path();
609            return Err(ExecutionError::IllegalInstruction {
610                address: PackedAddress::new(program_counter.old_pc(INSTRUCTION_SIZE)),
611            });
612        }
613    };
614    // `EMUL = 2*LMUL`; `None` when reserved (e.g. LMUL=8 -> EMUL=16).
615    let Some(wide_group) = vlmul.data_register_count(wide_eew, sew) else {
616        cold_path();
617        return Err(ExecutionError::IllegalInstruction {
618            address: PackedAddress::new(program_counter.old_pc(INSTRUCTION_SIZE)),
619        });
620    };
621    let wide_group = wide_group.get();
622    let vs2_idx = vs2.to_bits();
623    if !vs2_idx.is_multiple_of(wide_group) || vs2_idx + wide_group > 32 {
624        cold_path();
625        return Err(ExecutionError::IllegalInstruction {
626            address: PackedAddress::new(program_counter.old_pc(INSTRUCTION_SIZE)),
627        });
628    }
629    Ok(())
630}