diff --git a/crates/herkos-runtime/src/memory.rs b/crates/herkos-runtime/src/memory.rs index 79a1c17..096bae1 100644 --- a/crates/herkos-runtime/src/memory.rs +++ b/crates/herkos-runtime/src/memory.rs @@ -146,6 +146,33 @@ impl IsolatedMemory { Ok(()) } + /// Wasm `memory.fill` — fill `len` bytes starting at `dst` with `val`. + /// + /// Only the low 8 bits of `val` are used (Wasm spec). Traps (`OutOfBounds`) + /// if the region extends beyond the current active memory. + pub fn fill(&mut self, dst: usize, val: u8, len: usize) -> WasmResult<()> { + let active = self.active_size(); + fill_inner(self.flat_mut(), active, dst, val, len) + } + + /// Wasm `memory.init` — copy `len` bytes from `data[src_offset..]` into + /// linear memory at `dst`. + /// + /// Unlike `init_data` (which copies an entire slice), this copies a + /// sub-range of a passive data segment. Traps (`OutOfBounds`) if either + /// the source range extends beyond `data` or the destination region extends + /// beyond active memory. + pub fn init_data_partial( + &mut self, + dst: usize, + data: &[u8], + src_offset: usize, + len: usize, + ) -> WasmResult<()> { + let active = self.active_size(); + init_data_partial_inner(self.flat_mut(), active, dst, data, src_offset, len) + } + // ── Bounds-checked (safe) load/store ────────────────────────────── /// Load an i32 from linear memory with bounds checking. @@ -467,6 +494,38 @@ fn init_data_inner( Ok(()) } +#[inline(never)] +fn fill_inner( + memory: &mut [u8], + active_bytes: usize, + dst: usize, + val: u8, + len: usize, +) -> WasmResult<()> { + let region = checked_slice_mut(memory, active_bytes, dst, len)?; + region.fill(val); + Ok(()) +} + +#[inline(never)] +fn init_data_partial_inner( + memory: &mut [u8], + active_bytes: usize, + dst: usize, + data: &[u8], + src_offset: usize, + len: usize, +) -> WasmResult<()> { + let src_end = src_offset.checked_add(len).ok_or(WasmTrap::OutOfBounds)?; + if src_end > data.len() { + return Err(WasmTrap::OutOfBounds); + } + let src = &data[src_offset..src_end]; + let dst_region = checked_slice_mut(memory, active_bytes, dst, len)?; + dst_region.copy_from_slice(src); + Ok(()) +} + // ── Unchecked inner functions ───────────────────────────────────────── // // SAFETY: the caller (verified backend) guarantees the offset is in-bounds, @@ -747,6 +806,89 @@ mod tests { assert_eq!(mem.load_u8(5).unwrap(), 0xAB); } + // ── fill ── + + #[test] + fn fill_writes_byte_pattern() { + let mut mem = Mem::try_new(1).unwrap(); + mem.fill(100, 0xAB, 5).unwrap(); + for i in 0..5usize { + assert_eq!(mem.load_u8(100 + i).unwrap(), 0xAB); + } + } + + #[test] + fn fill_zero_len_is_noop() { + let mut mem = Mem::try_new(1).unwrap(); + assert!(mem.fill(0, 0xFF, 0).is_ok()); + } + + #[test] + fn fill_out_of_bounds() { + let mut mem = Mem::try_new(1).unwrap(); + assert_eq!(mem.fill(PAGE_SIZE - 3, 0, 10), Err(WasmTrap::OutOfBounds)); + } + + #[test] + fn fill_at_boundary() { + let mut mem = Mem::try_new(1).unwrap(); + assert!(mem.fill(PAGE_SIZE - 4, 0x42, 4).is_ok()); + assert_eq!(mem.load_u8(PAGE_SIZE - 1).unwrap(), 0x42); + } + + // ── init_data_partial ── + + #[test] + fn init_data_partial_copies_subrange() { + let mut mem = Mem::try_new(1).unwrap(); + let data = b"Hello, World!"; + mem.init_data_partial(0, data, 7, 5).unwrap(); // "World" + assert_eq!(mem.load_u8(0).unwrap(), b'W'); + assert_eq!(mem.load_u8(4).unwrap(), b'd'); + } + + #[test] + fn init_data_partial_zero_len_is_noop() { + let mut mem = Mem::try_new(1).unwrap(); + assert!(mem.init_data_partial(0, b"Hello", 0, 0).is_ok()); + } + + #[test] + fn init_data_partial_full_segment() { + let mut mem = Mem::try_new(1).unwrap(); + mem.init_data_partial(10, b"Hello", 0, 5).unwrap(); + assert_eq!(mem.load_u8(10).unwrap(), b'H'); + assert_eq!(mem.load_u8(14).unwrap(), b'o'); + } + + #[test] + fn init_data_partial_src_out_of_bounds() { + let mut mem = Mem::try_new(1).unwrap(); + // src_offset=3, len=5: 3+5=8 > 5 (data.len()) + assert_eq!( + mem.init_data_partial(0, b"Hello", 3, 5), + Err(WasmTrap::OutOfBounds) + ); + } + + #[test] + fn init_data_partial_dst_out_of_bounds() { + let mut mem = Mem::try_new(1).unwrap(); + assert_eq!( + mem.init_data_partial(PAGE_SIZE - 2, b"Hello", 0, 5), + Err(WasmTrap::OutOfBounds) + ); + } + + #[test] + fn init_data_partial_src_offset_overflow() { + let mut mem = Mem::try_new(1).unwrap(); + assert_eq!( + mem.init_data_partial(0, b"Hello", usize::MAX, 1), + Err(WasmTrap::OutOfBounds) + ); + } + // ── little-endian encoding ── #[test] diff --git a/crates/herkos-tests/data/wat/i32_extend16_s.wat b/crates/herkos-tests/data/wat/i32_extend16_s.wat new file mode 100644 index 0000000..745eae9 --- /dev/null +++ b/crates/herkos-tests/data/wat/i32_extend16_s.wat @@ -0,0 +1,6 @@ +(module + (func (export "func_0") (param i32) (result i32) + local.get 0 + i32.extend16_s + ) +) diff --git a/crates/herkos-tests/data/wat/i32_extend8_s.wat b/crates/herkos-tests/data/wat/i32_extend8_s.wat new file mode 100644 index 0000000..173ad41 --- /dev/null +++ b/crates/herkos-tests/data/wat/i32_extend8_s.wat @@ -0,0 +1,6 @@ +(module + (func (export "func_0") (param i32) (result i32) + local.get 0 + i32.extend8_s + ) +) diff --git a/crates/herkos-tests/data/wat/i64_extend16_s.wat b/crates/herkos-tests/data/wat/i64_extend16_s.wat new file mode 100644 index 0000000..be681d1 --- /dev/null +++ b/crates/herkos-tests/data/wat/i64_extend16_s.wat @@ -0,0 +1,6 @@ +(module + (func (export "func_0") (param i64) (result i64) + local.get 0 + i64.extend16_s + ) +) diff --git a/crates/herkos-tests/data/wat/i64_extend32_s.wat b/crates/herkos-tests/data/wat/i64_extend32_s.wat new file mode 100644 index 0000000..d276f50 --- /dev/null +++ b/crates/herkos-tests/data/wat/i64_extend32_s.wat @@ -0,0 +1,6 @@ +(module + (func (export "func_0") (param i64) (result i64) + local.get 0 + i64.extend32_s + ) +) diff --git a/crates/herkos-tests/data/wat/i64_extend8_s.wat b/crates/herkos-tests/data/wat/i64_extend8_s.wat new file mode 100644 index 0000000..1c0f6e6 --- /dev/null +++ b/crates/herkos-tests/data/wat/i64_extend8_s.wat @@ -0,0 +1,6 @@ +(module + (func (export "func_0") (param i64) (result i64) + local.get 0 + i64.extend8_s + ) +) diff --git a/crates/herkos-tests/data/wat/memory_fill.wat b/crates/herkos-tests/data/wat/memory_fill.wat new file mode 100644 index 0000000..5a1d968 --- /dev/null +++ b/crates/herkos-tests/data/wat/memory_fill.wat @@ -0,0 +1,14 @@ +(module + (memory 1 1) + + ;; Fill a region of memory with a byte value. + (func (export "fill_region") (param $dst i32) (param $val i32) (param $len i32) + local.get $dst + local.get $val + local.get $len + memory.fill) + + ;; Load a single byte (i32.load8_u) for verification. + (func (export "load_byte") (param $addr i32) (result i32) + local.get $addr + i32.load8_u)) diff --git a/crates/herkos-tests/data/wat/memory_init.wat b/crates/herkos-tests/data/wat/memory_init.wat new file mode 100644 index 0000000..6d6a39f --- /dev/null +++ b/crates/herkos-tests/data/wat/memory_init.wat @@ -0,0 +1,22 @@ +(module + (memory 1 1) + + ;; Passive data segment (index 0): "Hello" + (data "Hello") + + ;; Copy a sub-range of the passive segment into linear memory. + (func (export "init_region") + (param $dst i32) (param $src_offset i32) (param $len i32) + local.get $dst + local.get $src_offset + local.get $len + memory.init 0) + + ;; Drop the passive segment (no-op in the safe backend). + (func (export "drop_segment") + data.drop 0) + + ;; Load a single byte for verification. + (func (export "load_byte") (param $addr i32) (result i32) + local.get $addr + i32.load8_u)) diff --git a/crates/herkos-tests/tests/arithmetic.rs b/crates/herkos-tests/tests/arithmetic.rs index 888d9d2..10b5dac 100644 --- a/crates/herkos-tests/tests/arithmetic.rs +++ b/crates/herkos-tests/tests/arithmetic.rs @@ -6,7 +6,8 @@ //! 3. The semantics match WebAssembly's wrapping arithmetic use herkos_tests::{ - abs, add, add_i64, const_i64, const_return, factorial, fibonacci, gcd, mul, nop, sub, + abs, add, add_i64, const_i64, const_return, factorial, fibonacci, gcd, i32_extend16_s, + i32_extend8_s, i64_extend16_s, i64_extend32_s, i64_extend8_s, mul, nop, sub, }; #[test] @@ -307,3 +308,204 @@ fn test_add_matches_rust_wrapping() { ); } } + +// Sign-extension operations +#[test] +fn test_i32_extend8_s() { + let mut extend8_mod = i32_extend8_s::new().unwrap(); + + // Positive value: 0x42 sign-extended to i32 + assert_eq!( + extend8_mod.func_0(0x42).unwrap(), + 0x42, + "i32.extend8_s(0x42) should be 0x42" + ); + + // Negative value: 0xFF sign-extended to i32 (all bits set, -1) + assert_eq!( + extend8_mod.func_0(0xFF).unwrap(), + -1i32, + "i32.extend8_s(0xFF) should be -1" + ); + + // Another negative: 0x80 sign-extended (bit 7 set) + assert_eq!( + extend8_mod.func_0(0x80).unwrap(), + -128i32, + "i32.extend8_s(0x80) should be -128" + ); + + // Zero + assert_eq!( + extend8_mod.func_0(0).unwrap(), + 0, + "i32.extend8_s(0) should be 0" + ); + + // 0x7F sign-extended (max positive signed byte) + assert_eq!( + extend8_mod.func_0(0x7F).unwrap(), + 127i32, + "i32.extend8_s(0x7F) should be 127" + ); +} + +#[test] +fn test_i32_extend16_s() { + let mut extend16_mod = i32_extend16_s::new().unwrap(); + + // Positive value: 0x1234 sign-extended + assert_eq!( + extend16_mod.func_0(0x1234).unwrap(), + 0x1234, + "i32.extend16_s(0x1234) should be 0x1234" + ); + + // Negative value: 0xFFFF sign-extended (all bits set, -1) + assert_eq!( + extend16_mod.func_0(0xFFFF).unwrap(), + -1i32, + "i32.extend16_s(0xFFFF) should be -1" + ); + + // Another negative: 0x8000 sign-extended (bit 15 set) + assert_eq!( + extend16_mod.func_0(0x8000).unwrap(), + -32768i32, + "i32.extend16_s(0x8000) should be -32768" + ); + + // Zero + assert_eq!( + extend16_mod.func_0(0).unwrap(), + 0, + "i32.extend16_s(0) should be 0" + ); + + // 0x7FFF sign-extended (max positive signed short) + assert_eq!( + extend16_mod.func_0(0x7FFF).unwrap(), + 32767i32, + "i32.extend16_s(0x7FFF) should be 32767" + ); +} + +#[test] +fn test_i64_extend8_s() { + let mut extend8_mod = i64_extend8_s::new().unwrap(); + + // Positive value + assert_eq!( + extend8_mod.func_0(0x42).unwrap(), + 0x42i64, + "i64.extend8_s(0x42) should be 0x42" + ); + + // Negative value: 0xFF sign-extended to i64 + assert_eq!( + extend8_mod.func_0(0xFF).unwrap(), + -1i64, + "i64.extend8_s(0xFF) should be -1" + ); + + // Another negative: 0x80 sign-extended + assert_eq!( + extend8_mod.func_0(0x80).unwrap(), + -128i64, + "i64.extend8_s(0x80) should be -128" + ); + + // Zero + assert_eq!( + extend8_mod.func_0(0).unwrap(), + 0i64, + "i64.extend8_s(0) should be 0" + ); + + // Max positive signed byte + assert_eq!( + extend8_mod.func_0(0x7F).unwrap(), + 127i64, + "i64.extend8_s(0x7F) should be 127" + ); +} + +#[test] +fn test_i64_extend16_s() { + let mut extend16_mod = i64_extend16_s::new().unwrap(); + + // Positive value + assert_eq!( + extend16_mod.func_0(0x1234).unwrap(), + 0x1234i64, + "i64.extend16_s(0x1234) should be 0x1234" + ); + + // Negative value: 0xFFFF sign-extended to i64 + assert_eq!( + extend16_mod.func_0(0xFFFF).unwrap(), + -1i64, + "i64.extend16_s(0xFFFF) should be -1" + ); + + // Another negative: 0x8000 sign-extended + assert_eq!( + extend16_mod.func_0(0x8000).unwrap(), + -32768i64, + "i64.extend16_s(0x8000) should be -32768" + ); + + // Zero + assert_eq!( + extend16_mod.func_0(0).unwrap(), + 0i64, + "i64.extend16_s(0) should be 0" + ); + + // Max positive signed short + assert_eq!( + extend16_mod.func_0(0x7FFF).unwrap(), + 32767i64, + "i64.extend16_s(0x7FFF) should be 32767" + ); +} + +#[test] +fn test_i64_extend32_s() { + let mut extend32_mod = i64_extend32_s::new().unwrap(); + + // Positive value: 0x12345678 sign-extended + assert_eq!( + extend32_mod.func_0(0x12345678i64).unwrap(), + 0x12345678i64, + "i64.extend32_s(0x12345678) should be 0x12345678" + ); + + // Negative value: 0xFFFFFFFF sign-extended to i64 (all bits set, -1) + assert_eq!( + extend32_mod.func_0(0xFFFFFFFFi64).unwrap(), + -1i64, + "i64.extend32_s(0xFFFFFFFF) should be -1" + ); + + // Another negative: 0x80000000 sign-extended (bit 31 set) + assert_eq!( + extend32_mod.func_0(0x80000000i64).unwrap(), + -2147483648i64, + "i64.extend32_s(0x80000000) should be -2147483648" + ); + + // Zero + assert_eq!( + extend32_mod.func_0(0).unwrap(), + 0i64, + "i64.extend32_s(0) should be 0" + ); + + // Max positive signed int + assert_eq!( + extend32_mod.func_0(0x7FFFFFFFi64).unwrap(), + 2147483647i64, + "i64.extend32_s(0x7FFFFFFF) should be 2147483647" + ); +} diff --git a/crates/herkos-tests/tests/bulk_memory.rs b/crates/herkos-tests/tests/bulk_memory.rs new file mode 100644 index 0000000..7d9c301 --- /dev/null +++ b/crates/herkos-tests/tests/bulk_memory.rs @@ -0,0 +1,105 @@ +//! Integration tests for Wasm bulk-memory operations: memory.fill, memory.init, data.drop. + +use herkos_tests::{memory_fill, memory_init}; + +// ── memory.fill ────────────────────────────────────────────────────────────── + +#[test] +fn test_fill_writes_byte_pattern() { + let mut m = memory_fill::new().unwrap(); + m.fill_region(100, 0xAB, 5).unwrap(); + for i in 0..5i32 { + assert_eq!(m.load_byte(100 + i).unwrap(), 0xAB); + } +} + +#[test] +fn test_fill_zero_len_is_noop() { + let mut m = memory_fill::new().unwrap(); + m.fill_region(0, 0xFF, 0).unwrap(); + // Nothing was written — memory stays zero-initialized + assert_eq!(m.load_byte(0).unwrap(), 0); +} + +#[test] +fn test_fill_out_of_bounds_traps() { + let mut m = memory_fill::new().unwrap(); + // 1 page = 65536 bytes; fill 10 bytes starting near the end overflows + assert!(m.fill_region(65530, 0, 10).is_err()); +} + +#[test] +fn test_fill_byte_truncation() { + // val is i32 on the Wasm stack; only low 8 bits are used + let mut m = memory_fill::new().unwrap(); + m.fill_region(0, 0x1FF, 1).unwrap(); // 0x1FF & 0xFF = 0xFF + assert_eq!(m.load_byte(0).unwrap(), 0xFF); +} + +#[test] +fn test_fill_entire_region() { + let mut m = memory_fill::new().unwrap(); + m.fill_region(200, 42, 8).unwrap(); + for i in 0..8i32 { + assert_eq!(m.load_byte(200 + i).unwrap(), 42); + } + // Byte just before and just after should be untouched (zero) + assert_eq!(m.load_byte(199).unwrap(), 0); + assert_eq!(m.load_byte(208).unwrap(), 0); +} + +// ── memory.init ────────────────────────────────────────────────────────────── + +#[test] +fn test_init_full_segment() { + // PASSIVE_SEGMENT_0 = b"Hello" + let mut m = memory_init::new().unwrap(); + m.init_region(10, 0, 5).unwrap(); + assert_eq!(m.load_byte(10).unwrap(), b'H' as i32); + assert_eq!(m.load_byte(11).unwrap(), b'e' as i32); + assert_eq!(m.load_byte(14).unwrap(), b'o' as i32); +} + +#[test] +fn test_init_subrange() { + // Copy "ell" (bytes 1..4 of "Hello") into address 0 + let mut m = memory_init::new().unwrap(); + m.init_region(0, 1, 3).unwrap(); + assert_eq!(m.load_byte(0).unwrap(), b'e' as i32); + assert_eq!(m.load_byte(1).unwrap(), b'l' as i32); + assert_eq!(m.load_byte(2).unwrap(), b'l' as i32); + // Byte 3 should be zero (not written) + assert_eq!(m.load_byte(3).unwrap(), 0); +} + +#[test] +fn test_init_zero_len_is_noop() { + let mut m = memory_init::new().unwrap(); + m.init_region(0, 0, 0).unwrap(); + assert_eq!(m.load_byte(0).unwrap(), 0); +} + +#[test] +fn test_init_src_out_of_bounds_traps() { + let mut m = memory_init::new().unwrap(); + // src_offset=3, len=5: 3+5=8 > 5 ("Hello".len()) + assert!(m.init_region(0, 3, 5).is_err()); +} + +#[test] +fn test_init_dst_out_of_bounds_traps() { + let mut m = memory_init::new().unwrap(); + assert!(m.init_region(65534, 0, 5).is_err()); +} + +// ── data.drop ──────────────────────────────────────────────────────────────── + +#[test] +fn test_data_drop_is_noop() { + // drop_segment must not trap + let mut m = memory_init::new().unwrap(); + m.drop_segment().unwrap(); + // Segment data is still accessible after drop (no runtime enforcement) + m.init_region(0, 0, 5).unwrap(); + assert_eq!(m.load_byte(0).unwrap(), b'H' as i32); +} diff --git a/crates/herkos/src/backend/mod.rs b/crates/herkos/src/backend/mod.rs index 267f660..2ebc8da 100644 --- a/crates/herkos/src/backend/mod.rs +++ b/crates/herkos/src/backend/mod.rs @@ -92,6 +92,22 @@ pub trait Backend { /// Emit Rust code for memory.copy (copies len bytes from src to dst). fn emit_memory_copy(&self, dst: VarId, src: VarId, len: VarId) -> String; + /// Emit Rust code for memory.fill (fills len bytes at dst with byte val). + fn emit_memory_fill(&self, dst: VarId, val: VarId, len: VarId) -> String; + + /// Emit Rust code for memory.init (copies len bytes from passive segment + /// `segment_const_name` at src_offset into memory at dst). + fn emit_memory_init( + &self, + dst: VarId, + src_offset: VarId, + len: VarId, + segment_const_name: &str, + ) -> String; + + /// Emit Rust code for data.drop (no-op in the safe backend). + fn emit_data_drop(&self, segment: u32) -> String; + /// Emit Rust code for unreachable. fn emit_unreachable(&self) -> String; diff --git a/crates/herkos/src/backend/safe.rs b/crates/herkos/src/backend/safe.rs index 1951d3c..8200641 100644 --- a/crates/herkos/src/backend/safe.rs +++ b/crates/herkos/src/backend/safe.rs @@ -278,6 +278,12 @@ impl Backend for SafeBackend { UnOp::I32WrapI64 => format!(" {dest} = {operand} as i32;"), UnOp::I64ExtendI32S => format!(" {dest} = {operand} as i64;"), UnOp::I64ExtendI32U => format!(" {dest} = ({operand} as u32) as i64;"), + // Sign-extension ops + UnOp::I32Extend8S => format!(" {dest} = ({operand} as i8) as i32;"), + UnOp::I32Extend16S => format!(" {dest} = ({operand} as i16) as i32;"), + UnOp::I64Extend8S => format!(" {dest} = ({operand} as i8) as i64;"), + UnOp::I64Extend16S => format!(" {dest} = ({operand} as i16) as i64;"), + UnOp::I64Extend32S => format!(" {dest} = ({operand} as i32) as i64;"), // Float → i32 (trapping on NaN/overflow) — logic lives in herkos_runtime::ops UnOp::I32TruncF32S => { @@ -541,6 +547,24 @@ impl Backend for SafeBackend { format!(" memory.memory_copy({dst} as u32, {src} as u32, {len} as u32)?;") } + fn emit_memory_fill(&self, dst: VarId, val: VarId, len: VarId) -> String { + format!(" memory.fill({dst} as usize, {val} as u8, {len} as usize)?;") + } + + fn emit_memory_init( + &self, + dst: VarId, + src_offset: VarId, + len: VarId, + segment_const_name: &str, + ) -> String { + format!(" memory.init_data_partial({dst} as usize, {segment_const_name}, {src_offset} as usize, {len} as usize)?;") + } + + fn emit_data_drop(&self, segment: u32) -> String { + format!(" // data.drop segment {segment} (no-op: const slice)") + } + fn emit_unreachable(&self) -> String { " return Err(WasmTrap::Unreachable);".to_string() } diff --git a/crates/herkos/src/codegen/instruction.rs b/crates/herkos/src/codegen/instruction.rs index bbc7398..8add863 100644 --- a/crates/herkos/src/codegen/instruction.rs +++ b/crates/herkos/src/codegen/instruction.rs @@ -97,6 +97,22 @@ pub fn generate_instruction_with_info( IrInstr::MemoryCopy { dst, src, len } => backend.emit_memory_copy(*dst, *src, *len), + IrInstr::MemoryFill { dst, val, len } => backend.emit_memory_fill(*dst, *val, *len), + + IrInstr::MemoryInit { + dst, + src_offset, + len, + segment, + } => backend.emit_memory_init( + *dst, + *src_offset, + *len, + &format!("PASSIVE_SEGMENT_{segment}"), + ), + + IrInstr::DataDrop { segment } => backend.emit_data_drop(*segment), + IrInstr::Select { dest, val1, diff --git a/crates/herkos/src/codegen/mod.rs b/crates/herkos/src/codegen/mod.rs index 21feebd..87b3639 100644 --- a/crates/herkos/src/codegen/mod.rs +++ b/crates/herkos/src/codegen/mod.rs @@ -211,6 +211,7 @@ mod tests { element_segments: Vec::new(), globals: Vec::new(), data_segments: Vec::new(), + passive_data_segments: Vec::new(), func_exports: Vec::new(), type_signatures: Vec::new(), canonical_type: Vec::new(), @@ -262,6 +263,7 @@ mod tests { element_segments: Vec::new(), globals: Vec::new(), data_segments: Vec::new(), + passive_data_segments: Vec::new(), func_exports: Vec::new(), type_signatures: Vec::new(), canonical_type: Vec::new(), @@ -368,6 +370,7 @@ mod tests { element_segments: Vec::new(), globals: Vec::new(), data_segments: Vec::new(), + passive_data_segments: Vec::new(), func_exports: Vec::new(), type_signatures: Vec::new(), canonical_type: Vec::new(), @@ -432,6 +435,7 @@ mod tests { element_segments: Vec::new(), globals: Vec::new(), data_segments: Vec::new(), + passive_data_segments: Vec::new(), func_exports: Vec::new(), type_signatures: Vec::new(), canonical_type: Vec::new(), @@ -486,6 +490,7 @@ mod tests { init_value: GlobalInit::I32(0), }], data_segments: Vec::new(), + passive_data_segments: Vec::new(), func_exports: vec![FuncExport { name: "get_value".to_string(), func_index: LocalFuncIdx::new(0), @@ -553,6 +558,7 @@ mod tests { offset: 0, data: vec![72, 101, 108, 108, 111], // "Hello" }], + passive_data_segments: Vec::new(), func_exports: vec![FuncExport { name: "load_word".to_string(), func_index: LocalFuncIdx::new(0), @@ -621,6 +627,7 @@ mod tests { init_value: GlobalInit::I32(42), }], data_segments: Vec::new(), + passive_data_segments: Vec::new(), func_exports: vec![FuncExport { name: "get_const".to_string(), func_index: LocalFuncIdx::new(0), diff --git a/crates/herkos/src/codegen/module.rs b/crates/herkos/src/codegen/module.rs index 6e1b25f..bcf30dc 100644 --- a/crates/herkos/src/codegen/module.rs +++ b/crates/herkos/src/codegen/module.rs @@ -36,6 +36,19 @@ fn generate_wrapper_module(backend: &B, info: &ModuleInfo) -> Result } rust_code.push('\n'); + // Passive data segment consts (bulk-memory proposal) + for seg in &info.passive_data_segments { + let bytes: Vec = seg.data.iter().map(|b| format!("{b}u8")).collect(); + rust_code.push_str(&format!( + "#[allow(dead_code)]\nconst PASSIVE_SEGMENT_{}: &[u8] = &[{}];\n", + seg.wasm_index, + bytes.join(", ") + )); + } + if !info.passive_data_segments.is_empty() { + rust_code.push('\n'); + } + // Host trait definitions rust_code.push_str(&generate_host_traits(backend, info)); diff --git a/crates/herkos/src/ir/builder/assembly.rs b/crates/herkos/src/ir/builder/assembly.rs index c327948..485ec51 100644 --- a/crates/herkos/src/ir/builder/assembly.rs +++ b/crates/herkos/src/ir/builder/assembly.rs @@ -22,6 +22,7 @@ pub(super) fn assemble_module_metadata( ) -> Result { let globals = build_globals(parsed); let data_segments = build_data_segments(parsed); + let passive_data_segments = build_passive_data_segments(parsed); let element_segments = build_element_segments(parsed, num_imported_functions); let func_exports = build_function_exports(parsed, num_imported_functions); let type_signatures = build_call_indirect_signatures(parsed); @@ -45,6 +46,7 @@ pub(super) fn assemble_module_metadata( element_segments, globals, data_segments, + passive_data_segments, func_exports, type_signatures, canonical_type, @@ -75,6 +77,18 @@ fn build_globals(parsed: &ParsedModule) -> Vec { .collect() } +/// Builds passive data segment definitions. +fn build_passive_data_segments(parsed: &ParsedModule) -> Vec { + parsed + .passive_data_segments + .iter() + .map(|ps| PassiveDataSegment { + wasm_index: ps.wasm_index, + data: ps.data.clone(), + }) + .collect() +} + /// Builds data segment definitions. fn build_data_segments(parsed: &ParsedModule) -> Vec { parsed diff --git a/crates/herkos/src/ir/builder/translate.rs b/crates/herkos/src/ir/builder/translate.rs index d6bcbce..a6cdfb5 100644 --- a/crates/herkos/src/ir/builder/translate.rs +++ b/crates/herkos/src/ir/builder/translate.rs @@ -391,6 +391,13 @@ impl IrBuilder { Operator::F32ReinterpretI32 => self.emit_unop(UnOp::F32ReinterpretI32)?, Operator::F64ReinterpretI64 => self.emit_unop(UnOp::F64ReinterpretI64)?, + // Sign-extension ops + Operator::I32Extend8S => self.emit_unop(UnOp::I32Extend8S)?, + Operator::I32Extend16S => self.emit_unop(UnOp::I32Extend16S)?, + Operator::I64Extend8S => self.emit_unop(UnOp::I64Extend8S)?, + Operator::I64Extend16S => self.emit_unop(UnOp::I64Extend16S)?, + Operator::I64Extend32S => self.emit_unop(UnOp::I64Extend32S)?, + // Return Operator::Return => { self.emit_return()?; @@ -1254,6 +1261,55 @@ impl IrBuilder { }); } + Operator::MemoryFill { mem: 0 } => { + // Stack: [dst: i32, val: i32, len: i32] (len on top) + let len = self + .value_stack + .pop() + .ok_or_else(|| anyhow::anyhow!("Stack underflow for memory.fill (len)"))?; + let val = self + .value_stack + .pop() + .ok_or_else(|| anyhow::anyhow!("Stack underflow for memory.fill (val)"))?; + let dst = self + .value_stack + .pop() + .ok_or_else(|| anyhow::anyhow!("Stack underflow for memory.fill (dst)"))?; + self.emit_void(IrInstr::MemoryFill { + dst: dst.var_id(), + val: val.var_id(), + len: len.var_id(), + }); + } + + Operator::MemoryInit { mem: 0, data_index } => { + // Stack: [dst: i32, src_offset: i32, len: i32] (len on top) + let len = self + .value_stack + .pop() + .ok_or_else(|| anyhow::anyhow!("Stack underflow for memory.init (len)"))?; + let src_offset = self.value_stack.pop().ok_or_else(|| { + anyhow::anyhow!("Stack underflow for memory.init (src_offset)") + })?; + let dst = self + .value_stack + .pop() + .ok_or_else(|| anyhow::anyhow!("Stack underflow for memory.init (dst)"))?; + self.emit_void(IrInstr::MemoryInit { + dst: dst.var_id(), + src_offset: src_offset.var_id(), + len: len.var_id(), + segment: *data_index, + }); + } + + Operator::DataDrop { data_index } => { + // No-op: const slices have no runtime lifetime to drop. + self.emit_void(IrInstr::DataDrop { + segment: *data_index, + }); + } + _ => bail!("Unsupported operator: {:?}", op), } diff --git a/crates/herkos/src/ir/lower_phis.rs b/crates/herkos/src/ir/lower_phis.rs index ee81849..fcd7b8e 100644 --- a/crates/herkos/src/ir/lower_phis.rs +++ b/crates/herkos/src/ir/lower_phis.rs @@ -291,6 +291,7 @@ mod tests { element_segments: Vec::new(), globals: Vec::new(), data_segments: Vec::new(), + passive_data_segments: Vec::new(), func_exports: Vec::new(), type_signatures: Vec::new(), canonical_type: Vec::new(), diff --git a/crates/herkos/src/ir/types.rs b/crates/herkos/src/ir/types.rs index 22158e6..c331e28 100644 --- a/crates/herkos/src/ir/types.rs +++ b/crates/herkos/src/ir/types.rs @@ -346,6 +346,24 @@ pub enum IrInstr { /// Traps if either region is out of bounds. Returns nothing. MemoryCopy { dst: VarId, src: VarId, len: VarId }, + /// Fill `len` bytes starting at `dst` with `val` (low 8 bits used). + /// Traps if the region is out of bounds. Returns nothing. + MemoryFill { dst: VarId, val: VarId, len: VarId }, + + /// Copy `len` bytes from passive data segment `segment` at `src_offset` + /// into linear memory at `dst`. `segment` is a compile-time constant + /// (Wasm immediate). Traps if either range is out of bounds. + MemoryInit { + dst: VarId, + src_offset: VarId, + len: VarId, + segment: u32, + }, + + /// Drop a passive data segment (no-op in the safe backend — const slices + /// have no runtime lifetime to drop). + DataDrop { segment: u32 }, + /// Conditional select (dest = if condition != 0 { val1 } else { val2 }) Select { dest: VarId, @@ -563,6 +581,12 @@ pub enum UnOp { I32WrapI64, // i64 → i32 (truncate to low 32 bits) I64ExtendI32S, // i32 → i64 (sign-extend) I64ExtendI32U, // i32 → i64 (zero-extend) + // Sign-extension ops (Wasm sign-extension-ops proposal) + I32Extend8S, // sign-extend 8-bit value in i32 + I32Extend16S, // sign-extend 16-bit value in i32 + I64Extend8S, // sign-extend 8-bit value in i64 + I64Extend16S, // sign-extend 16-bit value in i64 + I64Extend32S, // sign-extend 32-bit value in i64 // Conversions: float → integer (trapping on NaN/overflow) I32TruncF32S, @@ -726,11 +750,16 @@ impl UnOp { | UnOp::I32TruncF32U | UnOp::I32TruncF64S | UnOp::I32TruncF64U - | UnOp::I32ReinterpretF32 => WasmType::I32, + | UnOp::I32ReinterpretF32 + | UnOp::I32Extend8S + | UnOp::I32Extend16S => WasmType::I32, // Conversions → i64 UnOp::I64ExtendI32S | UnOp::I64ExtendI32U + | UnOp::I64Extend8S + | UnOp::I64Extend16S + | UnOp::I64Extend32S | UnOp::I64TruncF32S | UnOp::I64TruncF32U | UnOp::I64TruncF64S @@ -818,6 +847,21 @@ pub struct DataSegmentDef { pub data: Vec, } +/// A passive data segment (bulk-memory proposal). +/// +/// Passive segments are not copied into memory at module instantiation. +/// They are accessed on demand via `memory.init`. The `wasm_index` is the +/// segment's position in the Wasm data section (counting both active and +/// passive segments in order), which determines the generated const name +/// `PASSIVE_SEGMENT_{wasm_index}`. +#[derive(Debug, Clone)] +pub struct PassiveDataSegment { + /// Global Wasm data segment index (position in the data section). + pub wasm_index: u32, + /// Raw data bytes. + pub data: Vec, +} + /// An exported function mapping. #[derive(Debug, Clone)] pub struct FuncExport { @@ -910,6 +954,9 @@ pub struct ModuleInfo { pub globals: Vec, /// Data segments for memory initialization. pub data_segments: Vec, + /// Passive data segments (bulk-memory proposal). + /// Not copied into memory at startup; accessed via `memory.init`. + pub passive_data_segments: Vec, /// Exported functions. pub func_exports: Vec, /// Type section signatures (for call_indirect dispatch). diff --git a/crates/herkos/src/parser/mod.rs b/crates/herkos/src/parser/mod.rs index 92c04a2..d4ffee5 100644 --- a/crates/herkos/src/parser/mod.rs +++ b/crates/herkos/src/parser/mod.rs @@ -64,6 +64,20 @@ pub struct DataSegment { pub data: Vec, } +/// A passive data segment (bulk-memory proposal). +/// +/// Passive segments are not copied into memory at module instantiation. +/// They are initialized on demand via `memory.init`. The `wasm_index` is the +/// segment's position in the Wasm data section (counting both active and +/// passive segments in order). +#[derive(Debug, Clone)] +pub struct PassiveDataSegment { + /// Global Wasm data segment index (position in the data section). + pub wasm_index: u32, + /// Raw data bytes. + pub data: Vec, +} + /// An export from the Wasm module. #[derive(Debug, Clone)] pub struct ExportInfo { @@ -139,6 +153,10 @@ pub struct ParsedModule { /// Data segments for memory initialization (Milestone 4) pub data_segments: Vec, + /// Passive data segments (bulk-memory proposal). + /// Not copied into memory at startup; accessed via `memory.init`. + pub passive_data_segments: Vec, + /// Exports (Milestone 4) pub exports: Vec, @@ -238,35 +256,6 @@ fn parse_element_segment(element: wasmparser::Element) -> Result Result> { - match data.kind { - wasmparser::DataKind::Active { - memory_index: 0, - offset_expr, - } => { - let offset = match eval_const_expr(offset_expr)? { - InitValue::I32(v) => v as u32, - _ => anyhow::bail!("Data segment offset must be i32"), - }; - Ok(Some(DataSegment { - offset, - data: data.data.to_vec(), - })) - } - wasmparser::DataKind::Passive => { - // Skip passive data segments (used with memory.init) - Ok(None) - } - wasmparser::DataKind::Active { memory_index, .. } => { - anyhow::bail!( - "Multi-memory data segments not supported (memory_index={})", - memory_index - ); - } - } -} - /// Parse a function code section entry, extracting locals and bytecode. fn parse_code_entry(body: wasmparser::FunctionBody, type_idx: u32) -> Result { // Extract locals @@ -308,6 +297,7 @@ pub fn parse_wasm(wasm_bytes: &[u8]) -> Result { let mut element_segments = Vec::new(); let mut globals = Vec::new(); let mut data_segments = Vec::new(); + let mut passive_data_segments = Vec::new(); let mut exports = Vec::new(); let mut imports = Vec::new(); let mut num_imported_functions: u32 = 0; @@ -451,10 +441,34 @@ pub fn parse_wasm(wasm_bytes: &[u8]) -> Result { } Payload::DataSection(reader) => { - for data in reader { + for (segment_index, data) in (0_u32..).zip(reader) { let data = data.context("reading data segment")?; - if let Some(segment) = parse_data_segment(data)? { - data_segments.push(segment); + match data.kind { + wasmparser::DataKind::Active { + memory_index: 0, + offset_expr, + } => { + let offset = match eval_const_expr(offset_expr)? { + InitValue::I32(v) => v as u32, + _ => anyhow::bail!("Data segment offset must be i32"), + }; + data_segments.push(DataSegment { + offset, + data: data.data.to_vec(), + }); + } + wasmparser::DataKind::Passive => { + passive_data_segments.push(PassiveDataSegment { + wasm_index: segment_index, + data: data.data.to_vec(), + }); + } + wasmparser::DataKind::Active { memory_index, .. } => { + anyhow::bail!( + "Multi-memory data segments not supported (memory_index={})", + memory_index + ); + } } } } @@ -471,6 +485,7 @@ pub fn parse_wasm(wasm_bytes: &[u8]) -> Result { element_segments, globals, data_segments, + passive_data_segments, exports, imports, num_imported_functions, @@ -607,6 +622,41 @@ mod tests { assert_eq!(module.data_segments.len(), 1); assert_eq!(module.data_segments[0].offset, 16); assert_eq!(module.data_segments[0].data, b"Hello"); + assert_eq!(module.passive_data_segments.len(), 0); + } + + #[test] + fn parse_passive_data_segment() { + let wat = r#" + (module + (memory 1) + (data "Hello") + ) + "#; + let wasm = wat::parse_str(wat).unwrap(); + let module = parse_wasm(&wasm).unwrap(); + assert_eq!(module.data_segments.len(), 0); + assert_eq!(module.passive_data_segments.len(), 1); + assert_eq!(module.passive_data_segments[0].wasm_index, 0); + assert_eq!(module.passive_data_segments[0].data, b"Hello"); + } + + #[test] + fn parse_mixed_active_and_passive_segments() { + let wat = r#" + (module + (memory 1) + (data (i32.const 0) "Active") + (data "Passive") + ) + "#; + let wasm = wat::parse_str(wat).unwrap(); + let module = parse_wasm(&wasm).unwrap(); + assert_eq!(module.data_segments.len(), 1); + assert_eq!(module.passive_data_segments.len(), 1); + // wasm_index=1 because the active segment at index 0 comes first + assert_eq!(module.passive_data_segments[0].wasm_index, 1); + assert_eq!(module.passive_data_segments[0].data, b"Passive"); } #[test]