///|
/// Mutable builder for one x86-64 machine-code buffer.
///
/// A `JITContext` owns one writable code mapping while it builds machine code.
/// It appends encoded bytes to an internal buffer and tracks the byte offset
/// used by labels and relative jumps.
struct JITContext {
  // Logical instruction trace retained for debugging and future diagnostics.
  instrs : Array[Instr]
  // Encoded machine-code bytes waiting to be copied into executable memory.
  code : @buffer.Buffer
  // Start address of the executable memory region.
  mem_ptr : UInt64
  // Size of the executable memory region.
  mem_size : UInt64
  // Current byte offset from `mem_ptr`.
  mut mem_offset : Int32
  // Whether this context has already produced a `JITFunction`.
  mut finalized : Bool
}

///|
/// Finalized machine-code function with a known callable signature.
struct JITFunction[T](FuncRef[T])

///|
/// Errors raised when an instruction helper cannot encode the requested form.
pub(all) suberror ASMContextError {
  // Instruction-specific fallbacks for forms that have no supported encoding.
  UnsupportedAddOperands(Operand, Operand)
  UnsupportedSubOperands(Operand, Operand)
  UnsupportedXorOperands(Operand, Operand)
  UnsupportedMulOperand(Operand)
  UnsupportedIncOperand(Operand)
  UnsupportedDecOperand(Operand)
  LoopRel8OutOfRange(Int32)
  UnsupportedIMulOperands(Operand, Operand)
  UnsupportedMovOperands(Operand, Operand)
  UnsupportedPushOperand(Operand)
  UnsupportedPopOperand(Operand)
  // Executable-memory lifecycle errors.
  ExecutableMemoryAllocationFailed(UInt64, Int32)
  GeneratedCodeExceedsMemory(UInt64, UInt64)
  ExecutableMemoryProtectionFailed(UInt64, UInt64, Int32)
  JITContextAlreadyFinalized
  // Generic validation errors shared by the encoding layer.
  UnsupportedOperandWidth(Operand)
  OperandWidthMismatch(Width, Width)
  ImmediateOutOfRange(Immediate)
  InvalidMemoryScale(Int32)
  InvalidMemoryBase(Register)
  InvalidMemoryIndex(Register)
  MemoryIndexCannotBeStackPointer(Register)
  High8RexConflict
} derive(Debug)

///|
/// Create a new code-generation context backed by one writable memory mapping.
pub fn JITContext::new(
  mem_size~ : UInt64 = default_exec_memory_size,
) -> JITContext raise ASMContextError {
  let start = allocate_jit_memory(mem_size)
  {
    instrs: [],
    code: Buffer(size_hint=25),
    mem_ptr: start,
    mem_size,
    mem_offset: 0,
    finalized: false,
  }
}

///|
/// Capture the current byte offset as a label.
///
/// Use this before emitting a backward branch or as a local call target.
pub fn JITContext::label(self : JITContext) -> Label {
  { offset: self.mem_offset, }
}

///|
/// Resolve a symbol in the current process for use with `call`.
///
/// The returned target stores an offset relative to this context's code buffer,
/// matching x86-64 CALL rel32 encoding.
pub fn JITContext::extern_fn(self : JITContext, name : String) -> Target {
  let handle = dlopen()
  let fn_pointer = dlsym(handle, name)
  let rel = if fn_pointer > self.mem_ptr {
    (fn_pointer - self.mem_ptr).to_int()
  } else {
    -(self.mem_ptr - fn_pointer).to_int()
  }
  Extern(rel)
}

///|
/// Append a single byte and advance the current code offset.
fn JITContext::emit_byte(self : JITContext, i : Byte) -> Unit {
  self.code.write_byte(i)
  self.mem_offset += 1
}

///|
/// Return a copy of the generated machine-code bytes.
pub fn JITContext::bytes(self : JITContext) -> Bytes {
  self.code.contents()
}

///|
/// Return a copy of the successfully emitted instruction trace.
pub fn JITContext::trace(self : JITContext) -> Array[Instr] {
  self.instrs.copy()
}

///|
/// Copy the generated code into its mapping and make it executable.
///
/// The generated function is expected to follow the platform ABI enough to
/// return its result in RAX and preserve any registers required by its caller.
pub fn JITContext::finalize_fn_i64(
  self : JITContext,
) -> JITFunction[() -> Int64] raise ASMContextError {
  if self.finalized {
    raise JITContextAlreadyFinalized
  }
  let machcode = self.code.contents()
  let code_size = machcode.length().to_uint64()
  if code_size > self.mem_size {
    raise GeneratedCodeExceedsMemory(code_size, self.mem_size)
  }
  copy_code(self.mem_ptr, machcode, code_size)
  mprotect_rx(self.mem_ptr, self.mem_size)
  self.finalized = true
  JITFunction(code_func_ref_i64(self.mem_ptr))
}

///|
/// Return this finalized function as a callable MoonBit function reference.
pub fn[T] JITFunction::get_fn(self : JITFunction[T]) -> FuncRef[T] {
  let JITFunction(func) = self
  func
}

///|
/// Call this finalized function.
pub fn JITFunction::call(self : JITFunction[() -> Int64]) -> Int64 {
  let f : FuncRef[() -> Int64] = self.get_fn()
  f()
}

///|
/// Emit a one-byte NOP instruction.
pub fn JITContext::nop(self : JITContext) -> Unit {
  self.emit_byte(0x90)
  self.instrs.push(NopOp90)
}

///|
/// Emit a RET instruction.
pub fn JITContext::ret(self : JITContext) -> Unit {
  self.emit_byte(0xC3)
  self.instrs.push(RetNearOpC3)
}

///|
fn rm_from_operand(operand : Operand) -> RmOperand raise ASMContextError {
  match operand {
    Reg(register) => reg_as_rm(register)
    Mem(memory) => mem_as_rm(memory)
    _ => raise UnsupportedOperandWidth(operand)
  }
}

///|
priv enum BinaryInstrKind {
  AddInstr
  SubInstr
  XorInstr
}

///|
fn binary_rm_reg8_instr(
  kind : BinaryInstrKind,
  dst : Operand,
  src : Register,
) -> Instr {
  match kind {
    AddInstr => AddRm8Reg8Op00(dst, src)
    SubInstr => SubRm8Reg8Op28(dst, src)
    XorInstr => XorRm8Reg8Op30(dst, src)
  }
}

///|
fn binary_rm_reg_instr(
  kind : BinaryInstrKind,
  dst : Operand,
  src : Register,
) -> Instr {
  match kind {
    AddInstr => AddRmRegOp01(dst, src)
    SubInstr => SubRmRegOp29(dst, src)
    XorInstr => XorRmRegOp31(dst, src)
  }
}

///|
fn binary_reg_rm8_instr(
  kind : BinaryInstrKind,
  dst : Register,
  src : Operand,
) -> Instr {
  match kind {
    AddInstr => AddReg8Rm8Op02(dst, src)
    SubInstr => SubReg8Rm8Op2A(dst, src)
    XorInstr => XorReg8Rm8Op32(dst, src)
  }
}

///|
fn binary_reg_rm_instr(
  kind : BinaryInstrKind,
  dst : Register,
  src : Operand,
) -> Instr {
  match kind {
    AddInstr => AddRegRmOp03(dst, src)
    SubInstr => SubRegRmOp2B(dst, src)
    XorInstr => XorRegRmOp33(dst, src)
  }
}

///|
fn binary_rm_imm8_instr(
  kind : BinaryInstrKind,
  dst : Operand,
  immediate : Immediate,
) -> Instr {
  match kind {
    AddInstr => AddRm8Imm8Op80Ext0(dst, immediate)
    SubInstr => SubRm8Imm8Op80Ext5(dst, immediate)
    XorInstr => XorRm8Imm8Op80Ext6(dst, immediate)
  }
}

///|
fn binary_rm_imm_instr(
  kind : BinaryInstrKind,
  dst : Operand,
  immediate : Immediate,
) -> Instr {
  match kind {
    AddInstr => AddRmImmOp81Ext0(dst, immediate)
    SubInstr => SubRmImmOp81Ext5(dst, immediate)
    XorInstr => XorRmImmOp81Ext6(dst, immediate)
  }
}

///|
fn binary_rm_imm8_sign_ext_instr(
  kind : BinaryInstrKind,
  dst : Operand,
  immediate : Immediate,
) -> Instr {
  match kind {
    AddInstr => AddRmImm8SignExtOp83Ext0(dst, immediate)
    SubInstr => SubRmImm8SignExtOp83Ext5(dst, immediate)
    XorInstr => XorRmImm8SignExtOp83Ext6(dst, immediate)
  }
}

///|
/// Emit an arithmetic/logical immediate form through opcode groups 80/81/83.
///
/// Width-specific matching is explicit here because x86-64 has no generic
/// "immediate with operand width" rule: 64-bit operations usually consume
/// sign-extended imm32, while byte operations use opcode 80.
fn JITContext::emit_binary_imm(
  self : JITContext,
  width : Width,
  extension : Byte,
  dst : Operand,
  immediate : Immediate,
  kind : BinaryInstrKind,
) -> Instr raise ASMContextError {
  let rm = rm_from_operand(dst)
  immediate.validate()
  match (width, immediate) {
    (W8, Imm8(_)) => {
      self.emit_group_rm(W8, 0x80, extension, rm, false)
      self.emit_immediate(immediate)
      binary_rm_imm8_instr(kind, dst, immediate)
    }
    (W16, Imm8(value)) => {
      if !fits_i8(value) {
        raise ImmediateOutOfRange(immediate)
      }
      self.emit_group_rm(W16, 0x83, extension, rm, false)
      self.emit_i8(value)
      binary_rm_imm8_sign_ext_instr(kind, dst, immediate)
    }
    (W16, Imm16(_)) => {
      self.emit_group_rm(W16, 0x81, extension, rm, false)
      self.emit_immediate(immediate)
      binary_rm_imm_instr(kind, dst, immediate)
    }
    (W32, Imm8(value)) => {
      if !fits_i8(value) {
        raise ImmediateOutOfRange(immediate)
      }
      self.emit_group_rm(W32, 0x83, extension, rm, false)
      self.emit_i8(value)
      binary_rm_imm8_sign_ext_instr(kind, dst, immediate)
    }
    (W32, Imm32(_)) => {
      self.emit_group_rm(W32, 0x81, extension, rm, false)
      self.emit_immediate(immediate)
      binary_rm_imm_instr(kind, dst, immediate)
    }
    (W64, Imm8(value)) => {
      if !fits_i8(value) {
        raise ImmediateOutOfRange(immediate)
      }
      self.emit_group_rm(W64, 0x83, extension, rm, true)
      self.emit_i8(value)
      binary_rm_imm8_sign_ext_instr(kind, dst, immediate)
    }
    (W64, Imm32(_)) => {
      self.emit_group_rm(W64, 0x81, extension, rm, true)
      self.emit_immediate(immediate)
      binary_rm_imm_instr(kind, dst, immediate)
    }
    _ => raise OperandWidthMismatch(width, immediate.width())
  }
}

///|
/// Shared implementation for ADD/SUB/XOR register, memory, and immediate
/// forms.
fn JITContext::emit_binary(
  self : JITContext,
  lhs : Operand,
  rhs : Operand,
  opcode_rm_reg8 : Byte,
  opcode_rm_reg : Byte,
  opcode_reg_rm8 : Byte,
  opcode_reg_rm : Byte,
  imm_extension : Byte,
  kind : BinaryInstrKind,
) -> Instr raise ASMContextError {
  match (lhs, rhs) {
    (Reg(dst), Reg(src)) => {
      let width = require_same_width(lhs, rhs)
      self.emit_reg_rm(
        width,
        if width == W8 {
          opcode_rm_reg8
        } else {
          opcode_rm_reg
        },
        src,
        reg_as_rm(dst),
        width == W64,
      )
      if width == W8 {
        binary_rm_reg8_instr(kind, lhs, src)
      } else {
        binary_rm_reg_instr(kind, lhs, src)
      }
    }
    (Mem(dst), Reg(src)) => {
      let width = require_same_width(lhs, rhs)
      self.emit_reg_rm(
        width,
        if width == W8 {
          opcode_rm_reg8
        } else {
          opcode_rm_reg
        },
        src,
        mem_as_rm(dst),
        width == W64,
      )
      if width == W8 {
        binary_rm_reg8_instr(kind, lhs, src)
      } else {
        binary_rm_reg_instr(kind, lhs, src)
      }
    }
    (Reg(dst), Mem(src)) => {
      let width = require_same_width(lhs, rhs)
      self.emit_reg_rm(
        width,
        if width == W8 {
          opcode_reg_rm8
        } else {
          opcode_reg_rm
        },
        dst,
        mem_as_rm(src),
        width == W64,
      )
      if width == W8 {
        binary_reg_rm8_instr(kind, dst, rhs)
      } else {
        binary_reg_rm_instr(kind, dst, rhs)
      }
    }
    (Reg(_) | Mem(_), Imm(immediate)) => {
      let width = width_or_error(lhs)
      self.emit_binary_imm(width, imm_extension, lhs, immediate, kind)
    }
    _ => raise UnsupportedOperandWidth(lhs)
  }
}

///|
/// Emit ADD.
///
/// Supported forms include `reg/mem += reg`, `reg += mem`, and `reg/mem += imm`
/// when operand widths and immediate encoding widths are legal.
pub fn JITContext::add(
  self : JITContext,
  lhs : Operand,
  rhs : Operand,
) -> Unit raise ASMContextError {
  let instr = match (lhs, rhs) {
    (Reg(_) | Mem(_), Reg(_) | Imm(_)) | (Reg(_), Mem(_)) =>
      self.emit_binary(lhs, rhs, 0x00, 0x01, 0x02, 0x03, 0, AddInstr)
    _ => raise UnsupportedAddOperands(lhs, rhs)
  }
  self.instrs.push(instr)
}

///|
/// Emit SUB.
///
/// Supported forms mirror `add`, with the left operand as the destination.
pub fn JITContext::sub(
  self : JITContext,
  lhs : Operand,
  rhs : Operand,
) -> Unit raise ASMContextError {
  let instr = match (lhs, rhs) {
    (Reg(_) | Mem(_), Reg(_) | Imm(_)) | (Reg(_), Mem(_)) =>
      self.emit_binary(lhs, rhs, 0x28, 0x29, 0x2A, 0x2B, 5, SubInstr)
    _ => raise UnsupportedSubOperands(lhs, rhs)
  }
  self.instrs.push(instr)
}

///|
/// Emit XOR.
///
/// Supported forms mirror `add`, with the left operand as the destination.
pub fn JITContext::xor(
  self : JITContext,
  lhs : Operand,
  rhs : Operand,
) -> Unit raise ASMContextError {
  let instr = match (lhs, rhs) {
    (Reg(_) | Mem(_), Reg(_) | Imm(_)) | (Reg(_), Mem(_)) =>
      self.emit_binary(lhs, rhs, 0x30, 0x31, 0x32, 0x33, 6, XorInstr)
    _ => raise UnsupportedXorOperands(lhs, rhs)
  }
  self.instrs.push(instr)
}

///|
/// Emit unsigned MUL, which multiplies AL/AX/EAX/RAX by the operand.
///
/// The implicit accumulator and result registers are defined by the operand
/// width, following the x86 integer `MUL r/m` instruction.
pub fn JITContext::mul(
  self : JITContext,
  operand : Operand,
) -> Unit raise ASMContextError {
  let instr = match operand {
    Reg(_) | Mem(_) => {
      let width = width_or_error(operand)
      self.emit_group_rm(
        width,
        if width == W8 {
          0xF6
        } else {
          0xF7
        },
        4,
        rm_from_operand(operand),
        width == W64,
      )
      if width == W8 {
        MulRm8OpF6Ext4(operand)
      } else {
        MulRmOpF7Ext4(operand)
      }
    }
    _ => raise UnsupportedMulOperand(operand)
  }
  self.instrs.push(instr)
}

///|
/// Emit INC.
///
/// Supports byte, word, dword, and qword register/memory operands.
pub fn JITContext::inc(
  self : JITContext,
  operand : Operand,
) -> Unit raise ASMContextError {
  let instr = match operand {
    Reg(_) | Mem(_) => {
      let width = width_or_error(operand)
      self.emit_group_rm(
        width,
        if width == W8 {
          0xFE
        } else {
          0xFF
        },
        0,
        rm_from_operand(operand),
        width == W64,
      )
      if width == W8 {
        IncRm8OpFEExt0(operand)
      } else {
        IncRmOpFFExt0(operand)
      }
    }
    _ => raise UnsupportedIncOperand(operand)
  }
  self.instrs.push(instr)
}

///|
/// Emit DEC.
///
/// Supports byte, word, dword, and qword register/memory operands.
pub fn JITContext::dec(
  self : JITContext,
  operand : Operand,
) -> Unit raise ASMContextError {
  let instr = match operand {
    Reg(_) | Mem(_) => {
      let width = width_or_error(operand)
      self.emit_group_rm(
        width,
        if width == W8 {
          0xFE
        } else {
          0xFF
        },
        1,
        rm_from_operand(operand),
        width == W64,
      )
      if width == W8 {
        DecRm8OpFEExt1(operand)
      } else {
        DecRmOpFFExt1(operand)
      }
    }
    _ => raise UnsupportedDecOperand(operand)
  }
  self.instrs.push(instr)
}

///|
/// Emit LOOP with a signed 8-bit relative displacement.
///
/// The branch target must be within -128..127 bytes from the next instruction.
/// `RCX` is decremented by the CPU and the branch is taken while it is nonzero.
pub fn JITContext::loop_rel8(
  self : JITContext,
  label : Label,
) -> Unit raise ASMContextError {
  let src = self.mem_offset
  let rel = label.offset - (src + 2)
  if !fits_i8(rel) {
    raise LoopRel8OutOfRange(rel)
  }
  self.emit_byte(0xE2)
  self.emit_i8(rel)
  self.instrs.push(LoopRcxRel8OpE2(label, rel))
}

///|
fn ensure_not_byte_width(
  width : Width,
  lhs : Operand,
  rhs : Operand,
) -> Unit raise ASMContextError {
  if width == W8 {
    raise UnsupportedIMulOperands(lhs, rhs)
  }
}

///|
fn JITContext::emit_imul_imm(
  self : JITContext,
  dst : Register,
  immediate : Immediate,
  lhs : Operand,
  rhs : Operand,
) -> Instr raise ASMContextError {
  let width = dst.width()
  ensure_not_byte_width(width, lhs, rhs)
  immediate.validate()
  match (width, immediate) {
    (W16 | W32 | W64, Imm8(value)) => {
      if !fits_i8(value) {
        raise ImmediateOutOfRange(immediate)
      }
      self.emit_reg_rm(width, 0x6B, dst, reg_as_rm(dst), width == W64)
      self.emit_i8(value)
      IMulRegRmImm8Op6B(dst, Reg(dst), immediate)
    }
    (W16, Imm16(_)) => {
      self.emit_reg_rm(W16, 0x69, dst, reg_as_rm(dst), false)
      self.emit_immediate(immediate)
      IMulRegRmImmOp69(dst, Reg(dst), immediate)
    }
    (W32, Imm32(_)) => {
      self.emit_reg_rm(W32, 0x69, dst, reg_as_rm(dst), false)
      self.emit_immediate(immediate)
      IMulRegRmImmOp69(dst, Reg(dst), immediate)
    }
    (W64, Imm32(_)) => {
      self.emit_reg_rm(W64, 0x69, dst, reg_as_rm(dst), true)
      self.emit_immediate(immediate)
      IMulRegRmImmOp69(dst, Reg(dst), immediate)
    }
    _ => raise OperandWidthMismatch(width, immediate.width())
  }
}

///|
/// Emit signed IMUL.
///
/// Supported forms are two-operand `imul(reg, reg/mem)` and
/// `imul(reg, imm)`. The register/register form uses the standard `0F AF /r`
/// encoding.
pub fn JITContext::imul(
  self : JITContext,
  lhs : Operand,
  rhs : Operand,
) -> Unit raise ASMContextError {
  let instr = match (lhs, rhs) {
    (Reg(dst), Reg(_) | Mem(_)) => {
      let width = require_same_width(lhs, rhs)
      ensure_not_byte_width(width, lhs, rhs)
      self.emit_reg_rm_2(
        width,
        0x0F,
        0xAF,
        dst,
        rm_from_operand(rhs),
        width == W64,
      )
      IMulRegRmOp0FAF(dst, rhs)
    }
    (Reg(dst), Imm(immediate)) => self.emit_imul_imm(dst, immediate, lhs, rhs)
    _ => raise UnsupportedIMulOperands(lhs, rhs)
  }
  self.instrs.push(instr)
}

///|
fn JITContext::emit_mov_reg_imm(
  self : JITContext,
  dst : Register,
  immediate : Immediate,
  lhs : Operand,
  rhs : Operand,
) -> Instr raise ASMContextError {
  let width = dst.width()
  immediate.validate()
  match (width, immediate) {
    (W8, Imm8(_)) => {
      self.emit_opcode_register(W8, 0xB0, dst, false)
      self.emit_immediate(immediate)
      MovReg8Imm8OpB0(dst, immediate)
    }
    (W16, Imm16(_)) => {
      self.emit_opcode_register(W16, 0xB8, dst, false)
      self.emit_immediate(immediate)
      MovRegImmOpB8(dst, immediate)
    }
    (W32, Imm32(_)) => {
      self.emit_opcode_register(W32, 0xB8, dst, false)
      self.emit_immediate(immediate)
      MovRegImmOpB8(dst, immediate)
    }
    (W64, Imm32(_)) => {
      self.emit_group_rm(W64, 0xC7, 0, reg_as_rm(dst), true)
      self.emit_immediate(immediate)
      MovRmImmOpC7Ext0(Reg(dst), immediate)
    }
    (W64, Imm64(_)) => {
      self.emit_opcode_register(W64, 0xB8, dst, true)
      self.emit_immediate(immediate)
      MovRegImmOpB8(dst, immediate)
    }
    _ => raise UnsupportedMovOperands(lhs, rhs)
  }
}

///|
fn JITContext::emit_mov_mem_imm(
  self : JITContext,
  dst : Memory,
  immediate : Immediate,
  lhs : Operand,
  rhs : Operand,
) -> Instr raise ASMContextError {
  immediate.validate()
  match (dst.size, immediate) {
    (W8, Imm8(_)) => {
      self.emit_group_rm(W8, 0xC6, 0, mem_as_rm(dst), false)
      self.emit_immediate(immediate)
      MovRm8Imm8OpC6Ext0(Mem(dst), immediate)
    }
    (W16, Imm16(_)) => {
      self.emit_group_rm(W16, 0xC7, 0, mem_as_rm(dst), false)
      self.emit_immediate(immediate)
      MovRmImmOpC7Ext0(Mem(dst), immediate)
    }
    (W32, Imm32(_)) => {
      self.emit_group_rm(W32, 0xC7, 0, mem_as_rm(dst), false)
      self.emit_immediate(immediate)
      MovRmImmOpC7Ext0(Mem(dst), immediate)
    }
    (W64, Imm32(_)) => {
      self.emit_group_rm(W64, 0xC7, 0, mem_as_rm(dst), true)
      self.emit_immediate(immediate)
      MovRmImmOpC7Ext0(Mem(dst), immediate)
    }
    _ => raise UnsupportedMovOperands(lhs, rhs)
  }
}

///|
/// Emit MOV.
///
/// Supports register/register, register/memory, memory/register,
/// register/immediate, and memory/immediate integer forms. Memory operands must
/// carry an explicit width.
pub fn JITContext::mov(
  self : JITContext,
  lhs : Operand,
  rhs : Operand,
) -> Unit raise ASMContextError {
  let instr = match (lhs, rhs) {
    (Reg(dst), Reg(src)) => {
      let width = require_same_width(lhs, rhs)
      self.emit_reg_rm(
        width,
        if width == W8 {
          0x88
        } else {
          0x89
        },
        src,
        reg_as_rm(dst),
        width == W64,
      )
      if width == W8 {
        MovRm8Reg8Op88(lhs, src)
      } else {
        MovRmRegOp89(lhs, src)
      }
    }
    (Reg(dst), Mem(src)) => {
      let width = require_same_width(lhs, rhs)
      self.emit_reg_rm(
        width,
        if width == W8 {
          0x8A
        } else {
          0x8B
        },
        dst,
        mem_as_rm(src),
        width == W64,
      )
      if width == W8 {
        MovReg8Rm8Op8A(dst, rhs)
      } else {
        MovRegRmOp8B(dst, rhs)
      }
    }
    (Mem(dst), Reg(src)) => {
      let width = require_same_width(lhs, rhs)
      self.emit_reg_rm(
        width,
        if width == W8 {
          0x88
        } else {
          0x89
        },
        src,
        mem_as_rm(dst),
        width == W64,
      )
      if width == W8 {
        MovRm8Reg8Op88(lhs, src)
      } else {
        MovRmRegOp89(lhs, src)
      }
    }
    (Reg(dst), Imm(immediate)) =>
      self.emit_mov_reg_imm(dst, immediate, lhs, rhs)
    (Mem(dst), Imm(immediate)) =>
      self.emit_mov_mem_imm(dst, immediate, lhs, rhs)
    _ => raise UnsupportedMovOperands(lhs, rhs)
  }
  self.instrs.push(instr)
}

///|
fn JITContext::emit_push_imm(
  self : JITContext,
  immediate : Immediate,
) -> Instr raise ASMContextError {
  immediate.validate()
  match immediate {
    Imm8(value) => {
      if !fits_i8(value) {
        raise ImmediateOutOfRange(immediate)
      }
      self.emit_byte(0x6A)
      self.emit_immediate(immediate)
      PushImm8Op6A(immediate)
    }
    Imm16(_) => {
      self.emit_byte(0x66)
      self.emit_byte(0x68)
      self.emit_immediate(immediate)
      PushImmOp68(immediate)
    }
    Imm32(_) => {
      self.emit_byte(0x68)
      self.emit_immediate(immediate)
      PushImmOp68(immediate)
    }
    Imm64(_) => raise UnsupportedOperandWidth(Imm(immediate))
  }
}

///|
/// Emit PUSH.
///
/// Supports 16-bit and 64-bit register/memory operands, plus encodable
/// immediate forms.
pub fn JITContext::push(
  self : JITContext,
  operand : Operand,
) -> Unit raise ASMContextError {
  let instr = match operand {
    Reg(register) =>
      match register.width() {
        W16 => {
          self.emit_opcode_register(W16, 0x50, register, false)
          PushRegOp50(register)
        }
        W64 => {
          self.emit_opcode_register(W64, 0x50, register, false)
          PushRegOp50(register)
        }
        _ => raise UnsupportedPushOperand(operand)
      }
    Mem(memory) =>
      match memory.size {
        W16 => {
          self.emit_group_rm(W16, 0xFF, 6, mem_as_rm(memory), false)
          PushRmOpFFExt6(operand)
        }
        W64 => {
          self.emit_group_rm(W64, 0xFF, 6, mem_as_rm(memory), false)
          PushRmOpFFExt6(operand)
        }
        _ => raise UnsupportedPushOperand(operand)
      }
    Imm(immediate) => self.emit_push_imm(immediate)
    _ => raise UnsupportedPushOperand(operand)
  }
  self.instrs.push(instr)
}

///|
/// Emit POP.
///
/// Supports 16-bit and 64-bit register/memory operands.
pub fn JITContext::pop(
  self : JITContext,
  operand : Operand,
) -> Unit raise ASMContextError {
  let instr = match operand {
    Reg(register) =>
      match register.width() {
        W16 => {
          self.emit_opcode_register(W16, 0x58, register, false)
          PopRegOp58(register)
        }
        W64 => {
          self.emit_opcode_register(W64, 0x58, register, false)
          PopRegOp58(register)
        }
        _ => raise UnsupportedPopOperand(operand)
      }
    Mem(memory) =>
      match memory.size {
        W16 => {
          self.emit_group_rm(W16, 0x8F, 0, mem_as_rm(memory), false)
          PopRmOp8FExt0(operand)
        }
        W64 => {
          self.emit_group_rm(W64, 0x8F, 0, mem_as_rm(memory), false)
          PopRmOp8FExt0(operand)
        }
        _ => raise UnsupportedPopOperand(operand)
      }
    _ => raise UnsupportedPopOperand(operand)
  }
  self.instrs.push(instr)
}

///|
/// Emit CALL rel32 to either an in-buffer label or an external symbol.
pub fn JITContext::call(self : JITContext, target : Target) -> Unit {
  let dst = match target {
    Label(label) => label.offset
    Extern(dst) => dst
  }
  let src = self.mem_offset
  // x86 CALL rel32 is relative to the first byte after the displacement.
  let rel = dst - (src + 5)
  self.emit_byte(0xE8)
  self.emit_i32(rel)
  self.instrs.push(CallRel32OpE8(target, rel))
}

///|
/// Emit SYSCALL.
///
/// The caller is responsible for loading the platform-specific syscall number
/// and arguments into the required registers.
pub fn JITContext::syscall(self : JITContext) -> Unit {
  self.emit_byte(0x0F)
  self.emit_byte(0x05)
  self.instrs.push(SyscallOp0F05)
}