/**
 * x64-emitter.ts - x86-64 machine code emitter for TSForge compiler
 *
 * Compiles HIR (High-level Intermediate Representation) nodes into a valid
 * COFF .obj file containing x86-64 machine code.  All JavaScript values are
 * represented as NaN-boxed IEEE 754 doubles (8 bytes).  The emitter follows
 * the Windows x64 calling convention:
 *
 *   - Float / NaN-boxed JS values: xmm0-xmm3 (first 4 args), then stack.
 *   - Integer / pointer args:       rcx, rdx, r8, r9, then stack.
 *   - Return value (JS):            xmm0
 *   - Return value (int32):         eax
 *   - Caller cleans up the stack.
 *
 * Compilation proceeds in two passes:
 *   1. Collect function names, string constants, and determine section layout.
 *   2. Emit x86-64 machine code for every function body.
 *
 * The output COFF .obj contains:
 *   - .text  (CODE | EXEC | READ)  -- machine code
 *   - .rdata (DATA | READ)         -- string constants
 *   - Symbol table with function labels, string labels, and external imports.
 *   - Relocation table for all cross-references (RIP-relative and direct calls).
 */

import {
  type HirExpr,
  BinOp,
  UnaryOp,
  UNDEFINED,
  NULL_VAL,
  TRUE_VAL,
  FALSE_VAL,
} from './hir';

// ---------------------------------------------------------------------------
// COFF format constants (duplicated here so the emitter is self-contained)
// ---------------------------------------------------------------------------

const IMAGE_FILE_MACHINE_AMD64 = 0x8664;

// Section characteristic flags
const IMAGE_SCN_CNT_CODE = 0x00000020;
const IMAGE_SCN_CNT_INITIALIZED_DATA = 0x00000040;
const IMAGE_SCN_MEM_EXECUTE = 0x20000000;
const IMAGE_SCN_MEM_READ = 0x40000000;

// Relocation types
const IMAGE_REL_AMD64_REL32 = 0x0004;

// Storage classes
const IMAGE_SYM_CLASS_EXTERNAL = 2;
const IMAGE_SYM_CLASS_STATIC = 3;

// ---------------------------------------------------------------------------
// CodeBuffer -- low-level byte buffer for machine code emission
// ---------------------------------------------------------------------------

/**
 * A growable byte buffer with helpers for emitting x86-64 instructions and
 * applying 32-bit fixups (relocations).
 */
class CodeBuffer {
  private bytes: number[] = [];

  /** Current emission offset (number of bytes written so far). */
  pos(): number {
    return this.bytes.length;
  }

  /** Emit a single byte. */
  emitByte(b: number): void {
    this.bytes.push(b & 0xff);
  }

  /** Emit an arbitrary sequence of bytes. */
  emitBytes(...bs: number[]): void {
    for (let i = 0; i < bs.length; i++) this.bytes.push(bs[i] & 0xff);
  }

  /** Emit a 16-bit unsigned integer in little-endian order. */
  emitU16(v: number): void {
    this.bytes.push(v & 0xff, (v >>> 8) & 0xff);
  }

  /** Emit a 32-bit unsigned integer in little-endian order. */
  emitU32(v: number): void {
    this.bytes.push(v & 0xff, (v >>> 8) & 0xff, (v >>> 16) & 0xff, (v >>> 24) & 0xff);
  }

  /** Emit a 64-bit unsigned integer (as BigInt) in little-endian order. */
  emitU64(v: bigint): void {
    let n = v;
    for (let i = 0; i < 8; i++) {
      this.bytes.push(Number(n & 0xffn));
      n >>= 8n;
    }
  }

  /**
   * Patch a 32-bit value at the given offset (little-endian).
   * Used to fill in relocation displacements after the full layout is known.
   */
  fixupU32(offset: number, value: number): void {
    this.bytes[offset] = value & 0xff;
    this.bytes[offset + 1] = (value >>> 8) & 0xff;
    this.bytes[offset + 2] = (value >>> 16) & 0xff;
    this.bytes[offset + 3] = (value >>> 24) & 0xff;
  }

  /** Return the accumulated bytes as a `Uint8Array`. */
  toUint8Array(): Uint8Array {
    return new Uint8Array(this.bytes);
  }

  /** Current length of the buffer in bytes. */
  get length(): number {
    return this.bytes.length;
  }
}

// ---------------------------------------------------------------------------
// x86-64 instruction helpers
// ---------------------------------------------------------------------------

/**
 * All instruction helpers write into the supplied `CodeBuffer`.
 * Each helper is a thin wrapper around raw byte sequences documented in the
 * Intel SDM Vol. 2.  Only the subset required by the TSForge emitter is
 * provided.
 */

/** `push rbp` */
function emitPushRbp(buf: CodeBuffer): void {
  buf.emitByte(0x55);
}

/** `mov rbp, rsp` */
function emitMovRbpRsp(buf: CodeBuffer): void {
  buf.emitBytes(0x48, 0x89, 0xe5);
}

/** `sub rsp, imm8` -- allocate stack frame (max 127 bytes). */
function emitSubRspImm8(buf: CodeBuffer, imm8: number): void {
  buf.emitBytes(0x48, 0x83, 0xec, imm8 & 0xff);
}

/** `add rsp, imm8` -- deallocate stack frame. */
function emitAddRspImm8(buf: CodeBuffer, imm8: number): void {
  buf.emitBytes(0x48, 0x83, 0xc4, imm8 & 0xff);
}

/** `movabs rax, imm64` -- load a 64-bit immediate into rax. */
function emitMovabsRaxImm64(buf: CodeBuffer, imm: bigint): void {
  buf.emitBytes(0x48, 0xb8);
  buf.emitU64(imm);
}

/** `movq xmm0, rax` -- move rax bits into xmm0. */
function emitMovqXmm0Rax(buf: CodeBuffer): void {
  buf.emitBytes(0x66, 0x0f, 0x6e, 0xc0);
}

/** `movq xmm1, rax` -- move rax bits into xmm1. */
function emitMovqXmm1Rax(buf: CodeBuffer): void {
  buf.emitBytes(0x66, 0x0f, 0x6e, 0xc8);
}

/** `movsd xmm0, xmm1` -- copy xmm1 to xmm0. */
function emitMovsdXmm0Xmm1(buf: CodeBuffer): void {
  buf.emitBytes(0xf2, 0x0f, 0x10, 0xc1);
}

/** `movsd xmm1, xmm0` -- copy xmm0 to xmm1. */
function emitMovsdXmm1Xmm0(buf: CodeBuffer): void {
  buf.emitBytes(0xf2, 0x0f, 0x10, 0xc8);
}

/** `addsd xmm0, xmm1` -- xmm0 += xmm1. */
function emitAddsdXmm0Xmm1(buf: CodeBuffer): void {
  buf.emitBytes(0xf2, 0x0f, 0x58, 0xc1);
}

/** `subsd xmm0, xmm1` -- xmm0 -= xmm1. */
function emitSubsdXmm0Xmm1(buf: CodeBuffer): void {
  buf.emitBytes(0xf2, 0x0f, 0x5c, 0xc1);
}

/** `mulsd xmm0, xmm1` -- xmm0 *= xmm1. */
function emitMulsdXmm0Xmm1(buf: CodeBuffer): void {
  buf.emitBytes(0xf2, 0x0f, 0x59, 0xc1);
}

/** `divsd xmm0, xmm1` -- xmm0 /= xmm1. */
function emitDivsdXmm0Xmm1(buf: CodeBuffer): void {
  buf.emitBytes(0xf2, 0x0f, 0x5e, 0xc1);
}

/** `addsd xmm1, xmm0` -- xmm1 += xmm0. */
function emitAddsdXmm1Xmm0(buf: CodeBuffer): void {
  buf.emitBytes(0xf2, 0x0f, 0x58, 0xc8);
}

/** `subsd xmm1, xmm0` -- xmm1 -= xmm0. */
function emitSubsdXmm1Xmm0(buf: CodeBuffer): void {
  buf.emitBytes(0xf2, 0x0f, 0x5c, 0xc8);
}

/** `xorpd xmm0, xmm0` -- zero xmm0. */
function emitXorpdXmm0Xmm0(buf: CodeBuffer): void {
  buf.emitBytes(0x66, 0x0f, 0x57, 0xc0);
}

/** `xorpd xmm1, xmm1` -- zero xmm1. */
function emitXorpdXmm1Xmm1(buf: CodeBuffer): void {
  buf.emitBytes(0x66, 0x0f, 0x57, 0xc9);
}

/** `ucomisd xmm0, xmm1` -- compare xmm0 with xmm1. */
function emitUcomisdXmm0Xmm1(buf: CodeBuffer): void {
  buf.emitBytes(0x66, 0x0f, 0x2e, 0xc1);
}

/** `cvtsi2sd xmm0, rax` -- signed 64-bit int in rax to double in xmm0. */
function emitCvtsi2sdXmm0Rax(buf: CodeBuffer): void {
  buf.emitBytes(0xf2, 0x48, 0x0f, 0x2a, 0xc0);
}

/** `cvttsd2si rax, xmm0` -- double in xmm0 to signed 64-bit int in rax. */
function emitCvttsd2siRaxXmm0(buf: CodeBuffer): void {
  buf.emitBytes(0xf2, 0x48, 0x0f, 0x2c, 0xc0);
}

/** `ret` */
function emitRet(buf: CodeBuffer): void {
  buf.emitByte(0xc3);
}

/** `int3` -- software breakpoint (used for stubs). */
function emitInt3(buf: CodeBuffer): void {
  buf.emitByte(0xcc);
}

/**
 * `call rel32` -- direct relative call.  Returns the offset of the 4-byte
 * displacement so the caller can apply a relocation fixup.
 */
function emitCallRel32(buf: CodeBuffer): number {
  buf.emitByte(0xe8);
  const fixupPos = buf.pos();
  buf.emitU32(0); // placeholder
  return fixupPos;
}

/**
 * `call [rip+disp32]` -- indirect call through IAT.  Returns the offset of
 * the 4-byte displacement for a relocation fixup.
 */
function emitCallRipDisp32(buf: CodeBuffer): number {
  buf.emitBytes(0xff, 0x15);
  const fixupPos = buf.pos();
  buf.emitU32(0); // placeholder
  return fixupPos;
}

/** `jmp rel32` -- unconditional relative jump. Returns fixup offset. */
function emitJmpRel32(buf: CodeBuffer): number {
  buf.emitByte(0xe9);
  const fixupPos = buf.pos();
  buf.emitU32(0);
  return fixupPos;
}

/** `je rel32` -- jump if equal (ZF=1). Returns fixup offset. */
function emitJeRel32(buf: CodeBuffer): number {
  buf.emitBytes(0x0f, 0x84);
  const fixupPos = buf.pos();
  buf.emitU32(0);
  return fixupPos;
}

/** `jne rel32` -- jump if not equal (ZF=0). Returns fixup offset. */
function emitJneRel32(buf: CodeBuffer): number {
  buf.emitBytes(0x0f, 0x85);
  const fixupPos = buf.pos();
  buf.emitU32(0);
  return fixupPos;
}

/** `jl rel32` -- jump if less (SF!=OF). Returns fixup offset. */
function emitJlRel32(buf: CodeBuffer): number {
  buf.emitBytes(0x0f, 0x8c);
  const fixupPos = buf.pos();
  buf.emitU32(0);
  return fixupPos;
}

/** `jg rel32` -- jump if greater (ZF=0 && SF=OF). Returns fixup offset. */
function emitJgRel32(buf: CodeBuffer): number {
  buf.emitBytes(0x0f, 0x8f);
  const fixupPos = buf.pos();
  buf.emitU32(0);
  return fixupPos;
}

/** `jle rel32` -- jump if less or equal (ZF=1 || SF!=OF). Returns fixup offset. */
function emitJleRel32(buf: CodeBuffer): number {
  buf.emitBytes(0x0f, 0x8e);
  const fixupPos = buf.pos();
  buf.emitU32(0);
  return fixupPos;
}

/** `jge rel32` -- jump if greater or equal (SF=OF). Returns fixup offset. */
function emitJgeRel32(buf: CodeBuffer): number {
  buf.emitBytes(0x0f, 0x8d);
  const fixupPos = buf.pos();
  buf.emitU32(0);
  return fixupPos;
}

/** `mov rcx, rax` */
function emitMovRcxRax(buf: CodeBuffer): void {
  buf.emitBytes(0x48, 0x89, 0xc1);
}

/** `mov rdx, rax` */
function emitMovRdxRax(buf: CodeBuffer): void {
  buf.emitBytes(0x48, 0x89, 0xc2);
}

/** `mov r8, rax` */
function emitMovR8Rax(buf: CodeBuffer): void {
  buf.emitBytes(0x49, 0x89, 0xc0);
}

/** `mov r9, rax` */
function emitMovR9Rax(buf: CodeBuffer): void {
  buf.emitBytes(0x49, 0x89, 0xc1);
}

/** `mov rax, [rip+disp32]` -- RIP-relative load. Returns fixup offset. */
function emitMovRaxRipDisp32(buf: CodeBuffer): number {
  buf.emitBytes(0x48, 0x8b, 0x05);
  const fixupPos = buf.pos();
  buf.emitU32(0);
  return fixupPos;
}

/** `lea rax, [rip+disp32]` -- RIP-relative LEA. Returns fixup offset. */
function emitLeaRaxRipDisp32(buf: CodeBuffer): number {
  buf.emitBytes(0x48, 0x8d, 0x05);
  const fixupPos = buf.pos();
  buf.emitU32(0);
  return fixupPos;
}

/** `movq xmm0, [rip+disp32]` -- RIP-relative load into xmm0. Returns fixup offset. */
function emitMovqXmm0RipDisp32(buf: CodeBuffer): number {
  buf.emitBytes(0xf3, 0x0f, 0x7e, 0x05);
  const fixupPos = buf.pos();
  buf.emitU32(0);
  return fixupPos;
}

/**
 * `movq xmm0, [rbp-disp8]` -- load a NaN-boxed value from a local variable
 * slot.  `disp8` is the negative offset from rbp (e.g. -8 for the first local).
 * Encoding: F2 0F 7E 45 disp8
 */
function emitMovqXmm0RbpNegDisp8(buf: CodeBuffer, disp8: number): void {
  buf.emitBytes(0xf2, 0x0f, 0x7e, 0x45, disp8 & 0xff);
}

/**
 * `movq [rbp-disp8], xmm0` -- store xmm0 into a local variable slot.
 * Encoding: F2 0F 7E 45 disp8 (same ModRM encoding for the rbp-disp8 form)
 */
function emitMovqRbpNegDisp8Xmm0(buf: CodeBuffer, disp8: number): void {
  buf.emitBytes(0xf2, 0x0f, 0x7e, 0x45, disp8 & 0xff);
}

/**
 * `movq xmm0, [rbp+disp8]` -- load from a positive rbp offset (e.g. params
 * above the saved rbp).
 * Encoding: F2 0F 7E 45 disp8
 */
function emitMovqXmm0RbpPosDisp8(buf: CodeBuffer, disp8: number): void {
  buf.emitBytes(0xf2, 0x0f, 0x7e, 0x45, disp8 & 0xff);
}

/**
 * `movq [rbp+disp8], xmm0` -- store to a positive rbp offset.
 * Encoding: F2 0F 7E 45 disp8
 */
function emitMovqRbpPosDisp8Xmm0(buf: CodeBuffer, disp8: number): void {
  buf.emitBytes(0xf2, 0x0f, 0x7e, 0x45, disp8 & 0xff);
}

/**
 * `movq xmm0, [rsp+disp8]` -- load xmm0 from a stack location.
 * Uses the ModRM + SIB form: F2 0F 7E 44 24 disp8.
 */
function emitMovqXmm0RspDisp8(buf: CodeBuffer, disp8: number): void {
  buf.emitBytes(0xf2, 0x0f, 0x7e, 0x44, 0x24, disp8 & 0xff);
}

/**
 * `movq [rsp+disp8], xmm0` -- store xmm0 to a stack location.
 * Encoding: F2 0F 7E 44 24 disp8
 */
function emitMovqRspDisp8Xmm0(buf: CodeBuffer, disp8: number): void {
  buf.emitBytes(0xf2, 0x0f, 0x7e, 0x44, 0x24, disp8 & 0xff);
}

/**
 * `movq xmm1, [rsp+disp8]` -- load xmm1 from a stack location.
 * Encoding: F2 0F 7E 4C 24 disp8
 */
function emitMovqXmm1RspDisp8(buf: CodeBuffer, disp8: number): void {
  buf.emitBytes(0xf2, 0x0f, 0x7e, 0x4c, 0x24, disp8 & 0xff);
}

/**
 * `movq [rsp+disp8], xmm1` -- store xmm1 to a stack location.
 * Encoding: F2 0F 7E 4C 24 disp8
 */
function emitMovqRspDisp8Xmm1(buf: CodeBuffer, disp8: number): void {
  buf.emitBytes(0xf2, 0x0f, 0x7e, 0x4c, 0x24, disp8 & 0xff);
}

/**
 * `movq xmm2, [rsp+disp8]` -- load xmm2 from a stack location.
 * Encoding: F2 0F 7E 54 24 disp8
 */
function emitMovqXmm2RspDisp8(buf: CodeBuffer, disp8: number): void {
  buf.emitBytes(0xf2, 0x0f, 0x7e, 0x54, 0x24, disp8 & 0xff);
}

/**
 * `movq [rsp+disp8], xmm2` -- store xmm2 to a stack location.
 * Encoding: F2 0F 7E 54 24 disp8
 */
function emitMovqRspDisp8Xmm2(buf: CodeBuffer, disp8: number): void {
  buf.emitBytes(0xf2, 0x0f, 0x7e, 0x54, 0x24, disp8 & 0xff);
}

/**
 * `movq xmm3, [rsp+disp8]` -- load xmm3 from a stack location.
 * Encoding: F2 0F 7E 5C 24 disp8
 */
function emitMovqXmm3RspDisp8(buf: CodeBuffer, disp8: number): void {
  buf.emitBytes(0xf2, 0x0f, 0x7e, 0x5c, 0x24, disp8 & 0xff);
}

/**
 * `movq [rsp+disp8], xmm3` -- store xmm3 to a stack location.
 * Encoding: F2 0F 7E 5C 24 disp8
 */
function emitMovqRspDisp8Xmm3(buf: CodeBuffer, disp8: number): void {
  buf.emitBytes(0xf2, 0x0f, 0x7e, 0x5c, 0x24, disp8 & 0xff);
}

/** `mov rax, [rsp+disp8]` -- load 64-bit integer from stack. */
function emitMovRaxRspDisp8(buf: CodeBuffer, disp8: number): void {
  buf.emitBytes(0x48, 0x8b, 0x44, 0x24, disp8 & 0xff);
}

/** `mov [rsp+disp8], rax` -- store 64-bit integer to stack. */
function emitMovRspDisp8Rax(buf: CodeBuffer, disp8: number): void {
  buf.emitBytes(0x48, 0x89, 0x44, 0x24, disp8 & 0xff);
}

/** `mov eax, imm32` -- load 32-bit immediate (zero-extends to rax). */
function emitMovEaxImm32(buf: CodeBuffer, imm: number): void {
  buf.emitByte(0xb8);
  buf.emitU32(imm);
}

/** `mov rcx, imm64` -- load 64-bit immediate into rcx. */
function emitMovRcxImm64(buf: CodeBuffer, imm: bigint): void {
  buf.emitBytes(0x48, 0xb9);
  buf.emitU64(imm);
}

/** `mov rax, [rbp-disp8]` -- load 64-bit integer from local. */
function emitMovRaxRbpNegDisp8(buf: CodeBuffer, disp8: number): void {
  buf.emitBytes(0x48, 0x8b, 0x45, disp8 & 0xff);
}

/** `mov [rbp-disp8], rax` -- store 64-bit integer to local. */
function emitMovRbpNegDisp8Rax(buf: CodeBuffer, disp8: number): void {
  buf.emitBytes(0x48, 0x89, 0x45, disp8 & 0xff);
}

/** `test eax, eax` -- test 32-bit register (sets flags). */
function emitTestEaxEax(buf: CodeBuffer): void {
  buf.emitBytes(0x85, 0xc0);
}

/** `xor eax, eax` -- zero eax. */
function emitXorEaxEax(buf: CodeBuffer): void {
  buf.emitBytes(0x31, 0xc0);
}

/** `pop rbp` */
function emitPopRbp(buf: CodeBuffer): void {
  buf.emitByte(0x5d);
}

/** `mov rsp, rbp` -- restore stack pointer (frame dealloc). */
function emitMovRspRbp(buf: CodeBuffer): void {
  buf.emitBytes(0x48, 0x89, 0xec);
}

/** `push rax` */
function emitPushRax(buf: CodeBuffer): void {
  buf.emitByte(0x50);
}

/** `pop rax` */
function emitPopRax(buf: CodeBuffer): void {
  buf.emitByte(0x58);
}

// ---------------------------------------------------------------------------
// FNV-1a hashing for property keys
// ---------------------------------------------------------------------------

/**
 * Compute a 64-bit FNV-1a hash of a string.  Used to generate property-key
 * identifiers at compile time.
 */
function fnv1a64(name: string): bigint {
  let hash = 0xcbf29ce484222325n;
  for (let i = 0; i < name.length; i++) {
    hash ^= BigInt(name.charCodeAt(i));
    hash = (hash * 0x100000001b3n) & 0xffffffffffffffffn;
  }
  return hash;
}

// ---------------------------------------------------------------------------
// Internal data structures for the two-pass compilation
// ---------------------------------------------------------------------------

/** A pending relocation that will be resolved once section layout is final. */
interface PendingReloc {
  /** Byte offset within .text where the relocation displacement lives. */
  offset: number;
  /** Index into the symbol table of the target symbol. */
  symbolIndex: number;
  /** Relocation type (always IMAGE_REL_AMD64_REL32 for our emitter). */
  type: number;
}

/** A string constant that will be placed in .rdata. */
interface StringConstant {
  /** Unique label (e.g. `__str_0`). */
  label: string;
  /** The raw UTF-8 bytes including the trailing NUL terminator. */
  data: Uint8Array;
  /** Byte offset within .rdata where this string starts. */
  rdataOffset: number;
}

/** Describes a user-defined function for code generation. */
interface FunctionInfo {
  /** The HIR Function node. */
  node: HirExpr & { type: 'Function' };
  /** Byte offset within .text where this function's code starts. */
  textOffset: number;
}

/**
 * Runtime helper functions that must be imported from the runtime library.
 * Each entry maps the internal name used by the emitter to the symbol that
 * will appear in the COFF symbol table.
 */
const RUNTIME_IMPORTS: string[] = [
  'console_log',
  'js_add',
  'js_sub',
  'js_mul',
  'js_div',
  'js_mod',
  'js_eq',
  'js_neq',
  'js_lt',
  'js_gt',
  'js_le',
  'js_ge',
  'js_is_truthy',
  'js_print',
  'js_string_from_static',
  'js_number_to_string',
  'js_typeof',
  'js_new',
  'js_get_property',
  'js_set_property',
  'js_get_element',
  'js_set_element',
  'js_bit_and',
  'js_bit_or',
  'js_bit_xor',
  'js_shl',
  'js_shr',
  'js_instanceof',
  'js_in',
  'js_alloc_object',
  'js_alloc_array',
  'js_throw',
];

// ---------------------------------------------------------------------------
// FunctionScope -- per-function variable / stack management
// ---------------------------------------------------------------------------

/**
 * Tracks local variable slots and parameter locations for a single function
 * during code generation.
 *
 * Stack layout (after prologue, rbp pushed and set):
 *
 *   [rbp+0x10]  1st parameter (xmm0 on entry)
 *   [rbp+0x18]  2nd parameter (xmm1 on entry)
 *   [rbp+0x20]  3rd parameter (xmm2 on entry)
 *   [rbp+0x28]  4th parameter (xmm3 on entry)
 *   [rbp+0x30]  5th parameter (on stack, above shadow space)
 *   ...
 *   [rbp]       saved rbp
 *   [rbp-0x08]  1st local variable
 *   [rbp-0x10]  2nd local variable
 *   ...
 */
class FunctionScope {
  /** Map from variable name to its rbp-relative byte offset (negative). */
  private locals = new Map<string, number>();

  /** Number of local variable slots allocated. */
  private slotCount = 0;

  /** Parameter names in declaration order. */
  private paramNames: string[];

  /** Total stack frame size in bytes (always a multiple of 16). */
  frameSize = 0;

  constructor(params: string[]) {
    this.paramNames = params;
  }

  /**
   * Allocate a new local variable slot.  Returns the rbp-relative byte offset
   * (always negative).  If the variable was already allocated, returns the
   * existing offset.
   */
  allocLocal(name: string): number {
    const existing = this.locals.get(name);
    if (existing !== undefined) return existing;

    this.slotCount++;
    const offset = -(this.slotCount * 8);
    this.locals.set(name, offset);
    return offset;
  }

  /**
   * Look up the rbp-relative offset for a variable.  Parameters are at
   * positive offsets; locals at negative offsets.
   */
  resolve(name: string): number {
    // Check locals first
    const localOffset = this.locals.get(name);
    if (localOffset !== undefined) return localOffset;

    // Check parameters
    const paramIdx = this.paramNames.indexOf(name);
    if (paramIdx >= 0) {
      // Parameters are at rbp+0x10, rbp+0x18, ...
      return 0x10 + paramIdx * 8;
    }

    // Unknown variable -- return 0 (will generate incorrect code, but
    // avoids a crash; the real error should be caught during validation).
    return 0;
  }

  /** Finalize the frame size, rounding up to 16-byte alignment. */
  finalize(): void {
    const rawSize = this.slotCount * 8;
    // Align to 16 bytes
    this.frameSize = (rawSize + 15) & ~15;
    // Minimum frame size of 32 bytes to keep things simple (shadow space
    // for potential runtime calls on Windows x64).
    if (this.frameSize < 0x20) this.frameSize = 0x20;
  }
}

// ---------------------------------------------------------------------------
// X64Emitter -- the main compilation driver
// ---------------------------------------------------------------------------

/**
 * Compiles an array of HIR nodes (typically top-level function declarations)
 * into a COFF .obj byte buffer.
 *
 * Public API:
 *   ```ts
 *   const emitter = new X64Emitter(hirNodes);
 *   const objBuffer = emitter.emit();
 *   ```
 */
export class X64Emitter {
  private hir: HirExpr[];

  // -- Pass 1 data --
  private functions: FunctionInfo[] = [];
  private stringConstants: StringConstant[] = [];
  private stringMap = new Map<string, number>(); // string value -> index
  private importNames: string[] = [];

  // -- Pass 2 data --
  private textBuf = new CodeBuffer();
  private rdataBuf = new CodeBuffer();
  private pendingRelocs: PendingReloc[] = [];

  // -- Symbol table (built during finalization) --
  private symbolNames: string[] = [];
  private symbolSections: number[] = [];
  private symbolValues: number[] = [];
  private symbolTypes: number[] = [];
  private symbolStorageClasses: number[] = [];
  private symbolAuxCounts: number[] = [];

  constructor(hir: HirExpr[]) {
    this.hir = hir;
  }

  // ========================================================================
  // Public entry point
  // ========================================================================

  /**
   * Compile the HIR to a COFF .obj and return the raw bytes.
   *
   * @returns A `Uint8Array` containing a valid COFF .obj file.
   */
  emit(): Uint8Array {
    this.pass1();
    this.pass2();
    return this.buildCoff();
  }

  // ========================================================================
  // Pass 1 -- collect symbols and lay out sections
  // ========================================================================

  private pass1(): void {
    // Collect all top-level function declarations
    for (const node of this.hir) {
      if (node.type === 'Function') {
        this.functions.push({
          node,
          textOffset: 0, // will be set during pass 2
        });
      }
    }

    // Collect all runtime imports
    this.importNames = [...RUNTIME_IMPORTS];

    // Walk all function bodies to collect string constants
    for (const fn of this.functions) {
      this.collectStrings(fn.node.body);
    }

    // Lay out .rdata section (string constants)
    let rdataOffset = 0;
    for (let i = 0; i < this.stringConstants.length; i++) {
      const str = this.stringConstants[i];
      str.rdataOffset = rdataOffset;
      rdataOffset += str.data.length;
    }
    // Pad .rdata to 4-byte alignment
    while (rdataOffset % 4 !== 0) rdataOffset++;
  }

  /**
   * Recursively walk HIR nodes to find all string literals and register them
   * as string constants in .rdata.
   */
  private collectStrings(nodes: HirExpr[]): void {
    for (const node of nodes) {
      switch (node.type) {
        case 'String': {
          const existing = this.stringMap.get(node.value);
          if (existing !== undefined) return;
          const idx = this.stringConstants.length;
          this.stringMap.set(node.value, idx);
          // Encode as UTF-8 with NUL terminator
          const encoder = new TextEncoder();
          const encoded = encoder.encode(node.value);
          const data = new Uint8Array(encoded.length + 1);
          data.set(encoded);
          data[encoded.length] = 0; // NUL terminator
          this.stringConstants.push({
            label: `__str_${idx}`,
            data,
            rdataOffset: 0,
          });
          break;
        }
        case 'Binary':
          this.collectStrings([node.left, node.right]);
          break;
        case 'Unary':
          this.collectStrings([node.operand]);
          break;
        case 'Call':
          this.collectStrings([node.callee, ...node.args]);
          break;
        case 'Ternary':
          this.collectStrings([node.cond, node.thenExpr, node.elseExpr]);
          break;
        case 'Function':
          this.collectStrings(node.body);
          break;
        case 'Return':
          if (node.value) this.collectStrings([node.value]);
          break;
        case 'Var':
          if (node.init) this.collectStrings([node.init]);
          break;
        case 'If':
          this.collectStrings([node.cond, ...node.thenBody, ...(node.elseBody ?? [])]);
          break;
        case 'While':
          this.collectStrings([node.cond, ...node.body]);
          break;
        case 'For':
          if (node.init) this.collectStrings([node.init]);
          if (node.cond) this.collectStrings([node.cond]);
          if (node.update) this.collectStrings([node.update]);
          this.collectStrings(node.body);
          break;
        case 'Block':
          this.collectStrings(node.body);
          break;
        case 'Assign':
          this.collectStrings([node.target, node.value]);
          break;
        case 'Array':
          this.collectStrings(node.elements);
          break;
        case 'Index':
          this.collectStrings([node.object, node.index]);
          break;
        case 'Object':
          for (const [, val] of node.properties) this.collectStrings([val]);
          break;
        case 'Property':
          this.collectStrings([node.object]);
          break;
        case 'ExprStmt':
          this.collectStrings([node.expr]);
          break;
        case 'TryCatch':
          this.collectStrings(node.tryBody);
          if (node.catchBody) this.collectStrings(node.catchBody);
          if (node.finallyBody) this.collectStrings(node.finallyBody);
          break;
        case 'Throw':
          if (node.value) this.collectStrings([node.value]);
          break;
        case 'New':
          this.collectStrings(node.args);
          break;
        // Number, Boolean, Null, Undefined, Identifier, Break, Continue,
        // Regex, Class, This have no string children to collect.
      }
    }
  }

  // ========================================================================
  // Pass 2 -- emit machine code
  // ========================================================================

  private pass2(): void {
    for (const fn of this.functions) {
      fn.textOffset = this.textBuf.pos();
      this.emitFunction(fn.node);
    }

    // Write .rdata section
    for (const str of this.stringConstants) {
      for (let i = 0; i < str.data.length; i++) {
        this.rdataBuf.emitByte(str.data[i]);
      }
    }
    // Pad to 4-byte alignment
    while (this.rdataBuf.pos() % 4 !== 0) {
      this.rdataBuf.emitByte(0);
    }
  }

  /**
   * Emit a complete function: prologue, body, epilogue.
   */
  private emitFunction(fn: HirExpr & { type: 'Function' }): void {
    const scope = new FunctionScope(fn.params);

    // Pre-scan the body to allocate all local variable slots
    this.preAllocateLocals(fn.body, scope);
    scope.finalize();

    // -- Prologue --
    emitPushRbp(this.textBuf);
    emitMovRbpRsp(this.textBuf);
    emitSubRspImm8(this.textBuf, scope.frameSize);

    // Save incoming xmm parameters to their stack slots
    // (Windows x64 passes first 4 float args in xmm0-xmm3)
    for (let i = 0; i < Math.min(fn.params.length, 4); i++) {
      const paramOffset = 0x10 + i * 8; // rbp-relative
      switch (i) {
        case 0:
          emitMovqRbpPosDisp8Xmm0(this.textBuf, paramOffset);
          break;
        case 1:
          // movq [rbp+disp8], xmm1: F2 0F 7E 4D disp8
          this.textBuf.emitBytes(0xf2, 0x0f, 0x7e, 0x4d, paramOffset & 0xff);
          break;
        case 2:
          // movq [rbp+disp8], xmm2: F2 0F 7E 55 disp8
          this.textBuf.emitBytes(0xf2, 0x0f, 0x7e, 0x55, paramOffset & 0xff);
          break;
        case 3:
          // movq [rbp+disp8], xmm3: F2 0F 7E 5D disp8
          this.textBuf.emitBytes(0xf2, 0x0f, 0x7e, 0x5d, paramOffset & 0xff);
          break;
      }
    }

    // For params beyond the first 4, they arrive on the stack at
    // [rbp+0x30], [rbp+0x38], etc.  They are already in place.

    // -- Body --
    // We use a simple "last value in xmm0" calling convention for expressions.
    // Statements are executed for side effects.
    this.emitBody(fn.body, scope);

    // -- Default return (undefined) --
    emitLoadNanBoxedImm(this.textBuf, UNDEFINED);

    // -- Epilogue --
    emitMovRspRbp(this.textBuf);
    emitPopRbp(this.textBuf);
    emitRet(this.textBuf);
  }

  /**
   * Pre-scan the function body to allocate local variable slots before code
   * generation starts.  This ensures all offsets are known up front.
   */
  private preAllocateLocals(nodes: HirExpr[], scope: FunctionScope): void {
    for (const node of nodes) {
      switch (node.type) {
        case 'Var':
          scope.allocLocal(node.name);
          break;
        case 'Function':
          if (node.name) scope.allocLocal(node.name);
          this.preAllocateLocals(node.body, scope);
          break;
        case 'If':
          this.preAllocateLocals(node.thenBody, scope);
          if (node.elseBody) this.preAllocateLocals(node.elseBody, scope);
          break;
        case 'While':
          this.preAllocateLocals(node.body, scope);
          break;
        case 'For':
          if (node.init) this.preAllocateLocals([node.init], scope);
          this.preAllocateLocals(node.body, scope);
          break;
        case 'Block':
          this.preAllocateLocals(node.body, scope);
          break;
        case 'TryCatch':
          this.preAllocateLocals(node.tryBody, scope);
          if (node.catchBody) this.preAllocateLocals(node.catchBody, scope);
          if (node.finallyBody) this.preAllocateLocals(node.finallyBody, scope);
          break;
        case 'ExprStmt':
          this.preAllocateLocals([node.expr], scope);
          break;
        case 'Return':
          if (node.value) this.preAllocateLocals([node.value], scope);
          break;
        case 'Assign':
          this.preAllocateLocals([node.target, node.value], scope);
          break;
        case 'Binary':
          this.preAllocateLocals([node.left, node.right], scope);
          break;
        case 'Unary':
          this.preAllocateLocals([node.operand], scope);
          break;
        case 'Call':
          this.preAllocateLocals([node.callee, ...node.args], scope);
          break;
        case 'Ternary':
          this.preAllocateLocals([node.cond, node.thenExpr, node.elseExpr], scope);
          break;
        case 'Array':
          this.preAllocateLocals(node.elements, scope);
          break;
        case 'Index':
          this.preAllocateLocals([node.object, node.index], scope);
          break;
        case 'Object':
          for (const [, val] of node.properties) this.preAllocateLocals([val], scope);
          break;
        case 'Property':
          this.preAllocateLocals([node.object], scope);
          break;
        case 'New':
          this.preAllocateLocals(node.args, scope);
          break;
        case 'Throw':
          if (node.value) this.preAllocateLocals([node.value], scope);
          break;
      }
    }
  }

  /**
   * Emit code for a list of statements.  The result of the last expression
   * is left in xmm0 (but typically discarded for pure statements).
   */
  private emitBody(nodes: HirExpr[], scope: FunctionScope): void {
    for (const node of nodes) {
      this.emitNode(node, scope);
    }
  }

  /**
   * Emit code for a single HIR node.  For expressions, the result is left
   * in xmm0.  For statements, xmm0 is undefined after execution.
   */
  private emitNode(node: HirExpr, scope: FunctionScope): void {
    switch (node.type) {
      case 'Number':
        this.emitNumber(node.value);
        break;

      case 'Boolean':
        emitLoadNanBoxedImm(this.textBuf, node.value ? TRUE_VAL : FALSE_VAL);
        break;

      case 'String':
        this.emitString(node.value);
        break;

      case 'Null':
        emitLoadNanBoxedImm(this.textBuf, NULL_VAL);
        break;

      case 'Undefined':
        emitLoadNanBoxedImm(this.textBuf, UNDEFINED);
        break;

      case 'Identifier':
        this.emitIdentifier(node.name, scope);
        break;

      case 'Binary':
        this.emitBinary(node, scope);
        break;

      case 'Unary':
        this.emitUnary(node, scope);
        break;

      case 'Call':
        this.emitCall(node, scope);
        break;

      case 'Ternary':
        this.emitTernary(node, scope);
        break;

      case 'Function':
        // Nested function -- for MVP, just load undefined
        emitLoadNanBoxedImm(this.textBuf, UNDEFINED);
        break;

      case 'Return':
        this.emitReturn(node, scope);
        break;

      case 'Var':
        this.emitVar(node, scope);
        break;

      case 'If':
        this.emitIf(node, scope);
        break;

      case 'While':
        this.emitWhile(node, scope);
        break;

      case 'For':
        this.emitFor(node, scope);
        break;

      case 'Block':
        this.emitBody(node.body, scope);
        break;

      case 'Break':
        // For MVP, emit int3 as a placeholder (break is handled by
        // control flow restructuring in a future optimization pass).
        emitInt3(this.textBuf);
        break;

      case 'Continue':
        emitInt3(this.textBuf);
        break;

      case 'Assign':
        this.emitAssign(node, scope);
        break;

      case 'Array':
        this.emitArray(node, scope);
        break;

      case 'Index':
        this.emitIndex(node, scope);
        break;

      case 'Object':
        this.emitObject(node, scope);
        break;

      case 'Property':
        this.emitProperty(node, scope);
        break;

      case 'ExprStmt':
        this.emitNode(node.expr, scope);
        break;

      case 'TryCatch':
        this.emitTryCatch(node, scope);
        break;

      case 'Throw':
        this.emitThrow(node, scope);
        break;

      case 'New':
        this.emitNew(node, scope);
        break;

      case 'This':
        // For MVP, load undefined
        emitLoadNanBoxedImm(this.textBuf, UNDEFINED);
        break;

      case 'Regex':
        // For MVP, load undefined (regex support is future work)
        emitLoadNanBoxedImm(this.textBuf, UNDEFINED);
        break;

      case 'Class':
        // For MVP, classes are not fully supported -- load undefined
        emitLoadNanBoxedImm(this.textBuf, UNDEFINED);
        break;
    }
  }

  // -----------------------------------------------------------------------
  // Expression emitters (result in xmm0)
  // -----------------------------------------------------------------------

  /**
   * Emit a number literal.  Loads the IEEE 754 bit pattern of the double
   * into xmm0 via rax.
   */
  private emitNumber(value: number): void {
    const bits = doubleToUint64BigInt(value);
    emitLoadNanBoxedImm(this.textBuf, bits);
  }

  /**
   * Emit a string literal.  Calls `js_string_from_static` with a pointer to
   * the string data in .rdata.  The result (NaN-boxed string) is in xmm0.
   */
  private emitString(value: string): void {
    const strIdx = this.stringMap.get(value);
    if (strIdx === undefined) {
      emitLoadNanBoxedImm(this.textBuf, UNDEFINED);
      return;
    }

    // lea rax, [rip+__str_N]
    const fixup = emitLeaRaxRipDisp32(this.textBuf);
    this.addReloc(fixup, this.getStringSymbolIndex(strIdx));

    // mov rcx, rax  (first integer arg = pointer)
    emitMovRcxRax(this.textBuf);

    // call [rip+__imp_js_string_from_static]
    const callFixup = emitCallRipDisp32(this.textBuf);
    this.addReloc(callFixup, this.getImportSymbolIndex('js_string_from_static'));
  }

  /**
   * Emit an identifier reference.  Loads the NaN-boxed value from the
   * variable's stack slot into xmm0.
   */
  private emitIdentifier(name: string, scope: FunctionScope): void {
    const offset = scope.resolve(name);
    if (offset < 0) {
      emitMovqXmm0RbpNegDisp8(this.textBuf, offset);
    } else {
      emitMovqXmm0RbpPosDisp8(this.textBuf, offset);
    }
  }

  /**
   * Emit a binary expression.  For arithmetic/comparison ops, delegates to
   * runtime helpers to handle full JS semantics (type coercion, etc.).
   */
  private emitBinary(node: HirExpr & { type: 'Binary' }, scope: FunctionScope): void {
    // Short-circuit operators need special handling
    if (node.op === BinOp.And) {
      this.emitShortCircuitAnd(node, scope);
      return;
    }
    if (node.op === BinOp.Or) {
      this.emitShortCircuitOr(node, scope);
      return;
    }

    // Evaluate left operand -> xmm0
    this.emitNode(node.left, scope);

    // Save left on the stack
    emitMovqRspDisp8Xmm0(this.textBuf, 0x00);

    // Evaluate right operand -> xmm0
    this.emitNode(node.right, scope);

    // Save right on the stack
    emitMovqRspDisp8Xmm0(this.textBuf, 0x08);

    // Determine which runtime helper to call
    const helperName = this.getRuntimeHelper(node.op);
    if (helperName) {
      // Load left into xmm0
      emitMovqXmm0RspDisp8(this.textBuf, 0x00);
      // Load right into xmm1
      emitMovqXmm1RspDisp8(this.textBuf, 0x08);
      // Call the runtime helper
      const callFixup = emitCallRipDisp32(this.textBuf);
      this.addReloc(callFixup, this.getImportSymbolIndex(helperName));
    } else {
      // Unsupported binary op for MVP -- load undefined
      emitLoadNanBoxedImm(this.textBuf, UNDEFINED);
    }
  }

  /**
   * Emit `&&` (logical AND) with short-circuit evaluation.
   * If the left operand is falsy, the result is the left operand without
   * evaluating the right.
   */
  private emitShortCircuitAnd(node: HirExpr & { type: 'Binary' }, scope: FunctionScope): void {
    // Evaluate left -> xmm0
    this.emitNode(node.left, scope);

    // Save left on stack
    emitMovqRspDisp8Xmm0(this.textBuf, 0x00);

    // Test truthiness
    const callFixup = emitCallRipDisp32(this.textBuf);
    this.addReloc(callFixup, this.getImportSymbolIndex('js_is_truthy'));
    emitTestEaxEax(this.textBuf);

    // If falsy, jump to end (result = left)
    const jeFixup = emitJeRel32(this.textBuf);

    // Evaluate right -> xmm0 (this is the result if truthy)
    this.emitNode(node.right, scope);

    // Jump over the "load left" path
    const jmpFixup = emitJmpRel32(this.textBuf);

    // Falsy path: reload left
    const falseTarget = this.textBuf.pos();
    emitMovqXmm0RspDisp8(this.textBuf, 0x00);

    // End
    const endTarget = this.textBuf.pos();

    // Fixup jumps
    this.textBuf.fixupU32(jeFixup, falseTarget - (jeFixup + 4));
    this.textBuf.fixupU32(jmpFixup, endTarget - (jmpFixup + 4));
  }

  /**
   * Emit `||` (logical OR) with short-circuit evaluation.
   * If the left operand is truthy, the result is the left operand without
   * evaluating the right.
   */
  private emitShortCircuitOr(node: HirExpr & { type: 'Binary' }, scope: FunctionScope): void {
    // Evaluate left -> xmm0
    this.emitNode(node.left, scope);

    // Save left on stack
    emitMovqRspDisp8Xmm0(this.textBuf, 0x00);

    // Test truthiness
    const callFixup = emitCallRipDisp32(this.textBuf);
    this.addReloc(callFixup, this.getImportSymbolIndex('js_is_truthy'));
    emitTestEaxEax(this.textBuf);

    // If truthy, jump to end (result = left)
    const jneFixup = emitJneRel32(this.textBuf);

    // Evaluate right -> xmm0 (this is the result if falsy)
    this.emitNode(node.right, scope);

    // Jump over the "load left" path
    const jmpFixup = emitJmpRel32(this.textBuf);

    // Truthy path: reload left
    const trueTarget = this.textBuf.pos();
    emitMovqXmm0RspDisp8(this.textBuf, 0x00);

    // End
    const endTarget = this.textBuf.pos();

    // Fixup jumps
    this.textBuf.fixupU32(jneFixup, trueTarget - (jneFixup + 4));
    this.textBuf.fixupU32(jmpFixup, endTarget - (jmpFixup + 4));
  }

  /**
   * Emit a unary expression.
   */
  private emitUnary(node: HirExpr & { type: 'Unary' }, scope: FunctionScope): void {
    switch (node.op) {
      case UnaryOp.Not: {
        // Evaluate operand -> xmm0
        this.emitNode(node.operand, scope);
        // Save on stack
        emitMovqRspDisp8Xmm0(this.textBuf, 0x00);
        // Call js_is_truthy
        const callFixup = emitCallRipDisp32(this.textBuf);
        this.addReloc(callFixup, this.getImportSymbolIndex('js_is_truthy'));
        // Negate the result: if 0 set to 1, if nonzero set to 0
        emitTestEaxEax(this.textBuf);
        emitXorEaxEax(this.textBuf);
        const jeFixup = emitJeRel32(this.textBuf);
        emitMovEaxImm32(this.textBuf, 1);
        const endTarget = this.textBuf.pos();
        this.textBuf.fixupU32(jeFixup, endTarget - (jeFixup + 4));
        // Convert i32 result (0 or 1) to NaN-boxed boolean
        emitCvtsi2sdXmm0Rax(this.textBuf);
        break;
      }

      case UnaryOp.Neg: {
        // Evaluate operand -> xmm0
        this.emitNode(node.operand, scope);
        // Negate: xorpd xmm1, xmm1; subsd xmm0, xmm1 (xmm0 = 0.0 - xmm0)
        emitXorpdXmm1Xmm1(this.textBuf);
        emitSubsdXmm0Xmm1(this.textBuf);
        break;
      }

      case UnaryOp.TypeOf: {
        this.emitNode(node.operand, scope);
        const callFixup = emitCallRipDisp32(this.textBuf);
        this.addReloc(callFixup, this.getImportSymbolIndex('js_typeof'));
        break;
      }

      case UnaryOp.PreInc:
      case UnaryOp.PreDec: {
        // Evaluate operand -> xmm0 (must be an identifier)
        this.emitNode(node.operand, scope);
        // Save original value
        emitMovqRspDisp8Xmm0(this.textBuf, 0x00);
        // Load 1.0 into xmm1
        emitLoadNanBoxedImm(this.textBuf, doubleToUint64BigInt(1.0));
        if (node.op === UnaryOp.PreInc) {
          // xmm0 = original, xmm1 = 1.0 -> xmm0 = original + 1.0
          emitAddsdXmm0Xmm1(this.textBuf);
        } else {
          // xmm0 = 1.0 (from load), xmm1 = original (on stack)
          emitMovqXmm1RspDisp8(this.textBuf, 0x00);
          emitSubsdXmm1Xmm0(this.textBuf); // xmm1 = original - 1.0
          emitMovsdXmm0Xmm1(this.textBuf);
        }
        // Store incremented/decremented value back
        this.emitStoreToTarget(node.operand, scope);
        break;
      }

      case UnaryOp.PostInc:
      case UnaryOp.PostDec: {
        // Evaluate operand -> xmm0
        this.emitNode(node.operand, scope);
        // Save original value (this is the result of post-inc/dec)
        emitMovqRspDisp8Xmm0(this.textBuf, 0x00);
        // Load 1.0
        emitLoadNanBoxedImm(this.textBuf, doubleToUint64BigInt(1.0));
        if (node.op === UnaryOp.PostInc) {
          // xmm0 = 1.0, xmm1 = original (on stack)
          emitMovqXmm1RspDisp8(this.textBuf, 0x00);
          emitAddsdXmm1Xmm0(this.textBuf); // xmm1 = original + 1.0
          emitMovsdXmm0Xmm1(this.textBuf);
        } else {
          // xmm0 = 1.0, xmm1 = original (on stack)
          emitMovqXmm1RspDisp8(this.textBuf, 0x00);
          emitSubsdXmm1Xmm0(this.textBuf); // xmm1 = original - 1.0
          emitMovsdXmm0Xmm1(this.textBuf);
        }
        // Store incremented/decremented value back
        this.emitStoreToTarget(node.operand, scope);
        // Restore original value as the expression result
        emitMovqXmm0RspDisp8(this.textBuf, 0x00);
        break;
      }
    }
  }

  /**
   * Emit a function call.
   */
  private emitCall(node: HirExpr & { type: 'Call' }, scope: FunctionScope): void {
    // Check if this is console.log
    if (node.callee.type === 'Property' &&
        node.callee.object.type === 'Identifier' &&
        node.callee.object.name === 'console' &&
        node.callee.name === 'log') {
      this.emitConsoleLog(node.args, scope);
      return;
    }

    // Check if this is console.print
    if (node.callee.type === 'Property' &&
        node.callee.object.type === 'Identifier' &&
        node.callee.object.name === 'console' &&
        node.callee.name === 'print') {
      this.emitConsolePrint(node.args, scope);
      return;
    }

    // Evaluate all arguments and store them in temporary stack slots
    const args = node.args;
    for (let i = 0; i < args.length; i++) {
      this.emitNode(args[i], scope);
      const tempSlot = 0x40 + i * 8; // use space above shadow area
      emitMovqRspDisp8Xmm0(this.textBuf, tempSlot);
    }

    // Load xmm registers from temporaries (first 4 args)
    for (let i = 0; i < Math.min(args.length, 4); i++) {
      const tempSlot = 0x40 + i * 8;
      switch (i) {
        case 0:
          emitMovqXmm0RspDisp8(this.textBuf, tempSlot);
          break;
        case 1:
          emitMovqXmm1RspDisp8(this.textBuf, tempSlot);
          break;
        case 2:
          emitMovqXmm2RspDisp8(this.textBuf, tempSlot);
          break;
        case 3:
          emitMovqXmm3RspDisp8(this.textBuf, tempSlot);
          break;
      }
    }

    // Push stack arguments (args 5+)
    const numStackArgs = Math.max(0, args.length - 4);
    if (numStackArgs > 0) {
      emitSubRspImm8(this.textBuf, numStackArgs * 8);
      for (let i = args.length - 1; i >= 4; i--) {
        const tempSlot = 0x40 + i * 8;
        // Load from adjusted rsp position
        emitMovRaxRspDisp8(this.textBuf, tempSlot + numStackArgs * 8);
        emitMovRspDisp8Rax(this.textBuf, (i - 4) * 8);
      }
    }

    // Determine if this is a call to a user-defined function or a runtime import
    if (node.callee.type === 'Identifier') {
      const fnName = node.callee.name;
      const fnInfo = this.functions.find(f => f.node.name === fnName);

      if (fnInfo) {
        // Direct call to user-defined function
        const callFixup = emitCallRel32(this.textBuf);
        this.addReloc(callFixup, this.getFunctionSymbolIndex(fnName));
      } else {
        // Might be a runtime function called directly
        const importIdx = this.importNames.indexOf(fnName);
        if (importIdx >= 0) {
          const callFixup = emitCallRipDisp32(this.textBuf);
          this.addReloc(callFixup, this.getImportSymbolIndex(fnName));
        } else {
          // Unknown function -- emit a stub (int3)
          emitInt3(this.textBuf);
        }
      }
    } else {
      // Complex callee expression (method call, etc.)
      // For MVP, this is a placeholder
      emitInt3(this.textBuf);
    }

    // Clean up stack arguments
    if (numStackArgs > 0) {
      emitAddRspImm8(this.textBuf, numStackArgs * 8);
    }
  }

  /**
   * Emit a ternary expression: `cond ? thenExpr : elseExpr`.
   */
  private emitTernary(node: HirExpr & { type: 'Ternary' }, scope: FunctionScope): void {
    // Evaluate condition -> xmm0
    this.emitNode(node.cond, scope);

    // Test truthiness
    emitMovqRspDisp8Xmm0(this.textBuf, 0x00);
    const callFixup = emitCallRipDisp32(this.textBuf);
    this.addReloc(callFixup, this.getImportSymbolIndex('js_is_truthy'));
    emitTestEaxEax(this.textBuf);

    // Jump to else if falsy
    const jeFixup = emitJeRel32(this.textBuf);

    // Evaluate then expression -> xmm0
    this.emitNode(node.thenExpr, scope);

    // Jump past else
    const jmpFixup = emitJmpRel32(this.textBuf);

    // Else branch
    const elseTarget = this.textBuf.pos();
    this.emitNode(node.elseExpr, scope);

    // End
    const endTarget = this.textBuf.pos();

    // Fixup jumps
    this.textBuf.fixupU32(jeFixup, elseTarget - (jeFixup + 4));
    this.textBuf.fixupU32(jmpFixup, endTarget - (jmpFixup + 4));
  }

  // -----------------------------------------------------------------------
  // Statement emitters
  // -----------------------------------------------------------------------

  /**
   * Emit a return statement.
   */
  private emitReturn(node: HirExpr & { type: 'Return' }, scope: FunctionScope): void {
    if (node.value) {
      this.emitNode(node.value, scope);
    } else {
      emitLoadNanBoxedImm(this.textBuf, UNDEFINED);
    }

    // Epilogue
    emitMovRspRbp(this.textBuf);
    emitPopRbp(this.textBuf);
    emitRet(this.textBuf);
  }

  /**
   * Emit a variable declaration with optional initializer.
   */
  private emitVar(node: HirExpr & { type: 'Var' }, scope: FunctionScope): void {
    if (node.init) {
      this.emitNode(node.init, scope);
    } else {
      emitLoadNanBoxedImm(this.textBuf, UNDEFINED);
    }
    // Store to the local slot
    const offset = scope.resolve(node.name);
    emitMovqRbpNegDisp8Xmm0(this.textBuf, offset);
  }

  /**
   * Emit an if/else statement.
   */
  private emitIf(node: HirExpr & { type: 'If' }, scope: FunctionScope): void {
    // Evaluate condition -> xmm0
    this.emitNode(node.cond, scope);

    // Test truthiness
    emitMovqRspDisp8Xmm0(this.textBuf, 0x00);
    const callFixup = emitCallRipDisp32(this.textBuf);
    this.addReloc(callFixup, this.getImportSymbolIndex('js_is_truthy'));
    emitTestEaxEax(this.textBuf);

    // Jump to else (or end) if falsy
    const jeFixup = emitJeRel32(this.textBuf);

    // Then body
    this.emitBody(node.thenBody, scope);

    if (node.elseBody && node.elseBody.length > 0) {
      // Jump past else
      const jmpFixup = emitJmpRel32(this.textBuf);

      // Else body
      const elseTarget = this.textBuf.pos();
      this.emitBody(node.elseBody, scope);

      // End
      const endTarget = this.textBuf.pos();
      this.textBuf.fixupU32(jeFixup, elseTarget - (jeFixup + 4));
      this.textBuf.fixupU32(jmpFixup, endTarget - (jmpFixup + 4));
    } else {
      // No else -- jump target is the end
      const endTarget = this.textBuf.pos();
      this.textBuf.fixupU32(jeFixup, endTarget - (jeFixup + 4));
    }
  }

  /**
   * Emit a while loop.
   */
  private emitWhile(node: HirExpr & { type: 'While' }, scope: FunctionScope): void {
    const loopHeader = this.textBuf.pos();

    // Evaluate condition -> xmm0
    this.emitNode(node.cond, scope);

    // Test truthiness
    emitMovqRspDisp8Xmm0(this.textBuf, 0x00);
    const callFixup = emitCallRipDisp32(this.textBuf);
    this.addReloc(callFixup, this.getImportSymbolIndex('js_is_truthy'));
    emitTestEaxEax(this.textBuf);

    // Jump to exit if falsy
    const jeFixup = emitJeRel32(this.textBuf);

    // Loop body
    this.emitBody(node.body, scope);

    // Jump back to header
    const jmpFixup = emitJmpRel32(this.textBuf);
    this.textBuf.fixupU32(jmpFixup, loopHeader - (jmpFixup + 4));

    // Loop exit
    const exitTarget = this.textBuf.pos();
    this.textBuf.fixupU32(jeFixup, exitTarget - (jeFixup + 4));
  }

  /**
   * Emit a for loop.
   */
  private emitFor(node: HirExpr & { type: 'For' }, scope: FunctionScope): void {
    // Init
    if (node.init) {
      this.emitNode(node.init, scope);
    }

    const loopHeader = this.textBuf.pos();

    // Condition
    let condJeFixup = -1;
    if (node.cond) {
      this.emitNode(node.cond, scope);
      emitMovqRspDisp8Xmm0(this.textBuf, 0x00);
      const callFixup = emitCallRipDisp32(this.textBuf);
      this.addReloc(callFixup, this.getImportSymbolIndex('js_is_truthy'));
      emitTestEaxEax(this.textBuf);
      condJeFixup = emitJeRel32(this.textBuf);
    }

    // Body
    this.emitBody(node.body, scope);

    // Update
    if (node.update) {
      this.emitNode(node.update, scope);
    }

    // Jump back to header
    const jmpFixup = emitJmpRel32(this.textBuf);
    this.textBuf.fixupU32(jmpFixup, loopHeader - (jmpFixup + 4));

    // Exit
    const exitTarget = this.textBuf.pos();
    if (condJeFixup >= 0) {
      this.textBuf.fixupU32(condJeFixup, exitTarget - (condJeFixup + 4));
    }
  }

  /**
   * Emit an assignment: `target = value`.
   */
  private emitAssign(node: HirExpr & { type: 'Assign' }, scope: FunctionScope): void {
    // Evaluate the value -> xmm0
    this.emitNode(node.value, scope);

    // Store to the target
    this.emitStoreToTarget(node.target, scope);
  }

  /**
   * Store xmm0 to a target (identifier or property).
   */
  private emitStoreToTarget(target: HirExpr, scope: FunctionScope): void {
    if (target.type === 'Identifier') {
      const offset = scope.resolve(target.name);
      emitMovqRbpNegDisp8Xmm0(this.textBuf, offset);
    } else if (target.type === 'Property') {
      // Object property assignment: obj.name = value
      // For MVP, save value, eval object, call js_set_property
      emitMovqRspDisp8Xmm0(this.textBuf, 0x10); // save value

      // Re-evaluate object
      this.emitNode(target.object, scope);
      emitMovqRspDisp8Xmm0(this.textBuf, 0x18); // save object

      // Load value into xmm0
      emitMovqXmm0RspDisp8(this.textBuf, 0x10);
      // Load object into xmm1
      emitMovqXmm1RspDisp8(this.textBuf, 0x18);

      // For MVP, skip actual property set -- just leave value in xmm0
    } else if (target.type === 'Index') {
      // Array/object index assignment: obj[key] = value
      // For MVP, leave value in xmm0
    }
  }

  /**
   * Emit an array literal.
   */
  private emitArray(node: HirExpr & { type: 'Array' }, scope: FunctionScope): void {
    // For MVP, call js_alloc_array(count)
    emitMovEaxImm32(this.textBuf, node.elements.length);
    emitCvtsi2sdXmm0Rax(this.textBuf);

    // Call js_alloc_array
    const callFixup = emitCallRipDisp32(this.textBuf);
    this.addReloc(callFixup, this.getImportSymbolIndex('js_alloc_array'));
  }

  /**
   * Emit an index access: `object[index]`.
   */
  private emitIndex(node: HirExpr & { type: 'Index' }, scope: FunctionScope): void {
    // Evaluate object -> xmm0
    this.emitNode(node.object, scope);
    emitMovqRspDisp8Xmm0(this.textBuf, 0x00);

    // Evaluate index -> xmm0
    this.emitNode(node.index, scope);
    emitMovqRspDisp8Xmm0(this.textBuf, 0x08);

    // Restore object to xmm0
    emitMovqXmm0RspDisp8(this.textBuf, 0x00);
    // Load index to xmm1
    emitMovqXmm1RspDisp8(this.textBuf, 0x08);

    // Call js_get_element(object, index)
    const callFixup = emitCallRipDisp32(this.textBuf);
    this.addReloc(callFixup, this.getImportSymbolIndex('js_get_element'));
  }

  /**
   * Emit an object literal.
   */
  private emitObject(node: HirExpr & { type: 'Object' }, scope: FunctionScope): void {
    // For MVP, call js_alloc_object(count)
    emitMovEaxImm32(this.textBuf, node.properties.length);
    emitCvtsi2sdXmm0Rax(this.textBuf);

    const callFixup = emitCallRipDisp32(this.textBuf);
    this.addReloc(callFixup, this.getImportSymbolIndex('js_alloc_object'));
  }

  /**
   * Emit a property access: `object.name`.
   */
  private emitProperty(node: HirExpr & { type: 'Property' }, scope: FunctionScope): void {
    // Evaluate object -> xmm0
    this.emitNode(node.object, scope);

    // Call js_get_property(object, name)
    // First, get the property name string
    const strIdx = this.getStringIndex(node.name);
    if (strIdx >= 0) {
      // Save object on stack
      emitMovqRspDisp8Xmm0(this.textBuf, 0x00);

      // lea rax, [rip+__str_N]
      const leaFixup = emitLeaRaxRipDisp32(this.textBuf);
      this.addReloc(leaFixup, this.getStringSymbolIndex(strIdx));
      emitMovRcxRax(this.textBuf); // rcx = name pointer

      // Restore object to xmm0
      emitMovqXmm0RspDisp8(this.textBuf, 0x00);

      // Call js_get_property
      const callFixup = emitCallRipDisp32(this.textBuf);
      this.addReloc(callFixup, this.getImportSymbolIndex('js_get_property'));
    } else {
      emitLoadNanBoxedImm(this.textBuf, UNDEFINED);
    }
  }

  /**
   * Emit a try/catch/finally block.
   * For MVP, this is simplified -- just emit the try body.
   */
  private emitTryCatch(node: HirExpr & { type: 'TryCatch' }, scope: FunctionScope): void {
    // For MVP, just emit the try body
    this.emitBody(node.tryBody, scope);
    if (node.finallyBody) {
      this.emitBody(node.finallyBody, scope);
    }
  }

  /**
   * Emit a throw statement.
   */
  private emitThrow(node: HirExpr & { type: 'Throw' }, scope: FunctionScope): void {
    if (node.value) {
      this.emitNode(node.value, scope);
    } else {
      emitLoadNanBoxedImm(this.textBuf, UNDEFINED);
    }
    // Call js_throw
    const callFixup = emitCallRipDisp32(this.textBuf);
    this.addReloc(callFixup, this.getImportSymbolIndex('js_throw'));
  }

  /**
   * Emit a `new ClassName(args)` expression.
   */
  private emitNew(node: HirExpr & { type: 'New' }, scope: FunctionScope): void {
    // For MVP, call js_new with the class name hash
    const hash = fnv1a64(node.className);
    emitMovabsRaxImm64(this.textBuf, hash);
    emitMovRcxRax(this.textBuf); // rcx = class name hash

    // Evaluate args and store on stack
    for (let i = 0; i < node.args.length; i++) {
      this.emitNode(node.args[i], scope);
      emitMovqRspDisp8Xmm0(this.textBuf, 0x10 + i * 8);
    }

    // Call js_new
    const callFixup = emitCallRipDisp32(this.textBuf);
    this.addReloc(callFixup, this.getImportSymbolIndex('js_new'));
  }

  // -----------------------------------------------------------------------
  // Built-in emitters
  // -----------------------------------------------------------------------

  /**
   * Emit `console.log(...)` -- calls `console_log` for each argument.
   */
  private emitConsoleLog(args: HirExpr[], scope: FunctionScope): void {
    for (const arg of args) {
      this.emitNode(arg, scope);
      const callFixup = emitCallRipDisp32(this.textBuf);
      this.addReloc(callFixup, this.getImportSymbolIndex('console_log'));
    }
    // Result is undefined
    emitLoadNanBoxedImm(this.textBuf, UNDEFINED);
  }

  /**
   * Emit `console.print(...)` -- calls `js_print` for the single argument.
   */
  private emitConsolePrint(args: HirExpr[], scope: FunctionScope): void {
    if (args.length > 0) {
      this.emitNode(args[0], scope);
      const callFixup = emitCallRipDisp32(this.textBuf);
      this.addReloc(callFixup, this.getImportSymbolIndex('js_print'));
    }
    emitLoadNanBoxedImm(this.textBuf, UNDEFINED);
  }

  // -----------------------------------------------------------------------
  // Helpers
  // -----------------------------------------------------------------------

  /**
   * Add a pending relocation to be resolved during COFF finalization.
   */
  private addReloc(offset: number, symbolIndex: number): void {
    this.pendingRelocs.push({
      offset,
      symbolIndex,
      type: IMAGE_REL_AMD64_REL32,
    });
  }

  /**
   * Get the symbol table index for a string constant.
   */
  private getStringSymbolIndex(strIdx: number): number {
    // Symbol table layout:
    //   [0]: .text section symbol
    //   [1]: .rdata section symbol
    //   [2..2+N-1]: function symbols
    //   [2+N..2+N+M-1]: string constant symbols
    //   [2+N+M..]: import symbols
    const numFunctions = this.functions.length;
    return 2 + numFunctions + strIdx;
  }

  /**
   * Get the symbol table index for a function.
   */
  private getFunctionSymbolIndex(name: string): number {
    const numFunctions = this.functions.length;
    for (let i = 0; i < numFunctions; i++) {
      if (this.functions[i].node.name === name) {
        return 2 + i;
      }
    }
    return 0; // fallback (should not happen)
  }

  /**
   * Get the symbol table index for a runtime import.
   */
  private getImportSymbolIndex(name: string): number {
    const numFunctions = this.functions.length;
    const numStrings = this.stringConstants.length;
    const base = 2 + numFunctions + numStrings;
    const idx = this.importNames.indexOf(name);
    if (idx >= 0) return base + idx;
    return 0; // fallback
  }

  /**
   * Get the string constant index for a given string value.
   */
  private getStringIndex(value: string): number {
    return this.stringMap.get(value) ?? -1;
  }

  /**
   * Map a BinOp to the corresponding runtime helper name.
   * Returns `null` if no helper exists.
   */
  private getRuntimeHelper(op: BinOp): string | null {
    switch (op) {
      case BinOp.Add: return 'js_add';
      case BinOp.Sub: return 'js_sub';
      case BinOp.Mul: return 'js_mul';
      case BinOp.Div: return 'js_div';
      case BinOp.Mod: return 'js_mod';
      case BinOp.Eq:  return 'js_eq';
      case BinOp.Ne:  return 'js_neq';
      case BinOp.Lt:  return 'js_lt';
      case BinOp.Gt:  return 'js_gt';
      case BinOp.Le:  return 'js_le';
      case BinOp.Ge:  return 'js_ge';
      case BinOp.BitAnd: return 'js_bit_and';
      case BinOp.BitOr:  return 'js_bit_or';
      case BinOp.BitXor: return 'js_bit_xor';
      case BinOp.Shl:    return 'js_shl';
      case BinOp.Shr:    return 'js_shr';
      case BinOp.InstanceOf: return 'js_instanceof';
      case BinOp.In:     return 'js_in';
      default: return null;
    }
  }

  // ========================================================================
  // COFF .obj file construction
  // ========================================================================

  /**
   * Build the complete COFF .obj binary from the emitted sections,
   * symbols, and relocations.
   */
  private buildCoff(): Uint8Array {
    const textData = this.textBuf.toUint8Array();
    const rdataData = this.rdataBuf.toUint8Array();

    // -- Build symbol table --
    this.buildSymbolTable();

    const numSymbols = this.symbolNames.length;

    // -- Build string table --
    const stringTable = this.buildStringTable();

    // -- Pad sections to 4-byte alignment --
    const textPadded = padTo4(textData);
    const rdataPadded = padTo4(rdataData);

    // -- Compute offsets --
    const headerSize = 20;
    const sectionHeadersSize = 40 * 2; // .text + .rdata
    const textStart = headerSize + sectionHeadersSize;
    const rdataStart = textStart + textPadded.length;

    // .text relocations
    const textRelocs = this.pendingRelocs;
    const textRelocBytes = textRelocs.length * 10;
    const textRelocStart = rdataStart + rdataPadded.length;

    // .rdata relocations (none for now)
    const rdataRelocStart = textRelocStart + textRelocBytes;

    const symbolTableStart = rdataRelocStart;
    const symbolTableSize = numSymbols * 18;

    // -- Build the output buffer --
    const out = new CodeBuffer();

    // == COFF Header (20 bytes) ==
    out.emitU16(IMAGE_FILE_MACHINE_AMD64);     // Machine
    out.emitU16(2);                              // NumberOfSections
    out.emitU32(0);                              // TimeDateStamp
    out.emitU32(symbolTableStart);               // PointerToSymbolTable
    out.emitU32(numSymbols);                     // NumberOfSymbols
    out.emitU16(0);                              // SizeOfOptionalHeader
    out.emitU16(0);                              // Characteristics

    // == Section Header 1: .text (40 bytes) ==
    out.emitBytes(0x2e, 0x74, 0x65, 0x78, 0x74, 0x00, 0x00, 0x00); // ".text\0\0\0"
    out.emitU32(0);                              // VirtualSize
    out.emitU32(0);                              // VirtualAddress
    out.emitU32(textPadded.length);              // SizeOfRawData
    out.emitU32(textStart);                      // PointerToRawData
    out.emitU32(textRelocStart);                 // PointerToRelocations
    out.emitU32(0);                              // PointerToLinenumbers
    out.emitU16(textRelocs.length);              // NumberOfRelocations
    out.emitU16(0);                              // NumberOfLinenumbers
    out.emitU32(IMAGE_SCN_CNT_CODE | IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_READ);

    // == Section Header 2: .rdata (40 bytes) ==
    out.emitBytes(0x2e, 0x72, 0x64, 0x61, 0x74, 0x61, 0x00, 0x00); // ".rdata\0\0"
    out.emitU32(0);                              // VirtualSize
    out.emitU32(0);                              // VirtualAddress
    out.emitU32(rdataPadded.length);             // SizeOfRawData
    out.emitU32(rdataStart);                     // PointerToRawData
    out.emitU32(0);                              // PointerToRelocations (none)
    out.emitU32(0);                              // PointerToLinenumbers
    out.emitU16(0);                              // NumberOfRelocations
    out.emitU16(0);                              // NumberOfLinenumbers
    out.emitU32(IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ);

    // == .text section data ==
    for (let i = 0; i < textPadded.length; i++) {
      out.emitByte(textPadded[i]);
    }

    // == .rdata section data ==
    for (let i = 0; i < rdataPadded.length; i++) {
      out.emitByte(rdataPadded[i]);
    }

    // == .text relocations (10 bytes each) ==
    for (const reloc of textRelocs) {
      out.emitU32(reloc.offset);                 // VirtualAddress
      out.emitU32(reloc.symbolIndex);            // SymbolTableIndex
      out.emitU16(reloc.type);                   // Type
    }

    // == Symbol table (18 bytes per entry) ==
    for (let i = 0; i < numSymbols; i++) {
      this.writeSymbolEntry(out, i);
    }

    // == String table ==
    for (let i = 0; i < stringTable.length; i++) {
      out.emitByte(stringTable[i]);
    }

    return out.toUint8Array();
  }

  /**
   * Build the symbol table arrays from the collected functions, strings,
   * and imports.
   */
  private buildSymbolTable(): void {
    this.symbolNames = [];
    this.symbolSections = [];
    this.symbolValues = [];
    this.symbolTypes = [];
    this.symbolStorageClasses = [];
    this.symbolAuxCounts = [];

    // [0] .text section symbol
    this.addSymbol('', 0, 1, 0, IMAGE_SYM_CLASS_STATIC, 1);

    // [1] .rdata section symbol
    this.addSymbol('', 0, 2, 0, IMAGE_SYM_CLASS_STATIC, 1);

    // [2..N+1] Function symbols
    for (const fn of this.functions) {
      this.addSymbol(fn.node.name, fn.textOffset, 1, 0x20, IMAGE_SYM_CLASS_EXTERNAL, 0);
    }

    // [N+2..N+M+1] String constant symbols
    for (const str of this.stringConstants) {
      this.addSymbol(str.label, str.rdataOffset, 2, 0, IMAGE_SYM_CLASS_STATIC, 0);
    }

    // [N+M+2..] External import symbols
    for (const name of this.importNames) {
      this.addSymbol(`__imp_${name}`, 0, 0, 0, IMAGE_SYM_CLASS_EXTERNAL, 0);
    }
  }

  /**
   * Add a symbol to the symbol table arrays.
   */
  private addSymbol(
    name: string,
    value: number,
    section: number,
    type: number,
    storageClass: number,
    auxCount: number,
  ): void {
    this.symbolNames.push(name);
    this.symbolValues.push(value);
    this.symbolSections.push(section);
    this.symbolTypes.push(type);
    this.symbolStorageClasses.push(storageClass);
    this.symbolAuxCounts.push(auxCount);
  }

  /**
   * Write a single 18-byte COFF symbol table entry.
   */
  private writeSymbolEntry(buf: CodeBuffer, index: number): void {
    const name = this.symbolNames[index];

    // Name field (8 bytes): short name if <= 8 chars, else offset into string table
    if (name.length <= 8) {
      for (let i = 0; i < 8; i++) {
        buf.emitByte(i < name.length ? name.charCodeAt(i) : 0);
      }
    } else {
      // Long name: first 4 bytes are 0, next 4 bytes are offset into string table
      buf.emitU32(0);
      buf.emitU32(this.getStringTableOffset(name));
    }

    buf.emitU32(this.symbolValues[index]);         // Value
    buf.emitU16(this.symbolSections[index]);       // SectionNumber (signed i16)
    buf.emitU16(this.symbolTypes[index]);           // Type
    buf.emitByte(this.symbolStorageClasses[index]);  // StorageClass
    buf.emitByte(this.symbolAuxCounts[index]);      // NumberOfAuxSymbols
  }

  /**
   * Build the COFF string table.  The first 4 bytes are the total size
   * (including the size field itself).
   */
  private buildStringTable(): Uint8Array {
    // Collect all long names
    const longNames: string[] = [];
    for (const name of this.symbolNames) {
      if (name.length > 8) {
        longNames.push(name);
      }
    }

    // Calculate total size
    let totalSize = 4; // size field
    for (const name of longNames) {
      totalSize += name.length + 1; // NUL terminated
    }

    const table = new Uint8Array(totalSize);
    const view = new DataView(table.buffer);

    // Write total size
    view.setUint32(0, totalSize, true);

    // Write strings
    let offset = 4;
    for (const name of longNames) {
      for (let i = 0; i < name.length; i++) {
        table[offset + i] = name.charCodeAt(i);
      }
      table[offset + name.length] = 0; // NUL terminator
      offset += name.length + 1;
    }

    return table;
  }

  /**
   * Get the offset of a symbol name within the COFF string table.
   * Names <= 8 chars are stored inline and never appear in the string table.
   */
  private getStringTableOffset(name: string): number {
    let offset = 4; // skip the 4-byte size field
    for (const symName of this.symbolNames) {
      if (symName.length > 8) {
        if (symName === name) return offset;
        offset += symName.length + 1;
      }
    }
    return 0; // should never happen
  }
}

// ---------------------------------------------------------------------------
// Utility functions
// ---------------------------------------------------------------------------

/**
 * Convert a JavaScript number to its IEEE 754 double bit pattern as a BigInt.
 * This is used for loading NaN-boxed immediates into registers.
 *
 * Uses the `DataView` approach for portability across all runtimes.
 */
function doubleToUint64BigInt(value: number): bigint {
  const buf = new ArrayBuffer(8);
  const view = new DataView(buf);
  view.setFloat64(0, value, false); // big-endian
  const high = view.getUint32(0, false);
  const low = view.getUint32(4, false);
  return (BigInt(high) << 32n) | BigInt(low);
}

/**
 * Load a NaN-boxed 64-bit immediate value into xmm0.
 * The value is loaded into rax first, then moved to xmm0 via movq.
 */
function emitLoadNanBoxedImm(buf: CodeBuffer, bits: bigint): void {
  emitMovabsRaxImm64(buf, bits);
  emitMovqXmm0Rax(buf);
}

/**
 * Pad a Uint8Array to a 4-byte boundary by appending zero bytes.
 */
function padTo4(data: Uint8Array): Uint8Array {
  const remainder = data.length % 4;
  if (remainder === 0) return data;
  const padded = new Uint8Array(data.length + (4 - remainder));
  padded.set(data);
  return padded;
}

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------

/**
 * Compile HIR nodes into a COFF .obj file buffer.
 *
 * This is the main entry point used by the CLI and other tools.
 *
 * @param hir - Array of HIR nodes (typically function declarations).
 * @returns An object containing:
 *   - `objBuffer`: The COFF .obj file as a `Uint8Array`.
 *   - `errors`: An array of error strings (empty on success).
 *
 * @example
 * ```ts
 * import { parse } from './parser';
 * import { compileToObj } from './x64-emitter';
 *
 * const hir = parse(sourceCode);
 * const { objBuffer, errors } = compileToObj(hir);
 * if (errors.length === 0) {
 *   // objBuffer is a valid COFF .obj
 * }
 * ```
 */
export function compileToObj(hir: HirExpr[]): { objBuffer: Uint8Array; errors: string[] } {
  const errors: string[] = [];

  try {
    const emitter = new X64Emitter(hir);
    const objBuffer = emitter.emit();
    return { objBuffer, errors };
  } catch (e) {
    errors.push(String(e));
    return { objBuffer: new Uint8Array(0), errors };
  }
}
