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