From fc22f02cca6312fa6f82955256e4ade6688f65ff Mon Sep 17 00:00:00 2001 From: int Date: Thu, 13 Aug 2026 17:45:37 +0200 Subject: [PATCH 1/3] fix(riscv): validate vector and privileged semantics Co-authored-by: carlos <102978772+carlosqwqqwq@users.noreply.github.com> --- src/isa/riscv/compressed.rs | 35 ++ src/isa/riscv/cpu.rs | 77 ++-- src/isa/riscv/cpu/csr_ops.rs | 86 ++++- src/isa/riscv/cpu/execution.rs | 26 +- src/isa/riscv/cpu/vector_mask.rs | 27 ++ src/isa/riscv/cpu/vector_memory.rs | 126 ++++++- src/isa/riscv/cpu/vector_permute.rs | 93 +++++ src/isa/riscv/cpu/vector_validation.rs | 340 +++++++++++++++++- src/isa/riscv/csr.rs | 3 + src/isa/riscv/decode.rs | 64 +++- src/isa/riscv/disasm.rs | 4 +- .../riscv/vector/reserved_encoding.rs | 118 ++++++ .../jit/riscv_x86_64/vector_validation.rs | 109 ++++++ 13 files changed, 1037 insertions(+), 71 deletions(-) create mode 100644 src/isa/riscv/cpu/vector_mask.rs create mode 100644 src/isa/riscv/cpu/vector_permute.rs diff --git a/src/isa/riscv/compressed.rs b/src/isa/riscv/compressed.rs index e8248210f..0170e6e2e 100644 --- a/src/isa/riscv/compressed.rs +++ b/src/isa/riscv/compressed.rs @@ -141,6 +141,9 @@ fn decode_q0(h: u16, funct3: u32, rv64: bool, isa: &Isa) -> Insn { mk(Op::LdPair, rd_, rs1_, 0, off_d as i64, h) } else { // C.FLW -> flw rd', off(rs1') + if !isa.f { + return ill(h); + } let off = (bits(h, 12, 10) << 3) | (bit(h, 6) << 2) | (bit(h, 5) << 6); mk(Op::Flw, rd_, rs1_, 0, off as i64, h) } @@ -171,6 +174,9 @@ fn decode_q0(h: u16, funct3: u32, rv64: bool, isa: &Isa) -> Insn { mk(Op::SdPair, 0, rs1_, rs2_, off_d as i64, h) } else { // C.FSW -> fsw rs2', off(rs1') + if !isa.f { + return ill(h); + } let off = (bits(h, 12, 10) << 3) | (bit(h, 6) << 2) | (bit(h, 5) << 6); mk(Op::Fsw, 0, rs1_, rvc_reg(bits(h, 4, 2)), off as i64, h) } @@ -384,6 +390,9 @@ fn decode_q2(h: u16, funct3: u32, rv64: bool, isa: &Isa) -> Insn { mk(Op::LdPair, rd, 2, 0, off as i64, h) } else { // C.FLWSP -> flw rd, off(x2) + if !isa.f { + return ill(h); + } let off = (bit(h, 12) << 5) | (bits(h, 6, 4) << 2) | (bits(h, 3, 2) << 6); mk(Op::Flw, rd, 2, 0, off as i64, h) } @@ -454,6 +463,9 @@ fn decode_q2(h: u16, funct3: u32, rv64: bool, isa: &Isa) -> Insn { mk(Op::SdPair, 0, 2, rs2, off as i64, h) } else { // C.FSWSP -> fsw rs2, off(x2) + if !isa.f { + return ill(h); + } let off = (bits(h, 12, 9) << 2) | (bits(h, 8, 7) << 6); mk(Op::Fsw, 0, 2, bits(h, 6, 2) as u8, off as i64, h) } @@ -715,6 +727,29 @@ mod tests { assert_eq!(decode_rvc(c_sd, Xlen::Rv32, &isa).op, Op::Fsw); } + #[test] + fn rv32_compressed_single_precision_memory_requires_f() { + let encodings = [ + ((0b011 << 13) | (2 << 7) | 0b00, Op::Flw), + ((0b111 << 13) | (2 << 7) | 0b00, Op::Fsw), + ((0b011 << 13) | (8 << 7) | 0b10, Op::Flw), + ((0b111 << 13) | (8 << 2) | 0b10, Op::Fsw), + ]; + + let enabled = Isa::rv64gc(); + let mut disabled = enabled; + disabled.f = false; + disabled.d = false; + disabled.zclsd = false; + for (raw, expected) in encodings { + assert_eq!(decode_rvc(raw as u16, Xlen::Rv32, &enabled).op, expected); + assert_eq!( + decode_rvc(raw as u16, Xlen::Rv32, &disabled).op, + Op::Illegal + ); + } + } + #[test] fn zcmp_zcmt_decode_overlap_slot() { let mut isa = Isa::rv64gc(); diff --git a/src/isa/riscv/cpu.rs b/src/isa/riscv/cpu.rs index 5caafc82c..86a6b94ff 100644 --- a/src/isa/riscv/cpu.rs +++ b/src/isa/riscv/cpu.rs @@ -24,7 +24,9 @@ mod execution; mod jit; mod vector_config; mod vector_conversion; +mod vector_mask; mod vector_memory; +mod vector_permute; mod vector_validation; /// Privilege level of the hart. @@ -187,6 +189,7 @@ pub struct RiscVCpu { mstatus: u64, mtvec: u64, mepc: u64, + sepc: u64, mcause: u64, mtval: u64, mscratch: u64, @@ -266,6 +269,7 @@ impl RiscVCpu { mstatus: 0, mtvec: 0, mepc: 0, + sepc: 0, mcause: 0, mtval: 0, mscratch: 0, @@ -310,6 +314,7 @@ impl RiscVCpu { self.mstatus = 0; self.mtvec = 0; self.mepc = 0; + self.sepc = 0; self.mcause = 0; self.mtval = 0; self.mscratch = 0; @@ -733,7 +738,6 @@ impl RiscVCpu { | Op::PrefetchI | Op::PrefetchR | Op::PrefetchW - | Op::SfenceVm | Op::SfenceVma | Op::SinvalVma | Op::SfenceWInval @@ -909,10 +913,17 @@ impl RiscVCpu { let mask = (1u64 << nbits) - 1; self.set_x(rd, (a >> (rs2 as u64)) & mask); } - Op::Wfi => return Ok(RiscVExit::Wfi), + Op::Wfi => { + return Ok(if self.locally_enabled_interrupt_pending() { + RiscVExit::Continue + } else { + RiscVExit::Wfi + }); + } Op::WrsNto | Op::WrsSto => {} + Op::Uret | Op::SfenceVm => return Err(Trap::illegal(insn.raw)), Op::Mret => self.mret(), - Op::Sret | Op::Uret => self.mret(), // single-mode model: same restore path + Op::Sret => self.sret(insn)?, // ---- Zicsr ---- Op::Csrrw | Op::Csrrs | Op::Csrrc | Op::Csrrwi | Op::Csrrsi | Op::Csrrci => { @@ -2193,18 +2204,19 @@ impl RiscVCpu { | Op::Vmsgt => { let eb = self.sew_bytes(); let mask = Self::sew_mask(eb); + let source = self.vector_snapshot(); let scalar = match insn.funct3 { 0b100 => self.x(insn.rs1) & mask, 0b011 => sext5(insn.rs1) & mask, _ => 0, }; for e in vstart..vl { - if !vm && !self.vmask_bit(e) { + if !vm && !Self::snapshot_mask_bit(&source, e) { continue; // masked-off: undisturbed } - let a = self.velem(vs2, e, eb); + let a = Self::snapshot_velem(&source, vs2, e, eb); let b = if insn.funct3 == 0b000 { - self.velem(insn.rs1, e, eb) + Self::snapshot_velem(&source, insn.rs1, e, eb) } else { scalar }; @@ -2600,6 +2612,7 @@ impl RiscVCpu { Op::Vmfeq | Op::Vmfne | Op::Vmflt | Op::Vmfle | Op::Vmfgt | Op::Vmfge => { let eb = self.sew_bytes(); let is_vv = insn.funct3 == 0b001; + let source = self.vector_snapshot(); let scalar = match eb { 2 => self.h(insn.rs1), 4 => self.s32(insn.rs1), @@ -2607,12 +2620,12 @@ impl RiscVCpu { }; let mut flags = 0u32; for e in vstart..vl { - if !vm && !self.vmask_bit(e) { + if !vm && !Self::snapshot_mask_bit(&source, e) { continue; } - let a = self.velem(vs2, e, eb); + let a = Self::snapshot_velem(&source, vs2, e, eb); let b = if is_vv { - self.velem(insn.rs1, e, eb) + Self::snapshot_velem(&source, insn.rs1, e, eb) } else { scalar }; @@ -3202,6 +3215,7 @@ impl RiscVCpu { // vd.mask[i] = carry/borrow-out; carry-in from v0 only when vm == 0. let eb = self.sew_bytes(); let mask = Self::sew_mask(eb) as u128; + let source = self.vector_snapshot(); let scalar = match insn.funct3 { 0b100 => self.x(insn.rs1) & Self::sew_mask(eb), 0b011 => sext5(insn.rs1) & Self::sew_mask(eb), @@ -3210,14 +3224,14 @@ impl RiscVCpu { let is_vv = insn.funct3 == 0b000; let use_cin = !vm; for e in vstart..vl { - let a = self.velem(vs2, e, eb) as u128; + let a = Self::snapshot_velem(&source, vs2, e, eb) as u128; let b = if is_vv { - self.velem(insn.rs1, e, eb) + Self::snapshot_velem(&source, insn.rs1, e, eb) } else { scalar } as u128; let cin = if use_cin { - self.vmask_bit(e) as u128 + Self::snapshot_mask_bit(&source, e) as u128 } else { 0 }; @@ -3262,25 +3276,7 @@ impl RiscVCpu { } } Op::Vmvr => { - // vmvr.v whole-register move: only nr in {1,2,4,8} (simm - // 0/1/3/7) is defined, the encoding must be unmasked, and both - // vd and vs2 must be aligned to the nr-register group. Reserved - // simm values, masked encodings, or misaligned groups trap. - let nreg = match insn.rs1 { - 0 => 1u8, - 1 => 2, - 3 => 4, - 7 => 8, - _ => return Err(Trap::illegal(insn.raw)), - }; - if !vm || vd % nreg != 0 || vs2 % nreg != 0 { - return Err(Trap::illegal(insn.raw)); - } - let total = nreg as usize * VLENB as usize; - for i in 0..total { - let b = self.velem(vs2, i, 1); - self.set_velem(vd, i, 1, b); - } + self.exec_whole_register_move(insn, vm)?; } Op::Vcompress => { // vcompress.vm is unmasked (vm=1), is not restartable (vstart @@ -5254,21 +5250,20 @@ mod tests { let sys = |funct7: u32, rs2: u32, rs1: u32| (funct7 << 25) | (rs2 << 20) | (rs1 << 15) | 0x73; for w in [ - sys(0x08, 0x04, 10), // sfence.vm a0 - sys(0x09, 11, 10), // sfence.vma a0, a1 - sys(0x0b, 11, 10), // sinval.vma a0, a1 - sys(0x0c, 0, 0), // sfence.w.inval - sys(0x0c, 1, 0), // sfence.inval.ir - sys(0x11, 11, 10), // hfence.vvma a0, a1 - sys(0x13, 11, 10), // hinval.vvma a0, a1 - sys(0x31, 11, 10), // hfence.gvma a0, a1 - sys(0x33, 11, 10), // hinval.gvma a0, a1 + sys(0x09, 11, 10), // sfence.vma a0, a1 + sys(0x0b, 11, 10), // sinval.vma a0, a1 + sys(0x0c, 0, 0), // sfence.w.inval + sys(0x0c, 1, 0), // sfence.inval.ir + sys(0x11, 11, 10), // hfence.vvma a0, a1 + sys(0x13, 11, 10), // hinval.vvma a0, a1 + sys(0x31, 11, 10), // hfence.gvma a0, a1 + sys(0x33, 11, 10), // hinval.gvma a0, a1 ] { assert_eq!(run_one(&mut c, w), RiscVExit::Continue); } assert_eq!(c.x(10), 0x4000); assert_eq!(c.x(11), 0x22); - assert_eq!(c.pc(), 0x300 + 9 * 4); + assert_eq!(c.pc(), 0x300 + 8 * 4); } #[test] diff --git a/src/isa/riscv/cpu/csr_ops.rs b/src/isa/riscv/cpu/csr_ops.rs index f6b929e0f..1fc460a64 100644 --- a/src/isa/riscv/cpu/csr_ops.rs +++ b/src/isa/riscv/cpu/csr_ops.rs @@ -70,6 +70,7 @@ impl RiscVCpu { Csr::Mideleg => self.mideleg, Csr::Mie => self.mie, Csr::Sie => self.mie & self.supervisor_interrupt_mask(), + Csr::Sepc => self.sepc_read_value(), Csr::Mtvec => self.mtvec, Csr::Mcounteren => self.mcounteren, Csr::Mscratch => self.mscratch, @@ -125,6 +126,7 @@ impl RiscVCpu { let mask = self.supervisor_interrupt_mask(); self.mie = (self.mie & !mask) | (value & mask); } + Csr::Sepc => self.sepc = value & self.epc_alignment_mask() & self.xmask(), Csr::Mtvec => { let base = value & !0b11 & self.xmask(); let mode = u64::from(value & 0b11 == 1); @@ -132,7 +134,7 @@ impl RiscVCpu { } Csr::Mcounteren => self.mcounteren = value, Csr::Mscratch => self.mscratch = value, - Csr::Mepc => self.mepc = value & self.mepc_alignment_mask() & self.xmask(), + Csr::Mepc => self.mepc = value & self.epc_alignment_mask() & self.xmask(), Csr::Mcause => self.mcause = value, Csr::Mtval => self.mtval = value, Csr::Mip => self.mip = value, @@ -207,13 +209,18 @@ impl RiscVCpu { } #[inline] - fn mepc_alignment_mask(&self) -> u64 { + fn epc_alignment_mask(&self) -> u64 { if self.cfg.isa.c { !1 } else { !3 } } #[inline] fn mepc_read_value(&self) -> u64 { - self.mepc & self.mepc_alignment_mask() & self.xmask() + self.mepc & self.epc_alignment_mask() & self.xmask() + } + + #[inline] + fn sepc_read_value(&self) -> u64 { + self.sepc & self.epc_alignment_mask() & self.xmask() } pub(super) fn mret(&mut self) { @@ -231,12 +238,39 @@ impl RiscVCpu { }; self.mstatus &= !(0b11 << 11); } + + pub(super) fn sret(&mut self, insn: &Insn) -> Result<(), Trap> { + const MSTATUS_SIE: u64 = 1 << 1; + const MSTATUS_SPIE: u64 = 1 << 5; + const MSTATUS_SPP: u64 = 1 << 8; + const MSTATUS_MPRV: u64 = 1 << 17; + const MSTATUS_TSR: u64 = 1 << 22; + + if self.priv_ < Priv::Supervisor + || (self.priv_ == Priv::Supervisor && self.mstatus & MSTATUS_TSR != 0) + { + return Err(Trap::illegal(insn.raw)); + } + + self.pc = self.sepc_read_value(); + let spie = self.mstatus & MSTATUS_SPIE != 0; + self.mstatus &= !MSTATUS_SIE; + self.mstatus |= u64::from(spie) * MSTATUS_SIE; + self.mstatus |= MSTATUS_SPIE; + self.priv_ = if self.mstatus & MSTATUS_SPP != 0 { + Priv::Supervisor + } else { + Priv::User + }; + self.mstatus &= !(MSTATUS_SPP | MSTATUS_MPRV); + Ok(()) + } } #[cfg(test)] mod tests { use super::super::*; - use crate::isa::riscv::FlatMemory; + use crate::isa::riscv::{FlatMemory, decode}; fn cpu_with_xlen(xlen: Xlen, isa: Isa) -> RiscVCpu { RiscVCpu::new( @@ -267,6 +301,50 @@ mod tests { assert_eq!(with_c.pc(), 0x1002); } + #[test] + fn sepc_masks_ialign_and_sret_restores_only_supervisor_stack() { + let mut hart = cpu(Isa::rv64gc()); + let sret = decode(0x1020_0073, Xlen::Rv64, &Isa::rv64gc()); + let machine_stack = (0b11 << 11) | (1 << 7) | (1 << 3); + hart.mstatus = machine_stack | (1 << 17) | (1 << 8) | (1 << 5); + hart.sepc = 0x2003; + hart.priv_ = Priv::Supervisor; + + assert_eq!(hart.execute_insn(&sret, 0x1000), Ok(RiscVExit::Continue)); + assert_eq!(hart.pc(), 0x2002); + assert_eq!(hart.privilege(), Priv::Supervisor); + assert_eq!(hart.mstatus & machine_stack, machine_stack); + assert_ne!(hart.mstatus & (1 << 1), 0, "SIE <- SPIE"); + assert_ne!(hart.mstatus & (1 << 5), 0, "SPIE <- 1"); + assert_eq!(hart.mstatus & (1 << 8), 0, "SPP <- U"); + assert_eq!(hart.mstatus & (1 << 17), 0, "MPRV clears below M-mode"); + + let mut no_c = cpu(Isa::rv_i()); + no_c.csr_write(0x141, 0x3003).unwrap(); + assert_eq!(no_c.csr_read(0x141), Ok(0x3000)); + no_c.priv_ = Priv::Supervisor; + assert_eq!(no_c.sret(&sret), Ok(())); + assert_eq!(no_c.pc(), 0x3000); + assert_eq!(no_c.privilege(), Priv::User); + assert_eq!(no_c.mstatus & (1 << 1), 0); + assert_ne!(no_c.mstatus & (1 << 5), 0); + } + + #[test] + fn sret_privilege_and_tsr_failures_do_not_commit_state() { + let sret = decode(0x1020_0073, Xlen::Rv64, &Isa::rv64gc()); + for (privilege, status) in [(Priv::User, 0), (Priv::Supervisor, 1 << 22)] { + let mut cpu = cpu(Isa::rv64gc()); + cpu.priv_ = privilege; + cpu.mstatus = status | (1 << 5) | (1 << 8); + cpu.sepc = 0x2000; + cpu.pc = 0x1000; + let before = (cpu.pc, cpu.priv_, cpu.mstatus, cpu.sepc); + assert_eq!(cpu.sret(&sret), Err(Trap::illegal(sret.raw))); + assert_eq!((cpu.pc, cpu.priv_, cpu.mstatus, cpu.sepc), before); + } + } + #[test] fn misa_reports_enabled_h_and_v_independently() { for (h, v) in [(false, false), (true, false), (false, true), (true, true)] { diff --git a/src/isa/riscv/cpu/execution.rs b/src/isa/riscv/cpu/execution.rs index 65cb89687..5a4339448 100644 --- a/src/isa/riscv/cpu/execution.rs +++ b/src/isa/riscv/cpu/execution.rs @@ -3,6 +3,10 @@ use super::*; impl RiscVCpu { + pub(super) fn locally_enabled_interrupt_pending(&self) -> bool { + self.mip & self.mie & (M_INTERRUPT_MASK | S_INTERRUPT_MASK) & self.xmask() != 0 + } + /// Fetch, decode and execute one instruction. pub fn step(&mut self) -> RiscVExit { if let Some(trap) = self.pending_machine_interrupt() { @@ -62,7 +66,7 @@ impl RiscVCpu { #[cfg(test)] mod tests { use super::*; - use crate::isa::riscv::{FlatMemory, MemResult, Memory}; + use crate::isa::riscv::{FlatMemory, MemResult, Memory, decode}; use std::sync::{ Arc, atomic::{AtomicUsize, Ordering}, @@ -157,6 +161,26 @@ mod tests { } } + #[test] + fn wfi_does_not_stall_for_a_locally_enabled_pending_interrupt() { + let meip = 1 << cause::INT_M_EXTERNAL; + + let mut pending = cpu(Isa::rv64gc()); + pending.csr_write(0x304, meip).unwrap(); + pending.csr_write(0x303, meip).unwrap(); // WFI wake ignores delegation. + pending.set_interrupt_pending(meip, true); + let wfi = decode(0x1050_0073, Xlen::Rv64, &Isa::rv64gc()); + assert_eq!(pending.execute_insn(&wfi, CODE), Ok(RiscVExit::Continue)); + assert_eq!(pending.pc(), CODE + 4); + + let mut individually_disabled = cpu(Isa::rv64gc()); + individually_disabled.set_interrupt_pending(meip, true); + assert_eq!( + individually_disabled.execute_insn(&wfi, CODE), + Ok(RiscVExit::Wfi) + ); + } + #[test] fn unavailable_csrs_trap_before_register_commit() { let isa = Isa { diff --git a/src/isa/riscv/cpu/vector_mask.rs b/src/isa/riscv/cpu/vector_mask.rs new file mode 100644 index 000000000..8f0169e64 --- /dev/null +++ b/src/isa/riscv/cpu/vector_mask.rs @@ -0,0 +1,27 @@ +//! Snapshot helpers for mask-producing RVV operations. + +use super::{RiscVCpu, VLENB}; + +impl RiscVCpu { + pub(super) fn vector_snapshot(&self) -> [u8; 32 * VLENB as usize] { + self.v + } + + pub(super) fn snapshot_velem( + snapshot: &[u8; 32 * VLENB as usize], + vreg: u8, + element: usize, + element_bytes: usize, + ) -> u64 { + let offset = vreg as usize * VLENB as usize + element * element_bytes; + let mut bytes = [0u8; 8]; + if offset + element_bytes <= snapshot.len() { + bytes[..element_bytes].copy_from_slice(&snapshot[offset..offset + element_bytes]); + } + u64::from_le_bytes(bytes) + } + + pub(super) fn snapshot_mask_bit(snapshot: &[u8; 32 * VLENB as usize], element: usize) -> bool { + snapshot[element / 8] >> (element % 8) & 1 != 0 + } +} diff --git a/src/isa/riscv/cpu/vector_memory.rs b/src/isa/riscv/cpu/vector_memory.rs index 2ba93ab3f..2df08d047 100644 --- a/src/isa/riscv/cpu/vector_memory.rs +++ b/src/isa/riscv/cpu/vector_memory.rs @@ -81,11 +81,19 @@ impl RiscVCpu { let ieb = encoded_width(insn)?; let eb = self.sew_bytes(); let base = self.x(insn.rs1) & self.xmask(); + // A non-segment indexed load may legally overlap its index + // group under the general mixed-EEW rules. Preserve the + // original indices before destination writes begin. + let indices = (insn.op == Op::Vlxei).then(|| self.vector_snapshot()); for e in vstart..vl { if !vm && !self.vmask_bit(e) { continue; } - let addr = base.wrapping_add(self.velem(insn.rs2, e, ieb)) & self.xmask(); + let index = indices.as_ref().map_or_else( + || self.velem(insn.rs2, e, ieb), + |snapshot| Self::snapshot_velem(snapshot, insn.rs2, e, ieb), + ); + let addr = base.wrapping_add(index) & self.xmask(); if insn.op == Op::Vlxei { let mut buf = [0u8; 8]; self.vector_read(e, addr, &mut buf[..eb])?; @@ -126,6 +134,8 @@ impl RiscVCpu { let is_load = insn.op == Op::Vlseg; let width = encoded_width(insn)?; let indexed = mop == 0b01 || mop == 0b11; + let fault_only_first = + is_load && mop == 0b00 && ((insn.raw >> 20) & 0x1f) == 0b10000; let eb = if indexed { self.sew_bytes() } else { width }; let sew_bits = 8u32 << ((self.vtype >> 3) & 0x7); @@ -151,7 +161,8 @@ impl RiscVCpu { let base = self.x(insn.rs1) & self.xmask(); let stride = self.x(insn.rs2) as i64; - for e in vstart..vl { + let mut new_vl = vl; + 'elements: for e in vstart..vl { if !vm && !self.vmask_bit(e) { continue; } @@ -165,7 +176,18 @@ impl RiscVCpu { let reg = (vd as usize + f * emul_regs) as u8; if is_load { let mut buf = [0u8; 8]; - self.vector_read(e, addr, &mut buf[..eb])?; + if fault_only_first { + if self.mem.read(addr, &mut buf[..eb]).is_err() { + if e == 0 { + self.vstart = 0; + return Err(acc_fault(false, addr)); + } + new_vl = e; + break 'elements; + } + } else { + self.vector_read(e, addr, &mut buf[..eb])?; + } self.set_velem(reg, e, eb, u64::from_le_bytes(buf)); } else { let val = self.velem(reg, e, eb); @@ -173,6 +195,9 @@ impl RiscVCpu { } } } + if fault_only_first { + self.vl = new_vl as u64; + } } Op::Vlm | Op::Vsm => { // Mask transfers use byte-sized elements and vstart is a byte @@ -251,6 +276,26 @@ mod tests { (nf << 29) | (vm << 25) | (sumop << 20) | (rs1 << 15) | (width << 12) | (vs3 << 7) | 0x27 } + fn memory_op( + opcode: u32, + vm: u32, + nf: u32, + mop: u32, + field: u32, + rs1: u32, + width: u32, + vd: u32, + ) -> u32 { + (nf << 29) + | (mop << 26) + | (vm << 25) + | (field << 20) + | (rs1 << 15) + | (width << 12) + | (vd << 7) + | opcode + } + #[test] fn mask_load_and_store_use_byte_indexed_vstart() { let mut load_cpu = cpu(FlatMemory::with_data(0x100, vec![0x11, 0x22, 0x33]), 24, 0); @@ -355,6 +400,81 @@ mod tests { assert_eq!(&cpu.vreg(1)[8..], &[0xaa; 8]); } + #[test] + fn memory_operands_validate_data_index_and_segment_groups() { + let mut cpu = cpu(FlatMemory::new(0x100, 0x100), 2, 0x11); // e32,m2 + cpu.set_x(10, 0x100); + + let misaligned_unit = memory_op(0x07, 1, 0, 0, 0, 10, 6, 1); + assert_eq!( + execute(&mut cpu, misaligned_unit), + Err(Trap::illegal(misaligned_unit)) + ); + let misaligned_unit_store = memory_op(0x27, 1, 0, 0, 0, 10, 6, 1); + assert_eq!( + execute(&mut cpu, misaligned_unit_store), + Err(Trap::illegal(misaligned_unit_store)) + ); + + let misaligned_index_data = memory_op(0x07, 1, 0, 1, 2, 10, 6, 1); + assert_eq!( + execute(&mut cpu, misaligned_index_data), + Err(Trap::illegal(misaligned_index_data)) + ); + + // EI64 at SEW=32, LMUL=2 gives the index operand EMUL=4, so v2 is + // misaligned even though the data group v4-v5 is valid. + let misaligned_index = memory_op(0x07, 1, 0, 1, 2, 10, 7, 4); + assert_eq!( + execute(&mut cpu, misaligned_index), + Err(Trap::illegal(misaligned_index)) + ); + + let misaligned_segment = memory_op(0x07, 1, 1, 0, 0, 10, 6, 1); + assert_eq!( + execute(&mut cpu, misaligned_segment), + Err(Trap::illegal(misaligned_segment)) + ); + let misaligned_segment_store = memory_op(0x27, 1, 1, 0, 0, 10, 6, 1); + assert_eq!( + execute(&mut cpu, misaligned_segment_store), + Err(Trap::illegal(misaligned_segment_store)) + ); + + let aligned_segment = memory_op(0x07, 1, 1, 0, 0, 10, 6, 2); + assert_eq!(execute(&mut cpu, aligned_segment), Ok(RiscVExit::Continue)); + + // Unlike an ordinary indexed load, an indexed segment load cannot + // overlap its index group even when the data and index EEWs match. + let overlapping_indexed_segment = memory_op(0x07, 1, 1, 1, 4, 10, 6, 4); + assert_eq!( + execute(&mut cpu, overlapping_indexed_segment), + Err(Trap::illegal(overlapping_indexed_segment)) + ); + } + + #[test] + fn segment_fault_only_first_traps_at_zero_and_trims_later_faults() { + let raw = memory_op(0x07, 1, 1, 0, 0b10000, 10, 0, 1); + + let mut later = cpu(FlatMemory::with_data(0x100, vec![0x10, 0x20, 0x30]), 3, 0); + later.set_x(10, 0x100); + later.set_vreg(1, &[0xaa; 16]); + later.set_vreg(2, &[0xbb; 16]); + assert_eq!(execute(&mut later, raw), Ok(RiscVExit::Continue)); + assert_eq!(later.vl(), 1); + assert_eq!(later.vstart(), 0); + assert_eq!(later.vreg(1)[0], 0x10); + assert_eq!(later.vreg(2)[0], 0x20); + assert_eq!(later.vreg(1)[1], 0x30); // partial faulting segment is allowed + + let mut first = cpu(FlatMemory::with_data(0x100, vec![0x10]), 3, 0); + first.set_x(10, 0x100); + assert_eq!(execute(&mut first, raw), Err(acc_fault(false, 0x101))); + assert_eq!(first.vl(), 3); + assert_eq!(first.vstart(), 0); + } + #[test] fn mask_and_whole_register_fault_indices_use_their_effective_elements() { let mut mask_cpu = cpu(FlatMemory::with_data(0x100, vec![0; 2]), 24, 0); diff --git a/src/isa/riscv/cpu/vector_permute.rs b/src/isa/riscv/cpu/vector_permute.rs new file mode 100644 index 000000000..771f2456c --- /dev/null +++ b/src/isa/riscv/cpu/vector_permute.rs @@ -0,0 +1,93 @@ +//! RVV register permutation operations with whole-group snapshot semantics. + +use super::{Insn, RiscVCpu, Trap, VLENB}; + +impl RiscVCpu { + pub(super) fn exec_whole_register_move(&mut self, insn: &Insn, vm: bool) -> Result<(), Trap> { + let nreg = match insn.rs1 { + 0 => 1u8, + 1 => 2, + 3 => 4, + 7 => 8, + _ => return Err(Trap::illegal(insn.raw)), + }; + if !vm || insn.rd % nreg != 0 || insn.rs2 % nreg != 0 { + return Err(Trap::illegal(insn.raw)); + } + + let total_bytes = usize::from(nreg) * VLENB as usize; + let sew_bytes = self.sew_bytes(); + let effective_length = total_bytes / sew_bytes; + let first_byte = (self.vstart as usize).min(effective_length) * sew_bytes; + + // Source and destination groups may overlap because they have the same + // EEW. Snapshot the complete source group so the result is independent + // of copy direction, then preserve every prestart element. + let mut source = [0u8; 8 * VLENB as usize]; + for (offset, byte) in source[..total_bytes].iter_mut().enumerate() { + *byte = self.velem(insn.rs2, offset, 1) as u8; + } + for (offset, byte) in source[first_byte..total_bytes].iter().enumerate() { + self.set_velem(insn.rd, first_byte + offset, 1, u64::from(*byte)); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::isa::riscv::{FlatMemory, Isa, RiscVConfig, RiscVExit, Xlen, decode}; + + fn vmvr(nreg_encoding: u32, vd: u32, vs2: u32) -> u32 { + (0b100111 << 26) + | (1 << 25) + | (vs2 << 20) + | (nreg_encoding << 15) + | (0b011 << 12) + | (vd << 7) + | 0x57 + } + + fn cpu(vtype: u64, vstart: u64) -> RiscVCpu { + let mut cpu = RiscVCpu::new(RiscVConfig::rv64gc(), Box::new(FlatMemory::new(0, 0x1000))); + cpu.set_vl_vtype(0, vtype); + cpu.set_vstart(vstart); + cpu + } + + fn execute(cpu: &mut RiscVCpu, raw: u32) { + let insn = decode(raw, Xlen::Rv64, &Isa::rv64gc()); + assert_eq!(cpu.execute_insn(&insn, 0x1000), Ok(RiscVExit::Continue)); + } + + #[test] + fn whole_register_move_resumes_in_sew_sized_elements() { + let mut cpu = cpu(0x10, 2); // e32,m1; resume after eight bytes + cpu.set_vreg(2, &[0x22; 16]); + cpu.set_vreg(3, &[0x33; 16]); + cpu.set_vreg(4, &[0xaa; 16]); + cpu.set_vreg(5, &[0xbb; 16]); + + execute(&mut cpu, vmvr(1, 4, 2)); + + assert_eq!(&cpu.vreg(4)[..8], &[0xaa; 8]); + assert_eq!(&cpu.vreg(4)[8..], &[0x22; 8]); + assert_eq!(cpu.vreg(5), [0x33; 16]); + assert_eq!(cpu.vstart(), 0); + } + + #[test] + fn whole_register_move_snapshots_overlapping_source_group() { + let mut cpu = cpu(0x00, 0); // e8,m1 + for register in 0..6u8 { + cpu.set_vreg(register, &[register; 16]); + } + + execute(&mut cpu, vmvr(3, 4, 0)); + + for register in 4..8u8 { + assert_eq!(cpu.vreg(register), [register - 4; 16]); + } + } +} diff --git a/src/isa/riscv/cpu/vector_validation.rs b/src/isa/riscv/cpu/vector_validation.rs index aad9976f6..4d7accae1 100644 --- a/src/isa/riscv/cpu/vector_validation.rs +++ b/src/isa/riscv/cpu/vector_validation.rs @@ -46,6 +46,10 @@ impl Emul { Self::new(self.numerator, self.denominator * factor) } + fn scale(self, numerator: u8, denominator: u8) -> Self { + Self::new(self.numerator * numerator, self.denominator * denominator) + } + fn is_at_least_one(self) -> bool { self.numerator >= self.denominator } @@ -223,6 +227,222 @@ fn validate_same_width_integer_alu( Ok(()) } +fn validate_same_width_unary(cpu: &RiscVCpu, insn: &Insn) -> Result<(), Trap> { + same_width_group(cpu, insn, insn.rd)?; + same_width_group(cpu, insn, insn.rs2)?; + Ok(()) +} + +fn validate_mask_result(cpu: &RiscVCpu, insn: &Insn, vector_vector_funct3: u8) -> Result<(), Trap> { + let destination = RegisterGroup { + first: insn.rd, + count: 1, + }; + let vs2 = same_width_group(cpu, insn, insn.rs2)?; + if destination.overlaps(vs2) && destination.first != vs2.first { + return Err(illegal(insn)); + } + + if insn.funct3 == vector_vector_funct3 { + let vs1 = same_width_group(cpu, insn, insn.rs1)?; + if destination.overlaps(vs1) && destination.first != vs1.first { + return Err(illegal(insn)); + } + } + Ok(()) +} + +fn encoded_memory_width(insn: &Insn) -> Result { + match insn.funct3 { + 0 => Ok(1), + 5 => Ok(2), + 6 => Ok(4), + 7 => Ok(8), + _ => Err(illegal(insn)), + } +} + +fn memory_emul(cpu: &RiscVCpu, insn: &Insn) -> Result { + let sew = u8::try_from(cpu.sew_bytes()).map_err(|_| illegal(insn))?; + if !matches!(sew, 1 | 2 | 4 | 8) { + return Err(illegal(insn)); + } + Ok(current_lmul(cpu, insn)?.scale(encoded_memory_width(insn)?, sew)) +} + +fn validate_segment_data_groups( + insn: &Insn, + emul: Emul, + fields: u8, +) -> Result, Trap> { + if u16::from(emul.numerator) * u16::from(fields) > 8 * u16::from(emul.denominator) { + return Err(illegal(insn)); + } + + let first = RegisterGroup::for_emul(insn.rd, emul).ok_or_else(|| illegal(insn))?; + let mut groups = Vec::with_capacity(usize::from(fields)); + for field in 0..fields { + let register = insn + .rd + .checked_add(field.saturating_mul(first.count)) + .ok_or_else(|| illegal(insn))?; + groups.push(RegisterGroup::for_emul(register, emul).ok_or_else(|| illegal(insn))?); + } + Ok(groups) +} + +fn validate_indexed_memory_overlap( + insn: &Insn, + data_groups: &[RegisterGroup], + data_emul: Emul, + index: RegisterGroup, + index_emul: Emul, + is_load: bool, +) -> Result<(), Trap> { + let same_eew = data_emul.numerator * index_emul.denominator + == index_emul.numerator * data_emul.denominator; + for data in data_groups { + if !data.overlaps(index) || same_eew { + continue; + } + + // An indexed store reads both groups, so sharing a register at two + // EEWs is always reserved. Loads use the ordinary destination/source + // overlap rules for the data destination and index source. + if !is_load { + return Err(illegal(insn)); + } + let data_is_narrower = data_emul.numerator * index_emul.denominator + < index_emul.numerator * data_emul.denominator; + let legal = if data_is_narrower { + data.first == index.first + } else { + index_emul.is_at_least_one() && data.last() == index.last() + }; + if !legal { + return Err(illegal(insn)); + } + } + Ok(()) +} + +fn validate_vector_memory(cpu: &RiscVCpu, insn: &Insn) -> Result<(), Trap> { + match insn.op { + Op::Vle | Op::Vse | Op::Vlse | Op::Vsse | Op::Vleff => { + RegisterGroup::for_emul(insn.rd, memory_emul(cpu, insn)?) + .ok_or_else(|| illegal(insn))?; + } + Op::Vlxei | Op::Vsxei => { + let data_emul = current_lmul(cpu, insn)?; + let data = same_width_group(cpu, insn, insn.rd)?; + let index_emul = memory_emul(cpu, insn)?; + let index = + RegisterGroup::for_emul(insn.rs2, index_emul).ok_or_else(|| illegal(insn))?; + validate_indexed_memory_overlap( + insn, + &[data], + data_emul, + index, + index_emul, + insn.op == Op::Vlxei, + )?; + } + Op::Vlseg | Op::Vsseg => { + let fields = ((insn.raw >> 29) & 7) as u8 + 1; + let indexed = (insn.raw >> 26) & 3 & 1 != 0; + let data_emul = if indexed { + current_lmul(cpu, insn)? + } else { + memory_emul(cpu, insn)? + }; + let data = validate_segment_data_groups(insn, data_emul, fields)?; + if indexed { + let index_emul = memory_emul(cpu, insn)?; + let index = + RegisterGroup::for_emul(insn.rs2, index_emul).ok_or_else(|| illegal(insn))?; + if insn.op == Op::Vlseg && data.iter().any(|group| group.overlaps(index)) { + // Indexed segment loads prohibit every destination/index + // overlap so a fault can be restarted without having + // overwritten an index needed by a later segment. + return Err(illegal(insn)); + } + validate_indexed_memory_overlap( + insn, + &data, + data_emul, + index, + index_emul, + insn.op == Op::Vlseg, + )?; + } + } + _ => {} + } + Ok(()) +} + +fn writes_nonmask_vector_destination(op: Op) -> bool { + !matches!( + op, + // Stores and scalar-result operations do not write a vector group. + Op::Vse + | Op::Vsse + | Op::Vsxei + | Op::Vsseg + | Op::Vsm + | Op::Vsre + | Op::VmvXS + | Op::VfmvFS + | Op::Vcpop + | Op::Vfirst + // These operations deliberately produce a single mask register. + | Op::Vmseq + | Op::Vmsne + | Op::Vmsltu + | Op::Vmslt + | Op::Vmsleu + | Op::Vmsle + | Op::Vmsgtu + | Op::Vmsgt + | Op::Vmfeq + | Op::Vmfne + | Op::Vmflt + | Op::Vmfle + | Op::Vmfgt + | Op::Vmfge + | Op::Vmand + | Op::Vmnand + | Op::Vmandn + | Op::Vmxor + | Op::Vmor + | Op::Vmnor + | Op::Vmorn + | Op::Vmxnor + | Op::Vmsbf + | Op::Vmsof + | Op::Vmsif + | Op::Vmadc + | Op::Vmsbc + // Reduction results are scalar values held in one vector register. + | Op::Vredsum + | Op::Vredand + | Op::Vredor + | Op::Vredxor + | Op::Vredminu + | Op::Vredmin + | Op::Vredmaxu + | Op::Vredmax + | Op::Vfredusum + | Op::Vfredosum + | Op::Vfredmin + | Op::Vfredmax + | Op::Vwredsumu + | Op::Vwredsum + | Op::Vfwredusum + | Op::Vfwredosum + ) +} + fn validate_gather(cpu: &RiscVCpu, insn: &Insn) -> Result<(), Trap> { let destination = same_width_group(cpu, insn, insn.rd)?; let data = same_width_group(cpu, insn, insn.rs2)?; @@ -306,6 +526,11 @@ pub(super) fn validate(cpu: &RiscVCpu, insn: &Insn, vm: bool) -> Result<(), Trap if vector_fp && cpu.sew_bytes() == 1 && !fp_operands_supported_at_sew8(insn) { return Err(illegal(insn)); } + if !vm && insn.rd == 0 && writes_nonmask_vector_destination(insn.op) { + return Err(illegal(insn)); + } + + validate_vector_memory(cpu, insn)?; match insn.op { Op::VmvXS | Op::VmvSX | Op::VfmvFS | Op::VfmvSF if !vm => { @@ -329,16 +554,22 @@ pub(super) fn validate(cpu: &RiscVCpu, insn: &Insn, vm: bool) -> Result<(), Trap if insn.rs2 != 0 { return Err(illegal(insn)); } + same_width_group(cpu, insn, insn.rd)?; } Op::Viota => validate_iota(cpu, insn, vm)?, Op::Vadc | Op::Vsbc => { if vm || insn.rd == 0 { return Err(illegal(insn)); } + validate_same_width_integer_alu(cpu, insn, 0b000)?; } + Op::Vmadc | Op::Vmsbc => validate_mask_result(cpu, insn, 0b000)?, Op::Vslideup | Op::Vslide1up | Op::Vfslide1up => { validate_slide_up(cpu, insn)?; } + Op::Vslidedown | Op::Vslide1down | Op::Vfslide1down => { + validate_same_width_unary(cpu, insn)?; + } Op::Vrgather | Op::Vrgatherei16 => validate_gather(cpu, insn)?, Op::Vadd | Op::Vsub @@ -352,10 +583,66 @@ pub(super) fn validate(cpu: &RiscVCpu, insn: &Insn, vm: bool) -> Result<(), Trap | Op::Vmax | Op::Vsll | Op::Vsrl - | Op::Vsra => validate_same_width_integer_alu(cpu, insn, 0b000)?, + | Op::Vsra + | Op::Vmerge + | Op::Vsaddu + | Op::Vsadd + | Op::Vssubu + | Op::Vssub + | Op::Vssrl + | Op::Vssra + | Op::Vsmul => validate_same_width_integer_alu(cpu, insn, 0b000)?, + Op::Vmul + | Op::Vmulh + | Op::Vmulhu + | Op::Vmulhsu + | Op::Vdivu + | Op::Vdiv + | Op::Vremu + | Op::Vrem => validate_same_width_integer_alu(cpu, insn, 0b010)?, Op::Vaaddu | Op::Vaadd | Op::Vasubu | Op::Vasub => { validate_same_width_integer_alu(cpu, insn, 0b010)?; } + Op::Vmseq + | Op::Vmsne + | Op::Vmsltu + | Op::Vmslt + | Op::Vmsleu + | Op::Vmsle + | Op::Vmsgtu + | Op::Vmsgt => validate_mask_result(cpu, insn, 0b000)?, + Op::Vmfeq | Op::Vmfne | Op::Vmflt | Op::Vmfle | Op::Vmfgt | Op::Vmfge => { + validate_mask_result(cpu, insn, 0b001)?; + } + Op::Vfadd + | Op::Vfsub + | Op::Vfmul + | Op::Vfdiv + | Op::Vfmin + | Op::Vfmax + | Op::Vfsgnj + | Op::Vfsgnjn + | Op::Vfsgnjx + | Op::Vfmacc + | Op::Vfnmacc + | Op::Vfmsac + | Op::Vfnmsac + | Op::Vfmadd + | Op::Vfnmadd + | Op::Vfmsub + | Op::Vfnmsub => validate_same_width_integer_alu(cpu, insn, 0b001)?, + Op::Vfrsub + | Op::Vfrdiv + | Op::Vfsqrt + | Op::Vfclass + | Op::Vfrsqrt7 + | Op::Vfrec7 + | Op::VfcvtXuF + | Op::VfcvtXF + | Op::VfcvtFXu + | Op::VfcvtFX + | Op::VfcvtRtzXuF + | Op::VfcvtRtzXF => validate_same_width_unary(cpu, insn)?, Op::Vnsrl | Op::Vnsra | Op::Vnclipu @@ -541,6 +828,10 @@ mod tests { assert_illegal(vid(0, 3), E8_M1, 4, 0, 0); assert_legal(vid(1, 0), E8_M1, 4, 0, 0); assert_legal(vid(0, 0), E8_M1, 4, 0, 0); + + // vid writes a normal LMUL-sized data group even though it has no + // vector data source. + assert_illegal(vid(1, 0) | (1 << 7), E32_M2, 4, 0, 0); } #[test] @@ -625,6 +916,53 @@ mod tests { assert_legal(vadd(0, 2, 5, 0b011), E32_M2, 2, 0, 0); } + #[test] + fn remaining_same_width_families_validate_every_vector_group() { + for (funct6, funct3) in [ + (0b100101, 0b010), // vmul.vv + (0b100000, 0b010), // vdivu.vv + (0b100000, 0b000), // vsaddu.vv + (0b101010, 0b000), // vssrl.vv + (0b100111, 0b000), // vsmul.vv + (0b000000, 0b001), // vfadd.vv + ] { + assert_illegal(op_v(funct6, 1, 2, 4, funct3, 1), E32_M2, 2, 0, 0); + assert_illegal(op_v(funct6, 1, 3, 4, funct3, 0), E32_M2, 2, 0, 0); + assert_illegal(op_v(funct6, 1, 2, 5, funct3, 0), E32_M2, 2, 0, 0); + assert_legal(op_v(funct6, 1, 2, 4, funct3, 0), E32_M2, 2, 0, 0); + } + + for funct3 in [0b100, 0b110, 0b101] { + assert_illegal(op_v(0b001111, 1, 2, 3, funct3, 1), E32_M2, 2, 0, 0); + assert_illegal(op_v(0b001111, 1, 3, 3, funct3, 2), E32_M2, 2, 0, 0); + assert_legal(op_v(0b001111, 1, 2, 3, funct3, 2), E32_M2, 2, 0, 0); + } + } + + #[test] + fn mask_results_validate_sources_and_lowest_register_overlap() { + for (funct6, funct3) in [ + (0b011000, 0b000), // vmseq.vv + (0b010001, 0b000), // vmadc.vv + (0b011000, 0b001), // vmfeq.vv + ] { + assert_illegal(op_v(funct6, 1, 2, 4, funct3, 3), E32_M2, 2, 0, 0); + assert_illegal(op_v(funct6, 1, 2, 5, funct3, 0), E32_M2, 2, 0, 0); + assert_legal(op_v(funct6, 1, 2, 4, funct3, 2), E32_M2, 2, 0, 0); + } + } + + #[test] + fn masked_nonmask_destinations_cannot_overlap_v0() { + assert_illegal(op_v(0b000000, 0, 2, 4, 0b000, 0), E32_M2, 2, 0, 0); + assert_illegal(op_v(0b000000, 0, 2, 4, 0b001, 0), E32_M2, 2, 0, 0); + + // Mask-producing comparisons and scalar reductions are the explicit + // architectural exceptions to the masked-destination rule. + assert_legal(op_v(0b011000, 0, 2, 4, 0b000, 0), E32_M2, 2, 0, 0); + assert_legal(op_v(0b000000, 0, 2, 3, 0b010, 0), E32_M2, 2, 0, 0); + } + #[test] fn carry_and_borrow_require_vm_zero_and_nonzero_destination() { let forms = [ diff --git a/src/isa/riscv/csr.rs b/src/isa/riscv/csr.rs index 6331f231a..50cbeec0b 100644 --- a/src/isa/riscv/csr.rs +++ b/src/isa/riscv/csr.rs @@ -43,6 +43,8 @@ pub enum Csr { Mie = 0x304, /// Supervisor interrupt-enable. Sie = 0x104, + /// Supervisor exception program counter. + Sepc = 0x141, /// Machine trap-vector base address. Mtvec = 0x305, /// Machine counter-enable. @@ -104,6 +106,7 @@ impl Csr { 0x303 => Csr::Mideleg, 0x304 => Csr::Mie, 0x104 => Csr::Sie, + 0x141 => Csr::Sepc, 0x305 => Csr::Mtvec, 0x306 => Csr::Mcounteren, 0x340 => Csr::Mscratch, diff --git a/src/isa/riscv/decode.rs b/src/isa/riscv/decode.rs index 309f63171..ab06d37a2 100644 --- a/src/isa/riscv/decode.rs +++ b/src/isa/riscv/decode.rs @@ -104,7 +104,9 @@ pub enum Op { Wfi, WrsNto, WrsSto, + /// Legacy user trap-return encoding; reserved by the current privileged ISA. Uret, + /// Legacy supervisor fence encoding; reserved by the current privileged ISA. SfenceVm, SfenceVma, SinvalVma, @@ -1173,8 +1175,8 @@ pub fn decode(w: u32, xlen: Xlen, isa: &Isa) -> Insn { 0x0f => decode_fence(w, isa), 0x73 => decode_system(w, rv64, isa), 0x2f if isa.a || isa.zacas => decode_amo(w, rv64, isa), - 0x07 if isa.f => decode_load_fp(w, isa), - 0x27 if isa.f => decode_store_fp(w, isa), + 0x07 if isa.f || isa.v => decode_load_fp(w, isa), + 0x27 if isa.f || isa.v => decode_store_fp(w, isa), 0x53 if isa.f => decode_op_fp(w, rv64, isa), 0x43 if isa.f => decode_fma(Op::FmaddS, Op::FmaddD, Op::FmaddH, Op::FmaddQ, w, isa), 0x47 if isa.f => decode_fma(Op::FmsubS, Op::FmsubD, Op::FmsubH, Op::FmsubQ, w, isa), @@ -2375,7 +2377,7 @@ fn decode_fence(w: u32, isa: &Isa) -> Insn { } 0 => base(Op::Fence, w), 1 if isa.zifencei => base(Op::FenceI, w), - 2 if rd(w) == 0 && ((w >> 27) & 0x1f) == 0 => match rs2(w) { + 2 if rd(w) == 0 && funct7(w) == 0 => match rs2(w) { 0 if isa.zicbom => base(Op::CboInval, w), 1 if isa.zicbom => base(Op::CboClean, w), 2 if isa.zicbom => base(Op::CboFlush, w), @@ -2397,12 +2399,10 @@ fn decode_system(w: u32, rv64: bool, isa: &Isa) -> Insn { 0x00 if rs1(w) == 0 => match rs2(w) { 0x00 => base(Op::Ecall, w), 0x01 => base(Op::Ebreak, w), - 0x02 => base(Op::Uret, w), 0x0d if isa.zawrs => base(Op::WrsNto, w), 0x1d if isa.zawrs => base(Op::WrsSto, w), _ => Insn::illegal(w, 4), }, - 0x08 if rs2(w) == 0x04 => base(Op::SfenceVm, w), 0x08 if rs1(w) == 0 && rs2(w) == 0x02 => base(Op::Sret, w), 0x08 if rs1(w) == 0 && rs2(w) == 0x05 => base(Op::Wfi, w), 0x09 => base(Op::SfenceVma, w), @@ -2516,6 +2516,7 @@ fn decode_load_fp(w: u32, isa: &Isa) -> Insn { 0b01000 => base(Op::Vlre, w), // whole register (nf+1 regs) 0b01011 if nf == 0 => base(Op::Vlm, w), 0b10000 if nf == 0 => base(Op::Vleff, w), // fault-only-first + 0b10000 => base(Op::Vlseg, w), // segment fault-only-first _ => Insn::illegal(w, 4), }, 0b10 if nf == 0 => base(Op::Vlse, w), // strided @@ -2526,10 +2527,10 @@ fn decode_load_fp(w: u32, isa: &Isa) -> Insn { }; } let op = match f3 { - 1 if isa.zfh => Op::Flh, - 2 => Op::Flw, - 3 if isa.d => Op::Fld, - 4 if isa.q => Op::Flq, + 1 if isa.f && isa.zfh => Op::Flh, + 2 if isa.f => Op::Flw, + 3 if isa.f && isa.d => Op::Fld, + 4 if isa.f && isa.q => Op::Flq, _ => return Insn::illegal(w, 4), }; with_imm(op, w, imm_i(w)) @@ -2558,10 +2559,10 @@ fn decode_store_fp(w: u32, isa: &Isa) -> Insn { }; } let op = match f3 { - 1 if isa.zfh => Op::Fsh, - 2 => Op::Fsw, - 3 if isa.d => Op::Fsd, - 4 if isa.q => Op::Fsq, + 1 if isa.f && isa.zfh => Op::Fsh, + 2 if isa.f => Op::Fsw, + 3 if isa.f && isa.d => Op::Fsd, + 4 if isa.f && isa.q => Op::Fsq, _ => return Insn::illegal(w, 4), }; with_imm(op, w, imm_s(w)) @@ -2892,6 +2893,11 @@ mod tests { let reserved_high_funct12 = cbo_zero | (1 << 31); assert_eq!((reserved_high_funct12 >> 20) & 0x1f, 4); assert!(decode(reserved_high_funct12, Xlen::Rv64, &Isa::rv64gc()).is_illegal()); + for reserved_funct7_bit in [1 << 25, 1 << 26] { + assert!( + decode(cbo_zero | reserved_funct7_bit, Xlen::Rv64, &Isa::rv64gc()).is_illegal() + ); + } } #[test] @@ -2901,6 +2907,11 @@ mod tests { assert_eq!(dec(cbo(1)).op, Op::CboClean); assert_eq!(dec(cbo(2)).op, Op::CboFlush); assert_eq!(dec(cbo(4)).op, Op::CboZero); + for operation in [0, 1, 2, 4] { + for reserved_funct7_bit in [1 << 25, 1 << 26] { + assert!(dec(cbo(operation) | reserved_funct7_bit).is_illegal()); + } + } let prefetch = |kind: u32, off: u32| (off << 25) | (kind << 20) | (10 << 15) | (6 << 12) | 0x13; @@ -2911,6 +2922,22 @@ mod tests { assert_eq!(dec(prefetch(3, 0)).op, Op::PrefetchW); } + #[test] + fn vector_memory_decode_is_independent_of_f_and_accepts_segment_fof() { + let mut vector_only = Isa::rv_i(); + vector_only.v = true; + + let vle8 = (1 << 25) | (10 << 15) | (1 << 7) | 0x07; + let vse8 = (1 << 25) | (10 << 15) | (1 << 7) | 0x27; + let vlseg2e8ff = 0x2305_0007; + assert_eq!(decode(vle8, Xlen::Rv64, &vector_only).op, Op::Vle); + assert_eq!(decode(vse8, Xlen::Rv64, &vector_only).op, Op::Vse); + assert_eq!(decode(vlseg2e8ff, Xlen::Rv64, &vector_only).op, Op::Vlseg); + + let flw = (10 << 15) | (2 << 12) | (1 << 7) | 0x07; + assert!(decode(flw, Xlen::Rv64, &vector_only).is_illegal()); + } + #[test] fn decode_zawrs_zihintpause_zihintntl_and_zacas() { assert_eq!(dec(0x0100_000f).op, Op::Pause); @@ -3088,14 +3115,13 @@ mod tests { #[test] fn decode_privileged_fence_and_hypervisor_tables() { let sys = |funct7: u32, rs2: u32, rs1: u32| enc(funct7, rs2, rs1, 0, 0, 0x73); + assert!(decode(sys(0x00, 0x02, 0), Xlen::Rv64, &Isa::rv64gc()).is_illegal()); assert_eq!( - decode(sys(0x00, 0x02, 0), Xlen::Rv64, &Isa::rv64gc()).op, - Op::Uret - ); - assert_eq!( - decode(sys(0x08, 0x04, 10), Xlen::Rv64, &Isa::rv64gc()).op, - Op::SfenceVm + decode(sys(0x08, 0x02, 0), Xlen::Rv64, &Isa::rv64gc()).op, + Op::Sret ); + assert!(decode(0x1040_0073, Xlen::Rv64, &Isa::rv64gc()).is_illegal()); + assert!(decode(sys(0x08, 0x04, 10), Xlen::Rv64, &Isa::rv64gc()).is_illegal()); assert_eq!( decode(sys(0x09, 11, 10), Xlen::Rv64, &Isa::rv64gc()).op, Op::SfenceVma diff --git a/src/isa/riscv/disasm.rs b/src/isa/riscv/disasm.rs index 07c79d690..998e1e257 100644 --- a/src/isa/riscv/disasm.rs +++ b/src/isa/riscv/disasm.rs @@ -1356,11 +1356,11 @@ mod tests { |funct7: u32, rs2: u32, rs1: u32| (funct7 << 25) | (rs2 << 20) | (rs1 << 15) | 0x73; assert_eq!( decode(sys(0x00, 0x02, 0), Xlen::Rv64, &Isa::rv64gc()).to_string(), - "uret" + "illegal" ); assert_eq!( decode(sys(0x08, 0x04, 10), Xlen::Rv64, &Isa::rv64gc()).to_string(), - "sfence.vm a0" + "illegal" ); assert_eq!( decode(sys(0x09, 0, 0), Xlen::Rv64, &Isa::rv64gc()).to_string(), diff --git a/tests/suites/differential/riscv/vector/reserved_encoding.rs b/tests/suites/differential/riscv/vector/reserved_encoding.rs index c34805522..d4556ca25 100644 --- a/tests/suites/differential/riscv/vector/reserved_encoding.rs +++ b/tests/suites/differential/riscv/vector/reserved_encoding.rs @@ -189,6 +189,11 @@ fn diff_v_reserved_encoding_validation() { state(E8_M1, 8), )); } + batch.push(( + "vmul.vv.aligned-control".into(), + op_iv(0b100101, 1, 2, 4, 0b010, 0), + state(E32_M2, 2), + )); // Upward slides prohibit any source/destination group overlap. The exact // slide-by-one encodings use OPMVX/OPFVF funct3 values 110/101. Downward @@ -228,6 +233,119 @@ fn diff_v_reserved_encoding_validation() { )); } + // Complete same-width groups, mask-result overlap, and the generic masked + // destination rule are all checked before any architectural state changes. + for (name, funct6, funct3) in [ + ("vmul.vv", 0b100101, 0b010), + ("vdivu.vv", 0b100000, 0b010), + ("vsaddu.vv", 0b100000, 0b000), + ("vssrl.vv", 0b101010, 0b000), + ("vsmul.vv", 0b100111, 0b000), + ] { + batch.push(( + format!("{name}.misaligned-vd"), + op_iv(funct6, 1, 2, 4, funct3, 1), + state(E32_M2, 2), + )); + } + batch.push(( + "vslide1down.vx.misaligned-vs2".into(), + op_iv(0b001111, 1, 3, 5, 0b110, 2), + state(E32_M2, 2), + )); + batch.push(( + "vid.v.misaligned-vd".into(), + op_iv(0b010100, 1, 0, 0b10001, 0b010, 1), + state(E32_M2, 2), + )); + batch.push(( + "vmseq.vv.nonlowest-source-overlap".into(), + op_iv(0b011000, 1, 2, 4, 0b000, 3), + state(E32_M2, 2), + )); + batch.push(( + "vmadc.vv.nonlowest-source-overlap".into(), + op_iv(0b010001, 1, 2, 4, 0b000, 3), + state(E32_M2, 2), + )); + for (name, funct6) in [("vmseq.vv", 0b011000), ("vmadc.vv", 0b010001)] { + batch.push(( + format!("{name}.lowest-source-overlap-control"), + op_iv(funct6, 1, 2, 4, 0b000, 2), + state(E32_M2, 2), + )); + } + batch.push(( + "vadd.vv.masked-vd-v0".into(), + op_iv(0b000000, 0, 2, 4, 0b000, 0), + state(E32_M2, 2), + )); + batch.push(( + "vmseq.vv.masked-vd-v0-control".into(), + op_iv(0b011000, 0, 2, 4, 0b000, 0), + state(E32_M2, 2), + )); + + // Vector memory operands use data or index EMUL as selected by the + // addressing mode; segment fields each start at an aligned group. + let vle32_misaligned = (1 << 25) | (10 << 15) | (0b110 << 12) | (1 << 7) | 0x07; + batch.push(( + "vle32.v.misaligned-vd".into(), + vle32_misaligned, + state(E32_M2, 2), + )); + let vluxei64_misaligned_index = + (0b01 << 26) | (1 << 25) | (2 << 20) | (10 << 15) | (0b111 << 12) | (4 << 7) | 0x07; + batch.push(( + "vluxei64.v.misaligned-index".into(), + vluxei64_misaligned_index, + state(E32_M2, 2), + )); + let vlseg2e32_misaligned = (1 << 29) | (1 << 25) | (10 << 15) | (0b110 << 12) | (1 << 7) | 0x07; + batch.push(( + "vlseg2e32.v.misaligned-vd".into(), + vlseg2e32_misaligned, + state(E32_M2, 2), + )); + batch.push(( + "vlseg2e32.v.aligned-control".into(), + (1 << 29) | (1 << 25) | (10 << 15) | (0b110 << 12) | (2 << 7) | 0x07, + { + let mut control = state(E32_M2, 2); + control.x[10] = SCRATCH_BASE; + control + }, + )); + batch.push(( + "vluxseg2ei32.v.destination-index-overlap".into(), + (1 << 29) + | (0b01 << 26) + | (1 << 25) + | (4 << 20) + | (10 << 15) + | (0b110 << 12) + | (4 << 7) + | 0x07, + state(E32_M2, 2), + )); + + // Whole-register moves resume in SEW-sized effective elements. Segment + // fault-only-first loads with nf>0 are legal unit-stride encodings. + let mut vmvr_resume = state(E32_M1, 0); + vmvr_resume.vstart = 2; + batch.push(( + "vmv2r.v.vstart-two".into(), + op_iv(0b100111, 1, 2, 1, 0b011, 4), + vmvr_resume, + )); + let mut segment_fof = state(E8_M1, 2); + segment_fof.x[10] = SCRATCH_BASE; + batch.push(( + "vlseg2e8ff.v.control".into(), + (1 << 29) | (1 << 25) | (0b10000 << 20) | (10 << 15) | (1 << 7) | 0x07, + segment_fof, + )); + let narrowing: Vec<(&str, u32, u32)> = [ ("vnsrl.wv", 0b101100, 0), ("vnsra.wv", 0b101101, 0), diff --git a/tests/suites/smir/jit/riscv_x86_64/vector_validation.rs b/tests/suites/smir/jit/riscv_x86_64/vector_validation.rs index bceb0d5b1..5eaa7aa51 100644 --- a/tests/suites/smir/jit/riscv_x86_64/vector_validation.rs +++ b/tests/suites/smir/jit/riscv_x86_64/vector_validation.rs @@ -205,6 +205,77 @@ fn lifted_rv_vector_reserved_encodings_fail_closed_transactionally() { ..initial }, ), + // A masked non-mask result cannot write the v0 source mask. + ( + (2 << 20) | (4 << 15) | (0 << 7) | 0x57, + RiscVGuestRegs { + vtype: 0x11, // e32,m2 + ..initial + }, + ), + // vmul.vv requires aligned LMUL-sized destination and source groups. + ( + (0b100101 << 26) | (1 << 25) | (2 << 20) | (4 << 15) | (0b010 << 12) | (1 << 7) | 0x57, + RiscVGuestRegs { + vtype: 0x11, + ..initial + }, + ), + // A mask result may overlap only the lowest register of a data source. + ( + (0b011000 << 26) | (1 << 25) | (2 << 20) | (4 << 15) | (3 << 7) | 0x57, + RiscVGuestRegs { + vtype: 0x11, + ..initial + }, + ), + // vid.v still writes a complete LMUL-sized destination group. + ( + (0b010100 << 26) | (1 << 25) | (0b10001 << 15) | (0b010 << 12) | (1 << 7) | 0x57, + RiscVGuestRegs { + vtype: 0x11, + ..initial + }, + ), + // Downward slide overlap is legal, but each group must be aligned. + ( + (0b001111 << 26) | (1 << 25) | (3 << 20) | (5 << 15) | (0b110 << 12) | (2 << 7) | 0x57, + RiscVGuestRegs { + vtype: 0x11, + ..initial + }, + ), + // Non-segment and segment memory destinations use their derived EMUL. + ( + (1 << 25) | (10 << 15) | (0b110 << 12) | (1 << 7) | 0x07, + RiscVGuestRegs { + vtype: 0x11, + ..initial + }, + ), + ( + (1 << 29) | (1 << 25) | (10 << 15) | (0b110 << 12) | (1 << 7) | 0x07, + RiscVGuestRegs { + vtype: 0x11, + ..initial + }, + ), + // Indexed segment loads prohibit every destination/index overlap, + // including same-EEW overlap that ordinary indexed loads can allow. + ( + (1 << 29) + | (0b01 << 26) + | (1 << 25) + | (4 << 20) + | (10 << 15) + | (0b110 << 12) + | (4 << 7) + | 0x07, + RiscVGuestRegs { + vtype: 0x11, + ..initial + }, + ), ]; for (instruction, state) in cases { @@ -316,6 +387,22 @@ fn lifted_followup_rvv_controls_match_direct_at_o0_and_o2() { false, ); + // Aligned multiply groups and lowest-register mask-result overlap remain legal. + for instruction in [ + (0b100101 << 26) | (1 << 25) | (2 << 20) | (4 << 15) | (0b010 << 12) | 0x57, + (0b011000 << 26) | (1 << 25) | (2 << 20) | (4 << 15) | (2 << 7) | 0x57, + ] { + run_vector_case( + instruction, + RiscVGuestRegs { + vtype: 0x11, + ..initial + }, + [0xa5; MEMORY_LEN], + false, + ); + } + // Masked viota.m remains legal when vd is disjoint from v0 and vs2. run_vector_case( (0b010100 << 26) | (4 << 20) | (0b10000 << 15) | (0b010 << 12) | (2 << 7) | 0x57, @@ -334,4 +421,26 @@ fn lifted_followup_rvv_controls_match_direct_at_o0_and_o2() { [0xa5; MEMORY_LEN], false, ); + + // vmv2r.v resumes at vstart in SEW-sized effective elements. + run_vector_case( + (0b100111 << 26) | (1 << 25) | (2 << 20) | (1 << 15) | (0b011 << 12) | (4 << 7) | 0x57, + RiscVGuestRegs { + vtype: 0x10, // e32,m1 + vstart: 2, + ..initial + }, + [0xa5; MEMORY_LEN], + false, + ); + + // Segment fault-only-first encodings share the transactional vector helper. + let mut fof = initial; + fof.x[10] = DATA; + run_vector_case( + (1 << 29) | (1 << 25) | (0b10000 << 20) | (10 << 15) | (1 << 7) | 0x07, + fof, + [0xa5; MEMORY_LEN], + false, + ); } From 39561be9ffa15f668e3f3d866bfb8d067e379b49 Mon Sep 17 00:00:00 2001 From: int Date: Thu, 13 Aug 2026 18:22:07 +0200 Subject: [PATCH 2/3] fix(x86_64): canonicalize scalar EVEX FMA replay Preserve architecturally accepted LLIG guest encodings while emitting L'L=00 for dynamic-rounding host replay; retain L'L for embedded rounding controls. Co-authored-by: carlos <102978772+carlosqwqqwq@users.noreply.github.com> --- src/smir/ir/x86_native_replay.rs | 5 +- src/smir/ir/x86_native_replay/classifiers.rs | 1 + .../classifiers/evex_scalar_fma_llig.rs | 69 +++++++++++++++++++ src/smir/ir/x86_native_replay/grouping.rs | 3 + .../evex_fma3_register_replay.rs | 15 ++-- 5 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 src/smir/ir/x86_native_replay/classifiers/evex_scalar_fma_llig.rs diff --git a/src/smir/ir/x86_native_replay.rs b/src/smir/ir/x86_native_replay.rs index 32da4da7a..d37018e4b 100644 --- a/src/smir/ir/x86_native_replay.rs +++ b/src/smir/ir/x86_native_replay.rs @@ -193,8 +193,9 @@ pub struct X86NativeReplaySpan { /// Exclusive semantic-op end index. pub end: usize, /// Exact instruction to emit. This is normally the source instruction; - /// documented generation-dependent scalar VEX.L=1 sources and inert- - /// prefixed high-byte MUL/IMUL sources carry deterministic canonical encodings. + /// documented generation-dependent scalar VEX.L=1 sources, scalar EVEX + /// FMA3 LLIG sources, and inert-prefixed high-byte MUL/IMUL sources carry + /// deterministic canonical encodings. pub instruction: X86InstructionBytes, /// Whether native execution requires AVX-512VL. pub needs_avx512vl: bool, diff --git a/src/smir/ir/x86_native_replay/classifiers.rs b/src/smir/ir/x86_native_replay/classifiers.rs index 6592bad37..d4860de06 100644 --- a/src/smir/ir/x86_native_replay/classifiers.rs +++ b/src/smir/ir/x86_native_replay/classifiers.rs @@ -52,6 +52,7 @@ mod evex_packed_rotate_memory; mod evex_packed_variable_shift_memory; mod evex_psadbw_memory; mod evex_range_memory; +mod evex_scalar_fma_llig; mod evex_scalar_fp_arithmetic_memory; mod evex_scalar_fp_compare_memory; mod evex_scalar_fp_to_int_memory; diff --git a/src/smir/ir/x86_native_replay/classifiers/evex_scalar_fma_llig.rs b/src/smir/ir/x86_native_replay/classifiers/evex_scalar_fma_llig.rs new file mode 100644 index 000000000..6d89cadc1 --- /dev/null +++ b/src/smir/ir/x86_native_replay/classifiers/evex_scalar_fma_llig.rs @@ -0,0 +1,69 @@ +//! Deterministic host replay for architecturally ignored scalar EVEX L'L bits. + +use super::X86InstructionBytes; + +impl X86InstructionBytes { + /// Validate a register-only scalar EVEX FMA3 whose `EVEX.b=0` makes L'L + /// architecturally ignored, and return the equivalent L'L=00 host image. + /// Embedded-rounding forms retain L'L because those bits select EVEX.RC. + pub(crate) fn evex_scalar_fma_llig_canonical_ll0(&self) -> Option { + let bytes = self.as_slice(); + if bytes.len() != 6 || bytes[0] != 0x62 || bytes[3] & 0x10 != 0 { + return None; + } + let valid = self.evex_register_scalar_fma_needs_vl() == Some(false) + || self.evex_register_scalar_fp16_fma_needs_vl() == Some(false); + if !valid { + return None; + } + + let mut canonical = *self; + canonical.bytes[3] &= !0x60; + let canonical_valid = canonical.evex_register_scalar_fma_needs_vl() == Some(false) + || canonical.evex_register_scalar_fp16_fma_needs_vl() == Some(false); + canonical_valid.then_some(canonical) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn canonicalizes_only_dynamic_scalar_fma_llig() { + for map_w in [(2, false), (2, true), (6, false)] { + for ll in 0..4 { + let p0 = 0xE0 | map_w.0; + let p1 = (u8::from(map_w.1) << 7) | 0x75; + let dynamic = + X86InstructionBytes::new(&[0x62, p0, p1, 0x09 | (ll << 5), 0x99, 0xC2]) + .unwrap(); + let canonical = dynamic + .evex_scalar_fma_llig_canonical_ll0() + .expect("dynamic scalar FMA3"); + assert_eq!(canonical.as_slice()[3], 0x09); + + let embedded = + X86InstructionBytes::new(&[0x62, p0, p1, 0x19 | (ll << 5), 0x99, 0xC2]) + .unwrap(); + assert_eq!( + embedded.evex_scalar_fma_llig_canonical_ll0(), + None, + "embedded rounding must retain RC={ll}" + ); + } + } + } + + #[test] + fn rejects_packed_memory_and_non_fma_shapes() { + for bytes in [ + [0x62, 0xE2, 0x75, 0x09, 0x98, 0xC2], + [0x62, 0xE2, 0x75, 0x09, 0x99, 0x02], + [0x62, 0xE2, 0x75, 0x09, 0x58, 0xC2], + ] { + let instruction = X86InstructionBytes::new(&bytes).unwrap(); + assert_eq!(instruction.evex_scalar_fma_llig_canonical_ll0(), None); + } + } +} diff --git a/src/smir/ir/x86_native_replay/grouping.rs b/src/smir/ir/x86_native_replay/grouping.rs index 8dd11ea8b..c02f11471 100644 --- a/src/smir/ir/x86_native_replay/grouping.rs +++ b/src/smir/ir/x86_native_replay/grouping.rs @@ -111,6 +111,9 @@ pub(super) fn x86_native_replay_spans_where( let replay_source = high_byte_multiply .map(|replay| replay.canonical_instruction) .unwrap_or(source_instruction); + let replay_source = replay_source + .evex_scalar_fma_llig_canonical_ll0() + .unwrap_or(replay_source); let (instruction, (needs_avx512vl, needs_avx512dq, needs_avx512fp16)) = if let Some(requirements) = classify(&replay_source) { (replay_source, requirements) diff --git a/src/smir/lower/runtime/jit_gate_tests/evex_fma3_register_replay.rs b/src/smir/lower/runtime/jit_gate_tests/evex_fma3_register_replay.rs index 03ba91412..714d97638 100644 --- a/src/smir/lower/runtime/jit_gate_tests/evex_fma3_register_replay.rs +++ b/src/smir/lower/runtime/jit_gate_tests/evex_fma3_register_replay.rs @@ -554,6 +554,10 @@ fn lift_graphs_preserve_rounding_width_mask_and_fp_format() { fn assert_admits_and_lowers(case: FmaCase) -> usize { let bytes = case.bytes(); + let mut replay = bytes; + if case.scalar && !case.embedded_rounding { + replay[3] &= !0x60; + } let mut lowered = 0usize; for level in LEVELS { let function = optimized_function(&bytes, level); @@ -562,7 +566,7 @@ fn assert_admits_and_lowers(case: FmaCase) -> usize { .get(&0) .unwrap_or_else(|| panic!("{level:?} {case:?}: {:#?}", function.blocks[0].ops)); assert_eq!(span.end, function.blocks[0].ops.len(), "{level:?} {case:?}"); - assert_eq!(span.instruction.as_slice(), bytes, "{level:?} {case:?}"); + assert_eq!(span.instruction.as_slice(), replay, "{level:?} {case:?}"); assert!(!span.needs_avx512vl, "{level:?} {case:?}"); assert_eq!( span.needs_avx512fp16, @@ -583,7 +587,7 @@ fn assert_admits_and_lowers(case: FmaCase) -> usize { .finalize() .unwrap_or_else(|error| panic!("{level:?} {case:?}: {error:?}")); assert!( - code.windows(bytes.len()).any(|window| window == bytes), + code.windows(replay.len()).any(|window| window == replay), "{level:?} {case:?}" ); lowered += 1; @@ -892,8 +896,11 @@ fn execute_native(case: FmaCase, initial: &FmaState, level: OptLevel) -> FmaStat let code = lowerer .finalize() .unwrap_or_else(|error| panic!("{level:?} {case:?}: {error:?}")); - let bytes = case.bytes(); - assert!(code.windows(bytes.len()).any(|window| window == bytes)); + let mut replay = case.bytes(); + if case.scalar && !case.embedded_rounding { + replay[3] &= !0x60; + } + assert!(code.windows(replay.len()).any(|window| window == replay)); let executable = ExecMem::new(&code).expect("map EVEX FMA3 register replay"); let mut registers = GuestRegs { From 75e71722762ac7619952c9de13b6a2df54ed9203 Mon Sep 17 00:00:00 2001 From: int Date: Thu, 13 Aug 2026 18:43:17 +0200 Subject: [PATCH 3/3] fix(x86_64): canonicalize scalar helper replay Preserve architecturally accepted VRANGE and VSCALEF LLIG guest images while emitting the deterministic L'L=00 form for hosted helper replay. Co-authored-by: carlos <102978772+carlosqwqqwq@users.noreply.github.com> --- .../classifiers/evex_range_memory.rs | 8 +++-- .../classifiers/evex_scale_f_memory.rs | 8 +++-- .../evex_range_memory_source/mod.rs | 29 ++++++++++++++++- .../evex_scale_f_memory_source/mod.rs | 32 ++++++++++++++++++- 4 files changed, 71 insertions(+), 6 deletions(-) diff --git a/src/smir/ir/x86_native_replay/classifiers/evex_range_memory.rs b/src/smir/ir/x86_native_replay/classifiers/evex_range_memory.rs index 75fbb187d..1c1b0db4a 100644 --- a/src/smir/ir/x86_native_replay/classifiers/evex_range_memory.rs +++ b/src/smir/ir/x86_native_replay/classifiers/evex_range_memory.rs @@ -129,6 +129,8 @@ impl X86InstructionBytes { /// Segment/address-size prefixes and APX B4/X4 extensions remain confined /// to helper address evaluation. Rewrites preserve every architectural /// vector operand, opmask, zeroing policy, vector length, and imm8 control. + /// Scalar helper replay canonicalizes architecturally ignored L'L to 00B + /// so the newly emitted host instruction is deterministic across CPUs. pub(crate) fn evex_range_memory_encoding(&self) -> Option { let bytes = self.as_slice(); let start = vector_legacy_prefix_len(bytes); @@ -218,8 +220,10 @@ impl X86InstructionBytes { (p0 & 0x97) | 0x60, // Preserve W/vvvv/pp and restore the ordinary EVEX.U bit. p1 | 0x04, - // Preserve z, L'L, b, V', and aaa exactly. - p2, + // Preserve every meaningful control. Scalar L'L is ignored by + // the guest ISA, so canonicalize it for hosted replay; packed + // L'L still selects the architectural vector width. + if scalar { p2 & !0x60 } else { p2 }, opcode, (modrm & 0x38) | 0x04, 0x24, diff --git a/src/smir/ir/x86_native_replay/classifiers/evex_scale_f_memory.rs b/src/smir/ir/x86_native_replay/classifiers/evex_scale_f_memory.rs index 655c8dc48..177c67df9 100644 --- a/src/smir/ir/x86_native_replay/classifiers/evex_scale_f_memory.rs +++ b/src/smir/ir/x86_native_replay/classifiers/evex_scale_f_memory.rs @@ -129,6 +129,8 @@ impl X86InstructionBytes { /// Segment/address-size prefixes and APX B4/X4 extensions remain confined /// to helper address evaluation. Rewrites preserve every architectural /// vector operand, opmask, zeroing policy, vector length, and precision. + /// Scalar helper replay canonicalizes architecturally ignored L'L to 00B + /// so the newly emitted host instruction is deterministic across CPUs. pub(crate) fn evex_scale_f_memory_encoding(&self) -> Option { let bytes = self.as_slice(); let start = vector_legacy_prefix_len(bytes); @@ -209,8 +211,10 @@ impl X86InstructionBytes { (p0 & 0x97) | 0x60, // Preserve W/vvvv/pp and restore the ordinary EVEX.U bit. p1 | 0x04, - // Preserve z, L'L, b, V', and aaa exactly. - p2, + // Preserve every meaningful control. Scalar L'L is ignored by + // the guest ISA, so canonicalize it for hosted replay; packed + // L'L still selects the architectural vector width. + if scalar { p2 & !0x60 } else { p2 }, opcode, (modrm & 0x38) | 0x04, 0x24, diff --git a/src/smir/lower/runtime/jit_gate_tests/evex_range_memory_source/mod.rs b/src/smir/lower/runtime/jit_gate_tests/evex_range_memory_source/mod.rs index 6f06bc65d..78f2c686d 100644 --- a/src/smir/lower/runtime/jit_gate_tests/evex_range_memory_source/mod.rs +++ b/src/smir/lower/runtime/jit_gate_tests/evex_range_memory_source/mod.rs @@ -196,7 +196,10 @@ fn memory_encoding( } fn stack_encoding(case: RangeMemoryCase, mask: u8, zeroing: bool) -> Vec { - let (p0, p1, p2) = evex_fields(case, mask, zeroing); + let (p0, p1, mut p2) = evex_fields(case, mask, zeroing); + if case.scalar() { + p2 &= !0x60; + } vec![ 0x62, p0, @@ -209,6 +212,30 @@ fn stack_encoding(case: RangeMemoryCase, mask: u8, zeroing: bool) -> Vec { ] } +#[test] +fn scalar_llig_is_accepted_and_canonicalized_for_host_replay() { + for elem in [VecElementType::F32, VecElementType::F64] { + for ll in 0..4 { + let case = RangeMemoryCase { + elem, + width: VecWidth::V128, + destination: 17, + source1: 18, + form: SourceForm::Scalar { ll }, + control: MaskControl::Merge, + immediate: 0x0D, + }; + let guest = case.bytes(); + assert_eq!((guest[3] >> 5) & 3, ll); + let replay = X86InstructionBytes::new(&guest) + .unwrap() + .evex_range_memory_encoding() + .expect("scalar VRANGE LLIG image"); + assert_eq!(replay_instruction(replay)[3] & 0x60, 0); + } + } +} + fn register_encoding(case: RangeMemoryCase, source2: u8) -> Vec { assert!(source2 < 32 && !case.broadcast()); let (mut p0, p1, mut p2) = evex_fields(case, case.mask(), case.zeroing()); diff --git a/src/smir/lower/runtime/jit_gate_tests/evex_scale_f_memory_source/mod.rs b/src/smir/lower/runtime/jit_gate_tests/evex_scale_f_memory_source/mod.rs index 331c4ec1d..eb9a4406b 100644 --- a/src/smir/lower/runtime/jit_gate_tests/evex_scale_f_memory_source/mod.rs +++ b/src/smir/lower/runtime/jit_gate_tests/evex_scale_f_memory_source/mod.rs @@ -202,7 +202,10 @@ fn memory_encoding( } fn stack_encoding(case: ScaleFMemoryCase, mask: u8, zeroing: bool) -> Vec { - let (p0, p1, p2) = evex_fields(case, mask, zeroing); + let (p0, p1, mut p2) = evex_fields(case, mask, zeroing); + if case.scalar() { + p2 &= !0x60; + } vec![ 0x62, p0, @@ -214,6 +217,33 @@ fn stack_encoding(case: ScaleFMemoryCase, mask: u8, zeroing: bool) -> Vec { ] } +#[test] +fn scalar_llig_is_accepted_and_canonicalized_for_host_replay() { + for elem in [ + VecElementType::F16, + VecElementType::F32, + VecElementType::F64, + ] { + for ll in 0..4 { + let case = ScaleFMemoryCase { + elem, + width: VecWidth::V128, + destination: 17, + source1: 18, + form: SourceForm::Scalar { ll }, + control: MaskControl::Merge, + }; + let guest = case.bytes(); + assert_eq!((guest[3] >> 5) & 3, ll); + let replay = X86InstructionBytes::new(&guest) + .unwrap() + .evex_scale_f_memory_encoding() + .expect("scalar VSCALEF LLIG image"); + assert_eq!(replay_instruction(replay)[3] & 0x60, 0); + } + } +} + fn register_encoding(case: ScaleFMemoryCase, source2: u8) -> Vec { assert!(source2 < 32 && !case.broadcast()); let (mut p0, p1, mut p2) = evex_fields(case, case.mask(), case.zeroing());