// FuncEnvironment - handles Wasm semantic operations by desugaring to IR primitives
//
// This module implements Standard desugaring where high-level Wasm operations
// (global.get, table.get, etc.) are translated to lower-level IR primitives
// (MemoryOp and CallOp) during IR translation, not during lowering.
//
// Benefits:
// - Simpler lowering phase (no special cases for these operations)
// - Centralized embedding layout knowledge
// - Easier to change ABI (modify here, not scattered across lowering/emit)
///|
/// FuncEnvironment holds VMContext layout info used during IR translation to
/// desugar Wasm operations.
priv struct FuncEnvironment {
runtime_layout : @embedding.RuntimeLayout
memory_descriptor_layout : @embedding.MemoryDescriptorLayout
runtime_symbols : @wasm_milkir.RuntimeSymbols
cancellation_symbol : @milkir.ExternalSymbol?
// Global variable types (for determining load/store width)
global_types : Array[@types.GlobalType]
// Minimum guaranteed memory size in bytes for each memory index
// Used to eliminate bounds checks for constant addresses within this range
memory_mins : Array[Int64]
// Whether each memory uses 64-bit addressing (memory64 proposal)
memory_is_64 : Array[Bool]
// Logical page size log2 for each memory (custom-page-sizes proposal)
memory_page_size_log2 : Array[Int]
// Shared trap block for memory out-of-bounds errors
// All bounds checks in the same function share this block to reduce CFG size
mut memory_trap_block : Block?
// Shared trap block for atomic unaligned accesses
mut atomic_unaligned_trap_block : Block?
// Per-block dynamic bounds-check cache:
// key = "block_id:memidx:wasm_addr_value_id", value = max checked
// (offset + access_size) for that address within the block.
//
// This mirrors Cranelift's "same-index checks can dedup" idea for repeated
// field/stack accesses and is safe because we only reuse checks when a later
// access has an equal-or-smaller range end in the same block.
bounds_check_cache : @hashmap.HashMap[String, Int64]
}
///|
fn FuncEnvironment::FuncEnvironment(
embedding_env : @embedding.EmbeddingEnvironment,
global_types : Array[@types.GlobalType],
memory_mins? : Array[Int64] = [],
memory_is_64? : Array[Bool] = [],
memory_page_size_log2? : Array[Int] = [],
) -> FuncEnvironment {
let runtime_layout = match embedding_env.runtime_layout() {
Some(layout) => layout
None => abort("wasm frontend IR translator requires runtime layout")
}
let memory_descriptor_layout = match
embedding_env.memory_descriptor_layout() {
Some(layout) => layout
None => abort("wasm frontend IR translator requires memory layout")
}
{
runtime_layout,
memory_descriptor_layout,
runtime_symbols: embedding_env.wasm_runtime_symbols(),
cancellation_symbol: if embedding_env.cancellation_safepoints() {
Some(
ExternalSymbol(
embedding_env.wasm_runtime_symbols().symbol_name(CancelPoll),
),
)
} else {
None
},
global_types,
memory_mins,
memory_is_64,
memory_page_size_log2,
memory_trap_block: None,
atomic_unaligned_trap_block: None,
bounds_check_cache: HashMap([]),
}
}
///|
fn FuncEnvironment::emit_cancellation_safepoint(
self : FuncEnvironment,
builder : FunctionBuilder,
vmctx : Value,
) -> Unit {
if self.cancellation_symbol is Some(symbol) {
builder.call_symbol(symbol, None, [vmctx]) |> ignore
}
}
// ============ Global Variable Operations ============
///|
/// Translate global.get to IR primitives:
/// 1. Load globals_ptr from vmctx
/// 2. Load value from globals_ptr + (global_idx * 16)
fn FuncEnvironment::translate_global_get(
self : FuncEnvironment,
builder : FunctionBuilder,
vmctx : Value,
global_idx : Int,
) -> Value {
let global_type = self.global_types[global_idx]
let ty = type_from_wasm(global_type.value_type)
let layout = self.runtime_layout
// Load globals_ptr from vmctx
let globals_offset = builder.iconst(I64, layout.globals_offset.to_int64())
let globals_ptr = builder.load_ptr(I64, vmctx, globals_offset)
// Load value from globals_ptr + field_offset
let field_offset = builder.iconst(
I64,
(global_idx * layout.global_value_stride).to_int64(),
)
builder.load_ptr(ty, globals_ptr, field_offset)
}
///|
/// Translate global.set to IR primitives:
/// 1. Load globals_ptr from vmctx
/// 2. Store value to globals_ptr + (global_idx * 16)
fn FuncEnvironment::translate_global_set(
self : FuncEnvironment,
builder : FunctionBuilder,
vmctx : Value,
global_idx : Int,
value : Value,
) -> Unit {
let global_type = self.global_types[global_idx]
let ty = type_from_wasm(global_type.value_type)
let layout = self.runtime_layout
// Load globals_ptr from vmctx
let globals_offset = builder.iconst(I64, layout.globals_offset.to_int64())
let globals_ptr = builder.load_ptr(I64, vmctx, globals_offset)
// Store value to globals_ptr + field_offset
let field_offset = builder.iconst(
I64,
(global_idx * layout.global_value_stride).to_int64(),
)
builder.store_ptr(ty, globals_ptr, value, field_offset)
}
// ============ Table Operations ============
///|
/// Translate table.size to IR primitives:
/// For table 0: Load from vmctx.table0_elements
/// For table N: Load from vmctx.table_sizes[N]
fn FuncEnvironment::translate_table_size(
self : FuncEnvironment,
builder : FunctionBuilder,
vmctx : Value,
table_idx : Int,
is_table64? : Bool = false,
) -> Value {
let layout = self.runtime_layout
if table_idx == 0 {
// Fast path for table 0
let offset = builder.iconst(I64, layout.table0_elements_offset.to_int64())
let size_i64 = builder.load_ptr(I64, vmctx, offset)
// For table64, return i64; for table32, reduce to i32
if is_table64 {
size_i64
} else {
builder.ireduce(I32, size_i64)
}
} else {
// General path: load from table_sizes array
let sizes_offset = builder.iconst(I64, layout.table_sizes_offset.to_int64())
let sizes_ptr = builder.load_ptr(I64, vmctx, sizes_offset)
let elem_offset = builder.iconst(
I64,
(table_idx * layout.pointer_stride).to_int64(),
)
let size_i64 = builder.load_ptr(I64, sizes_ptr, elem_offset)
if is_table64 {
size_i64
} else {
builder.ireduce(I32, size_i64)
}
}
}
///|
/// Translate table.get to IR primitives:
/// 1. Load table_size, check bounds
/// 2. Load table_base
/// 3. Calculate address: table_base + elem_idx * 16
/// 4. Load value
fn FuncEnvironment::translate_table_get(
self : FuncEnvironment,
builder : FunctionBuilder,
vmctx : Value,
table_idx : Int,
elem_idx : Value,
result_type : Type,
is_table64? : Bool = false,
) -> Value {
let layout = self.runtime_layout
// Get table size for bounds check
let (table_size, table_base) = if table_idx == 0 {
// Fast path for table 0
let size_offset = builder.iconst(
I64,
layout.table0_elements_offset.to_int64(),
)
let size = builder.load_ptr(I64, vmctx, size_offset)
let base_offset = builder.iconst(I64, layout.table0_base_offset.to_int64())
let base = builder.load_ptr(I64, vmctx, base_offset)
(size, base)
} else {
// General path: load from tables array
let sizes_offset = builder.iconst(I64, layout.table_sizes_offset.to_int64())
let sizes_ptr = builder.load_ptr(I64, vmctx, sizes_offset)
let idx_offset = builder.iconst(
I64,
(table_idx * layout.pointer_stride).to_int64(),
)
let size = builder.load_ptr(I64, sizes_ptr, idx_offset)
let tables_offset = builder.iconst(I64, layout.tables_offset.to_int64())
let tables_ptr = builder.load_ptr(I64, vmctx, tables_offset)
let base = builder.load_ptr(I64, tables_ptr, idx_offset)
(size, base)
}
// Bounds check: trap if elem_idx >= table_size
// For table64, elem_idx is already i64; for table32, extend to i64
let elem_idx_i64 = if is_table64 {
elem_idx
} else {
builder.uextend(I64, elem_idx)
}
let in_bounds = builder.icmp(Ult, elem_idx_i64, table_size)
// Create trap and continue blocks
let trap_block = builder.create_block()
let continue_block = builder.create_block()
builder.brnz(in_bounds, continue_block, trap_block)
// Trap block
builder.switch_to_block(trap_block)
builder.trap("table out of bounds")
// Continue block: load the value
builder.switch_to_block(continue_block)
// Calculate address: table_base + elem_idx * 16
let stride = builder.iconst(I64, layout.table_entry_stride.to_int64())
let byte_offset = builder.imul(elem_idx_i64, stride)
let addr = builder.iadd(table_base, byte_offset)
// Load the funcref value (first 8 bytes of entry)
let zero_offset = builder.iconst(I64, 0L)
builder.load_ptr(result_type, addr, zero_offset)
}
///|
/// Translate table.set to IR primitives:
/// 1. Load table_size, check bounds
/// 2. Load table_base
/// 3. Calculate address: table_base + elem_idx * 16
/// 4. Store value
fn FuncEnvironment::translate_table_set(
_self : FuncEnvironment,
builder : FunctionBuilder,
vmctx : Value,
table_idx : Int,
elem_idx : Value,
value : Value,
is_table64? : Bool = false,
) -> Unit {
// For the JIT, table entries are "fat" (16 bytes): value bits + type index.
// Use TableFill with size=1 so the runtime helper can also populate the type
// slot for funcref values (needed for call_indirect type checks).
//
// We keep is_table64 only to choose the correct immediate width for `size=1`.
ignore(is_table64)
let one = builder.iconst(elem_idx.ty, 1L)
@wasm_milkir.table_fill(
builder,
_self.runtime_symbols,
vmctx,
table_idx,
elem_idx,
value,
one,
)
}
// ============ Memory Operations ============
///|
/// Helper to load memory size in bytes for a given memidx.
/// For memidx 0: use fast path (direct vmctx field)
/// For memidx > 0: load from memories array
fn FuncEnvironment::load_memory_size(
self : FuncEnvironment,
builder : FunctionBuilder,
vmctx : Value,
memidx : Int,
) -> Value {
let layout = self.runtime_layout
if memidx == 0 {
let mem0_size_off = builder.iconst(
I64,
layout.memory0_size_offset.to_int64(),
)
return builder.load_ptr(I64, vmctx, mem0_size_off)
}
let mem_ptr = {
let memories_off = builder.iconst(I64, layout.memories_offset.to_int64())
let memories_ptr = builder.load_ptr(I64, vmctx, memories_off)
let idx_off = builder.iconst(
I64,
(memidx * layout.pointer_stride).to_int64(),
)
builder.load_ptr(I64, memories_ptr, idx_off)
}
let memory_layout = self.memory_descriptor_layout
let size_off = builder.iconst(
I64,
memory_layout.current_length_offset.to_int64(),
)
builder.load_ptr(I64, mem_ptr, size_off)
}
///|
/// Whether JIT should rely on guard pages (no explicit bounds checks) for memory access.
/// Currently only enabled for memory 0 and memory32.
fn FuncEnvironment::use_guard_pages(
self : FuncEnvironment,
memidx : Int,
) -> Bool {
if memidx != 0 {
return false
}
let is_memory64 = memidx < self.memory_is_64.length() &&
self.memory_is_64[memidx]
if is_memory64 {
return false
}
// Guarded allocations currently assume 64KiB wasm pages.
let l2 = if memidx < self.memory_page_size_log2.length() {
self.memory_page_size_log2[memidx]
} else {
16
}
l2 == 16
}
///|
fn FuncEnvironment::memory_base_stability(
self : FuncEnvironment,
memidx : Int,
) -> @milkir.GlobalValueStability {
if self.use_guard_pages(memidx) {
Stable
} else {
Mutable
}
}
///|
/// Get or create a shared trap block for memory out-of-bounds errors.
/// All bounds checks in the same function share this block to reduce CFG size.
fn FuncEnvironment::get_or_create_memory_trap_block(
self : FuncEnvironment,
builder : FunctionBuilder,
) -> Block {
match self.memory_trap_block {
Some(block) => block
None => {
let trap_block = builder.create_block()
let current = builder.current_block() // save current position
builder.switch_to_block(trap_block)
builder.trap("memory out of bounds")
builder.switch_to_block(current) // restore position
self.memory_trap_block = Some(trap_block)
trap_block
}
}
}
///|
/// Get or create a shared trap block for unaligned atomic accesses.
fn FuncEnvironment::get_or_create_atomic_unaligned_trap_block(
self : FuncEnvironment,
builder : FunctionBuilder,
) -> Block {
match self.atomic_unaligned_trap_block {
Some(block) => block
None => {
let trap_block = builder.create_block()
let current = builder.current_block()
builder.switch_to_block(trap_block)
builder.trap("unaligned atomic")
builder.switch_to_block(current)
self.atomic_unaligned_trap_block = Some(trap_block)
trap_block
}
}
}
///|
/// Emit an atomic alignment check. Atomics trap on misalignment.
///
/// The check is performed on the linear memory effective address (addr + offset).
fn FuncEnvironment::emit_atomic_alignment_check(
self : FuncEnvironment,
builder : FunctionBuilder,
memidx : Int,
wasm_addr : Value,
offset : Int64,
access_size : Int,
) -> Unit {
if access_size <= 1 {
return
}
let is_memory64 = memidx < self.memory_is_64.length() &&
self.memory_is_64[memidx]
let addr_i64 = if is_memory64 {
wasm_addr
} else {
builder.uextend(I64, wasm_addr)
}
let offset_val = builder.iconst(I64, offset)
let addr_plus_offset = builder.iadd(addr_i64, offset_val)
let mask = builder.iconst(I64, (access_size - 1).to_int64())
let masked = builder.band(addr_plus_offset, mask)
let zero = builder.iconst(I64, 0L)
let is_aligned = builder.icmp(Eq, masked, zero)
let trap_block = self.get_or_create_atomic_unaligned_trap_block(builder)
let continue_block = builder.create_block()
builder.brnz(is_aligned, continue_block, trap_block)
builder.switch_to_block(continue_block)
}
// Note: Linear memory base is represented in MilkIR and lowered by the target
// backend into generic MachV loads from the embedding runtime layout.
///|
/// Get the byte size for a type
fn type_byte_size(ty : Type) -> Int {
match ty {
I32 => 4
I64 => 8
F32 => 4
F64 => 8
V128 => 16 // SIMD vector
Ptr | Ref | CallableRef | OpaqueRef => 8 // Reference/pointer types are pointer-sized
}
}
///|
/// Check if bounds check can be eliminated for a constant address access
/// Returns true if the access is guaranteed to be within the minimum memory size
fn FuncEnvironment::can_eliminate_bounds_check(
self : FuncEnvironment,
builder : FunctionBuilder,
memidx : Int,
wasm_addr : Value,
offset : Int64,
access_size : Int,
) -> Bool {
// Get minimum memory size for this memory index
let memory_min = if memidx < self.memory_mins.length() {
self.memory_mins[memidx]
} else {
0L // No minimum known, cannot eliminate
}
if memory_min == 0L {
return false
}
// Check if wasm_addr is a constant
match builder.get_const_value(wasm_addr) {
None => false
Some(const_addr) => {
// Ensure const_addr is non-negative (valid wasm address)
if const_addr < 0L {
return false
}
// Check if const_addr + offset + access_size <= memory_min
// Use checked arithmetic to avoid overflow
let end_addr = const_addr + offset + access_size.to_int64()
// Also check for overflow (end_addr < const_addr would indicate overflow)
end_addr >= const_addr && end_addr <= memory_min
}
}
}
///|
fn bounds_check_cache_key(
block_id : Int,
memidx : Int,
addr_id : Int,
) -> String {
"\{block_id}:\{memidx}:\{addr_id}"
}
///|
fn checked_access_end(offset : Int64, access_size : Int) -> Int64? {
if offset < 0L || access_size < 0 {
return None
}
let end = offset + access_size.to_int64()
if end < offset {
None
} else {
Some(end)
}
}
///|
/// Cranelift-aligned local dedup: if we already checked the same wasm address
/// in this block with a larger (offset + size), we can skip the later check.
fn FuncEnvironment::can_eliminate_redundant_bounds_check(
self : FuncEnvironment,
builder : FunctionBuilder,
memidx : Int,
wasm_addr : Value,
offset : Int64,
access_size : Int,
) -> Bool {
let block = builder.current_block()
guard checked_access_end(offset, access_size) is Some(end) else {
return false
}
let key = bounds_check_cache_key(block.id, memidx, wasm_addr.id)
match self.bounds_check_cache.get(key) {
Some(max_end) => end <= max_end
None => false
}
}
///|
fn FuncEnvironment::record_bounds_check(
self : FuncEnvironment,
builder : FunctionBuilder,
memidx : Int,
wasm_addr : Value,
offset : Int64,
access_size : Int,
) -> Unit {
let block = builder.current_block()
guard checked_access_end(offset, access_size) is Some(end) else { return }
let key = bounds_check_cache_key(block.id, memidx, wasm_addr.id)
match self.bounds_check_cache.get(key) {
Some(max_end) => if end > max_end { self.bounds_check_cache.set(key, end) }
None => self.bounds_check_cache.set(key, end)
}
}
///|
/// Emit bounds check and return effective address for memory access.
/// Shared by SIMD and other complex load operations.
fn FuncEnvironment::emit_bounds_check(
self : FuncEnvironment,
builder : FunctionBuilder,
vmctx : Value,
memidx : Int,
wasm_addr : Value,
offset : Int64,
access_size : Int,
) -> Value {
if self.use_guard_pages(memidx) {
// Guard pages (memory32, memidx=0): rely on SIGSEGV for OOB trapping.
let memory_base = @wasm_milkir.load_mem_base(
builder,
vmctx,
memidx,
self.memory_base_stability(memidx),
)
let addr_i64 = builder.uextend(I64, wasm_addr)
let offset_val = builder.iconst(I64, offset)
let addr_plus_offset = builder.iadd(addr_i64, offset_val)
return builder.iadd(memory_base, addr_plus_offset)
}
// Check if bounds check can be eliminated:
// - statically (constant in min memory)
// - dynamically (already checked in this block with wider end range)
let skip_const_bounds_check = self.can_eliminate_bounds_check(
builder, memidx, wasm_addr, offset, access_size,
)
let skip_redundant_bounds_check = self.can_eliminate_redundant_bounds_check(
builder, memidx, wasm_addr, offset, access_size,
)
let skip_bounds_check = skip_const_bounds_check || skip_redundant_bounds_check
let memory_base = @wasm_milkir.load_mem_base(
builder,
vmctx,
memidx,
self.memory_base_stability(memidx),
)
let memory_size = self.load_memory_size(builder, vmctx, memidx)
// For memory64, address is already i64; for memory32, extend i32 to i64
let is_memory64 = memidx < self.memory_is_64.length() &&
self.memory_is_64[memidx]
let addr_i64 = if is_memory64 {
wasm_addr // Already i64
} else {
builder.uextend(I64, wasm_addr)
}
// Calculate addr + offset for effective address
let offset_val = builder.iconst(I64, offset)
let addr_plus_offset = builder.iadd(addr_i64, offset_val)
if !skip_bounds_check {
// Need runtime bounds check
let size_val = builder.iconst(I64, access_size.to_int64())
let end_addr = builder.iadd(addr_plus_offset, size_val)
// For memory64, we need overflow-safe bounds checking
let trap_block = self.get_or_create_memory_trap_block(builder)
let continue_block = builder.create_block()
if is_memory64 {
// Check for overflow in addr + offset
let no_overflow1 = builder.icmp(Uge, addr_plus_offset, addr_i64)
let check1_block = builder.create_block()
builder.brnz(no_overflow1, check1_block, trap_block)
builder.switch_to_block(check1_block)
// Check for overflow in (addr + offset) + size
let no_overflow2 = builder.icmp(Uge, end_addr, addr_plus_offset)
let check2_block = builder.create_block()
builder.brnz(no_overflow2, check2_block, trap_block)
builder.switch_to_block(check2_block)
// Check end_addr <= memory_size
let in_bounds = builder.icmp(Ule, end_addr, memory_size)
builder.brnz(in_bounds, continue_block, trap_block)
} else {
// For memory32, no overflow possible
let in_bounds = builder.icmp(Ule, end_addr, memory_size)
builder.brnz(in_bounds, continue_block, trap_block)
}
// Continue block
builder.switch_to_block(continue_block)
self.record_bounds_check(builder, memidx, wasm_addr, offset, access_size)
}
// Return effective address: memory_base + addr + offset
builder.iadd(memory_base, addr_plus_offset)
}
///|
/// Translate memory load to IR primitives:
/// 1. Load memory_base and memory_size from vmctx
/// 2. Bounds check: trap if addr + offset + size > memory_size (shared trap block)
/// - Skip bounds check for constant addresses within minimum memory size
/// 3. Calculate effective address: memory_base + addr + offset
/// 4. LoadPtr
fn FuncEnvironment::translate_memory_load(
self : FuncEnvironment,
builder : FunctionBuilder,
vmctx : Value,
memidx : Int,
ty : Type,
wasm_addr : Value,
offset : Int64,
) -> Value {
let access_size = type_byte_size(ty)
if self.use_guard_pages(memidx) {
// Guard pages (memory32, memidx=0): rely on SIGSEGV for OOB trapping.
let memory_base = @wasm_milkir.load_mem_base(
builder,
vmctx,
memidx,
self.memory_base_stability(memidx),
)
let addr_i64 = builder.uextend(I64, wasm_addr)
let offset_val = builder.iconst(I64, offset)
let addr_plus_offset = builder.iadd(addr_i64, offset_val)
let effective_addr = builder.iadd(memory_base, addr_plus_offset)
let zero_offset = builder.iconst(I64, 0L)
return builder.load_ptr(ty, effective_addr, zero_offset)
}
// Check if bounds check can be eliminated:
// - statically (constant in min memory)
// - dynamically (already checked in this block with wider end range)
let skip_const_bounds_check = self.can_eliminate_bounds_check(
builder, memidx, wasm_addr, offset, access_size,
)
let skip_redundant_bounds_check = self.can_eliminate_redundant_bounds_check(
builder, memidx, wasm_addr, offset, access_size,
)
let skip_bounds_check = skip_const_bounds_check || skip_redundant_bounds_check
let memory_base = @wasm_milkir.load_mem_base(
builder,
vmctx,
memidx,
self.memory_base_stability(memidx),
)
let memory_size = self.load_memory_size(builder, vmctx, memidx)
// For memory64, address is already i64; for memory32, extend i32 to i64
let is_memory64 = memidx < self.memory_is_64.length() &&
self.memory_is_64[memidx]
let addr_i64 = if is_memory64 {
wasm_addr // Already i64
} else {
builder.uextend(I64, wasm_addr)
}
// Calculate addr + offset for effective address
let offset_val = builder.iconst(I64, offset)
let addr_plus_offset = builder.iadd(addr_i64, offset_val)
if !skip_bounds_check {
// Need runtime bounds check
let size_val = builder.iconst(I64, access_size.to_int64())
let end_addr = builder.iadd(addr_plus_offset, size_val)
// For memory64, we need overflow-safe bounds checking.
// u64 addition can wrap, so we must detect overflow:
// - overflow1: addr + offset overflowed if addr_plus_offset < addr
// - overflow2: (addr + offset) + size overflowed if end_addr < addr_plus_offset
// - in_range: end_addr <= memory_size
// Trap if overflow1 || overflow2 || !in_range
let trap_block = self.get_or_create_memory_trap_block(builder)
let continue_block = builder.create_block()
if is_memory64 {
// Check for overflow in addr + offset
let no_overflow1 = builder.icmp(Uge, addr_plus_offset, addr_i64)
let check1_block = builder.create_block()
builder.brnz(no_overflow1, check1_block, trap_block)
builder.switch_to_block(check1_block)
// Check for overflow in (addr + offset) + size
let no_overflow2 = builder.icmp(Uge, end_addr, addr_plus_offset)
let check2_block = builder.create_block()
builder.brnz(no_overflow2, check2_block, trap_block)
builder.switch_to_block(check2_block)
// Check end_addr <= memory_size
let in_bounds = builder.icmp(Ule, end_addr, memory_size)
builder.brnz(in_bounds, continue_block, trap_block)
} else {
// For memory32, addr is u32 extended to u64, offset is u32, size is small
// No overflow possible, simple check suffices
let in_bounds = builder.icmp(Ule, end_addr, memory_size)
builder.brnz(in_bounds, continue_block, trap_block)
}
// Continue block: perform the load
builder.switch_to_block(continue_block)
self.record_bounds_check(builder, memidx, wasm_addr, offset, access_size)
}
// Calculate effective address: memory_base + addr + offset
let effective_addr = builder.iadd(memory_base, addr_plus_offset)
let zero_offset = builder.iconst(I64, 0L)
builder.load_ptr(ty, effective_addr, zero_offset)
}
///|
/// Translate memory store to IR primitives:
/// 1. Load memory_base and memory_size from vmctx
/// 2. Bounds check: trap if addr + offset + size > memory_size (shared trap block)
/// - Skip bounds check for constant addresses within minimum memory size
/// 3. Calculate effective address: memory_base + addr + offset
/// 4. StorePtr
fn FuncEnvironment::translate_memory_store(
self : FuncEnvironment,
builder : FunctionBuilder,
vmctx : Value,
memidx : Int,
ty : Type,
wasm_addr : Value,
value : Value,
offset : Int64,
) -> Unit {
let access_size = type_byte_size(ty)
if self.use_guard_pages(memidx) {
// Guard pages (memory32, memidx=0): rely on SIGSEGV for OOB trapping.
let memory_base = @wasm_milkir.load_mem_base(
builder,
vmctx,
memidx,
self.memory_base_stability(memidx),
)
let addr_i64 = builder.uextend(I64, wasm_addr)
let offset_val = builder.iconst(I64, offset)
let addr_plus_offset = builder.iadd(addr_i64, offset_val)
let effective_addr = builder.iadd(memory_base, addr_plus_offset)
let zero_offset = builder.iconst(I64, 0L)
return builder.store_ptr(ty, effective_addr, value, zero_offset)
}
// Check if bounds check can be eliminated:
// - statically (constant in min memory)
// - dynamically (already checked in this block with wider end range)
let skip_const_bounds_check = self.can_eliminate_bounds_check(
builder, memidx, wasm_addr, offset, access_size,
)
let skip_redundant_bounds_check = self.can_eliminate_redundant_bounds_check(
builder, memidx, wasm_addr, offset, access_size,
)
let skip_bounds_check = skip_const_bounds_check || skip_redundant_bounds_check
let memory_base = @wasm_milkir.load_mem_base(
builder,
vmctx,
memidx,
self.memory_base_stability(memidx),
)
let memory_size = self.load_memory_size(builder, vmctx, memidx)
// For memory64, address is already i64; for memory32, extend i32 to i64
let is_memory64 = memidx < self.memory_is_64.length() &&
self.memory_is_64[memidx]
let addr_i64 = if is_memory64 {
wasm_addr // Already i64
} else {
builder.uextend(I64, wasm_addr)
}
// Calculate addr + offset for effective address
let offset_val = builder.iconst(I64, offset)
let addr_plus_offset = builder.iadd(addr_i64, offset_val)
if !skip_bounds_check {
// Need runtime bounds check
let size_val = builder.iconst(I64, access_size.to_int64())
let end_addr = builder.iadd(addr_plus_offset, size_val)
// For memory64, we need overflow-safe bounds checking
let trap_block = self.get_or_create_memory_trap_block(builder)
let continue_block = builder.create_block()
if is_memory64 {
// Check for overflow in addr + offset
let no_overflow1 = builder.icmp(Uge, addr_plus_offset, addr_i64)
let check1_block = builder.create_block()
builder.brnz(no_overflow1, check1_block, trap_block)
builder.switch_to_block(check1_block)
// Check for overflow in (addr + offset) + size
let no_overflow2 = builder.icmp(Uge, end_addr, addr_plus_offset)
let check2_block = builder.create_block()
builder.brnz(no_overflow2, check2_block, trap_block)
builder.switch_to_block(check2_block)
// Check end_addr <= memory_size
let in_bounds = builder.icmp(Ule, end_addr, memory_size)
builder.brnz(in_bounds, continue_block, trap_block)
} else {
// For memory32, no overflow possible
let in_bounds = builder.icmp(Ule, end_addr, memory_size)
builder.brnz(in_bounds, continue_block, trap_block)
}
// Continue block: perform the store
builder.switch_to_block(continue_block)
self.record_bounds_check(builder, memidx, wasm_addr, offset, access_size)
}
// Calculate effective address: memory_base + addr + offset
let effective_addr = builder.iadd(memory_base, addr_plus_offset)
let zero_offset = builder.iconst(I64, 0L)
builder.store_ptr(ty, effective_addr, value, zero_offset)
}
///|
/// Translate narrow memory load (i32.load8_s, i32.load16_u, etc.) to IR primitives
fn FuncEnvironment::translate_memory_load_narrow(
self : FuncEnvironment,
builder : FunctionBuilder,
vmctx : Value,
memidx : Int,
result_ty : Type,
narrow_bits : Int,
signed : Bool,
wasm_addr : Value,
offset : Int64,
) -> Value {
let access_size = narrow_bits / 8
if self.use_guard_pages(memidx) {
// Guard pages (memory32, memidx=0): rely on SIGSEGV for OOB trapping.
let memory_base = @wasm_milkir.load_mem_base(
builder,
vmctx,
memidx,
self.memory_base_stability(memidx),
)
let addr_i64 = builder.uextend(I64, wasm_addr)
let offset_val = builder.iconst(I64, offset)
let addr_plus_offset = builder.iadd(addr_i64, offset_val)
let effective_addr = builder.iadd(memory_base, addr_plus_offset)
let zero_offset = builder.iconst(I64, 0L)
return builder.load_ptr_narrow(
result_ty, narrow_bits, signed, effective_addr, zero_offset,
)
}
// Check if bounds check can be eliminated:
// - statically (constant in min memory)
// - dynamically (already checked in this block with wider end range)
let skip_const_bounds_check = self.can_eliminate_bounds_check(
builder, memidx, wasm_addr, offset, access_size,
)
let skip_redundant_bounds_check = self.can_eliminate_redundant_bounds_check(
builder, memidx, wasm_addr, offset, access_size,
)
let skip_bounds_check = skip_const_bounds_check || skip_redundant_bounds_check
let memory_base = @wasm_milkir.load_mem_base(
builder,
vmctx,
memidx,
self.memory_base_stability(memidx),
)
let memory_size = self.load_memory_size(builder, vmctx, memidx)
// For memory64, address is already i64; for memory32, extend i32 to i64
let is_memory64 = memidx < self.memory_is_64.length() &&
self.memory_is_64[memidx]
let addr_i64 = if is_memory64 {
wasm_addr // Already i64
} else {
builder.uextend(I64, wasm_addr)
}
// Calculate addr + offset for effective address
let offset_val = builder.iconst(I64, offset)
let addr_plus_offset = builder.iadd(addr_i64, offset_val)
if !skip_bounds_check {
// Need runtime bounds check
let size_val = builder.iconst(I64, access_size.to_int64())
let end_addr = builder.iadd(addr_plus_offset, size_val)
// For memory64, we need overflow-safe bounds checking
let trap_block = self.get_or_create_memory_trap_block(builder)
let continue_block = builder.create_block()
if is_memory64 {
// Check for overflow in addr + offset
let no_overflow1 = builder.icmp(Uge, addr_plus_offset, addr_i64)
let check1_block = builder.create_block()
builder.brnz(no_overflow1, check1_block, trap_block)
builder.switch_to_block(check1_block)
// Check for overflow in (addr + offset) + size
let no_overflow2 = builder.icmp(Uge, end_addr, addr_plus_offset)
let check2_block = builder.create_block()
builder.brnz(no_overflow2, check2_block, trap_block)
builder.switch_to_block(check2_block)
// Check end_addr <= memory_size
let in_bounds = builder.icmp(Ule, end_addr, memory_size)
builder.brnz(in_bounds, continue_block, trap_block)
} else {
// For memory32, no overflow possible
let in_bounds = builder.icmp(Ule, end_addr, memory_size)
builder.brnz(in_bounds, continue_block, trap_block)
}
// Continue block: perform the load
builder.switch_to_block(continue_block)
self.record_bounds_check(builder, memidx, wasm_addr, offset, access_size)
}
// Calculate effective address
let effective_addr = builder.iadd(memory_base, addr_plus_offset)
let zero_offset = builder.iconst(I64, 0L)
builder.load_ptr_narrow(
result_ty, narrow_bits, signed, effective_addr, zero_offset,
)
}
///|
/// Translate narrow memory store (i32.store8, i32.store16, etc.) to IR primitives
fn FuncEnvironment::translate_memory_store_narrow(
self : FuncEnvironment,
builder : FunctionBuilder,
vmctx : Value,
memidx : Int,
narrow_bits : Int,
wasm_addr : Value,
value : Value,
offset : Int64,
) -> Unit {
let access_size = narrow_bits / 8
if self.use_guard_pages(memidx) {
// Guard pages (memory32, memidx=0): rely on SIGSEGV for OOB trapping.
let memory_base = @wasm_milkir.load_mem_base(
builder,
vmctx,
memidx,
self.memory_base_stability(memidx),
)
let addr_i64 = builder.uextend(I64, wasm_addr)
let offset_val = builder.iconst(I64, offset)
let addr_plus_offset = builder.iadd(addr_i64, offset_val)
let effective_addr = builder.iadd(memory_base, addr_plus_offset)
let zero_offset = builder.iconst(I64, 0L)
return builder.store_ptr_narrow(
narrow_bits, effective_addr, value, zero_offset,
)
}
// Check if bounds check can be eliminated:
// - statically (constant in min memory)
// - dynamically (already checked in this block with wider end range)
let skip_const_bounds_check = self.can_eliminate_bounds_check(
builder, memidx, wasm_addr, offset, access_size,
)
let skip_redundant_bounds_check = self.can_eliminate_redundant_bounds_check(
builder, memidx, wasm_addr, offset, access_size,
)
let skip_bounds_check = skip_const_bounds_check || skip_redundant_bounds_check
let memory_base = @wasm_milkir.load_mem_base(
builder,
vmctx,
memidx,
self.memory_base_stability(memidx),
)
let memory_size = self.load_memory_size(builder, vmctx, memidx)
// For memory64, address is already i64; for memory32, extend i32 to i64
let is_memory64 = memidx < self.memory_is_64.length() &&
self.memory_is_64[memidx]
let addr_i64 = if is_memory64 {
wasm_addr // Already i64
} else {
builder.uextend(I64, wasm_addr)
}
// Calculate addr + offset for effective address
let offset_val = builder.iconst(I64, offset)
let addr_plus_offset = builder.iadd(addr_i64, offset_val)
if !skip_bounds_check {
// Need runtime bounds check
let size_val = builder.iconst(I64, access_size.to_int64())
let end_addr = builder.iadd(addr_plus_offset, size_val)
// For memory64, we need overflow-safe bounds checking
let trap_block = self.get_or_create_memory_trap_block(builder)
let continue_block = builder.create_block()
if is_memory64 {
// Check for overflow in addr + offset
let no_overflow1 = builder.icmp(Uge, addr_plus_offset, addr_i64)
let check1_block = builder.create_block()
builder.brnz(no_overflow1, check1_block, trap_block)
builder.switch_to_block(check1_block)
// Check for overflow in (addr + offset) + size
let no_overflow2 = builder.icmp(Uge, end_addr, addr_plus_offset)
let check2_block = builder.create_block()
builder.brnz(no_overflow2, check2_block, trap_block)
builder.switch_to_block(check2_block)
// Check end_addr <= memory_size
let in_bounds = builder.icmp(Ule, end_addr, memory_size)
builder.brnz(in_bounds, continue_block, trap_block)
} else {
// For memory32, no overflow possible
let in_bounds = builder.icmp(Ule, end_addr, memory_size)
builder.brnz(in_bounds, continue_block, trap_block)
}
// Continue block: perform the store
builder.switch_to_block(continue_block)
self.record_bounds_check(builder, memidx, wasm_addr, offset, access_size)
}
// Calculate effective address
let effective_addr = builder.iadd(memory_base, addr_plus_offset)
let zero_offset = builder.iconst(I64, 0L)
builder.store_ptr_narrow(narrow_bits, effective_addr, value, zero_offset)
}