// Chunk: the compiled unit of MoonJS bytecode. One Chunk per function template
// (or the top-level script). Owns its code array, constant pool, source
// location table, and nested function templates.
//
// The wide-prefix convention is the trickiest part of this file. It lets any
// opcode with a wide-capable operand carry either a 24-bit or a full 32-bit
// value:
//
//   Single-instruction form (operand fits in 24 bits unsigned, or 24 bits
//   signed for signed variants):
//       [op | HI | MID | LO]
//
//   Two-instruction form (operand needs the full 32 bits):
//       [WIDE | W_HI | W_MID | W_LO]
//       [op   | LO8  | 0     | 0    ]
//   The wide slot supplies the high 24 bits; the following instruction's
//   operand A byte supplies the low 8 bits. Result is a 32-bit unsigned (or
//   signed for signed variants) value.
//
// Every emit helper picks the narrowest form that fits, so downstream tools
// (disassembler, VM) only need to handle both cases on the read side —
// `Chunk::read_operand_u24` and `Chunk::read_operand_i24` do that.

///|
/// A compiled bytecode chunk plus its metadata. One per function template
/// (and one for the top-level script). Mutable — the compiler builds a
/// `Chunk` incrementally via the `emit_*` helpers, then hands it to the VM.
///
/// - `code`: 32-bit instructions, encoded via `encode` from `instr.mbt`.
/// - `const_pool`: run-time constant values referenced by `push_const` and
///   friends. Deduplication (`add_const`) uses `JSValue`'s `==`, so equal
///   primitives share one slot and distinct object references never dedup.
/// - `source_locs`: parallel to `code`; `source_locs[pc]` is the source
///   location that produced `code[pc]`. Kept as a separate array (rather
///   than an interleaved encoding) so a release build can drop it with
///   `strip_debug()` (M6).
/// - `name`: the function's display name (or `""` for scripts). Used
///   by `Error.prototype.stack` and by the disassembler.
/// - `filename`: whatever `Engine::eval_script` received. Same consumers.
/// - `param_count`: number of declared parameters. VM Step 8 uses this to
///   decide arity fill / drop when a call passes too few or too many args.
/// - `local_count`: total number of local slots (params + `var` / `let` /
///   `const` bindings). Determines the size of `Frame.locals`.
/// - `upvalue_slots`: capture plan for this template. See `upvalue.mbt`.
/// - `nested_chunks`: function-template children. `new_closure` references
///   them by index; keeping them here rather than in `const_pool` matches
///   design.md §4.4 and avoids a special-case in the pool.
/// - `is_strict`: reserved for M2 strict-mode semantics; always `false` in
///   M1.
/// - `self_binding_slot`: for a named function expression, the local slot
///   that the VM should populate with the newly-created `Function` value at
///   call time so `function f() { ... f() ... }` sees `f` inside the body.
///   `-1` when the function has no self-binding (top-level scripts,
///   anonymous function expressions, `function foo` declarations — those
///   bind the name through their enclosing scope's mechanism, not a slot).
pub struct Chunk {
  code : Array[UInt]
  const_pool : Array[@value.JSValue]
  source_locs : Array[@util.SourceLoc]
  name : String
  filename : String
  param_count : Int
  mut local_count : Int
  upvalue_slots : Array[UpvalueSlotDecl]
  nested_chunks : Array[Chunk]
  mut is_strict : Bool
  mut self_binding_slot : Int
} derive(@debug.Debug)

///|
/// Type alias documenting the "reference to a Chunk" role — MoonBit struct
/// values are already reference-typed, so this is a naming aid rather than a
/// semantic wrapper (mirroring `ObjectRef` / `ShapeRef` in the value package).
pub type ChunkRef = Chunk

///|
/// Fresh, empty chunk. `local_count` starts at `param_count` because every
/// parameter occupies one local slot before the compiler even walks the
/// function body. Additional locals are recorded by the compiler by growing
/// `local_count` as it processes `var` / `let` / `const` declarators.
pub fn Chunk::new(name : String, filename : String, param_count : Int) -> Chunk {
  {
    code: [],
    const_pool: [],
    source_locs: [],
    name,
    filename,
    param_count,
    local_count: param_count,
    upvalue_slots: [],
    nested_chunks: [],
    is_strict: false,
    self_binding_slot: -1,
  }
}

///|
/// Bump `local_count` when the compiler introduces a new local slot beyond
/// the parameter list. Returns the newly-allocated slot index.
pub fn Chunk::alloc_local(self : Chunk) -> Int {
  let idx = self.local_count
  self.local_count = idx + 1
  idx
}

///|
/// Mark the chunk as strict-mode. M1 never calls this (the parser is not yet
/// strict-aware), but the setter is exposed so M2's strict-directive pass has
/// a place to write.
pub fn Chunk::set_strict(self : Chunk, is_strict : Bool) -> Unit {
  self.is_strict = is_strict
}

///|
/// Record which local slot the VM should populate with the newly-created
/// `Function` at call time. Emitted by the compiler for named function
/// expressions: the body's reference to its own name resolves to this local
/// slot, and the VM writes the function reference into the slot when a
/// frame for the chunk is created (see design.md §8.1 handoff notes).
///
/// Pass `-1` (the default from `Chunk::new`) to indicate no self-binding.
pub fn Chunk::set_self_binding_slot(self : Chunk, slot : Int) -> Unit {
  self.self_binding_slot = slot
}

///|
/// Add an upvalue slot to the capture plan. Returns the newly-allocated slot
/// index (used by the compiler to emit `get_upvalue` / `set_upvalue`).
pub fn Chunk::add_upvalue(self : Chunk, slot : UpvalueSlotDecl) -> Int {
  let idx = self.upvalue_slots.length()
  self.upvalue_slots.push(slot)
  idx
}

///|
/// Add a nested function template. Returns the index used by `OP_NEW_CLOSURE`.
pub fn Chunk::add_nested(self : Chunk, chunk : Chunk) -> Int {
  let idx = self.nested_chunks.length()
  self.nested_chunks.push(chunk)
  idx
}

///|
/// Deduplicating constant-pool insert. Two intern-equal `JSValue`s always
/// share one slot; two distinct `Object(_)` references never dedup because
/// `JSValue`'s `Eq` uses physical equality for objects (see `value/value.mbt`).
///
/// Returns the pool index. The dedup is `O(n)` in the pool size — acceptable
/// for M1 chunks (dozens to hundreds of entries) and easy to swap for a hash
/// index if a future compiler pushes it into the thousands.
pub fn Chunk::add_const(self : Chunk, v : @value.JSValue) -> Int {
  for i in 0.. (Byte, Byte, Byte) {
  (
    ((u >> 16) & 0xFFU).to_byte(),
    ((u >> 8) & 0xFFU).to_byte(),
    (u & 0xFFU).to_byte(),
  )
}

///|
/// Emit a raw four-byte instruction. The caller is responsible for choosing
/// wide-vs-narrow encoding; most callers use `emit_wide_u32` / `emit_wide_i32`
/// instead. `loc` is pushed to `source_locs` so the array stays parallel with
/// `code`.
pub fn Chunk::emit(
  self : Chunk,
  op : Byte,
  a : Byte,
  b : Byte,
  c : Byte,
  loc : @util.SourceLoc,
) -> Unit {
  self.code.push(encode(op, a, b, c))
  self.source_locs.push(loc)
}

///|
/// Emit an opcode carrying an unsigned integer operand up to 32 bits. Picks
/// the narrowest form:
///
/// - If `operand <= 0xFFFFFF`: single instruction `[op | HI | MID | LO]`.
/// - Otherwise: `[WIDE | W_HI | W_MID | W_LO] [op | LOW8 | 0 | 0]`, where the
///   wide slot supplies the high 24 bits.
///
/// The wide encoding is symmetric with `read_operand_u24`. Both entries in the
/// pair share `loc` — the disassembler treats a wide pair as one logical line.
pub fn Chunk::emit_wide_u32(
  self : Chunk,
  op : Byte,
  operand : UInt,
  loc : @util.SourceLoc,
) -> Unit {
  if operand <= 0xFFFFFFU {
    let (hi, mid, lo) = split_u24_bytes(operand)
    self.emit(op, hi, mid, lo, loc)
  } else {
    let (w_hi, w_mid, w_lo) = split_u24_bytes(operand >> 8)
    let low8 = (operand & 0xFFU).to_byte()
    self.emit(OP_WIDE, w_hi, w_mid, w_lo, loc)
    self.emit(op, low8, 0, 0, loc)
  }
}

///|
/// Emit an opcode carrying a signed 32-bit operand. Chooses the narrowest form
/// that fits — 24-bit signed range is `-8_388_608 ..= 8_388_607`; outside that,
/// falls back to the wide two-instruction form (high 24 bits + low 8 bits).
///
/// The reader side sign-extends the 24-bit narrow form; the 32-bit wide form
/// carries a full signed integer so no extension is needed. See
/// `read_operand_i24`.
pub fn Chunk::emit_wide_i32(
  self : Chunk,
  op : Byte,
  operand : Int,
  loc : @util.SourceLoc,
) -> Unit {
  // Narrow form: operand fits in signed 24 bits.
  if operand >= -8388608 && operand <= 8388607 {
    // Preserve the low 24 bits regardless of sign (reader sign-extends).
    let u = operand.reinterpret_as_uint() & 0xFFFFFFU
    let (hi, mid, lo) = split_u24_bytes(u)
    self.emit(op, hi, mid, lo, loc)
  } else {
    // Wide form. Reinterpret as UInt to get the full 32-bit pattern.
    let u = operand.reinterpret_as_uint()
    let (w_hi, w_mid, w_lo) = split_u24_bytes(u >> 8)
    let low8 = (u & 0xFFU).to_byte()
    self.emit(OP_WIDE, w_hi, w_mid, w_lo, loc)
    self.emit(op, low8, 0, 0, loc)
  }
}

///|
/// Decode the operand of an opcode expecting an unsigned integer. Handles both
/// the narrow (24-bit) and wide (32-bit) forms transparently.
///
/// Returns `(operand, pc_advance)` where `pc_advance` is `1` for the narrow
/// form and `2` for the wide form. The VM's `pc` should be advanced by that
/// amount to skip past both instructions in the wide case.
///
/// Precondition: `pc` must point at an instruction whose opcode carries an
/// unsigned operand (or at an `OP_WIDE` followed by such an instruction).
/// Reading past the end of `code` aborts.
pub fn Chunk::read_operand_u24(self : Chunk, pc : Int) -> (UInt, Int) {
  if pc < 0 || pc >= self.code.length() {
    abort("Chunk::read_operand_u24: pc out of range: " + pc.to_string())
  }
  let first = decode(self.code[pc])
  if first.op == OP_WIDE {
    if pc + 1 >= self.code.length() {
      abort(
        "Chunk::read_operand_u24: wide prefix at end of chunk (pc=" +
        pc.to_string() +
        ")",
      )
    }
    let second = decode(self.code[pc + 1])
    // Wide contributes high 24 bits; second contributes the low 8 bits from A.
    let high24 = (first.a.to_uint() << 16) |
      (first.b.to_uint() << 8) |
      first.c.to_uint()
    let low8 = second.a.to_uint()
    ((high24 << 8) | low8, 2)
  } else {
    let hi = first.a.to_uint()
    let mid = first.b.to_uint()
    let lo = first.c.to_uint()
    ((hi << 16) | (mid << 8) | lo, 1)
  }
}

///|
/// Decode the operand of an opcode expecting a signed integer. Narrow form:
/// sign-extends a 24-bit value; wide form: reinterprets the 32-bit UInt as a
/// signed Int (which is what MoonBit's `to_int` does bit-for-bit).
pub fn Chunk::read_operand_i24(self : Chunk, pc : Int) -> (Int, Int) {
  let (u, advance) = self.read_operand_u24(pc)
  if advance == 1 {
    // Narrow: sign-extend the 24-bit value.
    let signed = if u >= 0x800000U {
      // Set the top 8 bits to 0xFF (all ones) for negative values.
      (u | 0xFF000000U).reinterpret_as_int()
    } else {
      u.reinterpret_as_int()
    }
    (signed, 1)
  } else {
    (u.reinterpret_as_int(), 2)
  }
}

///|
/// Rewrite the operand of the jump instruction at `at_pc` so that it targets
/// `target_pc` (both are word indices into `code`). The offset stored is
/// signed, relative to the instruction *following* `at_pc` — i.e.
/// `target_pc - (at_pc + narrow_advance)`.
///
/// **Constraint**: this M1 helper only supports patching within whatever
/// encoding form the emit call originally chose. Because the compiler cannot
/// know the eventual distance to a forward target when it emits the jump, it
/// should always emit forward jumps via `emit_wide_u32` / `emit_wide_i32` with
/// a placeholder that already reserves the wide two-instruction form (i.e.
/// pass a placeholder operand `>= 0x1000000` so the emit picks wide). Backward
/// jumps have a known distance and can be emitted narrow directly.
///
/// If the wide slot is present but the new offset happens to fit in 24 bits,
/// this function still writes the wide encoding (keeping the code array shape
/// unchanged); if the wide slot is absent but the offset needs it, this
/// function aborts with a clear message.
pub fn Chunk::patch_jump(self : Chunk, at_pc : Int, target_pc : Int) -> Unit {
  if at_pc < 0 || at_pc >= self.code.length() {
    abort("Chunk::patch_jump: at_pc out of range: " + at_pc.to_string())
  }
  let first = decode(self.code[at_pc])
  let is_wide = first.op == OP_WIDE
  // Offset is signed, measured from the instruction after the whole jump
  // (i.e. after the wide+op pair when wide, or after the single instr).
  let advance = if is_wide { 2 } else { 1 }
  let offset = target_pc - (at_pc + advance)
  if is_wide {
    // Wide form: rewrite both instruction words.
    if at_pc + 1 >= self.code.length() {
      abort(
        "Chunk::patch_jump: wide slot has no following op at pc=" +
        at_pc.to_string(),
      )
    }
    let jump_op = decode(self.code[at_pc + 1]).op
    let u = offset.reinterpret_as_uint()
    let (w_hi, w_mid, w_lo) = split_u24_bytes(u >> 8)
    let low8 = (u & 0xFFU).to_byte()
    self.code[at_pc] = encode(OP_WIDE, w_hi, w_mid, w_lo)
    self.code[at_pc + 1] = encode(jump_op, low8, 0, 0)
  } else {
    // Narrow form: must fit in signed 24 bits.
    if offset < -8388608 || offset > 8388607 {
      abort(
        "Chunk::patch_jump: offset " +
        offset.to_string() +
        " doesn't fit in narrow 24-bit slot at pc=" +
        at_pc.to_string() +
        "; caller must emit the jump via emit_wide_* to reserve a wide slot",
      )
    }
    let u = offset.reinterpret_as_uint() & 0xFFFFFFU
    let (hi, mid, lo) = split_u24_bytes(u)
    self.code[at_pc] = encode(first.op, hi, mid, lo)
  }
}