/*
 * Copyright (c) 2026, the Ladybird developers.
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

use crate::allocator::{flatten_and_allocate, handler_uses_named_temps};
use crate::parser::{AsmInstruction, Handler, ObjectFormat, Operand, Program};
use crate::registers::{Arch, resolve_register};
use crate::shared::{
    HandlerState, ResolvedMemoryIndex, get_immediate_value, resolve_adjacent_memory_pair,
    resolve_field_ref, resolve_label, resolve_memory_operand, substitute_macro,
    uniquify_macro_labels, w,
};
use std::collections::HashMap;
use std::fmt::Write;

#[derive(Clone, Copy, PartialEq, Eq)]
enum X86_64Abi {
    SysV,
    Win64,
}

impl X86_64Abi {
    fn for_program(program: &Program) -> Self {
        match program.object_format {
            ObjectFormat::Coff => Self::Win64,
            ObjectFormat::Elf | ObjectFormat::MachO => Self::SysV,
        }
    }

    fn is_win64(self) -> bool {
        matches!(self, Self::Win64)
    }
}

const SYSV_VM_SLOT: &str = "[rbp - 48]";
const WIN64_VM_SLOT: &str = "[rbp - 64]";
const WIN64_RAW_NATIVE_RETURN_SLOT: &str = "[rbp - 80]";
const WIN64_RAW_NATIVE_VARIANT_SLOT: &str = "[rbp - 72]";

pub fn generate(program: &Program) -> String {
    let mut out = String::new();
    let abi = X86_64Abi::for_program(program);

    w!(out, "# Generated by asmintgen -- DO NOT EDIT");
    w!(out, ".intel_syntax noprefix");
    w!(out);

    // On macOS, symbols need an underscore prefix
    w!(out, "#ifdef __APPLE__");
    w!(out, "#define CSYM(name) _##name");
    w!(out, "#else");
    w!(out, "#define CSYM(name) name");
    w!(out, "#endif");
    w!(out);

    w!(out, ".text");
    w!(out);

    // Generate dispatch table
    // The table contains absolute addresses that need relocation, so on Linux
    // it must go in .data.rel.ro (not .rodata) to avoid DT_TEXTREL in PIE.
    match program.object_format {
        ObjectFormat::MachO => w!(out, ".section __DATA,__const"),
        ObjectFormat::Elf => w!(out, ".section .data.rel.ro"),
        ObjectFormat::Coff => w!(out, ".section .rdata,\"dr\""),
    }
    w!(out, ".p2align 3");
    w!(out, "asm_dispatch_table:");

    // Collect handler names for the dispatch table
    let handler_names: HashMap<String, usize> = program
        .handlers
        .iter()
        .enumerate()
        .map(|(i, h)| (h.name.clone(), i))
        .collect();

    // The opcode type is u8, so we need exactly 256 entries. Known opcodes
    // get their handler, everything else (including out-of-range values) gets
    // the fallback handler.
    for opcode in &program.opcode_list {
        if handler_names.contains_key(opcode.as_str()) {
            w!(out, "    .quad asm_handler_{opcode}");
        } else {
            w!(out, "    .quad asm_handler_fallback");
        }
    }
    for _ in program.opcode_list.len()..256 {
        w!(out, "    .quad asm_handler_fallback");
    }
    w!(out);

    w!(out, ".text");
    w!(out);

    emit_proc_start(&mut out, program.object_format);

    // Generate entry point
    generate_entry_point(&mut out, program, abi);

    // Generate fallback handler
    generate_fallback_handler(&mut out, program, abi);

    // Generate each handler
    for handler in &program.handlers {
        generate_handler(&mut out, handler, program, abi);
    }

    generate_exit_point(&mut out, program.object_format, abi);
    emit_proc_end(&mut out, program.object_format);
    emit_file_trailer(&mut out, program.object_format);

    out
}

fn emit_proc_start(out: &mut String, fmt: ObjectFormat) {
    // Start one unwind-covered region for the entire monolithic interpreter
    // blob, from the entry label through the last handler.
    w!(out, ".globl CSYM(asm_interpreter_entry)");
    w!(out, ".p2align 4");
    w!(out, "CSYM(asm_interpreter_entry):");
    match fmt {
        ObjectFormat::Coff => w!(out, ".seh_proc CSYM(asm_interpreter_entry)"),
        ObjectFormat::Elf | ObjectFormat::MachO => w!(out, "    .cfi_startproc"),
    }
}

fn emit_proc_end(out: &mut String, fmt: ObjectFormat) {
    // Close the interpreter's unwind frame after the last handler so any PC
    // within the handler blob can unwind back to the C++ caller.
    match fmt {
        ObjectFormat::Coff => w!(out, "    .seh_endproc"),
        ObjectFormat::Elf | ObjectFormat::MachO => w!(out, ".cfi_endproc"),
    }
    w!(out);
}

fn emit_file_trailer(out: &mut String, fmt: ObjectFormat) {
    // Mark stack as non-executable (required by Linux linker)
    if matches!(fmt, ObjectFormat::Elf) {
        w!(out, ".section .note.GNU-stack,\"\",@progbits");
    }
}

fn generate_exit_point(out: &mut String, fmt: ObjectFormat, abi: X86_64Abi) {
    // Shared exit path: restore callee-saved registers and return.
    // Keep this at the end of the proc so its CFI state does not affect
    // later handler PCs. Mach-O's assembler rejects epilogue CFI directives,
    // and x86_64 Windows unwinders recognize epilogues by instruction pattern,
    // so only emit explicit epilogue annotations for ELF.
    w!(out, ".Lexit:");
    if abi.is_win64() {
        w!(out, "    add rsp, 56");
    } else {
        w!(out, "    add rsp, 8");
    }
    w!(out, "    pop r15");
    if matches!(fmt, ObjectFormat::Elf) {
        w!(out, "    .cfi_restore r15");
    }
    w!(out, "    pop r14");
    if matches!(fmt, ObjectFormat::Elf) {
        w!(out, "    .cfi_restore r14");
    }
    w!(out, "    pop r13");
    if matches!(fmt, ObjectFormat::Elf) {
        w!(out, "    .cfi_restore r13");
    }
    w!(out, "    pop r12");
    if matches!(fmt, ObjectFormat::Elf) {
        w!(out, "    .cfi_restore r12");
    }
    if abi.is_win64() {
        w!(out, "    pop rdi");
        w!(out, "    pop rsi");
    }
    w!(out, "    pop rbx");
    if matches!(fmt, ObjectFormat::Elf) {
        w!(out, "    .cfi_restore rbx");
    }
    w!(out, "    pop rbp");
    if matches!(fmt, ObjectFormat::Elf) {
        w!(out, "    .cfi_restore rbp");
        w!(out, "    .cfi_def_cfa rsp, 8");
    }
    w!(out, "    ret");
    w!(out);
}

fn generate_entry_point(out: &mut String, program: &Program, abi: X86_64Abi) {
    if abi.is_win64() {
        generate_win64_entry_point(out, program);
    } else {
        generate_sysv_entry_point(out, program);
    }
}

fn generate_sysv_entry_point(out: &mut String, program: &Program) {
    // void asm_interpreter_entry(u8 const* bytecode, u32 entry_point, Value* values, VM* vm)
    // System V AMD64: rdi=bytecode, esi=entry_point, rdx=values, rcx=vm

    // Save callee-saved registers
    w!(out, "    push rbp");
    w!(out, "    .cfi_def_cfa_offset 16");
    w!(out, "    .cfi_offset rbp, -16");
    w!(out, "    mov rbp, rsp");
    w!(out, "    .cfi_def_cfa_register rbp");
    w!(out, "    push rbx");
    w!(out, "    .cfi_offset rbx, -24");
    w!(out, "    push r12");
    w!(out, "    .cfi_offset r12, -32");
    w!(out, "    push r13");
    w!(out, "    .cfi_offset r13, -40");
    w!(out, "    push r14");
    w!(out, "    .cfi_offset r14, -48");
    w!(out, "    push r15");
    w!(out, "    .cfi_offset r15, -56");
    // Align stack to 16 bytes (pushed rbp + 5 regs = 48 bytes, need one more for alignment)
    w!(out, "    sub rsp, 8");

    // Set up pinned registers
    // rdi=bytecode (pb), esi=entry_point (pc), rdx=values, rcx=vm
    w!(out, "    mov r14, rdi          # pb = bytecode base");
    w!(out, "    mov r13d, esi         # pc = entry_point");
    w!(out, "    mov r15, rdx          # values = values array");
    // Store VM* on the stack for C++ calls, pin exec_ctx instead
    let interp_ctx = program
        .constants
        .get("VM_RUNNING_EXECUTION_CONTEXT")
        .copied()
        .expect("VM_RUNNING_EXECUTION_CONTEXT constant required");
    w!(out, "    mov QWORD PTR {SYSV_VM_SLOT}, rcx  # save VM*");
    w!(
        out,
        "    mov rbx, QWORD PTR [rcx + {interp_ctx}]  # exec_ctx"
    );
    w!(
        out,
        "    lea r12, [rip + asm_dispatch_table]  # dispatch table"
    );

    // Dispatch to first instruction
    w!(out, "    movzx eax, BYTE PTR [r14 + r13]");
    w!(out, "    jmp [r12 + rax * 8]");
    w!(out);
}

fn generate_win64_entry_point(out: &mut String, program: &Program) {
    // void asm_interpreter_entry(u8 const* bytecode, u32 entry_point, Value* values, VM* vm)
    // Win64: rcx=bytecode, edx=entry_point, r8=values, r9=vm

    // Save callee-saved registers. Win64 also makes rsi/rdi non-volatile,
    // and the DSL exposes them as temporaries, so preserve them for the C++
    // caller. The fixed 56-byte frame below contains:
    //   [rsp, rsp + 31]    outbound Win64 shadow space for every C++ call
    //   [rbp - 80, -65]    raw-native sret slot
    //   [rbp - 64, -57]    saved VM*
    w!(out, "    push rbp");
    w!(out, "    .seh_pushreg rbp");
    w!(out, "    mov rbp, rsp");
    w!(out, "    push rbx");
    w!(out, "    .seh_pushreg rbx");
    w!(out, "    push rsi");
    w!(out, "    .seh_pushreg rsi");
    w!(out, "    push rdi");
    w!(out, "    .seh_pushreg rdi");
    w!(out, "    push r12");
    w!(out, "    .seh_pushreg r12");
    w!(out, "    push r13");
    w!(out, "    .seh_pushreg r13");
    w!(out, "    push r14");
    w!(out, "    .seh_pushreg r14");
    w!(out, "    push r15");
    w!(out, "    .seh_pushreg r15");
    w!(out, "    sub rsp, 56");
    w!(out, "    .seh_stackalloc 56");
    w!(out, "    .seh_endprologue");

    // Set up pinned registers
    w!(out, "    mov r14, rcx          # pb = bytecode base");
    w!(out, "    mov r13d, edx         # pc = entry_point");
    w!(out, "    mov r15, r8           # values = values array");
    let interp_ctx = program
        .constants
        .get("VM_RUNNING_EXECUTION_CONTEXT")
        .copied()
        .expect("VM_RUNNING_EXECUTION_CONTEXT constant required");
    w!(out, "    mov QWORD PTR {WIN64_VM_SLOT}, r9  # save VM*");
    w!(
        out,
        "    mov rbx, QWORD PTR [r9 + {interp_ctx}]  # exec_ctx"
    );
    w!(
        out,
        "    lea r12, [rip + asm_dispatch_table]  # dispatch table"
    );

    // Dispatch to first instruction
    w!(out, "    movzx eax, BYTE PTR [r14 + r13]");
    w!(out, "    jmp [r12 + rax * 8]");
    w!(out);
}

fn generate_fallback_handler(out: &mut String, program: &Program, abi: X86_64Abi) {
    // The fallback handler calls into C++ for any unhandled instruction.
    // extern "C" i64 asm_fallback_handler(VM* vm, u32 pc, u8 const* instruction);
    // Returns >= 0: new pc to dispatch to. Returns < 0: exit.
    w!(out, ".p2align 4");
    w!(out, "asm_handler_fallback:");
    emit_sync_pc_to_execution_context(out, program);
    emit_vm_pc_instruction_args(out, abi);
    w!(out, "    call CSYM(asm_fallback_handler)");
    // Check for exit (return < 0)
    w!(out, "    test rax, rax");
    w!(out, "    js .Lexit");
    // Reload exec_ctx, pb, and values (exception handling may have
    // unwound inline frames, changing the running execution context).
    emit_state_reload(out, program, abi);
    // New pc from return value
    w!(out, "    mov r13d, eax");
    emit_dispatch(out);
    w!(out);

    // Shared exit path is emitted at the end of the proc.
    w!(out);
}

/// Emit instructions to reload exec_ctx (rbx), pb (r14) and values (r15)
/// from the VM* saved on the stack. Uses rcx as scratch.
fn emit_state_reload(out: &mut String, program: &Program, abi: X86_64Abi) {
    let interp_ctx = program
        .constants
        .get("VM_RUNNING_EXECUTION_CONTEXT")
        .copied()
        .expect("VM_RUNNING_EXECUTION_CONTEXT constant required");
    let exec_executable = program
        .constants
        .get("EXECUTION_CONTEXT_EXECUTABLE")
        .copied()
        .expect("EXECUTION_CONTEXT_EXECUTABLE constant required");
    let exec_bytecode = program
        .constants
        .get("EXECUTABLE_BYTECODE_DATA")
        .copied()
        .expect("EXECUTABLE_BYTECODE_DATA constant required");
    let sizeof_execctx = program
        .constants
        .get("SIZEOF_EXECUTION_CONTEXT")
        .copied()
        .expect("SIZEOF_EXECUTION_CONTEXT constant required");
    emit_load_vm(out, "rcx", abi);
    w!(out, "    mov rbx, QWORD PTR [rcx + {interp_ctx}]");
    w!(out, "    mov rcx, QWORD PTR [rbx + {exec_executable}]");
    w!(out, "    mov r14, QWORD PTR [rcx + {exec_bytecode}]");
    w!(out, "    lea r15, [rbx + {sizeof_execctx}]");
}

fn vm_slot(abi: X86_64Abi) -> &'static str {
    if abi.is_win64() {
        WIN64_VM_SLOT
    } else {
        SYSV_VM_SLOT
    }
}

fn emit_load_vm(out: &mut String, dst: &str, abi: X86_64Abi) {
    let slot = vm_slot(abi);
    w!(out, "    mov {dst}, QWORD PTR {slot}");
}

fn emit_vm_pc_args(out: &mut String, abi: X86_64Abi) {
    if abi.is_win64() {
        emit_load_vm(out, "rcx", abi);
        w!(out, "    mov edx, r13d");
    } else {
        emit_load_vm(out, "rdi", abi);
        w!(out, "    mov esi, r13d");
    }
}

fn emit_vm_pc_instruction_args(out: &mut String, abi: X86_64Abi) {
    emit_vm_pc_args(out, abi);
    if abi.is_win64() {
        w!(out, "    lea r8, [r14 + r13]");
    } else {
        w!(out, "    lea rdx, [r14 + r13]");
    }
}

fn emit_sync_pc_to_execution_context(out: &mut String, program: &Program) {
    let program_counter = program
        .constants
        .get("EXECUTION_CONTEXT_PROGRAM_COUNTER")
        .copied()
        .expect("EXECUTION_CONTEXT_PROGRAM_COUNTER constant required");
    w!(out, "    mov DWORD PTR [rbx + {program_counter}], r13d");
}

fn emit_dispatch(out: &mut String) {
    w!(out, "    movzx eax, BYTE PTR [r14 + r13]");
    w!(out, "    jmp [r12 + rax * 8]");
}

fn emit_dispatch_with_size(out: &mut String, size: u32) {
    w!(out, "    add r13d, {size}");
    emit_dispatch(out);
}

/// Get the handler's instruction size: from explicit size= attribute, or from Bytecode.def.
fn handler_size(handler: &Handler, program: &Program) -> u32 {
    if let Some(size) = handler.size {
        return size;
    }
    let layout = program.op_layouts.get(&handler.name).unwrap_or_else(|| {
        panic!(
            "No Bytecode.def layout for handler '{}' and no explicit size",
            handler.name
        )
    });
    layout.size.unwrap_or_else(|| {
        panic!(
            "Handler '{}' is variable-length; use dispatch_variable instead of bare dispatch",
            handler.name
        )
    }) as u32
}

fn generate_handler(out: &mut String, handler: &Handler, program: &Program, abi: X86_64Abi) {
    w!(out, ".p2align 4");
    w!(out, "asm_handler_{}:", handler.name);

    let mut state = HandlerState::new();

    if handler_uses_named_temps(handler, program) {
        // The allocator pre-expands macros and rewrites named temps to
        // physical registers, so emit_instruction iterates a flat list and
        // never re-enters the macro-expansion arm. The shared
        // unique_counter is consumed for canonicalize_nan fixup labels
        // continuing after the macro-id range used by the allocator.
        let mut counter = state.unique_counter;
        let flat = flatten_and_allocate(handler, program, Arch::X86_64, &mut counter)
            .unwrap_or_else(|err| {
                panic!(
                    "register allocation failed for handler '{}': {}",
                    err.handler, err.message
                )
            });
        state.unique_counter = counter;
        for insn in &flat {
            emit_instruction(out, insn, handler, program, &mut state, abi);
        }
    } else {
        // Existing path: expand macros recursively in emit_instruction.
        for insn in &handler.instructions {
            emit_instruction(out, insn, handler, program, &mut state, abi);
        }
    }

    // Emit cold fixup blocks after the main handler body
    if !state.cold_blocks.is_empty() {
        out.push_str(&state.cold_blocks);
    }

    w!(out);
}

fn resolve_op(op: &Operand, handler: &Handler, program: &Program) -> String {
    match op {
        Operand::Register(name) => {
            resolve_register(name, Arch::X86_64).unwrap_or_else(|| name.clone())
        }
        Operand::Immediate(val) => format!("{val}"),
        Operand::Constant(name) => {
            if let Some(val) = program.constants.get(name) {
                format!("{val}")
            } else {
                name.clone()
            }
        }
        Operand::Memory { base, index, scale } => {
            let base_r = resolve_register(base, Arch::X86_64).unwrap_or_else(|| base.clone());
            match (index, scale) {
                (Some(idx), Some(sc)) => {
                    let idx_r = resolve_register(idx, Arch::X86_64).unwrap_or_else(|| idx.clone());
                    if let Some(field_offset) = resolve_field_ref(sc, handler, program) {
                        format!("[{base_r} + {idx_r} + {field_offset}]")
                    } else {
                        let scale = program
                            .constants
                            .get(sc.as_str())
                            .copied()
                            .unwrap_or_else(|| sc.parse().expect("invalid scale value"));
                        format!("[{base_r} + {idx_r} * {scale}]")
                    }
                }
                (Some(idx), None) => {
                    if let Some(offset) = resolve_field_ref(idx, handler, program) {
                        format!("[{base_r} + {offset}]")
                    } else if let Some(offset) = program.constants.get(idx.as_str()) {
                        format!("[{base_r} + {offset}]")
                    } else if let Ok(offset) = idx.parse::<i64>() {
                        format!("[{base_r} + {offset}]")
                    } else {
                        let idx_r =
                            resolve_register(idx, Arch::X86_64).unwrap_or_else(|| idx.clone());
                        format!("[{base_r} + {idx_r}]")
                    }
                }
                (None, _) => format!("[{base_r}]"),
            }
        }
        Operand::Label(name) => name.clone(),
        Operand::FieldRef(name) => {
            let layout = program
                .op_layouts
                .get(&handler.name)
                .unwrap_or_else(|| panic!("No Bytecode.def layout for handler '{}'", handler.name));
            let offset = layout.field_offsets.get(name).unwrap_or_else(|| {
                panic!(
                    "No field '{}' in Bytecode.def for opcode '{}'",
                    name, handler.name
                )
            });
            format!("{offset}")
        }
    }
}

fn format_x86_memory_operand(mem: &crate::shared::ResolvedMemoryOperand) -> String {
    match &mem.index {
        ResolvedMemoryIndex::None => format!("[{}]", mem.base),
        ResolvedMemoryIndex::Imm(offset) => format!("[{} + {}]", mem.base, offset),
        ResolvedMemoryIndex::Reg(index) => format!("[{} + {}]", mem.base, index),
        ResolvedMemoryIndex::RegScale(index, scale) => {
            format!("[{} + {} * {}]", mem.base, index, scale)
        }
        ResolvedMemoryIndex::RegImm(index, 0) => format!("[{} + {}]", mem.base, index),
        ResolvedMemoryIndex::RegImm(index, offset) => {
            format!("[{} + {} + {}]", mem.base, index, offset)
        }
    }
}

fn resolve_pair_memory_op(op: &Operand, handler: &Handler, program: &Program) -> String {
    let mem = resolve_memory_operand(op, handler, program, Arch::X86_64)
        .expect("x86_64 pair memory operands should always resolve");
    format_x86_memory_operand(&mem)
}

/// True when writing `dst1` would clobber a register that the second load
/// of a pair still needs (the base or index of either memory operand).
/// `mem1` / `mem2` are the already-formatted x86 memory text (e.g.
/// `[rcx + r10 * 8]`); we check whether the formatted text mentions the
/// destination register name as a whole word.
fn pair_dst_aliases_address(dst1: &str, mem1: &str, mem2: &str) -> bool {
    let mention_in = |s: &str, reg: &str| -> bool {
        s.split(|c: char| !c.is_ascii_alphanumeric() && c != '_')
            .any(|tok| tok == reg)
    };
    mention_in(mem1, dst1) || mention_in(mem2, dst1)
}

fn emit_assert_bits(
    out: &mut String,
    insn: &AsmInstruction,
    handler: &Handler,
    program: &Program,
    state: &mut HandlerState,
    want_set: bool,
) {
    if insn.operands.len() != 2 {
        return;
    }

    let a = resolve_op(&insn.operands[0], handler, program);
    let ok_label = format!(".Lasm_{}.assert_ok_{}", handler.name, state.unique_counter);
    state.unique_counter += 1;
    let pass_cc = if want_set { "jnz" } else { "jz" };
    let pass_bit_cc = if want_set { "jc" } else { "jnc" };

    if let Some(mask) = get_immediate_value(&insn.operands[1], program) {
        let mask = mask as u64;
        if mask >> 32 == 0 {
            let a32 = to_32bit_reg(&a);
            w!(out, "    test {a32}, {mask}");
            w!(out, "    {pass_cc} {ok_label}");
        } else if mask.is_power_of_two() {
            w!(out, "    bt {a}, {}", mask.trailing_zeros());
            w!(out, "    {pass_bit_cc} {ok_label}");
        } else {
            panic!(
                "x86_64 assert_bits requires a low-32-bit mask or a single-bit 64-bit mask in handler '{}', got {mask:#x}",
                handler.name
            );
        }
    } else {
        let mask = resolve_op(&insn.operands[1], handler, program);
        w!(out, "    test {a}, {mask}");
        w!(out, "    {pass_cc} {ok_label}");
    }
    w!(out, "    ud2");
    w!(out, "{ok_label}:");
}

fn emit_instruction(
    out: &mut String,
    insn: &AsmInstruction,
    handler: &Handler,
    program: &Program,
    state: &mut HandlerState,
    abi: X86_64Abi,
) {
    let m = &insn.mnemonic;

    match m.as_str() {
        "label" => {
            if let Some(Operand::Label(name)) = insn.operands.first() {
                // Local labels get handler prefix
                if name.starts_with('.') {
                    w!(out, ".Lasm_{}{name}:", handler.name);
                } else {
                    w!(out, "{name}:");
                }
            }
        }

        // Named-temporary declarations: `temp foo, bar` introduces GPR
        // temps named `foo` and `bar`; `ftemp baz` introduces an FPR temp.
        // The register allocator processes these before codegen runs --
        // by the time we get here they are pure annotations and emit
        // nothing.
        "temp" | "ftemp" => {}

        "assert_eq" | "assert_ne" if program.enable_assertions => {
            if insn.operands.len() == 2 {
                let a = resolve_op(&insn.operands[0], handler, program);
                let b = resolve_op(&insn.operands[1], handler, program);
                let ok_label = format!(".Lasm_{}.assert_ok_{}", handler.name, state.unique_counter);
                state.unique_counter += 1;
                let cc = if m == "assert_eq" { "je" } else { "jne" };
                w!(out, "    cmp {a}, {b}");
                w!(out, "    {cc} {ok_label}");
                w!(out, "    ud2");
                w!(out, "{ok_label}:");
            }
        }

        "assert_lt_unsigned" | "assert_ge_unsigned" if program.enable_assertions => {
            if insn.operands.len() == 2 {
                let a = resolve_op(&insn.operands[0], handler, program);
                let b = resolve_op(&insn.operands[1], handler, program);
                let ok_label = format!(".Lasm_{}.assert_ok_{}", handler.name, state.unique_counter);
                state.unique_counter += 1;
                let cc = if m == "assert_lt_unsigned" { "jb" } else { "jae" };
                w!(out, "    cmp {a}, {b}");
                w!(out, "    {cc} {ok_label}");
                w!(out, "    ud2");
                w!(out, "{ok_label}:");
            }
        }

        "assert_zero" | "assert_nonzero" if program.enable_assertions => {
            if insn.operands.len() == 1 {
                let a = resolve_op(&insn.operands[0], handler, program);
                let ok_label = format!(".Lasm_{}.assert_ok_{}", handler.name, state.unique_counter);
                state.unique_counter += 1;
                let cc = if m == "assert_zero" { "jz" } else { "jnz" };
                w!(out, "    test {a}, {a}");
                w!(out, "    {cc} {ok_label}");
                w!(out, "    ud2");
                w!(out, "{ok_label}:");
            }
        }

        "assert_bits_set" | "assert_bits_clear" if program.enable_assertions => {
            emit_assert_bits(out, insn, handler, program, state, m == "assert_bits_set");
        }

        "assert_tag" | "assert_not_tag" if program.enable_assertions => {
            if insn.operands.len() == 2 {
                let value = resolve_op(&insn.operands[0], handler, program);
                let ok_label = format!(".Lasm_{}.assert_ok_{}", handler.name, state.unique_counter);
                state.unique_counter += 1;
                let cc = if m == "assert_tag" { "je" } else { "jne" };
                w!(out, "    mov r11, {value}");
                w!(out, "    shr r11, 48");
                if let Some(tag) = get_immediate_value(&insn.operands[1], program) {
                    w!(out, "    cmp r11, {tag}");
                } else {
                    let tag = resolve_op(&insn.operands[1], handler, program);
                    w!(out, "    cmp r11, {tag}");
                }
                w!(out, "    {cc} {ok_label}");
                w!(out, "    ud2");
                w!(out, "{ok_label}:");
            }
        }

        "assert_eq" | "assert_ne" | "assert_lt_unsigned" | "assert_ge_unsigned" | "assert_zero"
        | "assert_nonzero" | "assert_bits_set" | "assert_bits_clear" | "assert_tag"
        | "assert_not_tag" => {}

        // Macro invocations
        _ if program.macros.contains_key(m) => {
            let mac = program.macros[m].clone();
            // Build parameter substitution map
            let mut param_map: HashMap<String, String> = HashMap::new();
            for (i, param) in mac.params.iter().enumerate() {
                if let Some(op) = insn.operands.get(i) {
                    param_map.insert(param.clone(), resolve_op(op, handler, program));
                }
            }
            // Uniquify local labels so the same macro can be used multiple
            // times in one handler without label collisions.
            let id = state.unique_counter;
            state.unique_counter += 1;
            let body = uniquify_macro_labels(&mac.body, id);
            // Expand macro body
            for body_insn in &body {
                let expanded = substitute_macro(body_insn, &param_map);
                emit_instruction(out, &expanded, handler, program, state, abi);
            }
        }

        // reload_exec_ctx: reload the pinned exec_ctx register from VM*
        "reload_exec_ctx" => {
            let interp_ctx = program
                .constants
                .get("VM_RUNNING_EXECUTION_CONTEXT")
                .copied()
                .expect("VM_RUNNING_EXECUTION_CONTEXT constant required");
            emit_load_vm(out, "rcx", abi);
            w!(out, "    mov rbx, QWORD PTR [rcx + {interp_ctx}]");
        }

        // load_vm dst: load the hidden VM* from the stack frame.
        "load_vm" => {
            if let Some(op) = insn.operands.first() {
                let dst = resolve_op(op, handler, program);
                emit_load_vm(out, &dst, abi);
            }
        }

        // inc32_mem [base, offset]: increment a 32-bit memory slot by 1.
        "inc32_mem" => {
            if let Some(op) = insn.operands.first() {
                let mem = resolve_op(op, handler, program);
                w!(out, "    add DWORD PTR {mem}, 1");
            }
        }

        // dispatch_variable pseudo-instruction: advance pc by value in register and dispatch
        "dispatch_variable" => {
            if let Some(op) = insn.operands.first() {
                let reg = resolve_op(op, handler, program);
                let reg32 = to_32bit_reg(&reg);
                w!(out, "    add r13d, {reg32}");
                emit_dispatch(out);
            }
        }

        // dispatch_next: advance pc by the handler's instruction size and dispatch
        "dispatch_next" => {
            let size = handler_size(handler, program);
            emit_dispatch_with_size(out, size);
        }

        // call_slow_path pseudo-instruction
        // Calls a slow path C function: i64 func(VM*, u32 pc, Op::Foo const* instruction)
        // Returns >= 0: new pc to dispatch to. Returns < 0: exit.
        // This is TERMINAL: it dispatches after return, control doesn't come back.
        // After the call, pb and values are reloaded from the running execution
        // context, since exception handling may have unwound inline frames.
        "call_slow_path" => {
            if let Some(Operand::Register(func_name)) = insn.operands.first() {
                emit_sync_pc_to_execution_context(out, program);
                emit_vm_pc_instruction_args(out, abi);
                w!(out, "    call CSYM({func_name})");
                // Check for exit (return < 0)
                w!(out, "    test rax, rax");
                w!(out, "    js .Lexit");
                // Reload pb and values from the running execution context.
                // This is necessary because exception handling may unwind
                // inline frames, changing the current executable and values.
                emit_state_reload(out, program, abi);
                // Update pc from return value and dispatch
                w!(out, "    mov r13d, eax");
                emit_dispatch(out);
            }
        }

        // call_helper pseudo-instruction
        // Calls a helper C function: u64 func(u64 value)
        // The value from t1 (rcx) is passed as the first argument.
        // Result is in rax (t0). NOT terminal: handler continues after.
        "call_helper" => {
            if let Some(Operand::Register(func_name)) = insn.operands.first() {
                if !abi.is_win64() {
                    w!(out, "    mov rdi, rcx");
                }
                w!(out, "    call CSYM({func_name})");
            }
        }

        // call_interp pseudo-instruction
        // Calls a C++ helper: i64 func(VM*, u32 pc, Op::Foo const* instruction)
        // Same args as call_slow_path, but NON-TERMINAL: result in rax (t0),
        // handler continues after.
        "call_interp" => {
            if let Some(Operand::Register(func_name)) = insn.operands.first() {
                emit_vm_pc_instruction_args(out, abi);
                w!(out, "    call CSYM({func_name})");
            }
        }

        // call_raw_native pseudo-instruction
        // Indirectly calls ThrowCompletionOr<Value> (*)(VM&) from a register.
        // Result payload lands in rax (t0) and the Variant index is normalized
        // into rcx (t1).
        //
        // x86_64 ABI difference when returning ThrowCompletionOr<Value>:
        // - SysV (ELF: Linux/BSD): returned in rax/rdx.
        // - Darwin (Mach-O): returned via sret (hidden pointer in rdi).
        // - Win64 (COFF): returned via sret (hidden pointer in rcx).
        //
        // Codegen is split based on the object format to match the ABI.
        "call_raw_native" => {
            if let Some(op) = insn.operands.first() {
                let func = resolve_op(op, handler, program);
                w!(out, "    mov r11, {func}");
                if abi.is_win64() {
                    // sret: hidden pointer in rcx, VM& in rdx. The return
                    // slot lives in the fixed frame so stack unwinding remains
                    // valid while the native function is running.
                    w!(out, "    lea rcx, {WIN64_RAW_NATIVE_RETURN_SLOT}");
                    emit_load_vm(out, "rdx", abi);
                    w!(out, "    call r11");
                    w!(out, "    mov rax, QWORD PTR {WIN64_RAW_NATIVE_RETURN_SLOT}");
                    w!(out, "    mov rdx, QWORD PTR {WIN64_RAW_NATIVE_VARIANT_SLOT}");
                } else if matches!(program.object_format, ObjectFormat::MachO) {
                    // sret: hidden pointer in rdi, VM& in rsi. Reserve a 16-byte
                    // return slot on the stack; rsp is already 16-byte aligned
                    // at this point, so sub rsp, 16 preserves the alignment the
                    // call requires.
                    w!(out, "    sub rsp, 16");
                    w!(out, "    mov rdi, rsp");
                    emit_load_vm(out, "rsi", abi);
                    w!(out, "    call r11");
                    w!(out, "    mov rax, QWORD PTR [rsp]");
                    w!(out, "    mov rdx, QWORD PTR [rsp + 8]");
                    w!(out, "    add rsp, 16");
                } else {
                    // Register return: VM& in rdi, result lands in rax (payload)
                    // and rdx (tag).
                    emit_load_vm(out, "rdi", abi);
                    w!(out, "    call r11");
                }
                w!(out, "    mov rcx, rdx");
            }
        }

        // double_to_int32 dst, src_fpr, fail_label
        // Truncate double to int32 with strict round-trip check.
        // Fails if fractional, out of i32 range, or NaN.
        // Clobbers rcx (t1) and xmm3 (ft3).
        "double_to_int32" => {
            if insn.operands.len() >= 3 {
                let dst = resolve_op(&insn.operands[0], handler, program);
                let src = resolve_op(&insn.operands[1], handler, program);
                let fail = resolve_label(&insn.operands[2], handler);
                // Truncate to i64 first, check for cvttsd2si overflow sentinel
                w!(out, "    cvttsd2si {dst}, {src}");
                w!(out, "    mov rcx, 0x8000000000000000");
                w!(out, "    cmp {dst}, rcx");
                w!(out, "    je {fail}");
                // Check it fits in i32
                w!(out, "    movsxd rcx, {}", to_32bit_reg(&dst));
                w!(out, "    cmp {dst}, rcx");
                w!(out, "    jne {fail}");
                // Round-trip: convert back and compare
                w!(out, "    cvtsi2sd xmm3, {dst}");
                w!(out, "    ucomisd {src}, xmm3");
                w!(out, "    jne {fail}");
            }
        }

        // js_to_int32 dst, src_fpr, fail_label
        // Truncate double to int64. cvttsd2si returns 0x8000000000000000 on
        // overflow/NaN, which we detect and branch to the slow path.
        // On success, zero-extend the low 32 bits so callers can use
        // box_int32_clean directly.
        // Clobbers rcx (t1).
        "js_to_int32" => {
            if insn.operands.len() >= 3 {
                let dst = resolve_op(&insn.operands[0], handler, program);
                let dst32 = to_32bit_reg(&dst);
                let src = resolve_op(&insn.operands[1], handler, program);
                let fail = resolve_label(&insn.operands[2], handler);
                w!(out, "    cvttsd2si {dst}, {src}");
                w!(out, "    mov rcx, 0x8000000000000000");
                w!(out, "    cmp {dst}, rcx");
                w!(out, "    je {fail}");
                w!(out, "    mov {dst32}, {dst32}");
            }
        }

        // divmod quot, rem, dividend, divisor
        // Signed integer divide: quot = dividend / divisor, rem = dividend % divisor.
        "divmod" => {
            if insn.operands.len() == 4 {
                let dividend = resolve_op(&insn.operands[2], handler, program);
                let divisor = resolve_op(&insn.operands[3], handler, program);
                // x86 idiv divides rdx:rax by operand.
                // Result: quotient in rax, remainder in rdx.
                w!(out, "    mov rax, {dividend}");
                w!(out, "    cqo");
                w!(out, "    idiv {divisor}");
                // rax (t0) = quotient, rdx (t2) = remainder -- matches DSL convention
            }
        }

        // fp_floor dst, src - Round toward negative infinity
        "fp_floor" => {
            if insn.operands.len() == 2 {
                let dst = resolve_op(&insn.operands[0], handler, program);
                let src = resolve_op(&insn.operands[1], handler, program);
                w!(out, "    roundsd {dst}, {src}, 1");
            }
        }

        // fp_ceil dst, src - Round toward positive infinity
        "fp_ceil" => {
            if insn.operands.len() == 2 {
                let dst = resolve_op(&insn.operands[0], handler, program);
                let src = resolve_op(&insn.operands[1], handler, program);
                w!(out, "    roundsd {dst}, {src}, 2");
            }
        }

        // toggle_bit dst, bit - Toggle (flip) a single bit: dst ^= (1 << bit)
        "toggle_bit" => {
            if insn.operands.len() == 2 {
                let dst = resolve_op(&insn.operands[0], handler, program);
                let bit = resolve_op(&insn.operands[1], handler, program);
                w!(out, "    btc {dst}, {bit}");
            }
        }

        // clear_bit dst, bit - Clear a single bit: dst &= ~(1 << bit)
        "clear_bit" => {
            if insn.operands.len() == 2 {
                let dst = resolve_op(&insn.operands[0], handler, program);
                let bit = resolve_op(&insn.operands[1], handler, program);
                w!(out, "    btr {dst}, {bit}");
            }
        }

        // Architecture-neutral FP arithmetic
        "fp_add" | "fp_sub" | "fp_mul" | "fp_div" | "fp_sqrt" => {
            let x86_mnemonic = match m.as_str() {
                "fp_add" => "addsd",
                "fp_sub" => "subsd",
                "fp_mul" => "mulsd",
                "fp_div" => "divsd",
                "fp_sqrt" => "sqrtsd",
                _ => unreachable!(),
            };
            let ops: Vec<String> = insn
                .operands
                .iter()
                .map(|o| resolve_op(o, handler, program))
                .collect();
            w!(out, "    {x86_mnemonic} {}", ops.join(", "));
        }

        // fp_mov dst, src - Move between int and FP registers
        "fp_mov" => {
            let ops: Vec<String> = insn
                .operands
                .iter()
                .map(|o| resolve_op(o, handler, program))
                .collect();
            w!(out, "    movq {}", ops.join(", "));
        }

        // int_to_double fpr, gpr - Convert signed int64 to double
        "int_to_double" => {
            let ops: Vec<String> = insn
                .operands
                .iter()
                .map(|o| resolve_op(o, handler, program))
                .collect();
            w!(out, "    cvtsi2sd {}", ops.join(", "));
        }

        "float_to_double" => {
            let ops: Vec<String> = insn
                .operands
                .iter()
                .map(|o| resolve_op(o, handler, program))
                .collect();
            w!(out, "    cvtss2sd {}", ops.join(", "));
        }

        "double_to_float" => {
            let ops: Vec<String> = insn
                .operands
                .iter()
                .map(|o| resolve_op(o, handler, program))
                .collect();
            w!(out, "    cvtsd2ss {}", ops.join(", "));
        }

        // canonicalize_nan dst_gpr, src_fpr
        // If src is NaN, write CANON_NAN_BITS to dst. Otherwise bitwise-copy src to dst.
        // Uses a cold fixup block to keep the movabs off the hot path, since NaN
        // results are extremely rare. The hot path is just: movq + ucomisd + jp.
        "canonicalize_nan" => {
            if insn.operands.len() == 2 {
                let dst = resolve_op(&insn.operands[0], handler, program);
                let src = resolve_op(&insn.operands[1], handler, program);
                let canon = program
                    .constants
                    .get("CANON_NAN_BITS")
                    .copied()
                    .expect("CANON_NAN_BITS constant required for canonicalize_nan");
                let id = state.unique_counter;
                state.unique_counter += 1;
                let fixup_label = format!(".Lasm_{}.canon_nan_{id}", handler.name);
                let ret_label = format!(".Lasm_{}.canon_nan_{id}_ret", handler.name);
                w!(out, "    movq {dst}, {src}");
                w!(out, "    ucomisd {src}, {src}");
                w!(out, "    jp {fixup_label}");
                w!(out, "{ret_label}:");
                // Cold fixup block: only reached when result is NaN
                w!(state.cold_blocks, "{fixup_label}:");
                w!(state.cold_blocks, "    movabs {dst}, {canon}");
                w!(state.cold_blocks, "    jmp {ret_label}");
            }
        }

        // 32-bit arithmetic with overflow detection.
        // These perform the operation on the low 32 bits and branch on signed overflow.
        // On x86_64, writing a 32-bit register zeros the upper 32 bits.
        "add32_overflow" | "sub32_overflow" | "mul32_overflow" => {
            if insn.operands.len() == 3 {
                let dst = resolve_op(&insn.operands[0], handler, program);
                let dst32 = to_32bit_reg(&dst);
                let label = resolve_label(&insn.operands[2], handler);
                let x86_op = match m.as_str() {
                    "add32_overflow" => "add",
                    "sub32_overflow" => "sub",
                    "mul32_overflow" => "imul",
                    _ => unreachable!(),
                };
                if let Some(val) = get_immediate_value(&insn.operands[1], program) {
                    w!(out, "    {x86_op} {dst32}, {val}");
                } else {
                    let src = resolve_op(&insn.operands[1], handler, program);
                    let src32 = to_32bit_reg(&src);
                    w!(out, "    {x86_op} {dst32}, {src32}");
                }
                w!(out, "    jo {label}");
            }
        }

        // neg32_overflow dst, fail_label
        // Negate the low 32 bits, branch on overflow (INT32_MIN).
        "neg32_overflow" => {
            if insn.operands.len() == 2 {
                let dst = resolve_op(&insn.operands[0], handler, program);
                let dst32 = to_32bit_reg(&dst);
                let label = resolve_label(&insn.operands[1], handler);
                w!(out, "    neg {dst32}");
                w!(out, "    jo {label}");
            }
        }

        // not32 dst - Bitwise NOT on the low 32 bits, zeroing the upper 32.
        "not32" => {
            if insn.operands.len() == 1 {
                let dst = resolve_op(&insn.operands[0], handler, program);
                let dst32 = to_32bit_reg(&dst);
                w!(out, "    not {dst32}");
            }
        }

        // mul: maps to x86 imul (2-operand: dst *= src, or 3-operand: dst = src * imm)
        "mul" => {
            let ops: Vec<String> = insn
                .operands
                .iter()
                .map(|o| resolve_op(o, handler, program))
                .collect();
            w!(out, "    imul {}", ops.join(", "));
        }

        "exit" => {
            w!(out, "    jmp .Lexit");
        }

        // load64 dst_reg, [base, offset] - Load 64-bit value from memory
        "load64" => {
            if insn.operands.len() >= 2 {
                let dst = resolve_op(&insn.operands[0], handler, program);
                let mem = resolve_op(&insn.operands[1], handler, program);
                w!(out, "    mov {dst}, QWORD PTR {mem}");
            }
        }

        // loadf32 dst_fpr, [base, offset] - Load 32-bit float from memory
        "loadf32" => {
            if insn.operands.len() >= 2 {
                let dst = resolve_op(&insn.operands[0], handler, program);
                let mem = resolve_op(&insn.operands[1], handler, program);
                w!(out, "    movss {dst}, DWORD PTR {mem}");
            }
        }

        // load32 dst_reg, [base, offset] - Load 32-bit value (zero-extended to 64-bit)
        "load32" => {
            if insn.operands.len() >= 2 {
                let dst = resolve_op(&insn.operands[0], handler, program);
                let dst32 = to_32bit_reg(&dst);
                let mem = resolve_op(&insn.operands[1], handler, program);
                w!(out, "    mov {dst32}, DWORD PTR {mem}");
            }
        }

        "load_pair64" => {
            if insn.operands.len() >= 4 {
                resolve_adjacent_memory_pair(
                    &insn.operands[2],
                    &insn.operands[3],
                    handler,
                    program,
                    Arch::X86_64,
                    8,
                )
                .unwrap_or_else(|error| {
                    panic!("invalid load_pair64 in handler '{}': {error}", handler.name)
                });

                let dst1 = resolve_op(&insn.operands[0], handler, program);
                let dst2 = resolve_op(&insn.operands[1], handler, program);
                let mem1 = resolve_pair_memory_op(&insn.operands[2], handler, program);
                let mem2 = resolve_pair_memory_op(&insn.operands[3], handler, program);
                // x86 has no real "load pair", so this is two movs. If dst1
                // is the same physical register as the address's base or
                // index, writing dst1 first clobbers the address before the
                // second load can use it; emit dst2 first in that case.
                if pair_dst_aliases_address(&dst1, &mem1, &mem2) {
                    w!(out, "    mov {dst2}, QWORD PTR {mem2}");
                    w!(out, "    mov {dst1}, QWORD PTR {mem1}");
                } else {
                    w!(out, "    mov {dst1}, QWORD PTR {mem1}");
                    w!(out, "    mov {dst2}, QWORD PTR {mem2}");
                }
            }
        }

        "load_pair32" => {
            if insn.operands.len() >= 4 {
                resolve_adjacent_memory_pair(
                    &insn.operands[2],
                    &insn.operands[3],
                    handler,
                    program,
                    Arch::X86_64,
                    4,
                )
                .unwrap_or_else(|error| {
                    panic!("invalid load_pair32 in handler '{}': {error}", handler.name)
                });

                let dst1 = resolve_op(&insn.operands[0], handler, program);
                let dst2 = resolve_op(&insn.operands[1], handler, program);
                let mem1 = resolve_pair_memory_op(&insn.operands[2], handler, program);
                let mem2 = resolve_pair_memory_op(&insn.operands[3], handler, program);
                if pair_dst_aliases_address(&dst1, &mem1, &mem2) {
                    w!(out, "    mov {}, DWORD PTR {mem2}", to_32bit_reg(&dst2));
                    w!(out, "    mov {}, DWORD PTR {mem1}", to_32bit_reg(&dst1));
                } else {
                    w!(out, "    mov {}, DWORD PTR {mem1}", to_32bit_reg(&dst1));
                    w!(out, "    mov {}, DWORD PTR {mem2}", to_32bit_reg(&dst2));
                }
            }
        }

        // load8 dst_reg, [base, offset] - Load 8-bit value (zero-extended to 64-bit)
        "load8" => {
            if insn.operands.len() >= 2 {
                let dst = resolve_op(&insn.operands[0], handler, program);
                let dst32 = to_32bit_reg(&dst);
                let mem = resolve_op(&insn.operands[1], handler, program);
                w!(out, "    movzx {dst32}, BYTE PTR {mem}");
            }
        }

        // load16 dst_reg, [base, offset] - Load 16-bit value (zero-extended to 64-bit)
        "load16" => {
            if insn.operands.len() >= 2 {
                let dst = resolve_op(&insn.operands[0], handler, program);
                let dst32 = to_32bit_reg(&dst);
                let mem = resolve_op(&insn.operands[1], handler, program);
                w!(out, "    movzx {dst32}, WORD PTR {mem}");
            }
        }

        // load8s dst_reg, [base, offset] - Load 8-bit value (sign-extended to 64-bit)
        "load8s" => {
            if insn.operands.len() >= 2 {
                let dst = resolve_op(&insn.operands[0], handler, program);
                let dst32 = to_32bit_reg(&dst);
                let mem = resolve_op(&insn.operands[1], handler, program);
                w!(out, "    movsx {dst32}, BYTE PTR {mem}");
            }
        }

        // load16s dst_reg, [base, offset] - Load 16-bit value (sign-extended to 64-bit)
        "load16s" => {
            if insn.operands.len() >= 2 {
                let dst = resolve_op(&insn.operands[0], handler, program);
                let dst32 = to_32bit_reg(&dst);
                let mem = resolve_op(&insn.operands[1], handler, program);
                w!(out, "    movsx {dst32}, WORD PTR {mem}");
            }
        }

        // storef32 [base, offset], src_fpr - Store 32-bit float to memory
        "storef32" => {
            if insn.operands.len() >= 2 {
                let mem = resolve_op(&insn.operands[0], handler, program);
                let src = resolve_op(&insn.operands[1], handler, program);
                w!(out, "    movss DWORD PTR {mem}, {src}");
            }
        }

        // store8 [base, offset], src_reg - Store low 8 bits to memory
        "store8" => {
            if insn.operands.len() >= 2 {
                let mem = resolve_op(&insn.operands[0], handler, program);
                let src = resolve_op(&insn.operands[1], handler, program);
                let src8 = to_8bit_reg(&src);
                w!(out, "    mov BYTE PTR {mem}, {src8}");
            }
        }

        // store16 [base, offset], src_reg - Store low 16 bits to memory
        "store16" => {
            if insn.operands.len() >= 2 {
                let mem = resolve_op(&insn.operands[0], handler, program);
                let src = resolve_op(&insn.operands[1], handler, program);
                let src16 = to_16bit_reg(&src);
                w!(out, "    mov WORD PTR {mem}, {src16}");
            }
        }

        // store32 [base, offset], src_reg - Store low 32 bits to memory
        "store32" => {
            if insn.operands.len() >= 2 {
                let mem = resolve_op(&insn.operands[0], handler, program);
                let src = resolve_op(&insn.operands[1], handler, program);
                let src32 = to_32bit_reg(&src);
                w!(out, "    mov DWORD PTR {mem}, {src32}");
            }
        }

        // store64 [base, offset], src_reg - Store 64-bit value to memory
        "store64" => {
            if insn.operands.len() >= 2 {
                let mem = resolve_op(&insn.operands[0], handler, program);
                let src = resolve_op(&insn.operands[1], handler, program);
                w!(out, "    mov QWORD PTR {mem}, {src}");
            }
        }

        "store_pair64" => {
            if insn.operands.len() >= 4 {
                resolve_adjacent_memory_pair(
                    &insn.operands[0],
                    &insn.operands[1],
                    handler,
                    program,
                    Arch::X86_64,
                    8,
                )
                .unwrap_or_else(|error| {
                    panic!(
                        "invalid store_pair64 in handler '{}': {error}",
                        handler.name
                    )
                });

                let mem1 = resolve_pair_memory_op(&insn.operands[0], handler, program);
                let mem2 = resolve_pair_memory_op(&insn.operands[1], handler, program);
                let src1 = resolve_op(&insn.operands[2], handler, program);
                let src2 = resolve_op(&insn.operands[3], handler, program);
                w!(out, "    mov QWORD PTR {mem1}, {src1}");
                w!(out, "    mov QWORD PTR {mem2}, {src2}");
            }
        }

        "store_pair32" => {
            if insn.operands.len() >= 4 {
                resolve_adjacent_memory_pair(
                    &insn.operands[0],
                    &insn.operands[1],
                    handler,
                    program,
                    Arch::X86_64,
                    4,
                )
                .unwrap_or_else(|error| {
                    panic!(
                        "invalid store_pair32 in handler '{}': {error}",
                        handler.name
                    )
                });

                let mem1 = resolve_pair_memory_op(&insn.operands[0], handler, program);
                let mem2 = resolve_pair_memory_op(&insn.operands[1], handler, program);
                let src1 = resolve_op(&insn.operands[2], handler, program);
                let src2 = resolve_op(&insn.operands[3], handler, program);
                w!(out, "    mov DWORD PTR {mem1}, {}", to_32bit_reg(&src1));
                w!(out, "    mov DWORD PTR {mem2}, {}", to_32bit_reg(&src2));
            }
        }

        // load_operand dst_reg, byte_offset
        "load_operand" => {
            if insn.operands.len() >= 2 {
                let dst = resolve_op(&insn.operands[0], handler, program);
                let offset = resolve_op(&insn.operands[1], handler, program);
                // Load the u32 operand raw value from bytecode
                w!(out, "    mov eax, DWORD PTR [r14 + r13 + {offset}]");
                // Use it as index into values array (scaled by 8)
                w!(out, "    mov {dst}, QWORD PTR [r15 + rax * 8]");
            }
        }

        // store_operand byte_offset, src_reg
        "store_operand" => {
            if insn.operands.len() >= 2 {
                let offset = resolve_op(&insn.operands[0], handler, program);
                let src = resolve_op(&insn.operands[1], handler, program);
                // Load the u32 operand raw value from bytecode without
                // clobbering the source value register.
                if src == "r11" {
                    w!(out, "    mov eax, DWORD PTR [r14 + r13 + {offset}]");
                    w!(out, "    mov QWORD PTR [r15 + rax * 8], {src}");
                } else {
                    w!(out, "    mov r11d, DWORD PTR [r14 + r13 + {offset}]");
                    // Store value into values array
                    w!(out, "    mov QWORD PTR [r15 + r11 * 8], {src}");
                }
            }
        }

        // load_label dst_reg, byte_offset (loads a Label = u32 address)
        "load_label" => {
            if insn.operands.len() >= 2 {
                let dst = resolve_op(&insn.operands[0], handler, program);
                let dst32 = to_32bit_reg(&dst);
                let offset = resolve_op(&insn.operands[1], handler, program);
                w!(out, "    mov {dst32}, DWORD PTR [r14 + r13 + {offset}]");
            }
        }

        // goto_handler label_reg - set pc and dispatch
        "goto_handler" => {
            if let Some(op) = insn.operands.first() {
                let reg = resolve_op(op, handler, program);
                let reg32 = to_32bit_reg(&reg);
                w!(out, "    mov r13d, {reg32}");
                emit_dispatch(out);
            }
        }

        // mov with large immediate needs movabs on x86_64.
        // Values in 0..0x7FFFFFFF fit in sign-extended imm32 (mov r64, imm32).
        // Values in 0x80000000..0xFFFFFFFF use mov r32, imm32 (zero-extends to 64-bit).
        // Values outside both ranges need movabs r64, imm64 (10 bytes).
        "mov" => {
            if insn.operands.len() == 2 {
                let dst = resolve_op(&insn.operands[0], handler, program);
                let src = resolve_op(&insn.operands[1], handler, program);
                if let Some(val) = get_immediate_value(&insn.operands[1], program) {
                    let uval = val as u64;
                    if uval <= 0x7FFFFFFF || val < 0 && val >= -0x80000000 {
                        // Fits in sign-extended imm32
                        w!(out, "    mov {dst}, {val}");
                    } else if uval <= 0xFFFFFFFF {
                        // Fits in unsigned 32-bit: use mov r32, imm32 (zero-extends)
                        let dst32 = to_32bit_reg(&dst);
                        w!(out, "    mov {dst32}, {val}");
                    } else {
                        w!(out, "    movabs {dst}, {val}");
                    }
                } else if dst != src {
                    w!(out, "    mov {dst}, {src}");
                }
            }
        }

        // movsxd needs 32-bit source register
        "movsxd" => {
            if insn.operands.len() == 2 {
                let dst = resolve_op(&insn.operands[0], handler, program);
                let src = resolve_op(&insn.operands[1], handler, program);
                let src32 = to_32bit_reg(&src);
                w!(out, "    movsxd {dst}, {src32}");
            }
        }

        // extract_tag dst, src -- Extract upper 16-bit NaN-boxing tag.
        // On x86_64: mov + shr (2 instructions, same as the old macro).
        "extract_tag" => {
            if insn.operands.len() == 2 {
                let dst = resolve_op(&insn.operands[0], handler, program);
                let src = resolve_op(&insn.operands[1], handler, program);
                if dst != src {
                    w!(out, "    mov {dst}, {src}");
                }
                w!(out, "    shr {dst}, 48");
            }
        }

        // unbox_int32 dst, src -- Sign-extend low 32 bits to 64.
        "unbox_int32" => {
            if insn.operands.len() == 2 {
                let dst = resolve_op(&insn.operands[0], handler, program);
                let src = resolve_op(&insn.operands[1], handler, program);
                let src32 = to_32bit_reg(&src);
                w!(out, "    movsxd {dst}, {src32}");
            }
        }

        // unbox_object dst, src -- Zero-extend lower 48 bits (extract pointer).
        // On x86_64: mov + shl + shr (3 instructions, same as the old macro).
        "unbox_object" => {
            if insn.operands.len() == 2 {
                let dst = resolve_op(&insn.operands[0], handler, program);
                let src = resolve_op(&insn.operands[1], handler, program);
                if dst != src {
                    w!(out, "    mov {dst}, {src}");
                }
                w!(out, "    shl {dst}, 16");
                w!(out, "    shr {dst}, 16");
            }
        }

        // box_int32 dst, src -- NaN-box a raw int32 (mask low 32, set tag).
        // On x86_64: mov r32 (zero-extends) + movabs tag + or.
        "box_int32" => {
            if insn.operands.len() == 2 {
                let dst = resolve_op(&insn.operands[0], handler, program);
                let src = resolve_op(&insn.operands[1], handler, program);
                let dst32 = to_32bit_reg(&dst);
                let src32 = to_32bit_reg(&src);
                let tag_shifted = program
                    .constants
                    .get("INT32_TAG_SHIFTED")
                    .copied()
                    .expect("INT32_TAG_SHIFTED constant required for box_int32");
                // NB: The mov r32 is always emitted even when dst == src, because
                // writing a 32-bit register zeros the upper 32 bits.
                w!(out, "    mov {dst32}, {src32}");
                w!(out, "    movabs rax, {tag_shifted}");
                w!(out, "    or {dst}, rax");
            }
        }

        // box_int32_clean dst, src -- NaN-box an already zero-extended int32.
        // On x86_64: same as box_int32 but skip the zero-extension.
        "box_int32_clean" => {
            if insn.operands.len() == 2 {
                let dst = resolve_op(&insn.operands[0], handler, program);
                let src = resolve_op(&insn.operands[1], handler, program);
                let tag_shifted = program
                    .constants
                    .get("INT32_TAG_SHIFTED")
                    .copied()
                    .expect("INT32_TAG_SHIFTED constant required for box_int32_clean");
                if dst != src {
                    w!(out, "    mov {dst}, {src}");
                }
                w!(out, "    movabs rax, {tag_shifted}");
                w!(out, "    or {dst}, rax");
            }
        }

        // and with 0xFFFFFFFF needs special handling on x86_64
        // (imm32 gets sign-extended to 64 bits, making it a no-op)
        "and" => {
            if insn.operands.len() == 2 {
                let dst = resolve_op(&insn.operands[0], handler, program);
                if let Some(val) = get_immediate_value(&insn.operands[1], program) {
                    if val as u64 == 0xFFFFFFFF {
                        // Use mov to 32-bit register to zero upper bits
                        let dst32 = to_32bit_reg(&dst);
                        w!(out, "    mov {dst32}, {dst32}");
                    } else {
                        let src = resolve_op(&insn.operands[1], handler, program);
                        w!(out, "    and {dst}, {src}");
                    }
                } else {
                    let src = resolve_op(&insn.operands[1], handler, program);
                    w!(out, "    and {dst}, {src}");
                }
            }
        }

        // Shift instructions: x86 variable shifts require count in cl register.
        // If the count is in a register other than rcx, move it there first.
        "shl" | "shr" | "sar" => {
            let dst = resolve_op(&insn.operands[0], handler, program);
            if insn.operands.len() == 2 {
                if get_immediate_value(&insn.operands[1], program).is_some() {
                    let count = resolve_op(&insn.operands[1], handler, program);
                    w!(out, "    {m} {dst}, {count}");
                } else {
                    let count = resolve_op(&insn.operands[1], handler, program);
                    if count != "rcx" {
                        w!(out, "    mov rcx, {count}");
                    }
                    w!(out, "    {m} {dst}, cl");
                }
            }
        }

        // Standard x86 instructions - pass through with register resolution
        "lea" | "add" | "sub" | "neg" | "not" | "or" | "xor" => {
            let ops: Vec<String> = insn
                .operands
                .iter()
                .map(|o| resolve_op(o, handler, program))
                .collect();
            if ops.is_empty() {
                w!(out, "    {m}");
            } else {
                w!(out, "    {m} {}", ops.join(", "));
            }
        }

        // Unconditional jump - resolve local labels
        "jmp" => {
            if let Some(Operand::Label(label)) = insn.operands.first() {
                if label.starts_with('.') {
                    w!(out, "    {m} .Lasm_{}{label}", handler.name);
                } else {
                    w!(out, "    {m} {label}");
                }
            } else if let Some(op) = insn.operands.first() {
                w!(out, "    {m} {}", resolve_op(op, handler, program));
            }
        }

        // Architecture-neutral branch operations.
        // These encode the comparison and branch as a single DSL operation,
        // freeing backends from x86 flag-register semantics.
        "branch_eq" | "branch_ne" | "branch_ge_unsigned" | "branch_lt_signed"
        | "branch_le_signed" | "branch_gt_signed" | "branch_ge_signed" => {
            // branch_XX a, b, label  =>  cmp a, b; jXX label
            if insn.operands.len() == 3 {
                let a = resolve_op(&insn.operands[0], handler, program);
                let b = resolve_op(&insn.operands[1], handler, program);
                let label = resolve_label(&insn.operands[2], handler);
                let cc = match m.as_str() {
                    "branch_eq" => "je",
                    "branch_ne" => "jne",
                    "branch_ge_unsigned" => "jae",
                    "branch_lt_signed" => "jl",
                    "branch_le_signed" => "jle",
                    "branch_gt_signed" => "jg",
                    "branch_ge_signed" => "jge",
                    _ => unreachable!(),
                };
                w!(out, "    cmp {a}, {b}");
                w!(out, "    {cc} {label}");
            }
        }

        "branch_zero" | "branch_nonzero" | "branch_negative" | "branch_not_negative" => {
            // branch_zero a, label  =>  test a, a; jz label
            if insn.operands.len() == 2 {
                let a = resolve_op(&insn.operands[0], handler, program);
                let label = resolve_label(&insn.operands[1], handler);
                let cc = match m.as_str() {
                    "branch_zero" => "jz",
                    "branch_nonzero" => "jnz",
                    "branch_negative" => "js",
                    "branch_not_negative" => "jns",
                    _ => unreachable!(),
                };
                w!(out, "    test {a}, {a}");
                w!(out, "    {cc} {label}");
            }
        }

        // branch_zero32 / branch_nonzero32: test only the low 32 bits.
        "branch_zero32" | "branch_nonzero32" => {
            if insn.operands.len() == 2 {
                let a = resolve_op(&insn.operands[0], handler, program);
                let a32 = to_32bit_reg(&a);
                let label = resolve_label(&insn.operands[1], handler);
                let cc = match m.as_str() {
                    "branch_zero32" => "jz",
                    "branch_nonzero32" => "jnz",
                    _ => unreachable!(),
                };
                w!(out, "    test {a32}, {a32}");
                w!(out, "    {cc} {label}");
            }
        }

        "branch_bits_set" | "branch_bits_clear" => {
            // branch_bits_set a, mask, label
            if insn.operands.len() == 3 {
                let a = resolve_op(&insn.operands[0], handler, program);
                let label = resolve_label(&insn.operands[2], handler);
                let test_cc = match m.as_str() {
                    "branch_bits_set" => "jnz",
                    "branch_bits_clear" => "jz",
                    _ => unreachable!(),
                };
                let bit_cc = match m.as_str() {
                    "branch_bits_set" => "jc",
                    "branch_bits_clear" => "jnc",
                    _ => unreachable!(),
                };

                if let Some(mask) = get_immediate_value(&insn.operands[1], program) {
                    let mask = mask as u64;
                    if mask >> 32 == 0 {
                        let a32 = to_32bit_reg(&a);
                        w!(out, "    test {a32}, {mask}");
                        w!(out, "    {test_cc} {label}");
                    } else if mask.is_power_of_two() {
                        w!(out, "    bt {a}, {}", mask.trailing_zeros());
                        w!(out, "    {bit_cc} {label}");
                    } else {
                        panic!(
                            "x86_64 branch_bits requires a low-32-bit mask or a single-bit 64-bit mask in handler '{}', got {mask:#x}",
                            handler.name
                        );
                    }
                } else {
                    let mask = resolve_op(&insn.operands[1], handler, program);
                    w!(out, "    test {a}, {mask}");
                    w!(out, "    {test_cc} {label}");
                }
            }
        }

        "branch_bit_set" => {
            // branch_bit_set a, N, label  =>  bt a, N; jc label
            if insn.operands.len() == 3 {
                let a = resolve_op(&insn.operands[0], handler, program);
                let bit = resolve_op(&insn.operands[1], handler, program);
                let label = resolve_label(&insn.operands[2], handler);
                w!(out, "    bt {a}, {bit}");
                w!(out, "    jc {label}");
            }
        }

        // Multi-value equality branch: branch if register equals any of the given values.
        // branch_any_eq reg, val1, val2, ..., label
        // On x86: emits cmp + je for each value.
        "branch_any_eq" => {
            if insn.operands.len() >= 3 {
                let reg = resolve_op(&insn.operands[0], handler, program);
                let label = resolve_label(insn.operands.last().unwrap(), handler);
                for op in &insn.operands[1..insn.operands.len() - 1] {
                    let val = resolve_op(op, handler, program);
                    w!(out, "    cmp {reg}, {val}");
                    w!(out, "    je {label}");
                }
            }
        }

        // Floating-point compare-and-branch operations.
        // Consecutive branch_fp_* with the same operands share one ucomisd,
        // since ucomisd sets all the flags these branches test.
        "branch_fp_unordered"
        | "branch_fp_equal"
        | "branch_fp_less"
        | "branch_fp_less_or_equal"
        | "branch_fp_greater"
        | "branch_fp_greater_or_equal" => {
            if insn.operands.len() == 3 {
                let a = resolve_op(&insn.operands[0], handler, program);
                let b = resolve_op(&insn.operands[1], handler, program);
                let label = resolve_label(&insn.operands[2], handler);
                let cc = match m.as_str() {
                    "branch_fp_unordered" => "jp",
                    "branch_fp_equal" => "je",
                    "branch_fp_less" => "jb",
                    "branch_fp_less_or_equal" => "jbe",
                    "branch_fp_greater" => "ja",
                    "branch_fp_greater_or_equal" => "jae",
                    _ => unreachable!(),
                };
                let need_compare = match &state.last_fp_compare {
                    Some((prev_a, prev_b)) => *prev_a != a || *prev_b != b,
                    None => true,
                };
                if need_compare {
                    w!(out, "    ucomisd {a}, {b}");
                    state.last_fp_compare = Some((a, b));
                }
                w!(out, "    {cc} {label}");
            }
            return;
        }

        _ => {
            panic!("Unknown instruction '{m}' in handler '{}'", handler.name);
        }
    }

    // Any non-branch_fp instruction may clobber flags, invalidating the
    // cached FP comparison. branch_fp_* returns early above to skip this.
    state.last_fp_compare = None;
}

/// Convert a 64-bit register name to its 8-bit counterpart.
fn to_8bit_reg(reg: &str) -> String {
    match reg {
        "rax" => "al".to_string(),
        "rbx" => "bl".to_string(),
        "rcx" => "cl".to_string(),
        "rdx" => "dl".to_string(),
        "rsi" => "sil".to_string(),
        "rdi" => "dil".to_string(),
        "rsp" => "spl".to_string(),
        "rbp" => "bpl".to_string(),
        "r8" => "r8b".to_string(),
        "r9" => "r9b".to_string(),
        "r10" => "r10b".to_string(),
        "r11" => "r11b".to_string(),
        "r12" => "r12b".to_string(),
        "r13" => "r13b".to_string(),
        "r14" => "r14b".to_string(),
        "r15" => "r15b".to_string(),
        _ => reg.to_string(),
    }
}

/// Convert a 64-bit register name to its 16-bit counterpart.
fn to_16bit_reg(reg: &str) -> String {
    match reg {
        "rax" => "ax".to_string(),
        "rbx" => "bx".to_string(),
        "rcx" => "cx".to_string(),
        "rdx" => "dx".to_string(),
        "rsi" => "si".to_string(),
        "rdi" => "di".to_string(),
        "rsp" => "sp".to_string(),
        "rbp" => "bp".to_string(),
        "r8" => "r8w".to_string(),
        "r9" => "r9w".to_string(),
        "r10" => "r10w".to_string(),
        "r11" => "r11w".to_string(),
        "r12" => "r12w".to_string(),
        "r13" => "r13w".to_string(),
        "r14" => "r14w".to_string(),
        "r15" => "r15w".to_string(),
        _ => reg.to_string(),
    }
}

/// Convert a 64-bit register name to its 32-bit counterpart.
fn to_32bit_reg(reg: &str) -> String {
    match reg {
        "rax" => "eax".to_string(),
        "rbx" => "ebx".to_string(),
        "rcx" => "ecx".to_string(),
        "rdx" => "edx".to_string(),
        "rsi" => "esi".to_string(),
        "rdi" => "edi".to_string(),
        "rsp" => "esp".to_string(),
        "rbp" => "ebp".to_string(),
        "r8" => "r8d".to_string(),
        "r9" => "r9d".to_string(),
        "r10" => "r10d".to_string(),
        "r11" => "r11d".to_string(),
        "r12" => "r12d".to_string(),
        "r13" => "r13d".to_string(),
        "r14" => "r14d".to_string(),
        "r15" => "r15d".to_string(),
        _ => reg.to_string(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::parser::{Handler, ObjectFormat, Program};
    use bytecode_def::OpLayout;

    fn test_program() -> Program {
        Program {
            constants: HashMap::new(),
            macros: HashMap::new(),
            handlers: Vec::new(),
            op_layouts: HashMap::from([(
                "Call".into(),
                OpLayout {
                    field_offsets: HashMap::new(),
                    size: None,
                },
            )]),
            opcode_list: Vec::new(),
            object_format: ObjectFormat::MachO,
            has_jscvt: false,
            enable_assertions: false,
        }
    }

    fn call_handler() -> Handler {
        Handler {
            name: "Call".into(),
            size: None,
            instructions: Vec::new(),
        }
    }

    #[test]
    fn preserves_scaled_index_memory_operands() {
        let program = test_program();
        let handler = call_handler();
        // Operands that already resolved to platform registers (post-
        // allocation) flow through resolve_op unchanged.
        let memory = Operand::Memory {
            base: "rax".into(),
            index: Some("r11".into()),
            scale: Some("8".into()),
        };

        assert_eq!(resolve_op(&memory, &handler, &program), "[rax + r11 * 8]");
    }

    #[test]
    fn formats_pair_memory_regimm_zero_without_invalid_scale() {
        let program = test_program();
        let handler = call_handler();
        let memory = Operand::Memory {
            base: "r9".into(),
            index: Some("rsi".into()),
            scale: Some("0".into()),
        };

        assert_eq!(
            resolve_pair_memory_op(&memory, &handler, &program),
            "[r9 + rsi]"
        );
    }

    #[test]
    fn pair_dst_aliasing_base_keeps_address_alive() {
        // load_pair64 dst1, dst2, [base + 0], [base + 8] must not start by
        // writing dst1 if dst1 happens to be the same physical register
        // as base; the second load would otherwise read from the wrong
        // address. The codegen swaps the load order in that case.
        assert!(pair_dst_aliases_address("rcx", "[rcx]", "[rcx + 8]"));
        assert!(pair_dst_aliases_address("r10", "[r10 + 16]", "[r10 + 24]"));
        assert!(pair_dst_aliases_address("rdx", "[rcx + rdx * 8]", "[rcx + rdx * 8 + 8]"));
        assert!(!pair_dst_aliases_address("rax", "[rcx + 16]", "[rcx + 24]"));
        assert!(!pair_dst_aliases_address("rax", "[r10]", "[r10 + 8]"));
    }

    #[test]
    fn lowers_high_single_bit_branch_mask_with_bt() {
        let mut program = test_program();
        program
            .constants
            .insert("HIGH_BIT".into(), 0x1_0000_0000);
        let handler = call_handler();
        let instruction = AsmInstruction {
            mnemonic: "branch_bits_clear".into(),
            operands: vec![
                Operand::Register("rdx".into()),
                Operand::Constant("HIGH_BIT".into()),
                Operand::Label(".slow".into()),
            ],
        };
        let mut out = String::new();
        let mut state = HandlerState::new();

        emit_instruction(
            &mut out,
            &instruction,
            &handler,
            &program,
            &mut state,
            X86_64Abi::SysV,
        );

        assert!(out.contains("bt rdx, 32"));
        assert!(out.contains("jnc .Lasm_Call.slow"));
        assert!(!out.contains("test rdx, 4294967296"));
    }

    #[test]
    fn win64_exit_omits_arm64_seh_epilogue_markers() {
        let mut out = String::new();

        generate_exit_point(&mut out, ObjectFormat::Coff, X86_64Abi::Win64);

        assert!(out.contains("    add rsp, 56"));
        assert!(out.contains("    ret"));
        assert!(!out.contains(".seh_startepilogue"));
        assert!(!out.contains(".seh_endepilogue"));
    }
}
