diff --git a/src/isa/riscv/cpu/jit.rs b/src/isa/riscv/cpu/jit.rs index 3c8728fdb..626f09308 100644 --- a/src/isa/riscv/cpu/jit.rs +++ b/src/isa/riscv/cpu/jit.rs @@ -1322,10 +1322,11 @@ mod tests { } fn cpu_with_word(word: u32) -> RiscVCpu { - let mut cpu = RiscVCpu::new( - RiscVConfig::rv64gc(), - Box::new(FlatMemory::new(0, MEMORY_LEN)), - ); + cpu_with_config_word(RiscVConfig::rv64gc(), word) + } + + fn cpu_with_config_word(config: RiscVConfig, word: u32) -> RiscVCpu { + let mut cpu = RiscVCpu::new(config, Box::new(FlatMemory::new(0, MEMORY_LEN))); cpu.write_memory(CODE, &word.to_le_bytes()).unwrap(); cpu.set_pc(CODE); cpu @@ -1427,6 +1428,48 @@ mod tests { } } + #[test] + fn jit_boundary_rejects_reserved_rv32_shift_immediates() { + let shift_imm = |funct6: u32, funct3: u32| { + (funct6 << 26) | (0b10_0000 << 20) | (2 << 15) | (funct3 << 12) | (1 << 7) | 0x13 + }; + let words = [ + shift_imm(0b000000, 0b001), // slli + shift_imm(0b001010, 0b001), // bseti + shift_imm(0b010010, 0b001), // bclri + shift_imm(0b011010, 0b001), // binvi + shift_imm(0b000000, 0b101), // srli + shift_imm(0b010000, 0b101), // srai + shift_imm(0b011000, 0b101), // rori + shift_imm(0b010010, 0b101), // bexti + ]; + let config = RiscVConfig::rv32(Isa::rv64gc()); + + for level in [OptLevel::O0, OptLevel::O2] { + for word in words { + let expected = RiscVExit::Trap(Trap::illegal(word)); + + let mut direct = cpu_with_config_word(config, word); + assert_eq!(direct.step(), expected, "direct: {word:#010x}"); + assert_eq!(direct.instret(), 0, "direct: {word:#010x}"); + + let mut jit = cpu_with_config_word(config, word); + assert_eq!(jit.step_jit(level), expected, "{level:?}: {word:#010x}"); + assert_eq!(jit.instret(), 0, "{level:?}: {word:#010x}"); + assert_eq!( + jit.jit_stats().native_executions, + 0, + "{level:?}: {word:#010x}" + ); + assert_eq!( + jit.jit_stats().interpreter_fallbacks, + 1, + "{level:?}: {word:#010x}" + ); + } + } + } + #[test] fn production_jit_keeps_successful_load_to_x0_side_effects_at_o0_and_o2() { for level in [OptLevel::O0, OptLevel::O2] { diff --git a/src/isa/riscv/cpu/vector_validation.rs b/src/isa/riscv/cpu/vector_validation.rs index 7f91d5d97..aad9976f6 100644 --- a/src/isa/riscv/cpu/vector_validation.rs +++ b/src/isa/riscv/cpu/vector_validation.rs @@ -208,17 +208,69 @@ fn validate_reduction(cpu: &RiscVCpu, insn: &Insn) -> Result<(), Trap> { validate_reduction_source_group(cpu, insn) } -fn validate_same_width_integer_alu(cpu: &RiscVCpu, insn: &Insn) -> Result<(), Trap> { +fn validate_same_width_integer_alu( + cpu: &RiscVCpu, + insn: &Insn, + vector_vector_funct3: u8, +) -> Result<(), Trap> { // Every same-width vector operand names the LMUL-sized register group. // Scalar/immediate forms retain only vd and vs2 as vector groups. same_width_group(cpu, insn, insn.rd)?; same_width_group(cpu, insn, insn.rs2)?; - if insn.funct3 == 0b000 { + if insn.funct3 == vector_vector_funct3 { same_width_group(cpu, insn, insn.rs1)?; } Ok(()) } +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)?; + if destination.overlaps(data) { + return Err(illegal(insn)); + } + + let index = match insn.op { + Op::Vrgatherei16 => { + let index_emul = match cpu.sew_bytes() { + 1 => current_lmul(cpu, insn)?.widen(), + 2 => current_lmul(cpu, insn)?, + 4 => current_lmul(cpu, insn)?.narrow(2), + 8 => current_lmul(cpu, insn)?.narrow(4), + _ => return Err(illegal(insn)), + }; + RegisterGroup::for_emul(insn.rs1, index_emul).ok_or_else(|| illegal(insn))? + } + Op::Vrgather if insn.funct3 == 0b000 => same_width_group(cpu, insn, insn.rs1)?, + Op::Vrgather => return Ok(()), + _ => unreachable!("gather validator called for non-gather operation"), + }; + + if destination.overlaps(index) + || (matches!(insn.op, Op::Vrgatherei16) && cpu.sew_bytes() != 2 && data.overlaps(index)) + { + return Err(illegal(insn)); + } + Ok(()) +} + +fn validate_iota(cpu: &RiscVCpu, insn: &Insn, vm: bool) -> Result<(), Trap> { + if cpu.vstart != 0 { + return Err(illegal(insn)); + } + + let destination = same_width_group(cpu, insn, insn.rd)?; + let source_mask = RegisterGroup { + first: insn.rs2, + count: 1, + }; + let execution_mask = RegisterGroup { first: 0, count: 1 }; + if destination.overlaps(source_mask) || (!vm && destination.overlaps(execution_mask)) { + return Err(illegal(insn)); + } + Ok(()) +} + fn is_vector_fp_encoding(insn: &Insn) -> bool { // OPFVV and OPFVF are the complete floating-point classes under OP-V. // Classifying the encoding, rather than maintaining an operation whitelist, @@ -256,6 +308,9 @@ pub(super) fn validate(cpu: &RiscVCpu, insn: &Insn, vm: bool) -> Result<(), Trap } match insn.op { + Op::VmvXS | Op::VmvSX | Op::VfmvFS | Op::VfmvSF if !vm => { + return Err(illegal(insn)); + } Op::Vmerge if vm => { // vmv.v.v/vx/vi share the vmerge encoding. The vm=1 move forms // reserve vs2 and require that field to encode v0. @@ -275,6 +330,7 @@ pub(super) fn validate(cpu: &RiscVCpu, insn: &Insn, vm: bool) -> Result<(), Trap return Err(illegal(insn)); } } + Op::Viota => validate_iota(cpu, insn, vm)?, Op::Vadc | Op::Vsbc => { if vm || insn.rd == 0 { return Err(illegal(insn)); @@ -283,6 +339,7 @@ pub(super) fn validate(cpu: &RiscVCpu, insn: &Insn, vm: bool) -> Result<(), Trap Op::Vslideup | Op::Vslide1up | Op::Vfslide1up => { validate_slide_up(cpu, insn)?; } + Op::Vrgather | Op::Vrgatherei16 => validate_gather(cpu, insn)?, Op::Vadd | Op::Vsub | Op::Vrsub @@ -295,7 +352,10 @@ 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)?, + | Op::Vsra => validate_same_width_integer_alu(cpu, insn, 0b000)?, + Op::Vaaddu | Op::Vaadd | Op::Vasubu | Op::Vasub => { + validate_same_width_integer_alu(cpu, insn, 0b010)?; + } Op::Vnsrl | Op::Vnsra | Op::Vnclipu @@ -491,6 +551,66 @@ mod tests { } } + #[test] + fn scalar_move_forms_reserve_masked_encodings() { + let forms = [ + (0b010, 2, 0, 1), // vmv.x.s x1,v2 + (0b110, 0, 3, 2), // vmv.s.x v2,x3 + (0b001, 2, 0, 1), // vfmv.f.s f1,v2 + (0b101, 0, 3, 2), // vfmv.s.f v2,f3 + ]; + for (funct3, vs2, src, vd) in forms { + assert_illegal(op_v(0b010000, 0, vs2, src, funct3, vd), E32_M1, 4, 0, 0); + assert_legal(op_v(0b010000, 1, vs2, src, funct3, vd), E32_M1, 4, 0, 0); + } + } + + #[test] + fn vrgatherei16_validates_exact_index_emul_and_source_aliasing() { + let gather = |vd, data, index| op_v(0b001110, 1, data, index, 0b000, vd); + + // At SEW=8, LMUL=2, the 16-bit index operand has EMUL=4. Its + // register group must start at a multiple of four and remain in v0-v31. + assert_illegal(gather(0, 2, 6), 0x01, 4, 0, 0); + assert_legal(gather(0, 2, 8), 0x01, 4, 0, 0); + assert_legal(gather(0, 8, 28), 0x01, 4, 0, 0); + assert_illegal(gather(0, 8, 30), 0x01, 4, 0, 0); + + // A source register cannot be read through both the 8-bit data group + // and the 16-bit index group. At SEW=16 their EEWs match, so an exact + // group alias remains legal. + assert_illegal(gather(0, 2, 2), E8_M1, 4, 0, 0); + assert_legal(gather(0, 2, 2), 0x09, 4, 0, 0); // e16,m2 + } + + #[test] + fn averaging_integer_groups_must_be_aligned() { + for funct6 in [0b001000, 0b001001, 0b001010, 0b001011] { + let form = |vd, vs2, src, funct3| op_v(funct6, 1, vs2, src, funct3, vd); + + assert_illegal(form(1, 2, 4, 0b010), E32_M2, 2, 0, 0); + assert_illegal(form(0, 3, 4, 0b010), E32_M2, 2, 0, 0); + assert_illegal(form(0, 2, 5, 0b010), E32_M2, 2, 0, 0); + assert_legal(form(0, 2, 4, 0b010), E32_M2, 2, 0, 0); + + // The vx form's rs1 field names a scalar integer register. + assert_legal(form(0, 2, 5, 0b110), E32_M2, 2, 0, 0); + } + } + + #[test] + fn viota_enforces_nonrestartable_and_operand_overlap_rules() { + let iota = |vm, vd, source| op_v(0b010100, vm, source, 0b10000, 0b010, vd); + + assert_illegal(iota(0, 0, 2), E8_M1, 4, 0, 0); + assert_illegal(iota(1, 2, 2), E8_M1, 4, 0, 0); + // Under LMUL=2, vd=v2 occupies v2-v3 and therefore overlaps source v3. + assert_illegal(iota(1, 2, 3), E32_M2, 4, 0, 0); + assert_illegal(iota(1, 2, 4), E8_M1, 4, 1, 0); + assert_legal(iota(0, 2, 4), E8_M1, 4, 0, 0); + assert_legal(iota(1, 2, 4), E8_M1, 4, 0, 0); + } + #[test] fn same_width_integer_groups_must_be_aligned() { // e32,m2 makes vd, vs2, and the vv-form vs1 two-register groups. diff --git a/src/isa/riscv/decode.rs b/src/isa/riscv/decode.rs index 8d2f94826..309f63171 100644 --- a/src/isa/riscv/decode.rs +++ b/src/isa/riscv/decode.rs @@ -12,6 +12,9 @@ use super::memory::{MemError, Memory}; use super::{Isa, Xlen}; +mod op_imm; +use op_imm::{decode_op_imm, decode_op_imm32}; + /// A decoded RISC-V operation. One variant per architectural operation across /// the I/M/A/F/D/C and Zb* extensions; operand fields live in [`Insn`]. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -2118,181 +2121,6 @@ fn decode_store(w: u32, rv64: bool, isa: &Isa) -> Insn { with_imm(op, w, imm_s(w)) } -fn decode_op_imm(w: u32, rv64: bool, isa: &Isa) -> Insn { - match funct3(w) { - 0 => with_imm(Op::Addi, w, imm_i(w)), - 2 => with_imm(Op::Slti, w, imm_i(w)), - 3 => with_imm(Op::Sltiu, w, imm_i(w)), - 4 => with_imm(Op::Xori, w, imm_i(w)), - 6 if isa.zicbop && rd(w) == 0 => { - let op = match rs2(w) { - 0 => Op::PrefetchI, - 1 => Op::PrefetchR, - 3 => Op::PrefetchW, - _ => Op::Ori, - }; - if matches!(op, Op::Ori) { - with_imm(op, w, imm_i(w)) - } else { - with_imm(op, w, imm_i(w) & !0x1f) - } - } - 6 => with_imm(Op::Ori, w, imm_i(w)), - 7 => with_imm(Op::Andi, w, imm_i(w)), - 1 => decode_shift_left_imm(w, rv64, isa), - 5 => decode_shift_right_imm(w, rv64, isa), - _ => Insn::illegal(w, 4), - } -} - -// OP-IMM funct3==1 (SLLI and Zbb/Zbs left-shift-immediate overlays). -fn decode_shift_left_imm(w: u32, rv64: bool, isa: &Isa) -> Insn { - let funct6 = (w >> 26) & 0x3f; - let funct7 = funct7(w); - let shamt = ((w >> 20) & if rv64 { 0x3f } else { 0x1f }) as i64; - let rs2f = rs2(w); - // SHA / SM3 message-schedule transforms (funct7 = 0b0001000). - if funct7 == 0b0001000 { - match rs2f { - 0b00000 if isa.zknh => return base(Op::Sha256Sum0, w), - 0b00001 if isa.zknh => return base(Op::Sha256Sum1, w), - 0b00010 if isa.zknh => return base(Op::Sha256Sig0, w), - 0b00011 if isa.zknh => return base(Op::Sha256Sig1, w), - 0b00100 if rv64 && isa.zknh => return base(Op::Sha512Sum0, w), - 0b00101 if rv64 && isa.zknh => return base(Op::Sha512Sum1, w), - 0b00110 if rv64 && isa.zknh => return base(Op::Sha512Sig0, w), - 0b00111 if rv64 && isa.zknh => return base(Op::Sha512Sig1, w), - 0b01000 if isa.zksh => return base(Op::Sm3p0, w), - 0b01001 if isa.zksh => return base(Op::Sm3p1, w), - _ => {} - } - } - // AES-64 decrypt InvMixColumns / key-schedule step 1 (funct7 = 0b0011000). - if rv64 && funct7 == 0b0011000 { - if rs2f == 0 && isa.zknd { - return base(Op::Aes64im, w); - } - if rs2f & 0b10000 != 0 && (isa.zkne || isa.zknd) { - // aes64ks1i: rnum in rs2[3:0], must be <= 0xA. - if (rs2f & 0xf) <= 0xA { - return base(Op::Aes64ks1i, w); - } - } - } - // CLZ/CTZ/CPOP/SEXT.B/SEXT.H share funct7=0b0110000. - if isa.zbb && funct7 == 0b0110000 { - let op = match rs2f { - 0b00000 => Op::Clz, - 0b00001 => Op::Ctz, - 0b00010 => Op::Cpop, - 0b00100 => Op::SextB, - 0b00101 => Op::SextH, - _ => return Insn::illegal(w, 4), - }; - return base(op, w); - } - // Zbkb zip: RV32-only, funct7=0b0000100, shamt/rs2 field = 15. - if isa.zbkb && !rv64 && funct7 == 0b0000100 && rs2f == 0b01111 { - return base(Op::Zip, w); - } - if isa.zbs { - match funct6 { - 0b010010 => return with_imm(Op::Bclri, w, shamt), - 0b011010 => return with_imm(Op::Binvi, w, shamt), - 0b001010 => return with_imm(Op::Bseti, w, shamt), - _ => {} - } - } - // SLLI: funct6 must be zero (RV64) / funct7 zero (RV32). - if (rv64 && funct6 == 0) || (!rv64 && funct7 == 0) { - return with_imm(Op::Slli, w, shamt); - } - Insn::illegal(w, 4) -} - -// OP-IMM funct3==5 (SRLI/SRAI and Zbb/Zbs right-shift-immediate overlays). -fn decode_shift_right_imm(w: u32, rv64: bool, isa: &Isa) -> Insn { - let funct6 = (w >> 26) & 0x3f; - let funct7 = funct7(w); - let rs2f = rs2(w); - let shamt = ((w >> 20) & if rv64 { 0x3f } else { 0x1f }) as i64; - if isa.zbb { - // ORC.B: funct7=0b0010100, rs2=0b00111. - if funct7 == 0b0010100 && rs2f == 0b00111 { - return base(Op::Orcb, w); - } - // REV8: RV64 funct12=0b011010111000, RV32 funct12=0b011010011000. - let funct12 = (w >> 20) & 0xfff; - if (rv64 && funct12 == 0b0110_1011_1000) || (!rv64 && funct12 == 0b0110_1001_1000) { - return base(Op::Rev8, w); - } - } - // Zbkb brev8: funct7=0b0110100, rs2=0b00111, funct3=5. - if isa.zbkb && funct7 == 0b0110100 && rs2f == 0b00111 { - return base(Op::Brev8, w); - } - // Zbkb unzip: RV32-only, funct7=0b0000100, shamt/rs2 field = 15. - if isa.zbkb && !rv64 && funct7 == 0b0000100 && rs2f == 0b01111 { - return base(Op::Unzip, w); - } - if isa.zbb { - if funct6 == 0b011000 { - return with_imm(Op::Rori, w, shamt); - } - } - if isa.zbs && funct6 == 0b010010 { - return with_imm(Op::Bexti, w, shamt); - } - match funct6 { - 0b000000 => with_imm(Op::Srli, w, shamt), - 0b010000 => with_imm(Op::Srai, w, shamt), - _ if !rv64 && funct7 == 0b0000000 => with_imm(Op::Srli, w, shamt), - _ if !rv64 && funct7 == 0b0100000 => with_imm(Op::Srai, w, shamt), - _ => Insn::illegal(w, 4), - } -} - -// OP-IMM-32 (RV64 word immediate ops + Zba/Zbb word overlays). -fn decode_op_imm32(w: u32, isa: &Isa) -> Insn { - let funct7 = funct7(w); - let funct6 = (w >> 26) & 0x3f; - let rs2f = rs2(w); - let shamt5 = ((w >> 20) & 0x1f) as i64; - let shamt6 = ((w >> 20) & 0x3f) as i64; - match funct3(w) { - 0 => with_imm(Op::Addiw, w, imm_i(w)), - 1 => { - if isa.zba && funct6 == 0b000010 { - return with_imm(Op::SlliUw, w, shamt6); - } - if isa.zbb && funct7 == 0b0110000 { - let op = match rs2f { - 0b00000 => Op::Clzw, - 0b00001 => Op::Ctzw, - 0b00010 => Op::Cpopw, - _ => return Insn::illegal(w, 4), - }; - return base(op, w); - } - if funct7 == 0 { - return with_imm(Op::Slliw, w, shamt5); - } - Insn::illegal(w, 4) - } - 5 => { - if isa.zbb && funct7 == 0b0110000 { - return with_imm(Op::Roriw, w, shamt5); - } - match funct7 { - 0b0000000 => with_imm(Op::Srliw, w, shamt5), - 0b0100000 => with_imm(Op::Sraiw, w, shamt5), - _ => Insn::illegal(w, 4), - } - } - _ => Insn::illegal(w, 4), - } -} - // OP (R-type): base, M, Zba/Zbb/Zbc/Zbs overlays. fn decode_op(w: u32, rv64: bool, isa: &Isa) -> Insn { let f3 = funct3(w); diff --git a/src/isa/riscv/decode/op_imm.rs b/src/isa/riscv/decode/op_imm.rs new file mode 100644 index 000000000..86db62c24 --- /dev/null +++ b/src/isa/riscv/decode/op_imm.rs @@ -0,0 +1,226 @@ +//! OP-IMM and OP-IMM-32 decoding. + +use super::*; + +pub(super) fn decode_op_imm(w: u32, rv64: bool, isa: &Isa) -> Insn { + match funct3(w) { + 0 => with_imm(Op::Addi, w, imm_i(w)), + 2 => with_imm(Op::Slti, w, imm_i(w)), + 3 => with_imm(Op::Sltiu, w, imm_i(w)), + 4 => with_imm(Op::Xori, w, imm_i(w)), + 6 if isa.zicbop && rd(w) == 0 => { + let op = match rs2(w) { + 0 => Op::PrefetchI, + 1 => Op::PrefetchR, + 3 => Op::PrefetchW, + _ => Op::Ori, + }; + if matches!(op, Op::Ori) { + with_imm(op, w, imm_i(w)) + } else { + with_imm(op, w, imm_i(w) & !0x1f) + } + } + 6 => with_imm(Op::Ori, w, imm_i(w)), + 7 => with_imm(Op::Andi, w, imm_i(w)), + 1 => decode_shift_left_imm(w, rv64, isa), + 5 => decode_shift_right_imm(w, rv64, isa), + _ => Insn::illegal(w, 4), + } +} + +// OP-IMM funct3==1 (SLLI and Zbb/Zbs left-shift-immediate overlays). +fn decode_shift_left_imm(w: u32, rv64: bool, isa: &Isa) -> Insn { + if !rv64 && w & (1 << 25) != 0 { + return Insn::illegal(w, 4); + } + + let funct6 = (w >> 26) & 0x3f; + let funct7 = funct7(w); + let shamt = ((w >> 20) & if rv64 { 0x3f } else { 0x1f }) as i64; + let rs2f = rs2(w); + // SHA / SM3 message-schedule transforms (funct7 = 0b0001000). + if funct7 == 0b0001000 { + match rs2f { + 0b00000 if isa.zknh => return base(Op::Sha256Sum0, w), + 0b00001 if isa.zknh => return base(Op::Sha256Sum1, w), + 0b00010 if isa.zknh => return base(Op::Sha256Sig0, w), + 0b00011 if isa.zknh => return base(Op::Sha256Sig1, w), + 0b00100 if rv64 && isa.zknh => return base(Op::Sha512Sum0, w), + 0b00101 if rv64 && isa.zknh => return base(Op::Sha512Sum1, w), + 0b00110 if rv64 && isa.zknh => return base(Op::Sha512Sig0, w), + 0b00111 if rv64 && isa.zknh => return base(Op::Sha512Sig1, w), + 0b01000 if isa.zksh => return base(Op::Sm3p0, w), + 0b01001 if isa.zksh => return base(Op::Sm3p1, w), + _ => {} + } + } + // AES-64 decrypt InvMixColumns / key-schedule step 1 (funct7 = 0b0011000). + if rv64 && funct7 == 0b0011000 { + if rs2f == 0 && isa.zknd { + return base(Op::Aes64im, w); + } + if rs2f & 0b10000 != 0 && (isa.zkne || isa.zknd) { + // aes64ks1i: rnum in rs2[3:0], must be <= 0xA. + if (rs2f & 0xf) <= 0xA { + return base(Op::Aes64ks1i, w); + } + } + } + // CLZ/CTZ/CPOP/SEXT.B/SEXT.H share funct7=0b0110000. + if isa.zbb && funct7 == 0b0110000 { + let op = match rs2f { + 0b00000 => Op::Clz, + 0b00001 => Op::Ctz, + 0b00010 => Op::Cpop, + 0b00100 => Op::SextB, + 0b00101 => Op::SextH, + _ => return Insn::illegal(w, 4), + }; + return base(op, w); + } + // Zbkb zip: RV32-only, funct7=0b0000100, shamt/rs2 field = 15. + if isa.zbkb && !rv64 && funct7 == 0b0000100 && rs2f == 0b01111 { + return base(Op::Zip, w); + } + if isa.zbs { + match funct6 { + 0b010010 => return with_imm(Op::Bclri, w, shamt), + 0b011010 => return with_imm(Op::Binvi, w, shamt), + 0b001010 => return with_imm(Op::Bseti, w, shamt), + _ => {} + } + } + // SLLI: funct6 must be zero (RV64) / funct7 zero (RV32). + if (rv64 && funct6 == 0) || (!rv64 && funct7 == 0) { + return with_imm(Op::Slli, w, shamt); + } + Insn::illegal(w, 4) +} + +// OP-IMM funct3==5 (SRLI/SRAI and Zbb/Zbs right-shift-immediate overlays). +fn decode_shift_right_imm(w: u32, rv64: bool, isa: &Isa) -> Insn { + if !rv64 && w & (1 << 25) != 0 { + return Insn::illegal(w, 4); + } + + let funct6 = (w >> 26) & 0x3f; + let funct7 = funct7(w); + let rs2f = rs2(w); + let shamt = ((w >> 20) & if rv64 { 0x3f } else { 0x1f }) as i64; + if isa.zbb { + // ORC.B: funct7=0b0010100, rs2=0b00111. + if funct7 == 0b0010100 && rs2f == 0b00111 { + return base(Op::Orcb, w); + } + // REV8: RV64 funct12=0b011010111000, RV32 funct12=0b011010011000. + let funct12 = (w >> 20) & 0xfff; + if (rv64 && funct12 == 0b0110_1011_1000) || (!rv64 && funct12 == 0b0110_1001_1000) { + return base(Op::Rev8, w); + } + } + // Zbkb brev8: funct7=0b0110100, rs2=0b00111, funct3=5. + if isa.zbkb && funct7 == 0b0110100 && rs2f == 0b00111 { + return base(Op::Brev8, w); + } + // Zbkb unzip: RV32-only, funct7=0b0000100, shamt/rs2 field = 15. + if isa.zbkb && !rv64 && funct7 == 0b0000100 && rs2f == 0b01111 { + return base(Op::Unzip, w); + } + if isa.zbb && funct6 == 0b011000 { + return with_imm(Op::Rori, w, shamt); + } + if isa.zbs && funct6 == 0b010010 { + return with_imm(Op::Bexti, w, shamt); + } + match funct6 { + 0b000000 => with_imm(Op::Srli, w, shamt), + 0b010000 => with_imm(Op::Srai, w, shamt), + _ if !rv64 && funct7 == 0b0000000 => with_imm(Op::Srli, w, shamt), + _ if !rv64 && funct7 == 0b0100000 => with_imm(Op::Srai, w, shamt), + _ => Insn::illegal(w, 4), + } +} + +// OP-IMM-32 (RV64 word immediate ops + Zba/Zbb word overlays). +pub(super) fn decode_op_imm32(w: u32, isa: &Isa) -> Insn { + let funct7 = funct7(w); + let funct6 = (w >> 26) & 0x3f; + let rs2f = rs2(w); + let shamt5 = ((w >> 20) & 0x1f) as i64; + let shamt6 = ((w >> 20) & 0x3f) as i64; + match funct3(w) { + 0 => with_imm(Op::Addiw, w, imm_i(w)), + 1 => { + if isa.zba && funct6 == 0b000010 { + return with_imm(Op::SlliUw, w, shamt6); + } + if isa.zbb && funct7 == 0b0110000 { + let op = match rs2f { + 0b00000 => Op::Clzw, + 0b00001 => Op::Ctzw, + 0b00010 => Op::Cpopw, + _ => return Insn::illegal(w, 4), + }; + return base(op, w); + } + if funct7 == 0 { + return with_imm(Op::Slliw, w, shamt5); + } + Insn::illegal(w, 4) + } + 5 => { + if isa.zbb && funct7 == 0b0110000 { + return with_imm(Op::Roriw, w, shamt5); + } + match funct7 { + 0b0000000 => with_imm(Op::Srliw, w, shamt5), + 0b0100000 => with_imm(Op::Sraiw, w, shamt5), + _ => Insn::illegal(w, 4), + } + } + _ => Insn::illegal(w, 4), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn shift_imm(funct6: u32, shamt: u32, funct3: u32) -> u32 { + (funct6 << 26) | (shamt << 20) | (2 << 15) | (funct3 << 12) | (1 << 7) | 0x13 + } + + #[test] + fn rv32_rejects_shamt_bit_five_for_every_shift_immediate_family() { + let isa = Isa::rv64gc(); + let forms = [ + (Op::Slli, 0b000000, 0b001), + (Op::Bseti, 0b001010, 0b001), + (Op::Bclri, 0b010010, 0b001), + (Op::Binvi, 0b011010, 0b001), + (Op::Srli, 0b000000, 0b101), + (Op::Srai, 0b010000, 0b101), + (Op::Rori, 0b011000, 0b101), + (Op::Bexti, 0b010010, 0b101), + ]; + + for (expected, funct6, funct3) in forms { + let reserved_rv32 = shift_imm(funct6, 0b10_0000, funct3); + assert_eq!( + decode(reserved_rv32, Xlen::Rv32, &isa).op, + Op::Illegal, + "RV32 accepted bit 25 for {expected:?}" + ); + + let legal_rv32 = shift_imm(funct6, 0b01_1111, funct3); + let decoded_rv32 = decode(legal_rv32, Xlen::Rv32, &isa); + assert_eq!(decoded_rv32.op, expected); + assert_eq!(decoded_rv32.imm, 31); + + let legal_rv64 = decode(reserved_rv32, Xlen::Rv64, &isa); + assert_eq!(legal_rv64.op, expected); + assert_eq!(legal_rv64.imm, 32); + } + } +} diff --git a/src/smir/ir/x86_native_replay/classifiers/evex_fixup_imm_memory.rs b/src/smir/ir/x86_native_replay/classifiers/evex_fixup_imm_memory.rs index f6ccaeb43..0ae369cf6 100644 --- a/src/smir/ir/x86_native_replay/classifiers/evex_fixup_imm_memory.rs +++ b/src/smir/ir/x86_native_replay/classifiers/evex_fixup_imm_memory.rs @@ -99,8 +99,11 @@ impl X86InstructionBytes { /// and binary64. Packed L'L selects 128/256/512 bits and memory EVEX.b /// selects m32bcst/m64bcst. Scalar L'L is ignored, including 11B, while /// scalar memory EVEX.b is reserved because scalar instructions do not - /// support broadcast and SAE applies only to register sources. Every form - /// carries an unconstrained imm8 response/reporting control. + /// support broadcast and SAE applies only to register sources. Scalar + /// helper replay canonicalizes LLIG to L'L=00B: this preserves the guest + /// semantics while avoiding processor-specific #UD behavior for ignored + /// values in a newly emitted hosted instruction. Every form carries an + /// unconstrained imm8 response/reporting control. /// /// Segment/address-size prefixes and APX B4/X4 extensions remain confined /// to helper address evaluation. Rewrites therefore remove those address @@ -184,8 +187,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/lift/riscv/mod.rs b/src/smir/lift/riscv/mod.rs index fdcecfd3b..5f803ab4d 100644 --- a/src/smir/lift/riscv/mod.rs +++ b/src/smir/lift/riscv/mod.rs @@ -735,6 +735,35 @@ mod tests { ); } + #[test] + fn rv32_shift_immediates_with_shamt_bit_five_fail_at_decode_frontier() { + let shift_imm = |funct6: u32, funct3: u32| { + (funct6 << 26) | (0b10_0000 << 20) | (2 << 15) | (funct3 << 12) | (1 << 7) | 0x13 + }; + let words = [ + shift_imm(0b000000, 0b001), + shift_imm(0b001010, 0b001), + shift_imm(0b010010, 0b001), + shift_imm(0b011010, 0b001), + shift_imm(0b000000, 0b101), + shift_imm(0b010000, 0b101), + shift_imm(0b011000, 0b101), + shift_imm(0b010010, 0b101), + ]; + + for word in words { + let mut lifter = RiscVLifter::new_rv32(RiscVExtensions::rv64gc()); + let mut context = LiftContext::new(SourceArch::RiscV32); + assert!( + matches!( + lifter.lift_insn(0x1000, &word.to_le_bytes(), &mut context), + Err(LiftError::InvalidEncoding { .. }) + ), + "reserved RV32 shift {word:#010x} passed the lift frontier" + ); + } + } + #[test] fn fp_and_vector_helpers_decode_with_the_configured_profile() { let fld = i_type(0, 1, 0b011, 1, 0x07); diff --git a/src/smir/lower/runtime/jit_gate_tests/evex_fixup_imm_memory_source/mod.rs b/src/smir/lower/runtime/jit_gate_tests/evex_fixup_imm_memory_source/mod.rs index 8dd2086c1..6e28eae82 100644 --- a/src/smir/lower/runtime/jit_gate_tests/evex_fixup_imm_memory_source/mod.rs +++ b/src/smir/lower/runtime/jit_gate_tests/evex_fixup_imm_memory_source/mod.rs @@ -228,6 +228,9 @@ fn stack_encoding( immediate, ); encoding[1] |= 0x20; + if scalar { + encoding[3] &= !0x60; + } [ encoding[0], encoding[1], @@ -720,7 +723,11 @@ fn scalar_fixup_llig_accepts_all_four_values_and_reserves_memory_sae() { let X86EvexFixupImmMemoryReplay::Scalar { stack_instruction } = encoding.replay else { panic!("{bytes:02X?}: scalar selected non-scalar replay") }; - assert_eq!(stack_instruction.as_slice()[3], bytes[3]); + assert_eq!( + stack_instruction.as_slice()[3], + bytes[3] & !0x60, + "guest LLIG must be accepted but canonicalized for hosted replay" + ); let mut reserved_memory_sae = bytes; reserved_memory_sae[3] |= 0x10; diff --git a/src/smir/lower/runtime/jit_gate_tests/evex_fixup_imm_memory_source/native.rs b/src/smir/lower/runtime/jit_gate_tests/evex_fixup_imm_memory_source/native.rs index d7fddd93c..40c9a2042 100644 --- a/src/smir/lower/runtime/jit_gate_tests/evex_fixup_imm_memory_source/native.rs +++ b/src/smir/lower/runtime/jit_gate_tests/evex_fixup_imm_memory_source/native.rs @@ -50,14 +50,13 @@ fn expected_vector_scratch(value: [u64; 8], width: VecWidth) -> [u64; 8] { std::array::from_fn(|word| if word < words { value[word] } else { 0 }) } -#[test] -fn native_fixup_memory_matches_interpretation_faults_mxcsr_and_mask_suppression() { - use super::super::vex_fma3_memory_source::{VectorMemoryContext, vector_load_helper}; +const CHILD_CASE_ENV: &str = "RAX_EVEX_FIXUP_IMM_MEMORY_CHILD_CASE"; +const TEST_NAME: &str = concat!( + "smir::lower::runtime::jit_gate_tests::evex_fixup_imm_memory_source::native::", + "native_fixup_memory_matches_interpretation_faults_mxcsr_and_mask_suppression" +); - if !std::is_x86_feature_detected!("avx512f") || !std::is_x86_feature_detected!("avx512bw") { - eprintln!("skipping native VFIXUPIMM memory differential: host lacks AVX-512F/BW"); - return; - } +fn native_cases() -> Vec { let has_vl = std::is_x86_feature_detected!("avx512vl"); let selected = [ FixupMemoryCase { @@ -110,179 +109,227 @@ fn native_fixup_memory_matches_interpretation_faults_mxcsr_and_mask_suppression( }, ]; - let cases: Vec<_> = selected + selected .into_iter() .filter(|case| !case.needs_avx512vl() || has_vl) - .collect(); - assert!(cases.len() >= 4); + .collect() +} + +fn execute_native_case(case: FixupMemoryCase, ordinal: usize) { + use super::super::vex_fma3_memory_source::{VectorMemoryContext, vector_load_helper}; + let mut successes = 0usize; let mut faults = 0usize; let mut suppressions = 0usize; - for (ordinal, case) in cases.into_iter().enumerate() { - for level in [OptLevel::O0, OptLevel::O2] { - let function = optimize(lift_case(case), level); - let (code, entry) = lower(&function, case); - let exec = - ExecMem::new(&code).unwrap_or_else(|error| panic!("{level:?} {case:?}: {error:?}")); - let value = memory_value(case, ordinal); - let bytes = memory_bytes(value); - - if case.form == SourceForm::Vector && case.control == MaskControl::None { - let mut context = VectorMemoryContext { - value, - ok: 1, - calls: 0, - last_addr: 0, - last_index: 0, - last_size: 0, - last_zero_upper: 0, - }; - let mut registers = initial_registers(case, ordinal); - registers.ctx = (&mut context as *mut VectorMemoryContext) as u64; - registers.vec_load_fn = vector_load_helper as *const () as usize as u64; - let mut expected = interpreter_success(&function, ®isters, value, case); - expected.vector_scratch = expected_vector_scratch(value, case.width); - - exec.run(entry, &mut registers); - expected.host_mxcsr = registers.host_mxcsr; - assert_eq!(registers, expected, "{level:?} {case:?}: success"); - assert_eq!(context.calls, 1, "{level:?} {case:?}"); - assert_eq!(context.last_addr, 0x2000, "{level:?} {case:?}"); - assert_eq!( - context.last_index, - crate::smir::lower::X86_JIT_VECTOR_SCRATCH_INDEX, - "{level:?} {case:?}" - ); - assert_eq!(context.last_size, case.width.bytes(), "{level:?} {case:?}"); - assert_eq!(context.last_zero_upper, 1, "{level:?} {case:?}"); - successes += 1; - - let mut context = VectorMemoryContext { - value, - ok: 0, - calls: 0, - last_addr: 0, - last_index: 0, - last_size: 0, - last_zero_upper: 0, - }; - let mut registers = initial_registers(case, ordinal ^ 0x55); - registers.ctx = (&mut context as *mut VectorMemoryContext) as u64; - registers.vec_load_fn = vector_load_helper as *const () as usize as u64; - let mut expected = registers; - expected.exit_pc = PC; - - exec.run(entry, &mut registers); - expected.host_mxcsr = registers.host_mxcsr; - assert_eq!(registers, expected, "{level:?} {case:?}: fault"); - assert_eq!(context.calls, 1, "{level:?} {case:?}: fault"); - faults += 1; - continue; - } + for level in [OptLevel::O0, OptLevel::O2] { + let function = optimize(lift_case(case), level); + let (code, entry) = lower(&function, case); + let exec = + ExecMem::new(&code).unwrap_or_else(|error| panic!("{level:?} {case:?}: {error:?}")); + let value = memory_value(case, ordinal); + let bytes = memory_bytes(value); - let mut registers = initial_registers(case, ordinal); - if case.mask() != 0 { - registers.k[usize::from(case.mask())] = if case.scalar() { 1 } else { 0x5555_5555 }; - } - let lanes = if case.scalar() { - 1 - } else { - case.width.lanes(case.elem) - }; - let active_mask = if case.mask() == 0 { - (1u64 << lanes) - 1 - } else { - registers.k[usize::from(case.mask())] - }; - let mut context = LaneMemoryContext { - base: 0x2000, - value: bytes, - lane_bytes: case.elem.bytes() as usize, - fail_address: None, + if case.form == SourceForm::Vector && case.control == MaskControl::None { + let mut context = VectorMemoryContext { + value, + ok: 1, calls: 0, - addresses: [0; 32], + last_addr: 0, + last_index: 0, + last_size: 0, + last_zero_upper: 0, }; - registers.ctx = (&mut context as *mut LaneMemoryContext) as u64; - registers.load_fn = lane_load_helper as *const () as usize as u64; + let mut registers = initial_registers(case, ordinal); + registers.ctx = (&mut context as *mut VectorMemoryContext) as u64; + registers.vec_load_fn = vector_load_helper as *const () as usize as u64; let mut expected = interpreter_success(&function, ®isters, value, case); + expected.vector_scratch = expected_vector_scratch(value, case.width); exec.run(entry, &mut registers); expected.host_mxcsr = registers.host_mxcsr; assert_eq!(registers, expected, "{level:?} {case:?}: success"); - let expected_addresses: Vec = if case.scalar() || case.broadcast() { - vec![0x2000] - } else { - (0..lanes) - .filter(|lane| active_mask & (1 << lane) != 0) - .map(|lane| 0x2000 + u64::from(lane) * u64::from(case.elem.bytes())) - .collect() - }; + assert_eq!(context.calls, 1, "{level:?} {case:?}"); + assert_eq!(context.last_addr, 0x2000, "{level:?} {case:?}"); assert_eq!( - &context.addresses[..context.calls], - expected_addresses, - "{level:?} {case:?}: active source addresses" + context.last_index, + crate::smir::lower::X86_JIT_VECTOR_SCRATCH_INDEX, + "{level:?} {case:?}" ); + assert_eq!(context.last_size, case.width.bytes(), "{level:?} {case:?}"); + assert_eq!(context.last_zero_upper, 1, "{level:?} {case:?}"); successes += 1; - let mut registers = initial_registers(case, ordinal ^ 0x55); - if case.mask() != 0 { - registers.k[usize::from(case.mask())] = if case.scalar() { 1 } else { 0b1101 }; - } - let fail_address = if case.scalar() || case.broadcast() { - 0x2000 - } else { - 0x2000 + 2 * u64::from(case.elem.bytes()) + let mut context = VectorMemoryContext { + value, + ok: 0, + calls: 0, + last_addr: 0, + last_index: 0, + last_size: 0, + last_zero_upper: 0, }; + let mut registers = initial_registers(case, ordinal ^ 0x55); + registers.ctx = (&mut context as *mut VectorMemoryContext) as u64; + registers.vec_load_fn = vector_load_helper as *const () as usize as u64; + let mut expected = registers; + expected.exit_pc = PC; + + exec.run(entry, &mut registers); + expected.host_mxcsr = registers.host_mxcsr; + assert_eq!(registers, expected, "{level:?} {case:?}: fault"); + assert_eq!(context.calls, 1, "{level:?} {case:?}: fault"); + faults += 1; + continue; + } + + let mut registers = initial_registers(case, ordinal); + if case.mask() != 0 { + registers.k[usize::from(case.mask())] = if case.scalar() { 1 } else { 0x5555_5555 }; + } + let lanes = if case.scalar() { + 1 + } else { + case.width.lanes(case.elem) + }; + let active_mask = if case.mask() == 0 { + (1u64 << lanes) - 1 + } else { + registers.k[usize::from(case.mask())] + }; + let mut context = LaneMemoryContext { + base: 0x2000, + value: bytes, + lane_bytes: case.elem.bytes() as usize, + fail_address: None, + calls: 0, + addresses: [0; 32], + }; + registers.ctx = (&mut context as *mut LaneMemoryContext) as u64; + registers.load_fn = lane_load_helper as *const () as usize as u64; + let mut expected = interpreter_success(&function, ®isters, value, case); + + exec.run(entry, &mut registers); + expected.host_mxcsr = registers.host_mxcsr; + assert_eq!(registers, expected, "{level:?} {case:?}: success"); + let expected_addresses: Vec = if case.scalar() || case.broadcast() { + vec![0x2000] + } else { + (0..lanes) + .filter(|lane| active_mask & (1 << lane) != 0) + .map(|lane| 0x2000 + u64::from(lane) * u64::from(case.elem.bytes())) + .collect() + }; + assert_eq!( + &context.addresses[..context.calls], + expected_addresses, + "{level:?} {case:?}: active source addresses" + ); + successes += 1; + + let mut registers = initial_registers(case, ordinal ^ 0x55); + if case.mask() != 0 { + registers.k[usize::from(case.mask())] = if case.scalar() { 1 } else { 0b1101 }; + } + let fail_address = if case.scalar() || case.broadcast() { + 0x2000 + } else { + 0x2000 + 2 * u64::from(case.elem.bytes()) + }; + let mut context = LaneMemoryContext { + base: 0x2000, + value: bytes, + lane_bytes: case.elem.bytes() as usize, + fail_address: Some(fail_address), + calls: 0, + addresses: [0; 32], + }; + registers.ctx = (&mut context as *mut LaneMemoryContext) as u64; + registers.load_fn = lane_load_helper as *const () as usize as u64; + let mut expected = registers; + expected.exit_pc = PC; + + exec.run(entry, &mut registers); + expected.host_mxcsr = registers.host_mxcsr; + assert_eq!(registers, expected, "{level:?} {case:?}: fault"); + assert_eq!( + context.addresses[context.calls - 1], + fail_address, + "{level:?} {case:?}: fault address" + ); + faults += 1; + + if case.mask() != 0 { + let mut registers = initial_registers(case, ordinal ^ 0xAA); + registers.k[usize::from(case.mask())] = 0; let mut context = LaneMemoryContext { base: 0x2000, value: bytes, lane_bytes: case.elem.bytes() as usize, - fail_address: Some(fail_address), + fail_address: Some(0x2000), calls: 0, addresses: [0; 32], }; registers.ctx = (&mut context as *mut LaneMemoryContext) as u64; registers.load_fn = lane_load_helper as *const () as usize as u64; - let mut expected = registers; - expected.exit_pc = PC; + let mut expected = interpreter_success(&function, ®isters, value, case); exec.run(entry, &mut registers); expected.host_mxcsr = registers.host_mxcsr; - assert_eq!(registers, expected, "{level:?} {case:?}: fault"); assert_eq!( - context.addresses[context.calls - 1], - fail_address, - "{level:?} {case:?}: fault address" + registers, expected, + "{level:?} {case:?}: all applicable lanes suppressed" ); - faults += 1; + assert_eq!(context.calls, 0, "{level:?} {case:?}"); + suppressions += 1; + } + } + assert_eq!(successes, 2, "{case:?}"); + assert_eq!(faults, 2, "{case:?}"); + assert_eq!( + suppressions, + if case.mask() == 0 { 0 } else { 2 }, + "{case:?}" + ); +} + +#[test] +fn native_fixup_memory_matches_interpretation_faults_mxcsr_and_mask_suppression() { + if !std::is_x86_feature_detected!("avx512f") || !std::is_x86_feature_detected!("avx512bw") { + eprintln!("skipping native VFIXUPIMM memory differential: host lacks AVX-512F/BW"); + return; + } - if case.mask() != 0 { - let mut registers = initial_registers(case, ordinal ^ 0xAA); - registers.k[usize::from(case.mask())] = 0; - let mut context = LaneMemoryContext { - base: 0x2000, - value: bytes, - lane_bytes: case.elem.bytes() as usize, - fail_address: Some(0x2000), - calls: 0, - addresses: [0; 32], - }; - registers.ctx = (&mut context as *mut LaneMemoryContext) as u64; - registers.load_fn = lane_load_helper as *const () as usize as u64; - let mut expected = interpreter_success(&function, ®isters, value, case); + let cases = native_cases(); + assert!(cases.len() >= 4); + if let Ok(value) = std::env::var(CHILD_CASE_ENV) { + let ordinal: usize = value + .parse() + .unwrap_or_else(|_| panic!("invalid {CHILD_CASE_ENV}: {value}")); + let case = *cases + .get(ordinal) + .unwrap_or_else(|| panic!("{CHILD_CASE_ENV} out of range: {ordinal}")); + execute_native_case(case, ordinal); + return; + } - exec.run(entry, &mut registers); - expected.host_mxcsr = registers.host_mxcsr; - assert_eq!( - registers, expected, - "{level:?} {case:?}: all applicable lanes suppressed" - ); - assert_eq!(context.calls, 0, "{level:?} {case:?}"); - suppressions += 1; - } - } + for (ordinal, case) in cases.into_iter().enumerate() { + let output = std::process::Command::new( + std::env::current_exe().expect("current unit-test executable"), + ) + .arg(TEST_NAME) + .arg("--exact") + .arg("--nocapture") + .env(CHILD_CASE_ENV, ordinal.to_string()) + .output() + .expect("run isolated native VFIXUPIMM memory differential"); + assert!( + output.status.success(), + "isolated native VFIXUPIMM failure at case {ordinal}/{}: {case:?}; status {}; \ + stdout: {}; stderr: {}", + ordinal + 1, + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); } - assert!(successes >= 8); - assert_eq!(successes, faults); - assert!(suppressions >= 6); } diff --git a/tests/suites/differential/riscv/vector/reserved_encoding.rs b/tests/suites/differential/riscv/vector/reserved_encoding.rs index 236282664..c34805522 100644 --- a/tests/suites/differential/riscv/vector/reserved_encoding.rs +++ b/tests/suites/differential/riscv/vector/reserved_encoding.rs @@ -5,6 +5,7 @@ use super::*; #[test] fn diff_v_reserved_encoding_validation() { const E8_M1: u64 = 0x00; + const E8_M2: u64 = 0x01; const E16_MF8: u64 = 0x0d; const E32_M1: u64 = 0x10; const E32_M2: u64 = 0x11; @@ -77,6 +78,97 @@ fn diff_v_reserved_encoding_validation() { )); } + // Vector/scalar moves reserve every masked encoding in both directions. + for (name, funct3, vs2, src, vd) in [ + ("vmv.x.s", 0b010, 2, 0, 1), + ("vmv.s.x", 0b110, 0, 5, 2), + ("vfmv.f.s", 0b001, 2, 0, 1), + ("vfmv.s.f", 0b101, 0, 5, 2), + ] { + batch.push(( + format!("{name}.masked-reserved"), + op_iv(0b010000, 0, vs2, src, funct3, vd), + state(E32_M1, 4), + )); + batch.push(( + format!("{name}.unmasked-control"), + op_iv(0b010000, 1, vs2, src, funct3, vd), + state(E32_M1, 4), + )); + } + + // vrgatherei16.vv derives index EMUL=(16/SEW)*LMUL. At e8,m2 the + // index occupies four registers and must start at a multiple of four. + for (name, index) in [("misaligned-index", 6), ("aligned-index-control", 8)] { + batch.push(( + format!("vrgatherei16.vv.{name}"), + op_iv(0b001110, 1, 2, index, 0b000, 0), + state(E8_M2, 4), + )); + } + batch.push(( + "vrgatherei16.vv.mixed-eew-source-alias".into(), + op_iv(0b001110, 1, 2, 2, 0b000, 0), + state(E8_M1, 4), + )); + batch.push(( + "vrgatherei16.vv.same-eew-source-alias-control".into(), + op_iv(0b001110, 1, 2, 2, 0b000, 0), + state(0x09, 4), // e16,m2 + )); + + // Averaging add/subtract forms use complete LMUL-sized groups for every + // vector operand; vx forms retain scalar rs1. + for (name, funct6) in [ + ("vaaddu", 0b001000), + ("vaadd", 0b001001), + ("vasubu", 0b001010), + ("vasub", 0b001011), + ] { + batch.push(( + format!("{name}.vv.misaligned-vd"), + op_iv(funct6, 1, 2, 4, 0b010, 1), + state(E32_M2, 2), + )); + batch.push(( + format!("{name}.vv.misaligned-vs1"), + op_iv(funct6, 1, 2, 5, 0b010, 0), + state(E32_M2, 2), + )); + batch.push(( + format!("{name}.vx.scalar-rs1-control"), + op_iv(funct6, 1, 2, 5, 0b110, 0), + state(E32_M2, 2), + )); + } + + // viota.m is nonrestartable; vd may overlap neither vs2 nor the masked + // execution register v0. + batch.push(( + "viota.m.masked-vd-v0".into(), + op_iv(0b010100, 0, 2, 0b10000, 0b010, 0), + state(E8_M1, 4), + )); + batch.push(( + "viota.m.vd-vs2-overlap".into(), + op_iv(0b010100, 1, 2, 0b10000, 0b010, 2), + state(E8_M1, 4), + )); + let mut iota_nonrestartable = state(E8_M1, 4); + iota_nonrestartable.vstart = 1; + batch.push(( + "viota.m.nonzero-vstart".into(), + op_iv(0b010100, 1, 4, 0b10000, 0b010, 2), + iota_nonrestartable, + )); + for (name, vm) in [("masked-control", 0), ("unmasked-control", 1)] { + batch.push(( + format!("viota.m.{name}"), + op_iv(0b010100, vm, 4, 0b10000, 0b010, 2), + state(E8_M1, 4), + )); + } + // vadc/vsbc consume v0 as carry/borrow-in, so vm=1 and vd=v0 are reserved // for every defined vv/vx/vi form. for (name, funct6, funct3) in [ 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 a72e0a1d3..bceb0d5b1 100644 --- a/tests/suites/smir/jit/riscv_x86_64/vector_validation.rs +++ b/tests/suites/smir/jit/riscv_x86_64/vector_validation.rs @@ -175,6 +175,36 @@ fn lifted_rv_vector_reserved_encodings_fail_closed_transactionally() { ..initial }, ), + // Masked scalar move encodings are reserved. + ( + (0b010000 << 26) | (2 << 20) | (0b010 << 12) | (1 << 7) | 0x57, + initial, + ), + // vrgatherei16.vv under e8,m2 gives the index an EMUL of four; + // v6 is therefore a misaligned index-group base. + ( + (0b001110 << 26) | (1 << 25) | (2 << 20) | (6 << 15) | (0b000 << 12) | 0x57, + RiscVGuestRegs { + vtype: 0x01, // e8,m2 + ..initial + }, + ), + // vaadd.vv v1,v2,v4 has a misaligned destination under LMUL=2. + ( + (0b001001 << 26) | (1 << 25) | (2 << 20) | (4 << 15) | (0b010 << 12) | (1 << 7) | 0x57, + RiscVGuestRegs { + vtype: 0x11, // e32,m2 + ..initial + }, + ), + // Masked viota.m cannot write the mask register v0. + ( + (0b010100 << 26) | (2 << 20) | (0b10000 << 15) | (0b010 << 12) | 0x57, + RiscVGuestRegs { + vtype: 0, // e8,m1 + ..initial + }, + ), ]; for (instruction, state) in cases { @@ -256,6 +286,44 @@ fn lifted_followup_rvv_controls_match_direct_at_o0_and_o2() { false, ); + // Unmasked vmv.x.s remains legal. + run_vector_case( + (0b010000 << 26) | (1 << 25) | (2 << 20) | (0b010 << 12) | (1 << 7) | 0x57, + initial, + [0xa5; MEMORY_LEN], + false, + ); + + // vrgatherei16.vv with an aligned four-register index group at e8,m2. + run_vector_case( + (0b001110 << 26) | (1 << 25) | (2 << 20) | (8 << 15) | 0x57, + RiscVGuestRegs { + vtype: 0x01, + ..initial + }, + [0xa5; MEMORY_LEN], + false, + ); + + // Aligned vaadd.vv groups under LMUL=2. + run_vector_case( + (0b001001 << 26) | (1 << 25) | (2 << 20) | (4 << 15) | (0b010 << 12) | 0x57, + 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, + initial, + [0xa5; MEMORY_LEN], + false, + ); + // vsetvli clears a guest-supplied nonzero vstart. run_vector_case( (7 << 12) | (1 << 7) | 0x57,