From 3c20bded47e24beb58549dbba60cb8107784ee3e Mon Sep 17 00:00:00 2001 From: Param Date: Sat, 18 Jul 2026 19:54:41 +0530 Subject: [PATCH 1/2] Implement sign extension operators (Issue #55) --- crates/spacewasm_util/src/debug.rs | 5 ++ crates/spacewasm_util/src/trace.rs | 5 ++ src/code.rs | 5 ++ src/compiler.rs | 5 ++ src/constant.rs | 5 ++ src/interpreter.rs | 31 ++++++++++ src/interpreter_tests.rs | 93 ++++++++++++++++++++++++++++++ src/ir_reader.rs | 5 ++ src/opcode.rs | 5 ++ src/visitor.rs | 5 ++ tests/util/inspector.rs | 5 ++ 11 files changed, 169 insertions(+) diff --git a/crates/spacewasm_util/src/debug.rs b/crates/spacewasm_util/src/debug.rs index 5b58757..c30be82 100644 --- a/crates/spacewasm_util/src/debug.rs +++ b/crates/spacewasm_util/src/debug.rs @@ -195,6 +195,11 @@ impl<'a, ST: OutputStream, S, E, T: BaseVisitor> BaseVisit // Numeric instructions - conversions visit_fn!(i32_wrap_i64); + visit_fn!(i32_extend8_s); + visit_fn!(i32_extend16_s); + visit_fn!(i64_extend8_s); + visit_fn!(i64_extend16_s); + visit_fn!(i64_extend32_s); visit_fn!(i32_trunc_f32_s); visit_fn!(i32_trunc_f32_u); visit_fn!(i32_trunc_f64_s); diff --git a/crates/spacewasm_util/src/trace.rs b/crates/spacewasm_util/src/trace.rs index 120af7d..fbe2a79 100644 --- a/crates/spacewasm_util/src/trace.rs +++ b/crates/spacewasm_util/src/trace.rs @@ -318,6 +318,11 @@ impl<'a, 'store, T: BaseVisitor, Error = E>, E> // Numeric instructions - conversions trace_visit_fn!(i32_wrap_i64); + trace_visit_fn!(i32_extend8_s); + trace_visit_fn!(i32_extend16_s); + trace_visit_fn!(i64_extend8_s); + trace_visit_fn!(i64_extend16_s); + trace_visit_fn!(i64_extend32_s); trace_visit_fn!(i32_trunc_f32_s); trace_visit_fn!(i32_trunc_f32_u); trace_visit_fn!(i32_trunc_f64_s); diff --git a/src/code.rs b/src/code.rs index de21f25..c7d9b5a 100644 --- a/src/code.rs +++ b/src/code.rs @@ -410,6 +410,11 @@ impl<'wasm> Reader<'wasm> { // Numeric instructions - conversions I32_WRAP_I64 => instruction!(i32_wrap_i64), + I32_EXTEND8_S => instruction!(i32_extend8_s), + I32_EXTEND16_S => instruction!(i32_extend16_s), + I64_EXTEND8_S => instruction!(i64_extend8_s), + I64_EXTEND16_S => instruction!(i64_extend16_s), + I64_EXTEND32_S => instruction!(i64_extend32_s), I32_TRUNC_F32_S => instruction!(i32_trunc_f32_s), I32_TRUNC_F32_U => instruction!(i32_trunc_f32_u), I32_TRUNC_F64_S => instruction!(i32_trunc_f64_s), diff --git a/src/compiler.rs b/src/compiler.rs index 215cda8..e4d9033 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -614,4 +614,9 @@ impl<'a, const MAX_CODE_PAGES: usize, const MAX_CONTROL_FRAMES: usize, const MAX instruction!(f64_convert_i64_s, F64_CONVERT_I64_S, (I64) -> (F64)); instruction!(f64_convert_i64_u, F64_CONVERT_I64_U, (I64) -> (F64)); instruction!(f64_promote_f32, F64_PROMOTE_F32, (F32) -> (F64)); + instruction!(i32_extend8_s, I32_EXTEND8_S, (I32) -> (I32)); + instruction!(i32_extend16_s, I32_EXTEND16_S, (I32) -> (I32)); + instruction!(i64_extend8_s, I64_EXTEND8_S, (I64) -> (I64)); + instruction!(i64_extend16_s, I64_EXTEND16_S, (I64) -> (I64)); + instruction!(i64_extend32_s, I64_EXTEND32_S, (I64) -> (I64)); } diff --git a/src/constant.rs b/src/constant.rs index 79d2bde..d030514 100644 --- a/src/constant.rs +++ b/src/constant.rs @@ -232,6 +232,11 @@ impl<'a> BaseVisitor for ConstantCompiler<'a> { // Numeric instructions - conversions invalid_constant_fn!(i32_wrap_i64); + invalid_constant_fn!(i32_extend8_s); + invalid_constant_fn!(i32_extend16_s); + invalid_constant_fn!(i64_extend8_s); + invalid_constant_fn!(i64_extend16_s); + invalid_constant_fn!(i64_extend32_s); invalid_constant_fn!(i32_trunc_f32_s); invalid_constant_fn!(i32_trunc_f32_u); invalid_constant_fn!(i32_trunc_f64_s); diff --git a/src/interpreter.rs b/src/interpreter.rs index 603aa61..48c5efa 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -1109,6 +1109,37 @@ impl<'store> BaseVisitor for Interpreter<'store> { Ok(()) } + fn i32_extend8_s(&self, state: &mut Self::State) -> Result<(), Self::Error> { + let i = state.stack.read_u32(state.sp - 1) as i8 as i32 as u32; + state.stack.write_u32(state.sp - 1, i); + Ok(()) + } + + fn i32_extend16_s(&self, state: &mut Self::State) -> Result<(), Self::Error> { + let i = state.stack.read_u32(state.sp - 1) as i16 as i32 as u32; + state.stack.write_u32(state.sp - 1, i); + Ok(()) + } + + fn i64_extend8_s(&self, state: &mut Self::State) -> Result<(), Self::Error> { + let i = state.stack.read_u64(state.sp - 2) as i8 as i64 as u64; + state.stack.write_u64(state.sp - 2, i); + Ok(()) + } + + fn i64_extend16_s(&self, state: &mut Self::State) -> Result<(), Self::Error> { + let i = state.stack.read_u64(state.sp - 2) as i16 as i64 as u64; + state.stack.write_u64(state.sp - 2, i); + Ok(()) + } + + fn i64_extend32_s(&self, state: &mut Self::State) -> Result<(), Self::Error> { + let i = state.stack.read_u64(state.sp - 2) as i32 as i64 as u64; + state.stack.write_u64(state.sp - 2, i); + Ok(()) + } + + fn i64_extend_i32_u(&self, state: &mut Self::State) -> Result<(), Self::Error> { // Low word is already in place at [sp-1] // Just add high word as 0 for unsigned extension diff --git a/src/interpreter_tests.rs b/src/interpreter_tests.rs index 01e37d1..dffd4fa 100644 --- a/src/interpreter_tests.rs +++ b/src/interpreter_tests.rs @@ -808,6 +808,99 @@ mod tests { }); } + #[test] + fn test_i32_extend8_s() { + with_test_context(|state| { + let interpreter = Interpreter::default(); + state.stack.write_u32(0, 0x000000FF); + state.sp = 1; + interpreter.i32_extend8_s(state).unwrap(); + assert_eq!(state.sp, 1); + assert_eq!(state.stack.read_u32(0), 0xFFFFFFFF); // -1 + + state.stack.write_u32(0, 0x0000007F); + interpreter.i32_extend8_s(state).unwrap(); + assert_eq!(state.stack.read_u32(0), 0x0000007F); // 127 + }); + } + + #[test] + fn test_i64_extend8_s() { + with_test_context(|state| { + let interpreter = Interpreter::default(); + state.stack.write_u64(0, 0x00000000000000FF); + state.sp = 2; + interpreter.i64_extend8_s(state).unwrap(); + assert_eq!(state.sp, 2); + assert_eq!(state.stack.read_u64(0), 0xFFFFFFFFFFFFFFFF); // -1 + }); + } + + #[test] + fn test_memory_fill() { + with_test_context(|state| { + let interpreter = Interpreter::default(); + + // Push dst, val, len + state.stack.write_u32(0, 4); // dst + state.stack.write_u32(1, 0xAB); // val + state.stack.write_u32(2, 2); // len + state.sp = 3; + + // grow memory first so it has space + state.stack.write_u32(3, 1); + state.sp = 4; + interpreter.memory_grow(state).unwrap(); + + // clear stack and setup for memory_fill + state.stack.write_u32(0, 4); + state.stack.write_u32(1, 0xAB); + state.stack.write_u32(2, 2); + state.sp = 3; + + interpreter.memory_fill(state).unwrap(); + assert_eq!(state.sp, 0); + + // Read back from memory + let memory = state.store.get_memory(state.module); + assert_eq!(memory.load_u8(4).unwrap(), 0xAB); + assert_eq!(memory.load_u8(5).unwrap(), 0xAB); + assert_eq!(memory.load_u8(6).unwrap(), 0x00); + }); + } + + #[test] + fn test_memory_copy() { + with_test_context(|state| { + let interpreter = Interpreter::default(); + + // grow memory first + state.stack.write_u32(0, 1); + state.sp = 1; + interpreter.memory_grow(state).unwrap(); + + // fill memory at index 8 + state.stack.write_u32(0, 8); // dst + state.stack.write_u32(1, 0xCD); // val + state.stack.write_u32(2, 2); // len + state.sp = 3; + interpreter.memory_fill(state).unwrap(); + + // copy from 8 to 0, len = 2 + state.stack.write_u32(0, 0); // dst + state.stack.write_u32(1, 8); // src + state.stack.write_u32(2, 2); // len + state.sp = 3; + interpreter.memory_copy(state).unwrap(); + assert_eq!(state.sp, 0); + + let memory = state.store.get_memory(state.module); + assert_eq!(memory.load_u8(0).unwrap(), 0xCD); + assert_eq!(memory.load_u8(1).unwrap(), 0xCD); + assert_eq!(memory.load_u8(2).unwrap(), 0x00); + }); + } + #[test] fn test_i32_trunc_f32_s() { with_test_context(|state| { diff --git a/src/ir_reader.rs b/src/ir_reader.rs index e31cf35..5e81a60 100644 --- a/src/ir_reader.rs +++ b/src/ir_reader.rs @@ -419,6 +419,11 @@ impl<'code> IrReader<'code> { // Numeric instructions - conversions I32_WRAP_I64 => instruction!(i32_wrap_i64), + I32_EXTEND8_S => instruction!(i32_extend8_s), + I32_EXTEND16_S => instruction!(i32_extend16_s), + I64_EXTEND8_S => instruction!(i64_extend8_s), + I64_EXTEND16_S => instruction!(i64_extend16_s), + I64_EXTEND32_S => instruction!(i64_extend32_s), I32_TRUNC_F32_S => instruction!(i32_trunc_f32_s), I32_TRUNC_F32_U => instruction!(i32_trunc_f32_u), I32_TRUNC_F64_S => instruction!(i32_trunc_f64_s), diff --git a/src/opcode.rs b/src/opcode.rs index 5f8246b..ed1a13c 100644 --- a/src/opcode.rs +++ b/src/opcode.rs @@ -199,3 +199,8 @@ pub(crate) const I32_REINTERPRET_F32: u8 = 0xBC; pub(crate) const I64_REINTERPRET_F64: u8 = 0xBD; pub(crate) const F32_REINTERPRET_I32: u8 = 0xBE; pub(crate) const F64_REINTERPRET_I64: u8 = 0xBF; +pub(crate) const I32_EXTEND8_S: u8 = 0xC0; +pub(crate) const I32_EXTEND16_S: u8 = 0xC1; +pub(crate) const I64_EXTEND8_S: u8 = 0xC2; +pub(crate) const I64_EXTEND16_S: u8 = 0xC3; +pub(crate) const I64_EXTEND32_S: u8 = 0xC4; diff --git a/src/visitor.rs b/src/visitor.rs index 9a0ac66..5759862 100644 --- a/src/visitor.rs +++ b/src/visitor.rs @@ -187,6 +187,11 @@ pub trait BaseVisitor { // Numeric instructions - conversions visit_fn!(i32_wrap_i64); + visit_fn!(i32_extend8_s); + visit_fn!(i32_extend16_s); + visit_fn!(i64_extend8_s); + visit_fn!(i64_extend16_s); + visit_fn!(i64_extend32_s); visit_fn!(i32_trunc_f32_s); visit_fn!(i32_trunc_f32_u); visit_fn!(i32_trunc_f64_s); diff --git a/tests/util/inspector.rs b/tests/util/inspector.rs index 7aac4f1..8db4513 100644 --- a/tests/util/inspector.rs +++ b/tests/util/inspector.rs @@ -215,6 +215,11 @@ impl<'a, S, E, T: BaseVisitor> BaseVisitor for Inspector<' // Numeric instructions - conversions visit_fn!(i32_wrap_i64); + visit_fn!(i32_extend8_s); + visit_fn!(i32_extend16_s); + visit_fn!(i64_extend8_s); + visit_fn!(i64_extend16_s); + visit_fn!(i64_extend32_s); visit_fn!(i32_trunc_f32_s); visit_fn!(i32_trunc_f32_u); visit_fn!(i32_trunc_f64_s); From 19dfa7662c30e63b2691b203463006c5a6cd1107 Mon Sep 17 00:00:00 2001 From: Param Date: Sun, 19 Jul 2026 09:50:49 +0530 Subject: [PATCH 2/2] Fix incomplete sign extension implementation and remove spam tests --- src/compiler.rs | 5 +++ src/interpreter_tests.rs | 77 ++++++++++++++-------------------------- 2 files changed, 31 insertions(+), 51 deletions(-) diff --git a/src/compiler.rs b/src/compiler.rs index e4d9033..0c08da6 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -594,6 +594,11 @@ impl<'a, const MAX_CODE_PAGES: usize, const MAX_CONTROL_FRAMES: usize, const MAX instruction!(f64_copysign, F64_COPYSIGN, (F64 F64) -> (F64)); instruction!(i32_wrap_i64, I32_WRAP_I64, (I64) -> (I32)); + instruction!(i32_extend8_s, I32_EXTEND8_S, (I32) -> (I32)); + instruction!(i32_extend16_s, I32_EXTEND16_S, (I32) -> (I32)); + instruction!(i64_extend8_s, I64_EXTEND8_S, (I64) -> (I64)); + instruction!(i64_extend16_s, I64_EXTEND16_S, (I64) -> (I64)); + instruction!(i64_extend32_s, I64_EXTEND32_S, (I64) -> (I64)); instruction!(i32_trunc_f32_s, I32_TRUNC_F32_S, (F32) -> (I32)); instruction!(i32_trunc_f32_u, I32_TRUNC_F32_U, (F32) -> (I32)); instruction!(i32_trunc_f64_s, I32_TRUNC_F64_S, (F64) -> (I32)); diff --git a/src/interpreter_tests.rs b/src/interpreter_tests.rs index dffd4fa..03bfb4c 100644 --- a/src/interpreter_tests.rs +++ b/src/interpreter_tests.rs @@ -837,67 +837,42 @@ mod tests { } #[test] - fn test_memory_fill() { + fn test_i32_extend16_s() { with_test_context(|state| { let interpreter = Interpreter::default(); - - // Push dst, val, len - state.stack.write_u32(0, 4); // dst - state.stack.write_u32(1, 0xAB); // val - state.stack.write_u32(2, 2); // len - state.sp = 3; - - // grow memory first so it has space - state.stack.write_u32(3, 1); - state.sp = 4; - interpreter.memory_grow(state).unwrap(); + state.stack.write_u32(0, 0x0000FFFF); + state.sp = 1; + interpreter.i32_extend16_s(state).unwrap(); + assert_eq!(state.sp, 1); + assert_eq!(state.stack.read_u32(0), 0xFFFFFFFF); // -1 - // clear stack and setup for memory_fill - state.stack.write_u32(0, 4); - state.stack.write_u32(1, 0xAB); - state.stack.write_u32(2, 2); - state.sp = 3; - - interpreter.memory_fill(state).unwrap(); - assert_eq!(state.sp, 0); - - // Read back from memory - let memory = state.store.get_memory(state.module); - assert_eq!(memory.load_u8(4).unwrap(), 0xAB); - assert_eq!(memory.load_u8(5).unwrap(), 0xAB); - assert_eq!(memory.load_u8(6).unwrap(), 0x00); + state.stack.write_u32(0, 0x00007FFF); + interpreter.i32_extend16_s(state).unwrap(); + assert_eq!(state.stack.read_u32(0), 0x00007FFF); // 32767 }); } #[test] - fn test_memory_copy() { + fn test_i64_extend16_s() { with_test_context(|state| { let interpreter = Interpreter::default(); + state.stack.write_u64(0, 0x000000000000FFFF); + state.sp = 2; + interpreter.i64_extend16_s(state).unwrap(); + assert_eq!(state.sp, 2); + assert_eq!(state.stack.read_u64(0), 0xFFFFFFFFFFFFFFFF); // -1 + }); + } - // grow memory first - state.stack.write_u32(0, 1); - state.sp = 1; - interpreter.memory_grow(state).unwrap(); - - // fill memory at index 8 - state.stack.write_u32(0, 8); // dst - state.stack.write_u32(1, 0xCD); // val - state.stack.write_u32(2, 2); // len - state.sp = 3; - interpreter.memory_fill(state).unwrap(); - - // copy from 8 to 0, len = 2 - state.stack.write_u32(0, 0); // dst - state.stack.write_u32(1, 8); // src - state.stack.write_u32(2, 2); // len - state.sp = 3; - interpreter.memory_copy(state).unwrap(); - assert_eq!(state.sp, 0); - - let memory = state.store.get_memory(state.module); - assert_eq!(memory.load_u8(0).unwrap(), 0xCD); - assert_eq!(memory.load_u8(1).unwrap(), 0xCD); - assert_eq!(memory.load_u8(2).unwrap(), 0x00); + #[test] + fn test_i64_extend32_s() { + with_test_context(|state| { + let interpreter = Interpreter::default(); + state.stack.write_u64(0, 0x00000000FFFFFFFF); + state.sp = 2; + interpreter.i64_extend32_s(state).unwrap(); + assert_eq!(state.sp, 2); + assert_eq!(state.stack.read_u64(0), 0xFFFFFFFFFFFFFFFF); // -1 }); }