// 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)
}