From 2325068e773c7c8195997d520cf71a96c656436a Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Thu, 19 Mar 2026 20:46:54 +0100 Subject: [PATCH] feat: i64 register pairs, globals/select, WAST pipeline, spec-testsuite i64 register pair support (all 38 operations): - Arithmetic: ADDS/ADC, SUBS/SBC for add/sub; UMULL+MLA for mul - Bitwise: AND/ORR/EOR on both halves - Comparisons: ORR+CMP for eqz; CMP hi then lo for relational - Shifts: funnel shift sequences for shl/shr_u/shr_s/rotl/rotr - Division: binary long division pseudo-ops - Conversions: extend (ASR #31), wrap (take low word), load/store pairs - 47 new tests (29 instruction selector + 18 encoder) GlobalGet/GlobalSet: - R9 as globals base register, LDR/STR with 4-byte stride - Both stack and non-stack instruction selection modes Select instruction: - CMP + MOV + SelectMove (IT EQ; MOV) pattern WAST test pipeline: - wast_multi_func_test Bazel macro replaces 2480 lines with 25 targets - All 22 .wast files wired into renode_test pipeline - Compiles with --all-exports, tests all functions per module Spec test suite: - Added WebAssembly/testsuite as git submodule (257 .wast files) - ~20 files runnable today (i32 + control flow + locals) 762 tests total (up from 687), clippy clean, fmt clean. Implements: FR-002 Implements: FR-005 Trace: skip Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitmodules | 3 + artifacts/sw-verification.yaml | 52 + crates/synth-backend/src/arm_encoder.rs | 767 ++++- .../src/instruction_selector.rs | 1474 +++++++++- tests/spec-testsuite | 2 +- tests/wast/BUILD.bazel | 2515 +---------------- 6 files changed, 2346 insertions(+), 2467 deletions(-) create mode 100644 .gitmodules diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..5130229 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "tests/spec-testsuite"] + path = tests/spec-testsuite + url = https://github.com/WebAssembly/testsuite diff --git a/artifacts/sw-verification.yaml b/artifacts/sw-verification.yaml index f606b85..9f19e61 100644 --- a/artifacts/sw-verification.yaml +++ b/artifacts/sw-verification.yaml @@ -106,3 +106,55 @@ artifacts: steps: run: "cargo test -p synth-verify && bazel test //coq:verify_proofs" coverage: "Z3 SMT suite, Rocq proof compilation, proptest" + + - id: SWVER-006 + type: sw-verification + title: GlobalGet/GlobalSet instruction selection tests + description: > + Unit tests verifying correct instruction selection for WASM global + variable access (global.get and global.set). R9 is the dedicated + globals base register. GlobalGet loads from [R9, #index*4] into a + destination register; GlobalSet stores from a source register to + [R9, #index*4]. Tests cover both select_default (non-stack) and + select_with_stack (stack-tracking) code paths, including index + offset calculation, round-trip get-modify-set, and composition + with other instructions. + status: implemented + tags: [globals, instruction-selection, unit-tests] + links: + - type: verifies + target: TR-001 + - type: verifies + target: TR-004 + fields: + method: automated-test + steps: + run: "cargo test -p synth-synthesis -- test_global" + coverage: > + 8 tests: select_default (get index 0, get index 3, set index 0, + set index 5), stack mode (get, set, get-set roundtrip), + composition with select + + - id: SWVER-007 + type: sw-verification + title: Select instruction selection tests + description: > + Unit tests verifying correct instruction selection for the WASM + select instruction. Select pops condition, val2, val1 from the + stack and pushes val1 if condition != 0, else val2. ARM lowering + uses CMP + MOV (default) + IT EQ; MOVEQ (conditional override). + Tests cover both select_default and select_with_stack code paths, + and composition with GlobalGet/GlobalSet. + status: implemented + tags: [select, instruction-selection, unit-tests] + links: + - type: verifies + target: TR-001 + fields: + method: automated-test + steps: + run: "cargo test -p synth-synthesis -- test_select" + coverage: > + 5 tests: select_default mode, stack mode with constants, + stack mode already tested in control_flow_select, + composition with global values diff --git a/crates/synth-backend/src/arm_encoder.rs b/crates/synth-backend/src/arm_encoder.rs index 14ef578..9fd451b 100644 --- a/crates/synth-backend/src/arm_encoder.rs +++ b/crates/synth-backend/src/arm_encoder.rs @@ -4223,31 +4223,392 @@ impl ArmEncoder { ArmOp::I32TruncF64S { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, true), ArmOp::I32TruncF64U { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, false), - // i64 ops that are only stubs in Thumb mode - ArmOp::I64Add { .. } - | ArmOp::I64Sub { .. } - | ArmOp::I64And { .. } - | ArmOp::I64Or { .. } - | ArmOp::I64Xor { .. } - | ArmOp::I64Eqz { .. } - | ArmOp::I64Eq { .. } - | ArmOp::I64Ne { .. } - | ArmOp::I64LtS { .. } - | ArmOp::I64LtU { .. } - | ArmOp::I64LeS { .. } - | ArmOp::I64LeU { .. } - | ArmOp::I64GtS { .. } - | ArmOp::I64GtU { .. } - | ArmOp::I64GeS { .. } - | ArmOp::I64GeU { .. } - | ArmOp::I64Const { .. } - | ArmOp::I64Ldr { .. } - | ArmOp::I64Str { .. } - | ArmOp::I64ExtendI32S { .. } - | ArmOp::I64ExtendI32U { .. } - | ArmOp::I32WrapI64 { .. } => { - let instr: u16 = 0xBF00; // NOP - Ok(instr.to_le_bytes().to_vec()) + // ===== i64 operations: encode as multi-instruction Thumb-2 sequences ===== + + // I64Add: ADDS rdlo, rnlo, rmlo; ADC.W rdhi, rnhi, rmhi + ArmOp::I64Add { + rdlo, + rdhi, + rnlo, + rnhi, + rmlo, + rmhi, + } => { + let mut bytes = Vec::new(); + // ADDS rdlo, rnlo, rmlo (16-bit) + bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adds { + rd: *rdlo, + rn: *rnlo, + op2: Operand2::Reg(*rmlo), + })?); + // ADC.W rdhi, rnhi, rmhi (32-bit) + bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adc { + rd: *rdhi, + rn: *rnhi, + op2: Operand2::Reg(*rmhi), + })?); + Ok(bytes) + } + + // I64Sub: SUBS rdlo, rnlo, rmlo; SBC.W rdhi, rnhi, rmhi + ArmOp::I64Sub { + rdlo, + rdhi, + rnlo, + rnhi, + rmlo, + rmhi, + } => { + let mut bytes = Vec::new(); + // SUBS rdlo, rnlo, rmlo (16-bit) + bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Subs { + rd: *rdlo, + rn: *rnlo, + op2: Operand2::Reg(*rmlo), + })?); + // SBC.W rdhi, rnhi, rmhi (32-bit) + bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Sbc { + rd: *rdhi, + rn: *rnhi, + op2: Operand2::Reg(*rmhi), + })?); + Ok(bytes) + } + + // I64And: AND rdlo, rnlo, rmlo; AND rdhi, rnhi, rmhi + ArmOp::I64And { + rdlo, + rdhi, + rnlo, + rnhi, + rmlo, + rmhi, + } => { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And { + rd: *rdlo, + rn: *rnlo, + op2: Operand2::Reg(*rmlo), + })?); + bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And { + rd: *rdhi, + rn: *rnhi, + op2: Operand2::Reg(*rmhi), + })?); + Ok(bytes) + } + + // I64Or: ORR rdlo, rnlo, rmlo; ORR rdhi, rnhi, rmhi + ArmOp::I64Or { + rdlo, + rdhi, + rnlo, + rnhi, + rmlo, + rmhi, + } => { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr { + rd: *rdlo, + rn: *rnlo, + op2: Operand2::Reg(*rmlo), + })?); + bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr { + rd: *rdhi, + rn: *rnhi, + op2: Operand2::Reg(*rmhi), + })?); + Ok(bytes) + } + + // I64Xor: EOR rdlo, rnlo, rmlo; EOR rdhi, rnhi, rmhi + ArmOp::I64Xor { + rdlo, + rdhi, + rnlo, + rnhi, + rmlo, + rmhi, + } => { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor { + rd: *rdlo, + rn: *rnlo, + op2: Operand2::Reg(*rmlo), + })?); + bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor { + rd: *rdhi, + rn: *rnhi, + op2: Operand2::Reg(*rmhi), + })?); + Ok(bytes) + } + + // I64Eqz: ORR scratch, lo, hi; ITE EQ; MOV rd, #1; MOV rd, #0 + ArmOp::I64Eqz { rd, rnlo, rnhi } => self.encode_thumb(&ArmOp::I64SetCondZ { + rd: *rd, + rn_lo: *rnlo, + rn_hi: *rnhi, + }), + + // I64 comparisons: delegate to I64SetCond + ArmOp::I64Eq { + rd, + rnlo, + rnhi, + rmlo, + rmhi, + } => self.encode_thumb(&ArmOp::I64SetCond { + rd: *rd, + rn_lo: *rnlo, + rn_hi: *rnhi, + rm_lo: *rmlo, + rm_hi: *rmhi, + cond: synth_synthesis::Condition::EQ, + }), + + ArmOp::I64Ne { + rd, + rnlo, + rnhi, + rmlo, + rmhi, + } => self.encode_thumb(&ArmOp::I64SetCond { + rd: *rd, + rn_lo: *rnlo, + rn_hi: *rnhi, + rm_lo: *rmlo, + rm_hi: *rmhi, + cond: synth_synthesis::Condition::NE, + }), + + ArmOp::I64LtS { + rd, + rnlo, + rnhi, + rmlo, + rmhi, + } => self.encode_thumb(&ArmOp::I64SetCond { + rd: *rd, + rn_lo: *rnlo, + rn_hi: *rnhi, + rm_lo: *rmlo, + rm_hi: *rmhi, + cond: synth_synthesis::Condition::LT, + }), + + ArmOp::I64LtU { + rd, + rnlo, + rnhi, + rmlo, + rmhi, + } => self.encode_thumb(&ArmOp::I64SetCond { + rd: *rd, + rn_lo: *rnlo, + rn_hi: *rnhi, + rm_lo: *rmlo, + rm_hi: *rmhi, + cond: synth_synthesis::Condition::LO, + }), + + ArmOp::I64LeS { + rd, + rnlo, + rnhi, + rmlo, + rmhi, + } => self.encode_thumb(&ArmOp::I64SetCond { + rd: *rd, + rn_lo: *rnlo, + rn_hi: *rnhi, + rm_lo: *rmlo, + rm_hi: *rmhi, + cond: synth_synthesis::Condition::LE, + }), + + ArmOp::I64LeU { + rd, + rnlo, + rnhi, + rmlo, + rmhi, + } => self.encode_thumb(&ArmOp::I64SetCond { + rd: *rd, + rn_lo: *rnlo, + rn_hi: *rnhi, + rm_lo: *rmlo, + rm_hi: *rmhi, + cond: synth_synthesis::Condition::LS, + }), + + ArmOp::I64GtS { + rd, + rnlo, + rnhi, + rmlo, + rmhi, + } => self.encode_thumb(&ArmOp::I64SetCond { + rd: *rd, + rn_lo: *rnlo, + rn_hi: *rnhi, + rm_lo: *rmlo, + rm_hi: *rmhi, + cond: synth_synthesis::Condition::GT, + }), + + ArmOp::I64GtU { + rd, + rnlo, + rnhi, + rmlo, + rmhi, + } => self.encode_thumb(&ArmOp::I64SetCond { + rd: *rd, + rn_lo: *rnlo, + rn_hi: *rnhi, + rm_lo: *rmlo, + rm_hi: *rmhi, + cond: synth_synthesis::Condition::HI, + }), + + ArmOp::I64GeS { + rd, + rnlo, + rnhi, + rmlo, + rmhi, + } => self.encode_thumb(&ArmOp::I64SetCond { + rd: *rd, + rn_lo: *rnlo, + rn_hi: *rnhi, + rm_lo: *rmlo, + rm_hi: *rmhi, + cond: synth_synthesis::Condition::GE, + }), + + ArmOp::I64GeU { + rd, + rnlo, + rnhi, + rmlo, + rmhi, + } => self.encode_thumb(&ArmOp::I64SetCond { + rd: *rd, + rn_lo: *rnlo, + rn_hi: *rnhi, + rm_lo: *rmlo, + rm_hi: *rmhi, + cond: synth_synthesis::Condition::HS, + }), + + // I64Const: MOVW rdlo, lo16; MOVT rdlo, hi16; MOVW rdhi, lo16_hi; MOVT rdhi, hi16_hi + ArmOp::I64Const { rdlo, rdhi, value } => { + let lo32 = *value as u32; + let hi32 = (*value >> 32) as u32; + let mut bytes = Vec::new(); + // Load low 32 bits into rdlo + bytes.extend_from_slice( + &self.encode_thumb32_movw_raw(reg_to_bits(rdlo), lo32 & 0xFFFF)?, + ); + if lo32 > 0xFFFF { + bytes.extend_from_slice( + &self.encode_thumb32_movt_raw(reg_to_bits(rdlo), lo32 >> 16)?, + ); + } + // Load high 32 bits into rdhi + bytes.extend_from_slice( + &self.encode_thumb32_movw_raw(reg_to_bits(rdhi), hi32 & 0xFFFF)?, + ); + if hi32 > 0xFFFF { + bytes.extend_from_slice( + &self.encode_thumb32_movt_raw(reg_to_bits(rdhi), hi32 >> 16)?, + ); + } + Ok(bytes) + } + + // I64Ldr: LDR rdlo, [base, offset]; LDR rdhi, [base, offset+4] + ArmOp::I64Ldr { rdlo, rdhi, addr } => { + let mut bytes = Vec::new(); + let offset = if addr.offset < 0 { + 0u32 + } else { + addr.offset as u32 + }; + bytes.extend_from_slice(&self.encode_thumb32_ldr(rdlo, &addr.base, offset)?); + bytes.extend_from_slice(&self.encode_thumb32_ldr( + rdhi, + &addr.base, + offset.wrapping_add(4), + )?); + Ok(bytes) + } + + // I64Str: STR rdlo, [base, offset]; STR rdhi, [base, offset+4] + ArmOp::I64Str { rdlo, rdhi, addr } => { + let mut bytes = Vec::new(); + let offset = if addr.offset < 0 { + 0u32 + } else { + addr.offset as u32 + }; + bytes.extend_from_slice(&self.encode_thumb32_str(rdlo, &addr.base, offset)?); + bytes.extend_from_slice(&self.encode_thumb32_str( + rdhi, + &addr.base, + offset.wrapping_add(4), + )?); + Ok(bytes) + } + + // I64ExtendI32S: MOV rdlo, rn; ASR rdhi, rdlo, #31 (sign-extend) + ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => { + let mut bytes = Vec::new(); + if rdlo != rn { + // MOV rdlo, rn (16-bit) + bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov { + rd: *rdlo, + op2: Operand2::Reg(*rn), + })?); + } + // ASR rdhi, rdlo, #31 (sign-extend: fill high word with sign bit) + bytes.extend_from_slice( + &self.encode_thumb32_shift(rdhi, rdlo, 31, 0b10)?, // ASR type + ); + Ok(bytes) + } + + // I64ExtendI32U: MOV rdlo, rn; MOV rdhi, #0 + ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => { + let mut bytes = Vec::new(); + if rdlo != rn { + // MOV rdlo, rn + bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov { + rd: *rdlo, + op2: Operand2::Reg(*rn), + })?); + } + // MOV rdhi, #0 (16-bit: MOVS Rd, #0) + let rdhi_bits = reg_to_bits(rdhi) as u16; + let instr: u16 = 0x2000 | (rdhi_bits << 8); + bytes.extend_from_slice(&instr.to_le_bytes()); + Ok(bytes) + } + + // I32WrapI64: MOV rd, rnlo (just take low 32 bits) + ArmOp::I32WrapI64 { rd, rnlo } => { + if rd == rnlo { + // No-op: already in the right register + let instr: u16 = 0xBF00; // NOP + Ok(instr.to_le_bytes().to_vec()) + } else { + // MOV rd, rnlo + self.encode_thumb(&ArmOp::Mov { + rd: *rd, + op2: Operand2::Reg(*rnlo), + }) + } } // Catch-all for any remaining ops @@ -6212,4 +6573,360 @@ mod tests { // NOP: 0xBF00 in little-endian assert_eq!(code, vec![0x00, 0xBF]); } + + // ========================================================================= + // i64 Thumb-2 encoding tests + // ========================================================================= + + #[test] + fn test_encode_i64_add_thumb2() { + let encoder = ArmEncoder::new_thumb2(); + let op = ArmOp::I64Add { + rdlo: Reg::R0, + rdhi: Reg::R1, + rnlo: Reg::R0, + rnhi: Reg::R1, + rmlo: Reg::R2, + rmhi: Reg::R3, + }; + let code = encoder.encode(&op).unwrap(); + // Should emit ADDS (2 bytes) + ADC.W (4 bytes) = 6 bytes + assert_eq!(code.len(), 6, "I64Add should be 6 bytes (ADDS + ADC.W)"); + } + + #[test] + fn test_encode_i64_sub_thumb2() { + let encoder = ArmEncoder::new_thumb2(); + let op = ArmOp::I64Sub { + rdlo: Reg::R0, + rdhi: Reg::R1, + rnlo: Reg::R0, + rnhi: Reg::R1, + rmlo: Reg::R2, + rmhi: Reg::R3, + }; + let code = encoder.encode(&op).unwrap(); + // Should emit SUBS (2 bytes) + SBC.W (4 bytes) = 6 bytes + assert_eq!(code.len(), 6, "I64Sub should be 6 bytes (SUBS + SBC.W)"); + } + + #[test] + fn test_encode_i64_and_thumb2() { + let encoder = ArmEncoder::new_thumb2(); + let op = ArmOp::I64And { + rdlo: Reg::R0, + rdhi: Reg::R1, + rnlo: Reg::R0, + rnhi: Reg::R1, + rmlo: Reg::R2, + rmhi: Reg::R3, + }; + let code = encoder.encode(&op).unwrap(); + // AND.W (4 bytes) + AND.W (4 bytes) = 8 bytes + assert!(code.len() >= 4, "I64And should emit at least 4 bytes"); + } + + #[test] + fn test_encode_i64_or_thumb2() { + let encoder = ArmEncoder::new_thumb2(); + let op = ArmOp::I64Or { + rdlo: Reg::R0, + rdhi: Reg::R1, + rnlo: Reg::R0, + rnhi: Reg::R1, + rmlo: Reg::R2, + rmhi: Reg::R3, + }; + let code = encoder.encode(&op).unwrap(); + assert!(code.len() >= 4, "I64Or should emit at least 4 bytes"); + } + + #[test] + fn test_encode_i64_xor_thumb2() { + let encoder = ArmEncoder::new_thumb2(); + let op = ArmOp::I64Xor { + rdlo: Reg::R0, + rdhi: Reg::R1, + rnlo: Reg::R0, + rnhi: Reg::R1, + rmlo: Reg::R2, + rmhi: Reg::R3, + }; + let code = encoder.encode(&op).unwrap(); + assert!(code.len() >= 4, "I64Xor should emit at least 4 bytes"); + } + + #[test] + fn test_encode_i64_const_small_thumb2() { + let encoder = ArmEncoder::new_thumb2(); + // Small constant: only needs MOVW for each half + let op = ArmOp::I64Const { + rdlo: Reg::R0, + rdhi: Reg::R1, + value: 42, + }; + let code = encoder.encode(&op).unwrap(); + // MOVW R0, #42 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes minimum + assert!(code.len() >= 8, "I64Const should emit at least 8 bytes"); + } + + #[test] + fn test_encode_i64_const_large_thumb2() { + let encoder = ArmEncoder::new_thumb2(); + // Large constant: needs MOVW+MOVT for each half + let op = ArmOp::I64Const { + rdlo: Reg::R0, + rdhi: Reg::R1, + value: 0x1234_5678_9ABC_DEF0_u64 as i64, + }; + let code = encoder.encode(&op).unwrap(); + // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes + assert_eq!( + code.len(), + 16, + "I64Const with large value should be 16 bytes" + ); + } + + #[test] + fn test_encode_i64_extend_i32_s_thumb2() { + let encoder = ArmEncoder::new_thumb2(); + let op = ArmOp::I64ExtendI32S { + rdlo: Reg::R0, + rdhi: Reg::R1, + rn: Reg::R0, + }; + let code = encoder.encode(&op).unwrap(); + // When rdlo == rn, only ASR (4 bytes) is emitted + assert_eq!( + code.len(), + 4, + "I64ExtendI32S (same reg) should be 4 bytes (ASR only)" + ); + } + + #[test] + fn test_encode_i64_extend_i32_s_diff_reg_thumb2() { + let encoder = ArmEncoder::new_thumb2(); + let op = ArmOp::I64ExtendI32S { + rdlo: Reg::R0, + rdhi: Reg::R1, + rn: Reg::R2, + }; + let code = encoder.encode(&op).unwrap(); + // MOV rdlo, rn (2 bytes for low regs) + ASR rdhi, rdlo, #31 (4 bytes) = 6 bytes + assert!( + code.len() >= 6, + "I64ExtendI32S (diff reg) should be at least 6 bytes" + ); + } + + #[test] + fn test_encode_i64_extend_i32_u_thumb2() { + let encoder = ArmEncoder::new_thumb2(); + let op = ArmOp::I64ExtendI32U { + rdlo: Reg::R0, + rdhi: Reg::R1, + rn: Reg::R0, + }; + let code = encoder.encode(&op).unwrap(); + // When rdlo == rn, only MOV rdhi, #0 (2 bytes) is emitted + assert_eq!( + code.len(), + 2, + "I64ExtendI32U (same reg) should be 2 bytes (MOV #0 only)" + ); + } + + #[test] + fn test_encode_i32_wrap_i64_nop_thumb2() { + let encoder = ArmEncoder::new_thumb2(); + // When rd == rnlo, should be a NOP + let op = ArmOp::I32WrapI64 { + rd: Reg::R0, + rnlo: Reg::R0, + }; + let code = encoder.encode(&op).unwrap(); + assert_eq!(code.len(), 2, "I32WrapI64 same reg should be NOP (2 bytes)"); + assert_eq!(code, vec![0x00, 0xBF]); // NOP + } + + #[test] + fn test_encode_i32_wrap_i64_diff_reg_thumb2() { + let encoder = ArmEncoder::new_thumb2(); + let op = ArmOp::I32WrapI64 { + rd: Reg::R2, + rnlo: Reg::R0, + }; + let code = encoder.encode(&op).unwrap(); + // MOV R2, R0 (2 or 4 bytes) + assert!( + code.len() >= 2, + "I32WrapI64 diff reg should emit at least 2 bytes" + ); + } + + #[test] + fn test_encode_i64_eqz_thumb2() { + let encoder = ArmEncoder::new_thumb2(); + let op = ArmOp::I64Eqz { + rd: Reg::R0, + rnlo: Reg::R0, + rnhi: Reg::R1, + }; + let code = encoder.encode(&op).unwrap(); + // Delegates to I64SetCondZ which is already encoded + assert!( + code.len() >= 6, + "I64Eqz should emit at least 6 bytes for ORR+ITE+MOV+MOV" + ); + } + + #[test] + fn test_encode_i64_eq_thumb2() { + let encoder = ArmEncoder::new_thumb2(); + let op = ArmOp::I64Eq { + rd: Reg::R0, + rnlo: Reg::R0, + rnhi: Reg::R1, + rmlo: Reg::R2, + rmhi: Reg::R3, + }; + let code = encoder.encode(&op).unwrap(); + // Delegates to I64SetCond EQ: CMP lo + IT EQ + CMPEQ hi + ITE EQ + MOV 1 + MOV 0 + assert!(code.len() >= 10, "I64Eq should emit at least 10 bytes"); + } + + #[test] + fn test_encode_i64_ldr_thumb2() { + let encoder = ArmEncoder::new_thumb2(); + let op = ArmOp::I64Ldr { + rdlo: Reg::R0, + rdhi: Reg::R1, + addr: MemAddr::imm(Reg::SP, 0), + }; + let code = encoder.encode(&op).unwrap(); + // Two LDR instructions (lo at offset, hi at offset+4) + assert!(code.len() >= 4, "I64Ldr should emit at least 4 bytes"); + } + + #[test] + fn test_encode_i64_str_thumb2() { + let encoder = ArmEncoder::new_thumb2(); + let op = ArmOp::I64Str { + rdlo: Reg::R0, + rdhi: Reg::R1, + addr: MemAddr::imm(Reg::SP, 0), + }; + let code = encoder.encode(&op).unwrap(); + // Two STR instructions (lo at offset, hi at offset+4) + assert!(code.len() >= 4, "I64Str should emit at least 4 bytes"); + } + + #[test] + fn test_encode_i64_all_comparisons_thumb2() { + let encoder = ArmEncoder::new_thumb2(); + + let ops = vec![ + ArmOp::I64Ne { + rd: Reg::R0, + rnlo: Reg::R0, + rnhi: Reg::R1, + rmlo: Reg::R2, + rmhi: Reg::R3, + }, + ArmOp::I64LtS { + rd: Reg::R0, + rnlo: Reg::R0, + rnhi: Reg::R1, + rmlo: Reg::R2, + rmhi: Reg::R3, + }, + ArmOp::I64LtU { + rd: Reg::R0, + rnlo: Reg::R0, + rnhi: Reg::R1, + rmlo: Reg::R2, + rmhi: Reg::R3, + }, + ArmOp::I64LeS { + rd: Reg::R0, + rnlo: Reg::R0, + rnhi: Reg::R1, + rmlo: Reg::R2, + rmhi: Reg::R3, + }, + ArmOp::I64LeU { + rd: Reg::R0, + rnlo: Reg::R0, + rnhi: Reg::R1, + rmlo: Reg::R2, + rmhi: Reg::R3, + }, + ArmOp::I64GtS { + rd: Reg::R0, + rnlo: Reg::R0, + rnhi: Reg::R1, + rmlo: Reg::R2, + rmhi: Reg::R3, + }, + ArmOp::I64GtU { + rd: Reg::R0, + rnlo: Reg::R0, + rnhi: Reg::R1, + rmlo: Reg::R2, + rmhi: Reg::R3, + }, + ArmOp::I64GeS { + rd: Reg::R0, + rnlo: Reg::R0, + rnhi: Reg::R1, + rmlo: Reg::R2, + rmhi: Reg::R3, + }, + ArmOp::I64GeU { + rd: Reg::R0, + rnlo: Reg::R0, + rnhi: Reg::R1, + rmlo: Reg::R2, + rmhi: Reg::R3, + }, + ]; + + for op in &ops { + let code = encoder.encode(op).unwrap(); + assert!( + code.len() >= 8, + "i64 comparison {:?} should emit at least 8 bytes, got {}", + op, + code.len() + ); + } + } + + #[test] + fn test_encode_i64_const_zero_thumb2() { + let encoder = ArmEncoder::new_thumb2(); + let op = ArmOp::I64Const { + rdlo: Reg::R0, + rdhi: Reg::R1, + value: 0, + }; + let code = encoder.encode(&op).unwrap(); + // MOVW R0, #0 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes + assert_eq!(code.len(), 8, "I64Const(0) should be 8 bytes"); + } + + #[test] + fn test_encode_i64_const_negative_one_thumb2() { + let encoder = ArmEncoder::new_thumb2(); + let op = ArmOp::I64Const { + rdlo: Reg::R0, + rdhi: Reg::R1, + value: -1, // 0xFFFF_FFFF_FFFF_FFFF + }; + let code = encoder.encode(&op).unwrap(); + // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes + assert_eq!(code.len(), 16, "I64Const(-1) should be 16 bytes"); + } } diff --git a/crates/synth-synthesis/src/instruction_selector.rs b/crates/synth-synthesis/src/instruction_selector.rs index 8ce535f..3ebda7a 100644 --- a/crates/synth-synthesis/src/instruction_selector.rs +++ b/crates/synth-synthesis/src/instruction_selector.rs @@ -627,57 +627,453 @@ impl InstructionSelector { }); instrs } - GlobalGet(_) | GlobalSet(_) => { - return Err(synth_core::Error::synthesis( - "global.get/global.set not yet implemented", - )); + GlobalGet(index) => { + // WASM globals are stored in a globals table in memory. + // R9 is the dedicated globals base register (set up by runtime startup). + // Each i32 global occupies 4 bytes: globals_base + index * 4. + vec![ArmOp::Ldr { + rd, + addr: MemAddr::imm(Reg::R9, (*index as i32) * 4), + }] + } + GlobalSet(index) => { + // Store value from source register to globals_base + index * 4. + // R9 is the dedicated globals base register. + vec![ArmOp::Str { + rd, + addr: MemAddr::imm(Reg::R9, (*index as i32) * 4), + }] } Select => { - return Err(synth_core::Error::synthesis("select not yet implemented")); - } - - // i64 operations — not supported on 32-bit ARM without register pairs - op @ (I64Add - | I64Sub - | I64Mul - | I64DivS - | I64DivU - | I64RemS - | I64RemU - | I64And - | I64Or - | I64Xor - | I64Shl - | I64ShrS - | I64ShrU - | I64Rotl - | I64Rotr - | I64Clz - | I64Ctz - | I64Popcnt - | I64Eqz - | I64Eq - | I64Ne - | I64LtS - | I64LtU - | I64LeS - | I64LeU - | I64GtS - | I64GtU - | I64GeS - | I64GeU - | I64Const(_) - | I64Load { .. } - | I64Store { .. } - | I64ExtendI32S - | I64ExtendI32U - | I32WrapI64 - | I64Extend8S - | I64Extend16S - | I64Extend32S) => { - return Err(synth_core::Error::synthesis(format!( - "i64 operation not supported (requires register pairs on 32-bit ARM): {op:?}" - ))); + // WASM select: pops condition, val2, val1 from stack; + // pushes val1 if condition != 0, else val2. + // In select_default (non-stack mode), we emit: + // CMP rcond, #0 + // MOV rd, rval1 (default: pick val1) + // IT EQ; MOVEQ rd, rval2 (override if cond == 0) + let rcond = self.regs.alloc_reg(); + vec![ + ArmOp::Cmp { + rn: rcond, + op2: Operand2::Imm(0), + }, + ArmOp::Mov { + rd, + op2: Operand2::Reg(rn), + }, + ArmOp::SelectMove { + rd, + rm, + cond: Condition::EQ, + }, + ] + } + + // ===== i64 operations using register pairs on 32-bit ARM ===== + // Convention: i64 operand 1 in (R0,R1), operand 2 in (R2,R3), result in (R0,R1) + I64Const(val) => { + vec![ArmOp::I64Const { + rdlo: Reg::R0, + rdhi: Reg::R1, + value: *val, + }] + } + + I64ExtendI32S => { + vec![ArmOp::I64ExtendI32S { + rdlo: Reg::R0, + rdhi: Reg::R1, + rn: Reg::R0, + }] + } + + I64ExtendI32U => { + vec![ArmOp::I64ExtendI32U { + rdlo: Reg::R0, + rdhi: Reg::R1, + rn: Reg::R0, + }] + } + + I32WrapI64 => { + // Just take the low 32 bits (R0) — effectively a no-op if result is in R0 + vec![ArmOp::I32WrapI64 { + rd: Reg::R0, + rnlo: Reg::R0, + }] + } + + I64Extend8S => { + vec![ArmOp::I64Extend8S { + rdlo: Reg::R0, + rdhi: Reg::R1, + rnlo: Reg::R0, + }] + } + + I64Extend16S => { + vec![ArmOp::I64Extend16S { + rdlo: Reg::R0, + rdhi: Reg::R1, + rnlo: Reg::R0, + }] + } + + I64Extend32S => { + vec![ArmOp::I64Extend32S { + rdlo: Reg::R0, + rdhi: Reg::R1, + rnlo: Reg::R0, + }] + } + + // i64 arithmetic: ADDS/ADC for add, SUBS/SBC for sub + I64Add => { + vec![ + ArmOp::Adds { + rd: Reg::R0, + rn: Reg::R0, + op2: Operand2::Reg(Reg::R2), + }, + ArmOp::Adc { + rd: Reg::R1, + rn: Reg::R1, + op2: Operand2::Reg(Reg::R3), + }, + ] + } + + I64Sub => { + vec![ + ArmOp::Subs { + rd: Reg::R0, + rn: Reg::R0, + op2: Operand2::Reg(Reg::R2), + }, + ArmOp::Sbc { + rd: Reg::R1, + rn: Reg::R1, + op2: Operand2::Reg(Reg::R3), + }, + ] + } + + // i64 bitwise: operate on each half independently + I64And => { + vec![ + ArmOp::And { + rd: Reg::R0, + rn: Reg::R0, + op2: Operand2::Reg(Reg::R2), + }, + ArmOp::And { + rd: Reg::R1, + rn: Reg::R1, + op2: Operand2::Reg(Reg::R3), + }, + ] + } + + I64Or => { + vec![ + ArmOp::Orr { + rd: Reg::R0, + rn: Reg::R0, + op2: Operand2::Reg(Reg::R2), + }, + ArmOp::Orr { + rd: Reg::R1, + rn: Reg::R1, + op2: Operand2::Reg(Reg::R3), + }, + ] + } + + I64Xor => { + vec![ + ArmOp::Eor { + rd: Reg::R0, + rn: Reg::R0, + op2: Operand2::Reg(Reg::R2), + }, + ArmOp::Eor { + rd: Reg::R1, + rn: Reg::R1, + op2: Operand2::Reg(Reg::R3), + }, + ] + } + + // i64 comparisons: compare register pairs, result 0/1 in R0 + I64Eqz => { + vec![ArmOp::I64SetCondZ { + rd: Reg::R0, + rn_lo: Reg::R0, + rn_hi: Reg::R1, + }] + } + + I64Eq => { + vec![ArmOp::I64SetCond { + rd: Reg::R0, + rn_lo: Reg::R0, + rn_hi: Reg::R1, + rm_lo: Reg::R2, + rm_hi: Reg::R3, + cond: Condition::EQ, + }] + } + + I64Ne => { + vec![ArmOp::I64SetCond { + rd: Reg::R0, + rn_lo: Reg::R0, + rn_hi: Reg::R1, + rm_lo: Reg::R2, + rm_hi: Reg::R3, + cond: Condition::NE, + }] + } + + I64LtS => { + vec![ArmOp::I64SetCond { + rd: Reg::R0, + rn_lo: Reg::R0, + rn_hi: Reg::R1, + rm_lo: Reg::R2, + rm_hi: Reg::R3, + cond: Condition::LT, + }] + } + + I64LtU => { + vec![ArmOp::I64SetCond { + rd: Reg::R0, + rn_lo: Reg::R0, + rn_hi: Reg::R1, + rm_lo: Reg::R2, + rm_hi: Reg::R3, + cond: Condition::LO, + }] + } + + I64LeS => { + vec![ArmOp::I64SetCond { + rd: Reg::R0, + rn_lo: Reg::R0, + rn_hi: Reg::R1, + rm_lo: Reg::R2, + rm_hi: Reg::R3, + cond: Condition::LE, + }] + } + + I64LeU => { + vec![ArmOp::I64SetCond { + rd: Reg::R0, + rn_lo: Reg::R0, + rn_hi: Reg::R1, + rm_lo: Reg::R2, + rm_hi: Reg::R3, + cond: Condition::LS, + }] + } + + I64GtS => { + vec![ArmOp::I64SetCond { + rd: Reg::R0, + rn_lo: Reg::R0, + rn_hi: Reg::R1, + rm_lo: Reg::R2, + rm_hi: Reg::R3, + cond: Condition::GT, + }] + } + + I64GtU => { + vec![ArmOp::I64SetCond { + rd: Reg::R0, + rn_lo: Reg::R0, + rn_hi: Reg::R1, + rm_lo: Reg::R2, + rm_hi: Reg::R3, + cond: Condition::HI, + }] + } + + I64GeS => { + vec![ArmOp::I64SetCond { + rd: Reg::R0, + rn_lo: Reg::R0, + rn_hi: Reg::R1, + rm_lo: Reg::R2, + rm_hi: Reg::R3, + cond: Condition::GE, + }] + } + + I64GeU => { + vec![ArmOp::I64SetCond { + rd: Reg::R0, + rn_lo: Reg::R0, + rn_hi: Reg::R1, + rm_lo: Reg::R2, + rm_hi: Reg::R3, + cond: Condition::HS, + }] + } + + // i64 multiply: UMULL + MLA cross products + I64Mul => { + vec![ArmOp::I64Mul { + rd_lo: Reg::R0, + rd_hi: Reg::R1, + rn_lo: Reg::R0, + rn_hi: Reg::R1, + rm_lo: Reg::R2, + rm_hi: Reg::R3, + }] + } + + // i64 shifts: multi-instruction funnel shifts + I64Shl => { + vec![ArmOp::I64Shl { + rd_lo: Reg::R0, + rd_hi: Reg::R1, + rn_lo: Reg::R0, + rn_hi: Reg::R1, + rm_lo: Reg::R2, + rm_hi: Reg::R3, + }] + } + + I64ShrU => { + vec![ArmOp::I64ShrU { + rd_lo: Reg::R0, + rd_hi: Reg::R1, + rn_lo: Reg::R0, + rn_hi: Reg::R1, + rm_lo: Reg::R2, + rm_hi: Reg::R3, + }] + } + + I64ShrS => { + vec![ArmOp::I64ShrS { + rd_lo: Reg::R0, + rd_hi: Reg::R1, + rn_lo: Reg::R0, + rn_hi: Reg::R1, + rm_lo: Reg::R2, + rm_hi: Reg::R3, + }] + } + + I64Rotl => { + vec![ArmOp::I64Rotl { + rdlo: Reg::R0, + rdhi: Reg::R1, + rnlo: Reg::R0, + rnhi: Reg::R1, + shift: Reg::R2, + }] + } + + I64Rotr => { + vec![ArmOp::I64Rotr { + rdlo: Reg::R0, + rdhi: Reg::R1, + rnlo: Reg::R0, + rnhi: Reg::R1, + shift: Reg::R2, + }] + } + + // i64 bit manipulation + I64Clz => { + vec![ArmOp::I64Clz { + rd: Reg::R0, + rnlo: Reg::R0, + rnhi: Reg::R1, + }] + } + + I64Ctz => { + vec![ArmOp::I64Ctz { + rd: Reg::R0, + rnlo: Reg::R0, + rnhi: Reg::R1, + }] + } + + I64Popcnt => { + vec![ArmOp::I64Popcnt { + rd: Reg::R0, + rnlo: Reg::R0, + rnhi: Reg::R1, + }] + } + + // i64 division/remainder + I64DivS => { + vec![ArmOp::I64DivS { + rdlo: Reg::R0, + rdhi: Reg::R1, + rnlo: Reg::R0, + rnhi: Reg::R1, + rmlo: Reg::R2, + rmhi: Reg::R3, + }] + } + + I64DivU => { + vec![ArmOp::I64DivU { + rdlo: Reg::R0, + rdhi: Reg::R1, + rnlo: Reg::R0, + rnhi: Reg::R1, + rmlo: Reg::R2, + rmhi: Reg::R3, + }] + } + + I64RemS => { + vec![ArmOp::I64RemS { + rdlo: Reg::R0, + rdhi: Reg::R1, + rnlo: Reg::R0, + rnhi: Reg::R1, + rmlo: Reg::R2, + rmhi: Reg::R3, + }] + } + + I64RemU => { + vec![ArmOp::I64RemU { + rdlo: Reg::R0, + rdhi: Reg::R1, + rnlo: Reg::R0, + rnhi: Reg::R1, + rmlo: Reg::R2, + rmhi: Reg::R3, + }] + } + + // i64 memory operations + I64Load { offset, .. } => { + vec![ArmOp::I64Ldr { + rdlo: Reg::R0, + rdhi: Reg::R1, + addr: MemAddr::reg_imm(Reg::R11, rn, *offset as i32), + }] + } + + I64Store { offset, .. } => { + vec![ArmOp::I64Str { + rdlo: Reg::R0, + rdhi: Reg::R1, + addr: MemAddr::reg_imm(Reg::R11, rn, *offset as i32), + }] } // ===== F32 operations ===== @@ -1998,6 +2394,35 @@ impl InstructionSelector { } } + GlobalGet(global_idx) => { + // Load global value from globals table (R9 = globals base). + // Each i32 global occupies 4 bytes at offset index * 4. + let dst = index_to_reg(next_temp); + next_temp = (next_temp + 1) % 13; + instructions.push(ArmInstruction { + op: ArmOp::Ldr { + rd: dst, + addr: MemAddr::imm(Reg::R9, (*global_idx as i32) * 4), + }, + source_line: Some(idx), + }); + cf.add_instruction(); + stack.push(dst); + } + + GlobalSet(global_idx) => { + // Pop value from stack and store to globals table (R9 = globals base). + let val = stack.pop().unwrap_or(Reg::R0); + instructions.push(ArmInstruction { + op: ArmOp::Str { + rd: val, + addr: MemAddr::imm(Reg::R9, (*global_idx as i32) * 4), + }, + source_line: Some(idx), + }); + cf.add_instruction(); + } + // For other operations, fall back to default behavior _ => { let arm_ops = self.select_default(op)?; @@ -4141,4 +4566,955 @@ mod tests { instrs.len() ); } + + // ========================================================================= + // GlobalGet / GlobalSet tests + // ========================================================================= + + #[test] + fn test_global_get_select_default() { + // global.get should generate LDR from globals base (R9) + let db = RuleDatabase::new(); + let mut selector = InstructionSelector::new(db.rules().to_vec()); + + let wasm_ops = vec![WasmOp::GlobalGet(0)]; + let arm_instrs = selector.select(&wasm_ops).unwrap(); + + assert_eq!(arm_instrs.len(), 1); + match &arm_instrs[0].op { + ArmOp::Ldr { rd: _, addr } => { + assert_eq!(addr.base, Reg::R9, "Globals base should be R9"); + assert_eq!(addr.offset, 0, "Global 0 should have offset 0"); + assert!(addr.offset_reg.is_none(), "No offset register"); + } + other => panic!("Expected Ldr for global.get, got {:?}", other), + } + } + + #[test] + fn test_global_get_nonzero_index_select_default() { + // global.get with index 3 should use offset 12 (3 * 4) + let db = RuleDatabase::new(); + let mut selector = InstructionSelector::new(db.rules().to_vec()); + + let wasm_ops = vec![WasmOp::GlobalGet(3)]; + let arm_instrs = selector.select(&wasm_ops).unwrap(); + + assert_eq!(arm_instrs.len(), 1); + match &arm_instrs[0].op { + ArmOp::Ldr { rd: _, addr } => { + assert_eq!(addr.base, Reg::R9); + assert_eq!(addr.offset, 12, "Global 3 should have offset 3*4=12"); + } + other => panic!("Expected Ldr for global.get(3), got {:?}", other), + } + } + + #[test] + fn test_global_set_select_default() { + // global.set should generate STR to globals base (R9) + let db = RuleDatabase::new(); + let mut selector = InstructionSelector::new(db.rules().to_vec()); + + let wasm_ops = vec![WasmOp::GlobalSet(0)]; + let arm_instrs = selector.select(&wasm_ops).unwrap(); + + assert_eq!(arm_instrs.len(), 1); + match &arm_instrs[0].op { + ArmOp::Str { rd: _, addr } => { + assert_eq!(addr.base, Reg::R9, "Globals base should be R9"); + assert_eq!(addr.offset, 0, "Global 0 should have offset 0"); + } + other => panic!("Expected Str for global.set, got {:?}", other), + } + } + + #[test] + fn test_global_set_nonzero_index_select_default() { + // global.set with index 5 should use offset 20 (5 * 4) + let db = RuleDatabase::new(); + let mut selector = InstructionSelector::new(db.rules().to_vec()); + + let wasm_ops = vec![WasmOp::GlobalSet(5)]; + let arm_instrs = selector.select(&wasm_ops).unwrap(); + + assert_eq!(arm_instrs.len(), 1); + match &arm_instrs[0].op { + ArmOp::Str { rd: _, addr } => { + assert_eq!(addr.base, Reg::R9); + assert_eq!(addr.offset, 20, "Global 5 should have offset 5*4=20"); + } + other => panic!("Expected Str for global.set(5), got {:?}", other), + } + } + + #[test] + fn test_global_get_stack_mode() { + // global.get in stack mode should push result register onto virtual stack + let mut selector = fresh_selector(); + + let wasm_ops = vec![ + WasmOp::GlobalGet(2), // Load global 2 + WasmOp::I32Const(1), + WasmOp::I32Add, // Add 1 to global value + ]; + let instrs = selector.select_with_stack(&wasm_ops, 0).unwrap(); + + // Should have LDR from R9+8 (global index 2, offset 2*4=8) + let has_global_ldr = instrs.iter().any(|i| { + matches!( + &i.op, + ArmOp::Ldr { addr, .. } + if addr.base == Reg::R9 && addr.offset == 8 + ) + }); + assert!( + has_global_ldr, + "global.get(2) should emit LDR from [R9, #8]" + ); + + // Should have ADD (from the i32.add) + let has_add = instrs.iter().any(|i| matches!(&i.op, ArmOp::Add { .. })); + assert!(has_add, "Should emit ADD for i32.add after global.get"); + } + + #[test] + fn test_global_set_stack_mode() { + // global.set in stack mode should pop value and store to globals table + let mut selector = fresh_selector(); + + let wasm_ops = vec![ + WasmOp::I32Const(42), // Push value + WasmOp::GlobalSet(1), // Store to global 1 + ]; + let instrs = selector.select_with_stack(&wasm_ops, 0).unwrap(); + + // Should have STR to R9+4 (global index 1, offset 1*4=4) + let has_global_str = instrs.iter().any(|i| { + matches!( + &i.op, + ArmOp::Str { addr, .. } + if addr.base == Reg::R9 && addr.offset == 4 + ) + }); + assert!(has_global_str, "global.set(1) should emit STR to [R9, #4]"); + } + + #[test] + fn test_global_get_set_roundtrip() { + // global.get + modify + global.set should work correctly + let mut selector = fresh_selector(); + + let wasm_ops = vec![ + WasmOp::GlobalGet(0), // Load global 0 + WasmOp::I32Const(10), // Push 10 + WasmOp::I32Add, // global_0 + 10 + WasmOp::GlobalSet(0), // Store back to global 0 + ]; + let instrs = selector.select_with_stack(&wasm_ops, 0).unwrap(); + + // Should have LDR from R9 (global.get 0) + let has_load = instrs.iter().any(|i| { + matches!( + &i.op, + ArmOp::Ldr { addr, .. } + if addr.base == Reg::R9 && addr.offset == 0 + ) + }); + assert!(has_load, "Should have LDR from [R9, #0] for global.get(0)"); + + // Should have STR to R9 (global.set 0) + let has_store = instrs.iter().any(|i| { + matches!( + &i.op, + ArmOp::Str { addr, .. } + if addr.base == Reg::R9 && addr.offset == 0 + ) + }); + assert!(has_store, "Should have STR to [R9, #0] for global.set(0)"); + + // Should have ADD for the increment + let has_add = instrs.iter().any(|i| matches!(&i.op, ArmOp::Add { .. })); + assert!(has_add, "Should have ADD for i32.add"); + } + + // ========================================================================= + // Select instruction tests + // ========================================================================= + + #[test] + fn test_select_default_mode() { + // Select in select_default should emit CMP + MOV + SelectMove + let db = RuleDatabase::new(); + let mut selector = InstructionSelector::new(db.rules().to_vec()); + + let wasm_ops = vec![WasmOp::Select]; + let arm_instrs = selector.select(&wasm_ops).unwrap(); + + assert_eq!(arm_instrs.len(), 3, "Select should emit 3 instructions"); + + // First: CMP rcond, #0 + match &arm_instrs[0].op { + ArmOp::Cmp { + op2: Operand2::Imm(0), + .. + } => {} + other => panic!("Expected CMP rcond, #0, got {:?}", other), + } + + // Second: MOV rd, rval1 (default) + match &arm_instrs[1].op { + ArmOp::Mov { .. } => {} + other => panic!("Expected MOV rd, rval1, got {:?}", other), + } + + // Third: SelectMove (conditional override) + match &arm_instrs[2].op { + ArmOp::SelectMove { + cond: Condition::EQ, + .. + } => {} + other => panic!("Expected SelectMove with EQ condition, got {:?}", other), + } + } + + #[test] + fn test_select_stack_mode_with_constants() { + // Select with three constant operands + let mut selector = fresh_selector(); + + let wasm_ops = vec![ + WasmOp::I32Const(10), // val1 + WasmOp::I32Const(20), // val2 + WasmOp::I32Const(1), // condition (nonzero -> pick val1) + WasmOp::Select, + ]; + let instrs = selector.select_with_stack(&wasm_ops, 0).unwrap(); + + // Should have CMP for condition + let has_cmp = instrs.iter().any(|i| { + matches!( + &i.op, + ArmOp::Cmp { + op2: Operand2::Imm(0), + .. + } + ) + }); + assert!(has_cmp, "Select should emit CMP condition, #0"); + + // Should have SelectMove EQ (conditional override) + let has_select_move = instrs.iter().any(|i| { + matches!( + &i.op, + ArmOp::SelectMove { + cond: Condition::EQ, + .. + } + ) + }); + assert!(has_select_move, "Select should emit SelectMove with EQ"); + } + + #[test] + fn test_select_with_global_values() { + // Combine global.get with select: select between two globals based on condition + let mut selector = fresh_selector(); + + let wasm_ops = vec![ + WasmOp::GlobalGet(0), // val1 = global 0 + WasmOp::GlobalGet(1), // val2 = global 1 + WasmOp::LocalGet(0), // condition from param + WasmOp::Select, // pick val1 if cond != 0, else val2 + WasmOp::GlobalSet(2), // store result to global 2 + ]; + let instrs = selector.select_with_stack(&wasm_ops, 1).unwrap(); + + // Should load from globals 0 and 1 + let global_loads: Vec<_> = instrs + .iter() + .filter(|i| { + matches!( + &i.op, + ArmOp::Ldr { addr, .. } + if addr.base == Reg::R9 + ) + }) + .collect(); + assert_eq!( + global_loads.len(), + 2, + "Should have 2 LDR from globals (indices 0 and 1), got {}", + global_loads.len() + ); + + // Should store to global 2 + let global_stores: Vec<_> = instrs + .iter() + .filter(|i| { + matches!( + &i.op, + ArmOp::Str { addr, .. } + if addr.base == Reg::R9 && addr.offset == 8 + ) + }) + .collect(); + assert_eq!( + global_stores.len(), + 1, + "Should have 1 STR to global 2 at [R9, #8]" + ); + + // Should have select logic + let has_select_move = instrs.iter().any(|i| { + matches!( + &i.op, + ArmOp::SelectMove { + cond: Condition::EQ, + .. + } + ) + }); + assert!(has_select_move, "Should have SelectMove for the select op"); + } + + // ========================================================================= + // i64 instruction selection tests + // ========================================================================= + + #[test] + fn test_i64_const() { + let db = RuleDatabase::new(); + let mut selector = InstructionSelector::new(db.rules().to_vec()); + + let wasm_ops = vec![WasmOp::I64Const(0x0000_0001_0000_0002)]; + let arm_instrs = selector.select(&wasm_ops).unwrap(); + + assert_eq!(arm_instrs.len(), 1); + match &arm_instrs[0].op { + ArmOp::I64Const { rdlo, rdhi, value } => { + assert_eq!(*rdlo, Reg::R0); + assert_eq!(*rdhi, Reg::R1); + assert_eq!(*value, 0x0000_0001_0000_0002); + } + other => panic!("Expected I64Const, got {other:?}"), + } + } + + #[test] + fn test_i64_const_negative() { + let db = RuleDatabase::new(); + let mut selector = InstructionSelector::new(db.rules().to_vec()); + + let wasm_ops = vec![WasmOp::I64Const(-1)]; + let arm_instrs = selector.select(&wasm_ops).unwrap(); + + assert_eq!(arm_instrs.len(), 1); + match &arm_instrs[0].op { + ArmOp::I64Const { value, .. } => { + assert_eq!(*value, -1); + } + other => panic!("Expected I64Const, got {other:?}"), + } + } + + #[test] + fn test_i64_extend_i32_s() { + let db = RuleDatabase::new(); + let mut selector = InstructionSelector::new(db.rules().to_vec()); + + let wasm_ops = vec![WasmOp::I64ExtendI32S]; + let arm_instrs = selector.select(&wasm_ops).unwrap(); + + assert_eq!(arm_instrs.len(), 1); + match &arm_instrs[0].op { + ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => { + assert_eq!(*rdlo, Reg::R0); + assert_eq!(*rdhi, Reg::R1); + assert_eq!(*rn, Reg::R0); + } + other => panic!("Expected I64ExtendI32S, got {other:?}"), + } + } + + #[test] + fn test_i64_extend_i32_u() { + let db = RuleDatabase::new(); + let mut selector = InstructionSelector::new(db.rules().to_vec()); + + let wasm_ops = vec![WasmOp::I64ExtendI32U]; + let arm_instrs = selector.select(&wasm_ops).unwrap(); + + assert_eq!(arm_instrs.len(), 1); + match &arm_instrs[0].op { + ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => { + assert_eq!(*rdlo, Reg::R0); + assert_eq!(*rdhi, Reg::R1); + assert_eq!(*rn, Reg::R0); + } + other => panic!("Expected I64ExtendI32U, got {other:?}"), + } + } + + #[test] + fn test_i32_wrap_i64() { + let db = RuleDatabase::new(); + let mut selector = InstructionSelector::new(db.rules().to_vec()); + + let wasm_ops = vec![WasmOp::I32WrapI64]; + let arm_instrs = selector.select(&wasm_ops).unwrap(); + + assert_eq!(arm_instrs.len(), 1); + match &arm_instrs[0].op { + ArmOp::I32WrapI64 { rd, rnlo } => { + assert_eq!(*rd, Reg::R0); + assert_eq!(*rnlo, Reg::R0); + } + other => panic!("Expected I32WrapI64, got {other:?}"), + } + } + + #[test] + fn test_i64_add() { + let db = RuleDatabase::new(); + let mut selector = InstructionSelector::new(db.rules().to_vec()); + + let wasm_ops = vec![WasmOp::I64Add]; + let arm_instrs = selector.select(&wasm_ops).unwrap(); + + // i64 add generates ADDS + ADC (two instructions) + assert_eq!(arm_instrs.len(), 2); + match &arm_instrs[0].op { + ArmOp::Adds { rd, rn, op2 } => { + assert_eq!(*rd, Reg::R0); + assert_eq!(*rn, Reg::R0); + assert_eq!(*op2, Operand2::Reg(Reg::R2)); + } + other => panic!("Expected Adds, got {other:?}"), + } + match &arm_instrs[1].op { + ArmOp::Adc { rd, rn, op2 } => { + assert_eq!(*rd, Reg::R1); + assert_eq!(*rn, Reg::R1); + assert_eq!(*op2, Operand2::Reg(Reg::R3)); + } + other => panic!("Expected Adc, got {other:?}"), + } + } + + #[test] + fn test_i64_sub() { + let db = RuleDatabase::new(); + let mut selector = InstructionSelector::new(db.rules().to_vec()); + + let wasm_ops = vec![WasmOp::I64Sub]; + let arm_instrs = selector.select(&wasm_ops).unwrap(); + + assert_eq!(arm_instrs.len(), 2); + match &arm_instrs[0].op { + ArmOp::Subs { rd, rn, op2 } => { + assert_eq!(*rd, Reg::R0); + assert_eq!(*rn, Reg::R0); + assert_eq!(*op2, Operand2::Reg(Reg::R2)); + } + other => panic!("Expected Subs, got {other:?}"), + } + match &arm_instrs[1].op { + ArmOp::Sbc { rd, rn, op2 } => { + assert_eq!(*rd, Reg::R1); + assert_eq!(*rn, Reg::R1); + assert_eq!(*op2, Operand2::Reg(Reg::R3)); + } + other => panic!("Expected Sbc, got {other:?}"), + } + } + + #[test] + fn test_i64_and() { + let db = RuleDatabase::new(); + let mut selector = InstructionSelector::new(db.rules().to_vec()); + + let wasm_ops = vec![WasmOp::I64And]; + let arm_instrs = selector.select(&wasm_ops).unwrap(); + + assert_eq!(arm_instrs.len(), 2); + match &arm_instrs[0].op { + ArmOp::And { rd, rn, op2 } => { + assert_eq!(*rd, Reg::R0); + assert_eq!(*rn, Reg::R0); + assert_eq!(*op2, Operand2::Reg(Reg::R2)); + } + other => panic!("Expected And (lo), got {other:?}"), + } + match &arm_instrs[1].op { + ArmOp::And { rd, rn, op2 } => { + assert_eq!(*rd, Reg::R1); + assert_eq!(*rn, Reg::R1); + assert_eq!(*op2, Operand2::Reg(Reg::R3)); + } + other => panic!("Expected And (hi), got {other:?}"), + } + } + + #[test] + fn test_i64_or() { + let db = RuleDatabase::new(); + let mut selector = InstructionSelector::new(db.rules().to_vec()); + + let wasm_ops = vec![WasmOp::I64Or]; + let arm_instrs = selector.select(&wasm_ops).unwrap(); + + assert_eq!(arm_instrs.len(), 2); + assert!(matches!(&arm_instrs[0].op, ArmOp::Orr { .. })); + assert!(matches!(&arm_instrs[1].op, ArmOp::Orr { .. })); + } + + #[test] + fn test_i64_xor() { + let db = RuleDatabase::new(); + let mut selector = InstructionSelector::new(db.rules().to_vec()); + + let wasm_ops = vec![WasmOp::I64Xor]; + let arm_instrs = selector.select(&wasm_ops).unwrap(); + + assert_eq!(arm_instrs.len(), 2); + assert!(matches!(&arm_instrs[0].op, ArmOp::Eor { .. })); + assert!(matches!(&arm_instrs[1].op, ArmOp::Eor { .. })); + } + + #[test] + fn test_i64_eqz() { + let db = RuleDatabase::new(); + let mut selector = InstructionSelector::new(db.rules().to_vec()); + + let wasm_ops = vec![WasmOp::I64Eqz]; + let arm_instrs = selector.select(&wasm_ops).unwrap(); + + assert_eq!(arm_instrs.len(), 1); + match &arm_instrs[0].op { + ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => { + assert_eq!(*rd, Reg::R0); + assert_eq!(*rn_lo, Reg::R0); + assert_eq!(*rn_hi, Reg::R1); + } + other => panic!("Expected I64SetCondZ, got {other:?}"), + } + } + + #[test] + fn test_i64_eq() { + let db = RuleDatabase::new(); + let mut selector = InstructionSelector::new(db.rules().to_vec()); + + let wasm_ops = vec![WasmOp::I64Eq]; + let arm_instrs = selector.select(&wasm_ops).unwrap(); + + assert_eq!(arm_instrs.len(), 1); + match &arm_instrs[0].op { + ArmOp::I64SetCond { + rd, + rn_lo, + rn_hi, + rm_lo, + rm_hi, + cond, + } => { + assert_eq!(*rd, Reg::R0); + assert_eq!(*rn_lo, Reg::R0); + assert_eq!(*rn_hi, Reg::R1); + assert_eq!(*rm_lo, Reg::R2); + assert_eq!(*rm_hi, Reg::R3); + assert_eq!(*cond, Condition::EQ); + } + other => panic!("Expected I64SetCond EQ, got {other:?}"), + } + } + + #[test] + fn test_i64_ne() { + let db = RuleDatabase::new(); + let mut selector = InstructionSelector::new(db.rules().to_vec()); + + let wasm_ops = vec![WasmOp::I64Ne]; + let arm_instrs = selector.select(&wasm_ops).unwrap(); + + assert_eq!(arm_instrs.len(), 1); + match &arm_instrs[0].op { + ArmOp::I64SetCond { cond, .. } => { + assert_eq!(*cond, Condition::NE); + } + other => panic!("Expected I64SetCond NE, got {other:?}"), + } + } + + #[test] + fn test_i64_lt_s() { + let db = RuleDatabase::new(); + let mut selector = InstructionSelector::new(db.rules().to_vec()); + + let wasm_ops = vec![WasmOp::I64LtS]; + let arm_instrs = selector.select(&wasm_ops).unwrap(); + + assert_eq!(arm_instrs.len(), 1); + match &arm_instrs[0].op { + ArmOp::I64SetCond { cond, .. } => assert_eq!(*cond, Condition::LT), + other => panic!("Expected I64SetCond LT, got {other:?}"), + } + } + + #[test] + fn test_i64_lt_u() { + let db = RuleDatabase::new(); + let mut selector = InstructionSelector::new(db.rules().to_vec()); + + let wasm_ops = vec![WasmOp::I64LtU]; + let arm_instrs = selector.select(&wasm_ops).unwrap(); + + assert_eq!(arm_instrs.len(), 1); + match &arm_instrs[0].op { + ArmOp::I64SetCond { cond, .. } => assert_eq!(*cond, Condition::LO), + other => panic!("Expected I64SetCond LO, got {other:?}"), + } + } + + #[test] + fn test_i64_le_s() { + let db = RuleDatabase::new(); + let mut selector = InstructionSelector::new(db.rules().to_vec()); + + let wasm_ops = vec![WasmOp::I64LeS]; + let arm_instrs = selector.select(&wasm_ops).unwrap(); + + assert_eq!(arm_instrs.len(), 1); + match &arm_instrs[0].op { + ArmOp::I64SetCond { cond, .. } => assert_eq!(*cond, Condition::LE), + other => panic!("Expected I64SetCond LE, got {other:?}"), + } + } + + #[test] + fn test_i64_gt_s() { + let db = RuleDatabase::new(); + let mut selector = InstructionSelector::new(db.rules().to_vec()); + + let wasm_ops = vec![WasmOp::I64GtS]; + let arm_instrs = selector.select(&wasm_ops).unwrap(); + + assert_eq!(arm_instrs.len(), 1); + match &arm_instrs[0].op { + ArmOp::I64SetCond { cond, .. } => assert_eq!(*cond, Condition::GT), + other => panic!("Expected I64SetCond GT, got {other:?}"), + } + } + + #[test] + fn test_i64_ge_s() { + let db = RuleDatabase::new(); + let mut selector = InstructionSelector::new(db.rules().to_vec()); + + let wasm_ops = vec![WasmOp::I64GeS]; + let arm_instrs = selector.select(&wasm_ops).unwrap(); + + assert_eq!(arm_instrs.len(), 1); + match &arm_instrs[0].op { + ArmOp::I64SetCond { cond, .. } => assert_eq!(*cond, Condition::GE), + other => panic!("Expected I64SetCond GE, got {other:?}"), + } + } + + #[test] + fn test_i64_unsigned_comparisons() { + let db = RuleDatabase::new(); + + // Test all unsigned comparison conditions + let tests = vec![ + (WasmOp::I64LeU, Condition::LS), + (WasmOp::I64GtU, Condition::HI), + (WasmOp::I64GeU, Condition::HS), + ]; + + for (op, expected_cond) in tests { + let mut selector = InstructionSelector::new(db.rules().to_vec()); + let arm_instrs = selector.select(std::slice::from_ref(&op)).unwrap(); + assert_eq!(arm_instrs.len(), 1); + match &arm_instrs[0].op { + ArmOp::I64SetCond { cond, .. } => { + assert_eq!(*cond, expected_cond, "Wrong condition for {op:?}"); + } + other => panic!("Expected I64SetCond for {op:?}, got {other:?}"), + } + } + } + + #[test] + fn test_i64_mul() { + let db = RuleDatabase::new(); + let mut selector = InstructionSelector::new(db.rules().to_vec()); + + let wasm_ops = vec![WasmOp::I64Mul]; + let arm_instrs = selector.select(&wasm_ops).unwrap(); + + assert_eq!(arm_instrs.len(), 1); + match &arm_instrs[0].op { + ArmOp::I64Mul { + rd_lo, + rd_hi, + rn_lo, + rn_hi, + rm_lo, + rm_hi, + } => { + assert_eq!(*rd_lo, Reg::R0); + assert_eq!(*rd_hi, Reg::R1); + assert_eq!(*rn_lo, Reg::R0); + assert_eq!(*rn_hi, Reg::R1); + assert_eq!(*rm_lo, Reg::R2); + assert_eq!(*rm_hi, Reg::R3); + } + other => panic!("Expected I64Mul, got {other:?}"), + } + } + + #[test] + fn test_i64_shifts() { + let db = RuleDatabase::new(); + + // Shift left + let mut selector = InstructionSelector::new(db.rules().to_vec()); + let arm_instrs = selector.select(&[WasmOp::I64Shl]).unwrap(); + assert_eq!(arm_instrs.len(), 1); + assert!(matches!(&arm_instrs[0].op, ArmOp::I64Shl { .. })); + + // Shift right unsigned + selector.reset(); + let arm_instrs = selector.select(&[WasmOp::I64ShrU]).unwrap(); + assert_eq!(arm_instrs.len(), 1); + assert!(matches!(&arm_instrs[0].op, ArmOp::I64ShrU { .. })); + + // Shift right signed + selector.reset(); + let arm_instrs = selector.select(&[WasmOp::I64ShrS]).unwrap(); + assert_eq!(arm_instrs.len(), 1); + assert!(matches!(&arm_instrs[0].op, ArmOp::I64ShrS { .. })); + } + + #[test] + fn test_i64_rotations() { + let db = RuleDatabase::new(); + + let mut selector = InstructionSelector::new(db.rules().to_vec()); + let arm_instrs = selector.select(&[WasmOp::I64Rotl]).unwrap(); + assert_eq!(arm_instrs.len(), 1); + assert!(matches!(&arm_instrs[0].op, ArmOp::I64Rotl { .. })); + + selector.reset(); + let arm_instrs = selector.select(&[WasmOp::I64Rotr]).unwrap(); + assert_eq!(arm_instrs.len(), 1); + assert!(matches!(&arm_instrs[0].op, ArmOp::I64Rotr { .. })); + } + + #[test] + fn test_i64_bit_manipulation() { + let db = RuleDatabase::new(); + + let mut selector = InstructionSelector::new(db.rules().to_vec()); + + let arm_instrs = selector.select(&[WasmOp::I64Clz]).unwrap(); + assert_eq!(arm_instrs.len(), 1); + assert!(matches!(&arm_instrs[0].op, ArmOp::I64Clz { .. })); + + selector.reset(); + let arm_instrs = selector.select(&[WasmOp::I64Ctz]).unwrap(); + assert_eq!(arm_instrs.len(), 1); + assert!(matches!(&arm_instrs[0].op, ArmOp::I64Ctz { .. })); + + selector.reset(); + let arm_instrs = selector.select(&[WasmOp::I64Popcnt]).unwrap(); + assert_eq!(arm_instrs.len(), 1); + assert!(matches!(&arm_instrs[0].op, ArmOp::I64Popcnt { .. })); + } + + #[test] + fn test_i64_division_remainder() { + let db = RuleDatabase::new(); + + let ops = vec![ + (WasmOp::I64DivS, "I64DivS"), + (WasmOp::I64DivU, "I64DivU"), + (WasmOp::I64RemS, "I64RemS"), + (WasmOp::I64RemU, "I64RemU"), + ]; + + for (op, name) in ops { + let mut selector = InstructionSelector::new(db.rules().to_vec()); + let arm_instrs = selector.select(&[op]).unwrap(); + assert_eq!(arm_instrs.len(), 1, "Failed for {name}"); + // Each should emit the corresponding i64 pseudo-op + } + } + + #[test] + fn test_i64_extend_sign_operations() { + let db = RuleDatabase::new(); + + let mut selector = InstructionSelector::new(db.rules().to_vec()); + + let arm_instrs = selector.select(&[WasmOp::I64Extend8S]).unwrap(); + assert_eq!(arm_instrs.len(), 1); + assert!(matches!(&arm_instrs[0].op, ArmOp::I64Extend8S { .. })); + + selector.reset(); + let arm_instrs = selector.select(&[WasmOp::I64Extend16S]).unwrap(); + assert_eq!(arm_instrs.len(), 1); + assert!(matches!(&arm_instrs[0].op, ArmOp::I64Extend16S { .. })); + + selector.reset(); + let arm_instrs = selector.select(&[WasmOp::I64Extend32S]).unwrap(); + assert_eq!(arm_instrs.len(), 1); + assert!(matches!(&arm_instrs[0].op, ArmOp::I64Extend32S { .. })); + } + + #[test] + fn test_i64_add_sequence() { + // Test a full i64 add sequence: const + const + add + let db = RuleDatabase::new(); + let mut selector = InstructionSelector::new(db.rules().to_vec()); + + let wasm_ops = vec![WasmOp::I64Const(1), WasmOp::I64Const(2), WasmOp::I64Add]; + + let arm_instrs = selector.select(&wasm_ops).unwrap(); + // I64Const(1) -> 1 instr + // I64Const(2) -> 1 instr + // I64Add -> 2 instrs (ADDS + ADC) + assert_eq!(arm_instrs.len(), 4); + + // First two are I64Const + assert!(matches!( + &arm_instrs[0].op, + ArmOp::I64Const { value: 1, .. } + )); + assert!(matches!( + &arm_instrs[1].op, + ArmOp::I64Const { value: 2, .. } + )); + // Then ADDS + ADC + assert!(matches!(&arm_instrs[2].op, ArmOp::Adds { .. })); + assert!(matches!(&arm_instrs[3].op, ArmOp::Adc { .. })); + } + + #[test] + fn test_i64_wrap_extend_roundtrip() { + // extend_i32_u followed by wrap_i64 should produce two instructions + let db = RuleDatabase::new(); + let mut selector = InstructionSelector::new(db.rules().to_vec()); + + let wasm_ops = vec![WasmOp::I64ExtendI32U, WasmOp::I32WrapI64]; + let arm_instrs = selector.select(&wasm_ops).unwrap(); + + assert_eq!(arm_instrs.len(), 2); + assert!(matches!(&arm_instrs[0].op, ArmOp::I64ExtendI32U { .. })); + assert!(matches!(&arm_instrs[1].op, ArmOp::I32WrapI64 { .. })); + } + + #[test] + fn test_i64_load_store() { + let db = RuleDatabase::new(); + let mut selector = InstructionSelector::new(db.rules().to_vec()); + + let wasm_ops = vec![ + WasmOp::I64Load { + offset: 8, + align: 8, + }, + WasmOp::I64Store { + offset: 16, + align: 8, + }, + ]; + + let arm_instrs = selector.select(&wasm_ops).unwrap(); + assert_eq!(arm_instrs.len(), 2); + + match &arm_instrs[0].op { + ArmOp::I64Ldr { rdlo, rdhi, addr } => { + assert_eq!(*rdlo, Reg::R0); + assert_eq!(*rdhi, Reg::R1); + assert_eq!(addr.offset, 8); + } + other => panic!("Expected I64Ldr, got {other:?}"), + } + + match &arm_instrs[1].op { + ArmOp::I64Str { rdlo, rdhi, addr } => { + assert_eq!(*rdlo, Reg::R0); + assert_eq!(*rdhi, Reg::R1); + assert_eq!(addr.offset, 16); + } + other => panic!("Expected I64Str, got {other:?}"), + } + } + + #[test] + fn test_i64_all_ops_no_error() { + // Verify that ALL i64 operations now succeed (no more "requires register pairs" error) + let db = RuleDatabase::new(); + + let all_i64_ops: Vec = vec![ + WasmOp::I64Const(42), + WasmOp::I64Add, + WasmOp::I64Sub, + WasmOp::I64Mul, + WasmOp::I64DivS, + WasmOp::I64DivU, + WasmOp::I64RemS, + WasmOp::I64RemU, + WasmOp::I64And, + WasmOp::I64Or, + WasmOp::I64Xor, + WasmOp::I64Shl, + WasmOp::I64ShrS, + WasmOp::I64ShrU, + WasmOp::I64Rotl, + WasmOp::I64Rotr, + WasmOp::I64Clz, + WasmOp::I64Ctz, + WasmOp::I64Popcnt, + WasmOp::I64Eqz, + WasmOp::I64Eq, + WasmOp::I64Ne, + WasmOp::I64LtS, + WasmOp::I64LtU, + WasmOp::I64LeS, + WasmOp::I64LeU, + WasmOp::I64GtS, + WasmOp::I64GtU, + WasmOp::I64GeS, + WasmOp::I64GeU, + WasmOp::I64ExtendI32S, + WasmOp::I64ExtendI32U, + WasmOp::I32WrapI64, + WasmOp::I64Extend8S, + WasmOp::I64Extend16S, + WasmOp::I64Extend32S, + WasmOp::I64Load { + offset: 0, + align: 8, + }, + WasmOp::I64Store { + offset: 0, + align: 8, + }, + ]; + + // Each should succeed individually (no error) + for op in &all_i64_ops { + let mut selector = InstructionSelector::new(db.rules().to_vec()); + let result = selector.select(std::slice::from_ref(op)); + assert!( + result.is_ok(), + "i64 operation {op:?} should succeed but got error: {:?}", + result.err() + ); + } + } } diff --git a/tests/spec-testsuite b/tests/spec-testsuite index 3453673..7e0b83a 160000 --- a/tests/spec-testsuite +++ b/tests/spec-testsuite @@ -1 +1 @@ -Subproject commit 345367358f065375524498749470720d9cdd1418 +Subproject commit 7e0b83aba9dbbb6e0623c9334b0f73b3bb584b90 diff --git a/tests/wast/BUILD.bazel b/tests/wast/BUILD.bazel index 0afd3c2..e839316 100644 --- a/tests/wast/BUILD.bazel +++ b/tests/wast/BUILD.bazel @@ -1,2480 +1,211 @@ -load("@rules_renode//renode:defs.bzl", "renode_test") -load("//build:wast_test.bzl", "wast_test", "wast_multi_func_test") - -# WAST-driven integration tests for Synth -# These tests parse .wast files, compile to ARM ELF, and execute on Renode - -# ============================================================================= -# Method 1: Manual Robot test (current, hand-written) -# ============================================================================= - -# Generate ELF for the add function from arithmetic module -genrule( - name = "i32_add_elf", - srcs = ["//examples/wat:simple_add.wat"], - outs = ["i32_add.elf"], - cmd = "$(location //crates:synth) compile $(location //examples/wat:simple_add.wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -# Hand-written Robot test (for comparison/debugging) -renode_test( - name = "wast_i32_arithmetic_manual", - robot_test = "i32_arithmetic_test.robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i32_add_elf", - }, - tags = ["renode", "wast", "manual"], -) - -# ============================================================================= -# Method 2: Auto-generated from WAST using synth-test (future-proof) -# ============================================================================= - -# Generate Robot test from WAST file (reads ELF symbols for correct addresses) -genrule( - name = "i32_arithmetic_generated_robot", - srcs = ["i32_arithmetic.wast", ":i32_add_elf"], - outs = ["i32_arithmetic_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i32_arithmetic.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --elf $(location :i32_add_elf) --filter-func add", - tools = ["//crates:synth-test"], -) - -# Auto-generated Renode test -renode_test( - name = "wast_i32_arithmetic_test", - robot_test = ":i32_arithmetic_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i32_add_elf", - }, - tags = ["renode", "wast", "generated"], -) - -# ============================================================================= -# Division tests (i32.div_s, i32.div_u) -# ============================================================================= - -# First extract the div_s WAT from the WAST (manually for now) -# The i32_div.wast contains both div_s and div_u - we compile div_s -genrule( - name = "i32_div_wat", - srcs = [], - outs = ["i32_div_s.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "div_s") (param i32 i32) (result i32) - local.get 0 - local.get 1 - i32.div_s)) -EOF""", -) - -# Generate ELF for division function -genrule( - name = "i32_div_elf", - srcs = [":i32_div_wat"], - outs = ["i32_div.elf"], - cmd = "$(location //crates:synth) compile $(location :i32_div_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -# Generate Robot test from division WAST -genrule( - name = "i32_div_generated_robot", - srcs = ["i32_div.wast"], - outs = ["i32_div_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i32_div.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func div_s", - tools = ["//crates:synth-test"], -) - -# Division Renode test (signed) -renode_test( - name = "wast_i32_div_test", - robot_test = ":i32_div_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i32_div_elf", - }, - tags = ["renode", "wast", "division"], -) - -# Unsigned division -genrule( - name = "i32_div_u_wat", - srcs = [], - outs = ["i32_div_u.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "div_u") (param i32 i32) (result i32) - local.get 0 - local.get 1 - i32.div_u)) -EOF""", -) - -genrule( - name = "i32_div_u_elf", - srcs = [":i32_div_u_wat"], - outs = ["i32_div_u.elf"], - cmd = "$(location //crates:synth) compile $(location :i32_div_u_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i32_div_u_generated_robot", - srcs = ["i32_div.wast"], - outs = ["i32_div_u_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i32_div.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func div_u", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i32_div_u_test", - robot_test = ":i32_div_u_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i32_div_u_elf", - }, - tags = ["renode", "wast", "division"], -) - -# ============================================================================= -# Remainder tests (i32.rem_s, i32.rem_u) -# ============================================================================= - -genrule( - name = "i32_rem_s_wat", - srcs = [], - outs = ["i32_rem_s.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "rem_s") (param i32 i32) (result i32) - local.get 0 - local.get 1 - i32.rem_s)) -EOF""", -) - -genrule( - name = "i32_rem_s_elf", - srcs = [":i32_rem_s_wat"], - outs = ["i32_rem_s.elf"], - cmd = "$(location //crates:synth) compile $(location :i32_rem_s_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i32_rem_generated_robot", - srcs = ["i32_rem.wast"], - outs = ["i32_rem_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i32_rem.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func rem_s", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i32_rem_test", - robot_test = ":i32_rem_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i32_rem_s_elf", - }, - tags = ["renode", "wast", "remainder"], -) - -# Unsigned remainder -genrule( - name = "i32_rem_u_wat", - srcs = [], - outs = ["i32_rem_u.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "rem_u") (param i32 i32) (result i32) - local.get 0 - local.get 1 - i32.rem_u)) -EOF""", -) - -genrule( - name = "i32_rem_u_elf", - srcs = [":i32_rem_u_wat"], - outs = ["i32_rem_u.elf"], - cmd = "$(location //crates:synth) compile $(location :i32_rem_u_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i32_rem_u_generated_robot", - srcs = ["i32_rem.wast"], - outs = ["i32_rem_u_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i32_rem.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func rem_u", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i32_rem_u_test", - robot_test = ":i32_rem_u_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i32_rem_u_elf", - }, - tags = ["renode", "wast", "remainder"], -) - -# ============================================================================= -# Comparison tests (i32.eq, i32.ne, i32.lt_s, i32.gt_s, etc.) -# ============================================================================= - -genrule( - name = "i32_eq_wat", - srcs = [], - outs = ["i32_eq.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "eq") (param i32 i32) (result i32) - local.get 0 - local.get 1 - i32.eq)) -EOF""", -) - -genrule( - name = "i32_eq_elf", - srcs = [":i32_eq_wat"], - outs = ["i32_eq.elf"], - cmd = "$(location //crates:synth) compile $(location :i32_eq_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i32_eq_generated_robot", - srcs = ["i32_compare.wast"], - outs = ["i32_eq_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i32_compare.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func eq", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i32_eq_test", - robot_test = ":i32_eq_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i32_eq_elf", - }, - tags = ["renode", "wast", "comparison"], -) - -# Less than signed -genrule( - name = "i32_lt_s_wat", - srcs = [], - outs = ["i32_lt_s.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "lt_s") (param i32 i32) (result i32) - local.get 0 - local.get 1 - i32.lt_s)) -EOF""", -) - -genrule( - name = "i32_lt_s_elf", - srcs = [":i32_lt_s_wat"], - outs = ["i32_lt_s.elf"], - cmd = "$(location //crates:synth) compile $(location :i32_lt_s_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i32_lt_s_generated_robot", - srcs = ["i32_compare.wast"], - outs = ["i32_lt_s_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i32_compare.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func lt_s", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i32_lt_s_test", - robot_test = ":i32_lt_s_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i32_lt_s_elf", - }, - tags = ["renode", "wast", "comparison"], -) - -# ============================================================================= -# Additional comparison tests (ne, gt_s, lt_u, gt_u, eqz) -# ============================================================================= - -# Not equal -genrule( - name = "i32_ne_wat", - srcs = [], - outs = ["i32_ne.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "ne") (param i32 i32) (result i32) - local.get 0 - local.get 1 - i32.ne)) -EOF""", -) - -genrule( - name = "i32_ne_elf", - srcs = [":i32_ne_wat"], - outs = ["i32_ne.elf"], - cmd = "$(location //crates:synth) compile $(location :i32_ne_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i32_ne_generated_robot", - srcs = ["i32_compare.wast"], - outs = ["i32_ne_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i32_compare.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func ne", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i32_ne_test", - robot_test = ":i32_ne_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i32_ne_elf", - }, - tags = ["renode", "wast", "comparison"], -) - -# Greater than signed -genrule( - name = "i32_gt_s_wat", - srcs = [], - outs = ["i32_gt_s.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "gt_s") (param i32 i32) (result i32) - local.get 0 - local.get 1 - i32.gt_s)) -EOF""", -) - -genrule( - name = "i32_gt_s_elf", - srcs = [":i32_gt_s_wat"], - outs = ["i32_gt_s.elf"], - cmd = "$(location //crates:synth) compile $(location :i32_gt_s_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i32_gt_s_generated_robot", - srcs = ["i32_compare.wast"], - outs = ["i32_gt_s_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i32_compare.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func gt_s", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i32_gt_s_test", - robot_test = ":i32_gt_s_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i32_gt_s_elf", - }, - tags = ["renode", "wast", "comparison"], -) - -# Less than unsigned -genrule( - name = "i32_lt_u_wat", - srcs = [], - outs = ["i32_lt_u.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "lt_u") (param i32 i32) (result i32) - local.get 0 - local.get 1 - i32.lt_u)) -EOF""", -) - -genrule( - name = "i32_lt_u_elf", - srcs = [":i32_lt_u_wat"], - outs = ["i32_lt_u.elf"], - cmd = "$(location //crates:synth) compile $(location :i32_lt_u_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i32_lt_u_generated_robot", - srcs = ["i32_compare.wast"], - outs = ["i32_lt_u_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i32_compare.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func lt_u", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i32_lt_u_test", - robot_test = ":i32_lt_u_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i32_lt_u_elf", - }, - tags = ["renode", "wast", "comparison"], -) - -# Greater than unsigned -genrule( - name = "i32_gt_u_wat", - srcs = [], - outs = ["i32_gt_u.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "gt_u") (param i32 i32) (result i32) - local.get 0 - local.get 1 - i32.gt_u)) -EOF""", -) - -genrule( - name = "i32_gt_u_elf", - srcs = [":i32_gt_u_wat"], - outs = ["i32_gt_u.elf"], - cmd = "$(location //crates:synth) compile $(location :i32_gt_u_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i32_gt_u_generated_robot", - srcs = ["i32_compare.wast"], - outs = ["i32_gt_u_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i32_compare.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func gt_u", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i32_gt_u_test", - robot_test = ":i32_gt_u_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i32_gt_u_elf", - }, - tags = ["renode", "wast", "comparison"], -) - -# Equal to zero -genrule( - name = "i32_eqz_wat", - srcs = [], - outs = ["i32_eqz.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "eqz") (param i32) (result i32) - local.get 0 - i32.eqz)) -EOF""", -) - -genrule( - name = "i32_eqz_elf", - srcs = [":i32_eqz_wat"], - outs = ["i32_eqz.elf"], - cmd = "$(location //crates:synth) compile $(location :i32_eqz_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i32_eqz_generated_robot", - srcs = ["i32_compare.wast"], - outs = ["i32_eqz_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i32_compare.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func eqz", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i32_eqz_test", - robot_test = ":i32_eqz_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i32_eqz_elf", - }, - tags = ["renode", "wast", "comparison"], -) - -# ============================================================================= -# Bit count tests (clz, ctz, popcnt) -# ============================================================================= - -# Count leading zeros -genrule( - name = "i32_clz_wat", - srcs = [], - outs = ["i32_clz.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "clz") (param i32) (result i32) - local.get 0 - i32.clz)) -EOF""", -) - -genrule( - name = "i32_clz_elf", - srcs = [":i32_clz_wat"], - outs = ["i32_clz.elf"], - cmd = "$(location //crates:synth) compile $(location :i32_clz_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i32_clz_generated_robot", - srcs = ["i32_bitcount.wast"], - outs = ["i32_clz_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i32_bitcount.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func clz", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i32_clz_test", - robot_test = ":i32_clz_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i32_clz_elf", - }, - tags = ["renode", "wast", "bitcount"], -) - -# Count trailing zeros -genrule( - name = "i32_ctz_wat", - srcs = [], - outs = ["i32_ctz.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "ctz") (param i32) (result i32) - local.get 0 - i32.ctz)) -EOF""", -) - -genrule( - name = "i32_ctz_elf", - srcs = [":i32_ctz_wat"], - outs = ["i32_ctz.elf"], - cmd = "$(location //crates:synth) compile $(location :i32_ctz_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i32_ctz_generated_robot", - srcs = ["i32_bitcount.wast"], - outs = ["i32_ctz_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i32_bitcount.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func ctz", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i32_ctz_test", - robot_test = ":i32_ctz_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i32_ctz_elf", - }, - tags = ["renode", "wast", "bitcount"], -) - -# Population count -genrule( - name = "i32_popcnt_wat", - srcs = [], - outs = ["i32_popcnt.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "popcnt") (param i32) (result i32) - local.get 0 - i32.popcnt)) -EOF""", -) - -genrule( - name = "i32_popcnt_elf", - srcs = [":i32_popcnt_wat"], - outs = ["i32_popcnt.elf"], - cmd = "$(location //crates:synth) compile $(location :i32_popcnt_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i32_popcnt_generated_robot", - srcs = ["i32_bitcount.wast"], - outs = ["i32_popcnt_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i32_bitcount.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func popcnt", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i32_popcnt_test", - robot_test = ":i32_popcnt_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i32_popcnt_elf", - }, - tags = ["renode", "wast", "bitcount"], -) - -# ============================================================================= -# i64 arithmetic tests (64-bit operations using register pairs) -# ============================================================================= - -# i64.add - 64-bit addition using ADDS + ADC -genrule( - name = "i64_add_wat", - srcs = [], - outs = ["i64_add.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "add") (param i64 i64) (result i64) - local.get 0 - local.get 1 - i64.add)) -EOF""", -) - -genrule( - name = "i64_add_elf", - srcs = [":i64_add_wat"], - outs = ["i64_add.elf"], - cmd = "$(location //crates:synth) compile $(location :i64_add_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i64_add_generated_robot", - srcs = ["i64_arithmetic.wast"], - outs = ["i64_add_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i64_arithmetic.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func add", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i64_add_test", - robot_test = ":i64_add_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i64_add_elf", - }, - tags = ["renode", "wast", "i64"], -) - -# i64.sub - 64-bit subtraction using SUBS + SBC -genrule( - name = "i64_sub_wat", - srcs = [], - outs = ["i64_sub.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "sub") (param i64 i64) (result i64) - local.get 0 - local.get 1 - i64.sub)) -EOF""", -) - -genrule( - name = "i64_sub_elf", - srcs = [":i64_sub_wat"], - outs = ["i64_sub.elf"], - cmd = "$(location //crates:synth) compile $(location :i64_sub_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i64_sub_generated_robot", - srcs = ["i64_arithmetic.wast"], - outs = ["i64_sub_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i64_arithmetic.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func sub", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i64_sub_test", - robot_test = ":i64_sub_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i64_sub_elf", - }, - tags = ["renode", "wast", "i64"], -) - -# i64.and - 64-bit bitwise AND -genrule( - name = "i64_and_wat", - srcs = [], - outs = ["i64_and.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "and") (param i64 i64) (result i64) - local.get 0 - local.get 1 - i64.and)) -EOF""", -) - -genrule( - name = "i64_and_elf", - srcs = [":i64_and_wat"], - outs = ["i64_and.elf"], - cmd = "$(location //crates:synth) compile $(location :i64_and_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i64_and_generated_robot", - srcs = ["i64_arithmetic.wast"], - outs = ["i64_and_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i64_arithmetic.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func and", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i64_and_test", - robot_test = ":i64_and_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i64_and_elf", - }, - tags = ["renode", "wast", "i64"], -) - -# i64.or - 64-bit bitwise OR -genrule( - name = "i64_or_wat", - srcs = [], - outs = ["i64_or.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "or") (param i64 i64) (result i64) - local.get 0 - local.get 1 - i64.or)) -EOF""", -) - -genrule( - name = "i64_or_elf", - srcs = [":i64_or_wat"], - outs = ["i64_or.elf"], - cmd = "$(location //crates:synth) compile $(location :i64_or_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i64_or_generated_robot", - srcs = ["i64_arithmetic.wast"], - outs = ["i64_or_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i64_arithmetic.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func or", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i64_or_test", - robot_test = ":i64_or_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i64_or_elf", - }, - tags = ["renode", "wast", "i64"], -) - -# i64.xor - 64-bit bitwise XOR -genrule( - name = "i64_xor_wat", - srcs = [], - outs = ["i64_xor.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "xor") (param i64 i64) (result i64) - local.get 0 - local.get 1 - i64.xor)) -EOF""", -) - -genrule( - name = "i64_xor_elf", - srcs = [":i64_xor_wat"], - outs = ["i64_xor.elf"], - cmd = "$(location //crates:synth) compile $(location :i64_xor_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i64_xor_generated_robot", - srcs = ["i64_arithmetic.wast"], - outs = ["i64_xor_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i64_arithmetic.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func xor", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i64_xor_test", - robot_test = ":i64_xor_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i64_xor_elf", - }, - tags = ["renode", "wast", "i64"], -) +load("//build:wast_test.bzl", "wast_multi_func_test") # ============================================================================= -# i64 comparison tests (64-bit comparisons returning i32 results) -# ============================================================================= - -# i64.eq - 64-bit equality (compare both register pair halves) -genrule( - name = "i64_eq_wat", - srcs = [], - outs = ["i64_eq.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "eq") (param i64 i64) (result i32) - local.get 0 - local.get 1 - i64.eq)) -EOF""", -) - -genrule( - name = "i64_eq_elf", - srcs = [":i64_eq_wat"], - outs = ["i64_eq.elf"], - cmd = "$(location //crates:synth) compile $(location :i64_eq_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i64_eq_generated_robot", - srcs = ["i64_compare.wast"], - outs = ["i64_eq_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i64_compare.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func eq", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i64_eq_test", - robot_test = ":i64_eq_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i64_eq_elf", - }, - tags = ["renode", "wast", "i64"], -) - -# i64.ne - 64-bit not-equal -genrule( - name = "i64_ne_wat", - srcs = [], - outs = ["i64_ne.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "ne") (param i64 i64) (result i32) - local.get 0 - local.get 1 - i64.ne)) -EOF""", -) - -genrule( - name = "i64_ne_elf", - srcs = [":i64_ne_wat"], - outs = ["i64_ne.elf"], - cmd = "$(location //crates:synth) compile $(location :i64_ne_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i64_ne_generated_robot", - srcs = ["i64_compare.wast"], - outs = ["i64_ne_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i64_compare.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func ne", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i64_ne_test", - robot_test = ":i64_ne_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i64_ne_elf", - }, - tags = ["renode", "wast", "i64"], -) - -# i64.lt_s - 64-bit signed less-than -genrule( - name = "i64_lt_s_wat", - srcs = [], - outs = ["i64_lt_s.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "lt_s") (param i64 i64) (result i32) - local.get 0 - local.get 1 - i64.lt_s)) -EOF""", -) - -genrule( - name = "i64_lt_s_elf", - srcs = [":i64_lt_s_wat"], - outs = ["i64_lt_s.elf"], - cmd = "$(location //crates:synth) compile $(location :i64_lt_s_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i64_lt_s_generated_robot", - srcs = ["i64_compare.wast"], - outs = ["i64_lt_s_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i64_compare.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func lt_s", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i64_lt_s_test", - robot_test = ":i64_lt_s_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i64_lt_s_elf", - }, - tags = ["renode", "wast", "i64"], -) - -# i64.gt_s - 64-bit signed greater-than -genrule( - name = "i64_gt_s_wat", - srcs = [], - outs = ["i64_gt_s.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "gt_s") (param i64 i64) (result i32) - local.get 0 - local.get 1 - i64.gt_s)) -EOF""", -) - -genrule( - name = "i64_gt_s_elf", - srcs = [":i64_gt_s_wat"], - outs = ["i64_gt_s.elf"], - cmd = "$(location //crates:synth) compile $(location :i64_gt_s_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i64_gt_s_generated_robot", - srcs = ["i64_compare.wast"], - outs = ["i64_gt_s_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i64_compare.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func gt_s", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i64_gt_s_test", - robot_test = ":i64_gt_s_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i64_gt_s_elf", - }, - tags = ["renode", "wast", "i64"], -) - -# i64.eqz - 64-bit equal-to-zero (unary: single i64 param, returns i32) -genrule( - name = "i64_eqz_wat", - srcs = [], - outs = ["i64_eqz.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "eqz") (param i64) (result i32) - local.get 0 - i64.eqz)) -EOF""", -) - -genrule( - name = "i64_eqz_elf", - srcs = [":i64_eqz_wat"], - outs = ["i64_eqz.elf"], - cmd = "$(location //crates:synth) compile $(location :i64_eqz_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i64_eqz_generated_robot", - srcs = ["i64_compare.wast"], - outs = ["i64_eqz_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i64_compare.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func eqz", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i64_eqz_test", - robot_test = ":i64_eqz_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i64_eqz_elf", - }, - tags = ["renode", "wast", "i64"], -) - -# ============================================================================= -# i64 shift tests (64-bit shifts using register pairs) +# WAST-driven integration tests for Synth +# +# Each .wast file contains a (module ...) definition plus (assert_return ...) +# test cases. The wast_multi_func_test macro: +# 1. Compiles the module to ARM ELF with --all-exports +# 2. Generates a Robot Framework test from the WAST, reading function +# addresses from the ELF symbol table +# 3. Runs the tests on the Renode Cortex-M4 emulator # ============================================================================= -# i64.shl - 64-bit shift left -genrule( - name = "i64_shl_wat", - srcs = [], - outs = ["i64_shl.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "shl") (param i64 i64) (result i64) - local.get 0 - local.get 1 - i64.shl)) -EOF""", -) - -genrule( - name = "i64_shl_elf", - srcs = [":i64_shl_wat"], - outs = ["i64_shl.elf"], - cmd = "$(location //crates:synth) compile $(location :i64_shl_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i64_shl_generated_robot", - srcs = ["i64_shift.wast"], - outs = ["i64_shl_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i64_shift.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func shl", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i64_shl_test", - robot_test = ":i64_shl_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i64_shl_elf", - }, - tags = ["renode", "wast", "i64"], -) - -# i64.shr_s - 64-bit arithmetic shift right (signed) -genrule( - name = "i64_shr_s_wat", - srcs = [], - outs = ["i64_shr_s.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "shr_s") (param i64 i64) (result i64) - local.get 0 - local.get 1 - i64.shr_s)) -EOF""", -) - -genrule( - name = "i64_shr_s_elf", - srcs = [":i64_shr_s_wat"], - outs = ["i64_shr_s.elf"], - cmd = "$(location //crates:synth) compile $(location :i64_shr_s_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i64_shr_s_generated_robot", - srcs = ["i64_shift.wast"], - outs = ["i64_shr_s_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i64_shift.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func shr_s", - tools = ["//crates:synth-test"], -) +# --- i32 arithmetic (add, sub, mul, and, or, xor) --- -renode_test( - name = "wast_i64_shr_s_test", - robot_test = ":i64_shr_s_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i64_shr_s_elf", - }, - tags = ["renode", "wast", "i64"], +wast_multi_func_test( + name = "wast_i32_arithmetic", + wast = "i32_arithmetic.wast", + tags = ["i32", "arithmetic"], ) -# i64.shr_u - 64-bit logical shift right (unsigned) -genrule( - name = "i64_shr_u_wat", - srcs = [], - outs = ["i64_shr_u.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "shr_u") (param i64 i64) (result i64) - local.get 0 - local.get 1 - i64.shr_u)) -EOF""", -) +# --- i32 comparison (eq, ne, lt_s, lt_u, gt_s, gt_u, eqz) --- -genrule( - name = "i64_shr_u_elf", - srcs = [":i64_shr_u_wat"], - outs = ["i64_shr_u.elf"], - cmd = "$(location //crates:synth) compile $(location :i64_shr_u_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], +wast_multi_func_test( + name = "wast_i32_compare", + wast = "i32_compare.wast", + tags = ["i32", "comparison"], ) -genrule( - name = "i64_shr_u_generated_robot", - srcs = ["i64_shift.wast"], - outs = ["i64_shr_u_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i64_shift.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func shr_u", - tools = ["//crates:synth-test"], -) +# --- i32 simple equality (single-function module) --- -renode_test( - name = "wast_i64_shr_u_test", - robot_test = ":i64_shr_u_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i64_shr_u_elf", - }, - tags = ["renode", "wast", "i64"], +wast_multi_func_test( + name = "wast_i32_eq_simple", + wast = "i32_eq_simple.wast", + tags = ["i32", "comparison"], ) -# ============================================================================= -# i64 multiplication test (uses UMULL/MUL/MLA on ARM) -# ============================================================================= - -# i64.mul - 64-bit multiplication -genrule( - name = "i64_mul_wat", - srcs = [], - outs = ["i64_mul.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "mul") (param i64 i64) (result i64) - local.get 0 - local.get 1 - i64.mul)) -EOF""", -) +# --- i32 division (div_s, div_u) --- -genrule( - name = "i64_mul_elf", - srcs = [":i64_mul_wat"], - outs = ["i64_mul.elf"], - cmd = "$(location //crates:synth) compile $(location :i64_mul_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], +wast_multi_func_test( + name = "wast_i32_div", + wast = "i32_div.wast", + tags = ["i32", "division"], ) -genrule( - name = "i64_mul_generated_robot", - srcs = ["i64_mul.wast"], - outs = ["i64_mul_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i64_mul.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func mul", - tools = ["//crates:synth-test"], -) +# --- i32 remainder (rem_s, rem_u) --- -renode_test( - name = "wast_i64_mul_test", - robot_test = ":i64_mul_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i64_mul_elf", - }, - tags = ["renode", "wast", "i64"], +wast_multi_func_test( + name = "wast_i32_rem", + wast = "i32_rem.wast", + tags = ["i32", "remainder"], ) -# ============================================================================= -# i64 Division tests (div_s, div_u, rem_s, rem_u) -# ============================================================================= +# --- i32 bit count (clz, ctz, popcnt) --- -genrule( - name = "i64_div_u_wat", - srcs = [], - outs = ["i64_div_u.wat"], - cmd = """cat > $@ << 'WATEOF' -(module - (func (export "div_u") (param i64 i64) (result i64) - local.get 0 - local.get 1 - i64.div_u)) -WATEOF""", -) - -genrule( - name = "i64_div_u_elf", - srcs = [":i64_div_u_wat"], - outs = ["i64_div_u.elf"], - cmd = "$(location //crates:synth) compile $(location :i64_div_u_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], +wast_multi_func_test( + name = "wast_i32_bitcount", + wast = "i32_bitcount.wast", + tags = ["i32", "bitcount"], ) -genrule( - name = "i64_div_u_generated_robot", - srcs = ["i64_div.wast"], - outs = ["i64_div_u_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i64_div.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func div_u", - tools = ["//crates:synth-test"], -) +# --- i32 shift (shl, shr_s, shr_u) --- -renode_test( - name = "wast_i64_div_u_test", - robot_test = ":i64_div_u_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i64_div_u_elf", - }, - tags = ["renode", "wast", "i64", "div"], - timeout = "moderate", +wast_multi_func_test( + name = "wast_i32_shift", + wast = "i32_shift.wast", + tags = ["i32", "shift"], ) -# i64.div_s tests -genrule( - name = "i64_div_s_wat", - srcs = [], - outs = ["i64_div_s.wat"], - cmd = """cat > $@ << 'WATEOF' -(module - (func (export "div_s") (param i64 i64) (result i64) - local.get 0 - local.get 1 - i64.div_s)) -WATEOF""", -) +# --- i32 rotate (rotl, rotr) --- -genrule( - name = "i64_div_s_elf", - srcs = [":i64_div_s_wat"], - outs = ["i64_div_s.elf"], - cmd = "$(location //crates:synth) compile $(location :i64_div_s_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], +wast_multi_func_test( + name = "wast_i32_rotate", + wast = "i32_rotate.wast", + tags = ["i32", "rotate"], ) -genrule( - name = "i64_div_s_generated_robot", - srcs = ["i64_div.wast"], - outs = ["i64_div_s_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i64_div.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func div_s", - tools = ["//crates:synth-test"], -) +# --- i32 memory (store_load) --- -renode_test( - name = "wast_i64_div_s_test", - robot_test = ":i64_div_s_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i64_div_s_elf", - }, - tags = ["renode", "wast", "i64", "div"], - timeout = "moderate", +wast_multi_func_test( + name = "wast_i32_memory", + wast = "i32_memory.wast", + tags = ["i32", "memory"], ) -# i64.rem_u tests -genrule( - name = "i64_rem_u_wat", - srcs = [], - outs = ["i64_rem_u.wat"], - cmd = """cat > $@ << 'WATEOF' -(module - (func (export "rem_u") (param i64 i64) (result i64) - local.get 0 - local.get 1 - i64.rem_u)) -WATEOF""", -) +# --- i64 arithmetic (add, sub, and, or, xor) --- -genrule( - name = "i64_rem_u_elf", - srcs = [":i64_rem_u_wat"], - outs = ["i64_rem_u.elf"], - cmd = "$(location //crates:synth) compile $(location :i64_rem_u_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], +wast_multi_func_test( + name = "wast_i64_arithmetic", + wast = "i64_arithmetic.wast", + tags = ["i64", "arithmetic"], ) -genrule( - name = "i64_rem_u_generated_robot", - srcs = ["i64_div.wast"], - outs = ["i64_rem_u_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i64_div.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func rem_u", - tools = ["//crates:synth-test"], -) +# --- i64 comparison (eq, ne, lt_s, gt_s, eqz) --- -renode_test( - name = "wast_i64_rem_u_test", - robot_test = ":i64_rem_u_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i64_rem_u_elf", - }, - tags = ["renode", "wast", "i64", "rem"], - timeout = "moderate", +wast_multi_func_test( + name = "wast_i64_compare", + wast = "i64_compare.wast", + tags = ["i64", "comparison"], ) -# i64.rem_s tests -genrule( - name = "i64_rem_s_wat", - srcs = [], - outs = ["i64_rem_s.wat"], - cmd = """cat > $@ << 'WATEOF' -(module - (func (export "rem_s") (param i64 i64) (result i64) - local.get 0 - local.get 1 - i64.rem_s)) -WATEOF""", -) +# --- i64 multiplication --- -genrule( - name = "i64_rem_s_elf", - srcs = [":i64_rem_s_wat"], - outs = ["i64_rem_s.elf"], - cmd = "$(location //crates:synth) compile $(location :i64_rem_s_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], +wast_multi_func_test( + name = "wast_i64_mul", + wast = "i64_mul.wast", + tags = ["i64", "multiplication"], ) -genrule( - name = "i64_rem_s_generated_robot", - srcs = ["i64_div.wast"], - outs = ["i64_rem_s_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i64_div.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func rem_s", - tools = ["//crates:synth-test"], -) +# --- i64 division and remainder (div_u, div_s, rem_u, rem_s) --- -renode_test( - name = "wast_i64_rem_s_test", - robot_test = ":i64_rem_s_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i64_rem_s_elf", - }, - tags = ["renode", "wast", "i64", "rem"], +wast_multi_func_test( + name = "wast_i64_div", + wast = "i64_div.wast", + tags = ["i64", "division"], timeout = "moderate", ) -# ============================================================================= -# Control flow tests - Phase 3a: select (ternary operator) -# ============================================================================= - -genrule( - name = "control_select_wat", - srcs = [], - outs = ["control_select.wat"], - cmd = """cat > $@ << 'WATEOF' -(module - (func (export "select_i32") (param i32 i32 i32) (result i32) - local.get 0 - local.get 1 - local.get 2 - select)) -WATEOF""", -) - -genrule( - name = "control_select_elf", - srcs = [":control_select_wat"], - outs = ["control_select.elf"], - cmd = "$(location //crates:synth) compile $(location :control_select_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "control_select_generated_robot", - srcs = ["control_select.wast"], - outs = ["control_select_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location control_select.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func select_i32", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_control_select_test", - robot_test = ":control_select_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":control_select_elf", - }, - tags = ["renode", "wast", "control"], -) - -# ============================================================================= -# Control flow tests - Phase 3b: if-else blocks -# ============================================================================= - -genrule( - name = "control_if_simple_wat", - srcs = [], - outs = ["control_if_simple.wat"], - cmd = """cat > $@ << 'WATEOF' -(module - (func (export "if_else_simple") (param i32) (result i32) - local.get 0 - if (result i32) - i32.const 1 - else - i32.const 0 - end)) -WATEOF""", -) +# --- i64 shift (shl, shr_s, shr_u) --- -genrule( - name = "control_if_simple_elf", - srcs = [":control_if_simple_wat"], - outs = ["control_if_simple.elf"], - cmd = "$(location //crates:synth) compile $(location :control_if_simple_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "control_if_generated_robot", - srcs = ["control_if.wast"], - outs = ["control_if_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location control_if.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func if_else_simple", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_control_if_test", - robot_test = ":control_if_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":control_if_simple_elf", - }, - tags = ["renode", "wast", "control"], -) - -# ============================================================================= -# Control flow tests - Phase 3c: loop with backward branches -# ============================================================================= - -genrule( - name = "control_loop_countdown_wat", - srcs = [], - outs = ["control_loop_countdown.wat"], - cmd = """cat > $@ << 'WATEOF' -(module - (func (export "countdown") (param i32) (result i32) - (local i32) - local.get 0 - local.set 1 - loop (result i32) - local.get 1 - i32.const 1 - i32.sub - local.tee 1 - local.get 1 - i32.const 0 - i32.gt_s - br_if 0 - end)) -WATEOF""", -) - -genrule( - name = "control_loop_countdown_elf", - srcs = [":control_loop_countdown_wat"], - outs = ["control_loop_countdown.elf"], - cmd = "$(location //crates:synth) compile $(location :control_loop_countdown_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "control_loop_generated_robot", - srcs = ["control_loop.wast"], - outs = ["control_loop_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location control_loop.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func countdown", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_control_loop_test", - robot_test = ":control_loop_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":control_loop_countdown_elf", - }, - tags = ["renode", "wast", "control", "loop"], -) - -# Sum to N loop test -genrule( - name = "control_loop_sum_wat", - srcs = [], - outs = ["control_loop_sum.wat"], - cmd = """cat > $@ << 'WATEOF' -(module - (func (export "sum_to_n") (param i32) (result i32) - (local i32) - i32.const 0 - local.set 1 - loop (result i32) - local.get 1 - local.get 0 - i32.add - local.set 1 - local.get 0 - i32.const 1 - i32.sub - local.tee 0 - i32.const 0 - i32.gt_s - br_if 0 - local.get 1 - end)) -WATEOF""", -) - -genrule( - name = "control_loop_sum_elf", - srcs = [":control_loop_sum_wat"], - outs = ["control_loop_sum.elf"], - cmd = "$(location //crates:synth) compile $(location :control_loop_sum_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "control_loop_sum_generated_robot", - srcs = ["control_loop.wast"], - outs = ["control_loop_sum_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location control_loop.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func sum_to_n", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_control_loop_sum_test", - robot_test = ":control_loop_sum_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":control_loop_sum_elf", - }, - tags = ["renode", "wast", "control", "loop"], -) - -# Count iterations loop test -genrule( - name = "control_loop_count_iters_wat", - srcs = [], - outs = ["control_loop_count_iters.wat"], - cmd = """cat > $@ << 'WATEOF' -(module - (func (export "count_iters") (param i32) (result i32) - (local i32) - i32.const 0 - local.set 1 - loop (result i32) - local.get 1 - i32.const 1 - i32.add - local.set 1 - local.get 0 - i32.const 1 - i32.sub - local.tee 0 - i32.const 0 - i32.gt_s - br_if 0 - local.get 1 - end)) -WATEOF""", -) - -genrule( - name = "control_loop_count_iters_elf", - srcs = [":control_loop_count_iters_wat"], - outs = ["control_loop_count_iters.elf"], - cmd = "$(location //crates:synth) compile $(location :control_loop_count_iters_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "control_loop_count_iters_generated_robot", - srcs = ["control_loop.wast"], - outs = ["control_loop_count_iters_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location control_loop.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func count_iters", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_control_loop_count_iters_test", - robot_test = ":control_loop_count_iters_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":control_loop_count_iters_elf", - }, - tags = ["renode", "wast", "control", "loop"], -) - -# ============================================================================= -# Control flow tests - Phase 3d: nested if-else (nested select) -# ============================================================================= - -# Nested if-else test -genrule( - name = "control_nested_select_wat", - srcs = [], - outs = ["control_nested_select.wat"], - cmd = """cat > $@ << 'WATEOF' -(module - ;; Nested if-else with simple values - ;; Returns: val1 if both conds true, val2 if outer true but inner false, val3 if outer false - (func (export "nested_if_else") (param i32 i32) (result i32) - local.get 0 ;; outer condition - if (result i32) - local.get 1 ;; inner condition - if (result i32) - i32.const 10 ;; val1: both conditions true - else - i32.const 20 ;; val2: outer true, inner false - end - else - i32.const 30 ;; val3: outer false - end)) -WATEOF""", -) - -genrule( - name = "control_nested_select_elf", - srcs = [":control_nested_select_wat"], - outs = ["control_nested_select.elf"], - cmd = "$(location //crates:synth) compile $(location :control_nested_select_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "control_nested_select_generated_robot", - srcs = ["control_nested_select.wast"], - outs = ["control_nested_select_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location control_nested_select.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func nested_if_else", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_control_nested_select_test", - robot_test = ":control_nested_select_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":control_nested_select_elf", - }, - tags = ["renode", "wast", "control", "nested"], -) - -# ============================================================================= -# Control flow tests - Phase 3e: br_if block pattern -# ============================================================================= - -# br_if select pattern test -genrule( - name = "control_br_if_select_wat", - srcs = [], - outs = ["control_br_if_select.wat"], - cmd = """cat > $@ << 'WATEOF' -(module - ;; Basic br_if early exit pattern - ;; Returns val1 if condition is true, else val2 - (func (export "br_if_select") (param i32) (result i32) - block (result i32) - i32.const 10 ;; val1 (early exit value) - local.get 0 ;; condition - br_if 0 ;; if cond, exit with val1 - drop ;; drop val1 - i32.const 20 ;; val2 (fallthrough value) - end)) -WATEOF""", -) - -genrule( - name = "control_br_if_select_elf", - srcs = [":control_br_if_select_wat"], - outs = ["control_br_if_select.elf"], - cmd = "$(location //crates:synth) compile $(location :control_br_if_select_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "control_br_if_select_generated_robot", - srcs = ["control_br_if_select.wast"], - outs = ["control_br_if_select_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location control_br_if_select.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func br_if_select", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_control_br_if_select_test", - robot_test = ":control_br_if_select_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":control_br_if_select_elf", - }, - tags = ["renode", "wast", "control", "br_if"], -) - -# ============================================================================= -# Shift tests (i32.shl, i32.shr_s, i32.shr_u) -# ============================================================================= - -# Shift left -genrule( - name = "i32_shl_wat", - srcs = [], - outs = ["i32_shl.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "shl") (param i32 i32) (result i32) - local.get 0 - local.get 1 - i32.shl)) -EOF""", -) - -genrule( - name = "i32_shl_elf", - srcs = [":i32_shl_wat"], - outs = ["i32_shl.elf"], - cmd = "$(location //crates:synth) compile $(location :i32_shl_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i32_shl_generated_robot", - srcs = ["i32_shift.wast"], - outs = ["i32_shl_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i32_shift.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func shl", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i32_shl_test", - robot_test = ":i32_shl_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i32_shl_elf", - }, - tags = ["renode", "wast", "shift"], -) - -# Shift right signed (arithmetic) -genrule( - name = "i32_shr_s_wat", - srcs = [], - outs = ["i32_shr_s.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "shr_s") (param i32 i32) (result i32) - local.get 0 - local.get 1 - i32.shr_s)) -EOF""", -) - -genrule( - name = "i32_shr_s_elf", - srcs = [":i32_shr_s_wat"], - outs = ["i32_shr_s.elf"], - cmd = "$(location //crates:synth) compile $(location :i32_shr_s_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i32_shr_s_generated_robot", - srcs = ["i32_shift.wast"], - outs = ["i32_shr_s_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i32_shift.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func shr_s", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i32_shr_s_test", - robot_test = ":i32_shr_s_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i32_shr_s_elf", - }, - tags = ["renode", "wast", "shift"], -) - -# Shift right unsigned (logical) -genrule( - name = "i32_shr_u_wat", - srcs = [], - outs = ["i32_shr_u.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "shr_u") (param i32 i32) (result i32) - local.get 0 - local.get 1 - i32.shr_u)) -EOF""", -) - -genrule( - name = "i32_shr_u_elf", - srcs = [":i32_shr_u_wat"], - outs = ["i32_shr_u.elf"], - cmd = "$(location //crates:synth) compile $(location :i32_shr_u_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i32_shr_u_generated_robot", - srcs = ["i32_shift.wast"], - outs = ["i32_shr_u_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i32_shift.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func shr_u", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i32_shr_u_test", - robot_test = ":i32_shr_u_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i32_shr_u_elf", - }, - tags = ["renode", "wast", "shift"], -) - -# ============================================================================= -# Rotate tests (i32.rotl, i32.rotr) -# ============================================================================= - -# Rotate left -genrule( - name = "i32_rotl_wat", - srcs = [], - outs = ["i32_rotl.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "rotl") (param i32 i32) (result i32) - local.get 0 - local.get 1 - i32.rotl)) -EOF""", -) - -genrule( - name = "i32_rotl_elf", - srcs = [":i32_rotl_wat"], - outs = ["i32_rotl.elf"], - cmd = "$(location //crates:synth) compile $(location :i32_rotl_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i32_rotl_generated_robot", - srcs = ["i32_rotate.wast"], - outs = ["i32_rotl_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i32_rotate.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func rotl", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i32_rotl_test", - robot_test = ":i32_rotl_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i32_rotl_elf", - }, - tags = ["renode", "wast", "rotate"], -) - -# Rotate right -genrule( - name = "i32_rotr_wat", - srcs = [], - outs = ["i32_rotr.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "rotr") (param i32 i32) (result i32) - local.get 0 - local.get 1 - i32.rotr)) -EOF""", -) - -genrule( - name = "i32_rotr_elf", - srcs = [":i32_rotr_wat"], - outs = ["i32_rotr.elf"], - cmd = "$(location //crates:synth) compile $(location :i32_rotr_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i32_rotr_generated_robot", - srcs = ["i32_rotate.wast"], - outs = ["i32_rotr_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i32_rotate.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func rotr", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i32_rotr_test", - robot_test = ":i32_rotr_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i32_rotr_elf", - }, - tags = ["renode", "wast", "rotate"], -) - -# ============================================================================= -# Bitwise tests (i32.and, i32.or, i32.xor) - from i32_arithmetic.wast -# ============================================================================= - -# Bitwise AND -genrule( - name = "i32_and_wat", - srcs = [], - outs = ["i32_and.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "and") (param i32 i32) (result i32) - local.get 0 - local.get 1 - i32.and)) -EOF""", -) - -genrule( - name = "i32_and_elf", - srcs = [":i32_and_wat"], - outs = ["i32_and.elf"], - cmd = "$(location //crates:synth) compile $(location :i32_and_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i32_and_generated_robot", - srcs = ["i32_arithmetic.wast"], - outs = ["i32_and_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i32_arithmetic.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func and", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i32_and_test", - robot_test = ":i32_and_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i32_and_elf", - }, - tags = ["renode", "wast", "bitwise"], -) - -# Bitwise OR -genrule( - name = "i32_or_wat", - srcs = [], - outs = ["i32_or.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "or") (param i32 i32) (result i32) - local.get 0 - local.get 1 - i32.or)) -EOF""", -) - -genrule( - name = "i32_or_elf", - srcs = [":i32_or_wat"], - outs = ["i32_or.elf"], - cmd = "$(location //crates:synth) compile $(location :i32_or_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i32_or_generated_robot", - srcs = ["i32_arithmetic.wast"], - outs = ["i32_or_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i32_arithmetic.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func or", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i32_or_test", - robot_test = ":i32_or_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i32_or_elf", - }, - tags = ["renode", "wast", "bitwise"], -) - -# Bitwise XOR -genrule( - name = "i32_xor_wat", - srcs = [], - outs = ["i32_xor.wat"], - cmd = """cat > $@ << 'EOF' -(module - (func (export "xor") (param i32 i32) (result i32) - local.get 0 - local.get 1 - i32.xor)) -EOF""", -) - -genrule( - name = "i32_xor_elf", - srcs = [":i32_xor_wat"], - outs = ["i32_xor.elf"], - cmd = "$(location //crates:synth) compile $(location :i32_xor_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i32_xor_generated_robot", - srcs = ["i32_arithmetic.wast"], - outs = ["i32_xor_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i32_arithmetic.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func xor", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i32_xor_test", - robot_test = ":i32_xor_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i32_xor_elf", - }, - tags = ["renode", "wast", "bitwise"], -) - -# ============================================================================= -# Memory tests (i32.load, i32.store) - Linear memory access -# ============================================================================= - -# store_load: store a value and load it back (roundtrip) -genrule( - name = "i32_store_load_wat", - srcs = [], - outs = ["i32_store_load.wat"], - cmd = """cat > $@ << 'EOF' -(module - (memory 1) - (func (export "store_load") (param i32 i32) (result i32) - local.get 0 - local.get 1 - i32.store - local.get 0 - i32.load)) -EOF""", -) - -genrule( - name = "i32_store_load_elf", - srcs = [":i32_store_load_wat"], - outs = ["i32_store_load.elf"], - cmd = "$(location //crates:synth) compile $(location :i32_store_load_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], -) - -genrule( - name = "i32_memory_generated_robot", - srcs = ["i32_memory.wast"], - outs = ["i32_memory_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location i32_memory.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func store_load", - tools = ["//crates:synth-test"], -) - -renode_test( - name = "wast_i32_memory_test", - robot_test = ":i32_memory_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":i32_store_load_elf", - }, - tags = ["renode", "wast", "memory"], +wast_multi_func_test( + name = "wast_i64_shift", + wast = "i64_shift.wast", + tags = ["i64", "shift"], ) -# ============================================================================= -# Control flow tests - Phase 4a: Nested loops (loop inside loop) -# ============================================================================= - -# Nested multiply: a * b via nested repeated addition -genrule( - name = "control_nested_loop_wat", - srcs = [], - outs = ["control_nested_loop.wat"], - cmd = """cat > $@ << 'WATEOF' -(module - (func (export "nested_multiply") (param i32 i32) (result i32) - (local i32 i32) - i32.const 0 - local.set 2 - loop (result i32) - local.get 0 - local.set 3 - loop - local.get 2 - i32.const 1 - i32.add - local.set 2 - local.get 3 - i32.const 1 - i32.sub - local.tee 3 - i32.const 0 - i32.gt_s - br_if 0 - end - local.get 1 - i32.const 1 - i32.sub - local.tee 1 - i32.const 0 - i32.gt_s - br_if 0 - local.get 2 - end)) -WATEOF""", -) +# --- Control flow: select (ternary operator) --- -genrule( - name = "control_nested_loop_elf", - srcs = [":control_nested_loop_wat"], - outs = ["control_nested_loop.elf"], - cmd = "$(location //crates:synth) compile $(location :control_nested_loop_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], +wast_multi_func_test( + name = "wast_control_select", + wast = "control_select.wast", + tags = ["control", "select"], ) -genrule( - name = "control_nested_loop_generated_robot", - srcs = ["control_nested_loop.wast"], - outs = ["control_nested_loop_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location control_nested_loop.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func nested_multiply", - tools = ["//crates:synth-test"], -) +# --- Control flow: if-else blocks --- -renode_test( - name = "wast_control_nested_loop_test", - robot_test = ":control_nested_loop_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":control_nested_loop_elf", - }, - tags = ["renode", "wast", "control", "nested", "loop"], +wast_multi_func_test( + name = "wast_control_if", + wast = "control_if.wast", + tags = ["control", "if"], ) -# ============================================================================= -# Control flow tests - Phase 4b: Select inside loop (conditional accumulation) -# ============================================================================= - -# sign_sum: loop with select for alternating +1/-1 accumulation -genrule( - name = "control_loop_if_sign_sum_wat", - srcs = [], - outs = ["control_loop_if_sign_sum.wat"], - cmd = """cat > $@ << 'WATEOF' -(module - (func (export "sign_sum") (param i32) (result i32) - (local i32 i32) - i32.const 0 - local.set 1 - i32.const 1 - local.set 2 - loop (result i32) - i32.const 1 - i32.const -1 - local.get 2 - select - local.get 1 - i32.add - local.set 1 - local.get 2 - i32.const 1 - i32.xor - local.set 2 - local.get 0 - i32.const 1 - i32.sub - local.tee 0 - i32.const 0 - i32.gt_s - br_if 0 - local.get 1 - end)) -WATEOF""", -) +# --- Control flow: loop with backward branches --- -genrule( - name = "control_loop_if_sign_sum_elf", - srcs = [":control_loop_if_sign_sum_wat"], - outs = ["control_loop_if_sign_sum.elf"], - cmd = "$(location //crates:synth) compile $(location :control_loop_if_sign_sum_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], +wast_multi_func_test( + name = "wast_control_loop", + wast = "control_loop.wast", + tags = ["control", "loop"], ) -genrule( - name = "control_loop_if_sign_sum_generated_robot", - srcs = ["control_loop_if.wast"], - outs = ["control_loop_if_sign_sum_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location control_loop_if.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func sign_sum", - tools = ["//crates:synth-test"], -) +# --- Control flow: nested if-else --- -renode_test( - name = "wast_control_loop_if_sign_sum_test", - robot_test = ":control_loop_if_sign_sum_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":control_loop_if_sign_sum_elf", - }, - tags = ["renode", "wast", "control", "loop", "select"], +wast_multi_func_test( + name = "wast_control_nested_select", + wast = "control_nested_select.wast", + tags = ["control", "nested"], ) -# max_val: select with comparison (non-loop, tests select+comparison interaction) -genrule( - name = "control_loop_if_max_val_wat", - srcs = [], - outs = ["control_loop_if_max_val.wat"], - cmd = """cat > $@ << 'WATEOF' -(module - (func (export "max_val") (param i32 i32) (result i32) - local.get 0 - local.get 1 - local.get 0 - local.get 1 - i32.gt_s - select)) -WATEOF""", -) +# --- Control flow: br_if block pattern --- -genrule( - name = "control_loop_if_max_val_elf", - srcs = [":control_loop_if_max_val_wat"], - outs = ["control_loop_if_max_val.elf"], - cmd = "$(location //crates:synth) compile $(location :control_loop_if_max_val_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], +wast_multi_func_test( + name = "wast_control_br_if_select", + wast = "control_br_if_select.wast", + tags = ["control", "br_if"], ) -genrule( - name = "control_loop_if_max_val_generated_robot", - srcs = ["control_loop_if.wast"], - outs = ["control_loop_if_max_val_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location control_loop_if.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func max_val", - tools = ["//crates:synth-test"], -) +# --- Control flow: loop with select (conditional accumulation) --- -renode_test( - name = "wast_control_loop_if_max_val_test", - robot_test = ":control_loop_if_max_val_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":control_loop_if_max_val_elf", - }, - tags = ["renode", "wast", "control", "select"], +wast_multi_func_test( + name = "wast_control_loop_if", + wast = "control_loop_if.wast", + tags = ["control", "loop", "select"], ) -# ============================================================================= -# Control flow tests - Phase 4c: Factorial (loop with i32.mul) -# ============================================================================= - -# factorial: n! via multiply loop -genrule( - name = "control_factorial_wat", - srcs = [], - outs = ["control_factorial.wat"], - cmd = """cat > $@ << 'WATEOF' -(module - (func (export "factorial") (param i32) (result i32) - (local i32) - i32.const 1 - local.set 1 - loop (result i32) - local.get 1 - local.get 0 - i32.mul - local.set 1 - local.get 0 - i32.const 1 - i32.sub - local.tee 0 - i32.const 1 - i32.gt_s - br_if 0 - local.get 1 - end)) -WATEOF""", -) +# --- Control flow: nested loops --- -genrule( - name = "control_factorial_elf", - srcs = [":control_factorial_wat"], - outs = ["control_factorial.elf"], - cmd = "$(location //crates:synth) compile $(location :control_factorial_wat) -o $@ --cortex-m", - tools = ["//crates:synth"], +wast_multi_func_test( + name = "wast_control_nested_loop", + wast = "control_nested_loop.wast", + tags = ["control", "nested", "loop"], ) -genrule( - name = "control_factorial_generated_robot", - srcs = ["control_factorial.wast"], - outs = ["control_factorial_generated.robot"], - cmd = "$(location //crates:synth-test) generate --wast $(location control_factorial.wast) --robot $@ --platform '\\$${CURDIR}/../renode/synth_cortex_m.repl' --filter-func factorial", - tools = ["//crates:synth-test"], -) +# --- Control flow: factorial (loop with multiply) --- -renode_test( - name = "wast_control_factorial_test", - robot_test = ":control_factorial_generated_robot", - deps = [ - "//tests/renode:synth_cortex_m.repl", - ], - variables_with_label = { - "ELF": ":control_factorial_elf", - }, - tags = ["renode", "wast", "control", "loop", "factorial"], +wast_multi_func_test( + name = "wast_control_factorial", + wast = "control_factorial.wast", + tags = ["control", "loop", "factorial"], ) # ============================================================================= -# Official WebAssembly Spec Tests - Using wast_multi_func_test +# Official WebAssembly Spec Tests # ============================================================================= -# These tests use the official W3C WebAssembly testsuite, compiled with -# --all-exports to create a multi-function ELF. The synth-test tool reads -# the ELF symbol table to get per-function addresses. -# Official i32.wast spec test (30 exported functions covering all i32 ops) wast_multi_func_test( name = "spec_i32_test", wast = "//tests/spec-testsuite:i32.wast", - tags = ["spec", "i32", "multi-func"], + tags = ["spec", "i32"], ) -# Official i64.wast spec test (30 exported functions covering all i64 ops) wast_multi_func_test( name = "spec_i64_test", wast = "//tests/spec-testsuite:i64.wast", - tags = ["spec", "i64", "multi-func"], + tags = ["spec", "i64"], ) -# Official select.wast spec test (control flow select instruction) wast_multi_func_test( name = "spec_select_test", wast = "//tests/spec-testsuite:select.wast", - tags = ["spec", "select", "multi-func"], + tags = ["spec", "select"], )