// qbe_emit.cm — mcode IR → QBE IL compiler
// Takes mcode IR (from mcode.cm) and uses qbe.cm macros to produce
// a complete QBE IL program ready for the qbe compiler.
// qbe module is passed via env as 'qbe'

// ============================================================
// QBE IL helper function generation
// Generates helper functions that are defined once and called
// from each operation site, reducing code duplication.
// ============================================================

var emit_helpers = function(qbe) {
  var h = []

  // --- Slot access IL fragments ---
  var sr = function(name, slot) {
    return `  %${name}.o =l shl ${slot}, 3
  %${name}.p =l add %fp, %${name}.o
  %${name} =l loadl %${name}.p`
  }

  var sw = function(name, fp_var, slot, val) {
    return `  %${name}.o =l shl ${slot}, 3
  %${name}.p =l add ${fp_var}, %${name}.o
  storel ${val}, %${name}.p`
  }

  // --- Allocating tail: refresh fp, write result, return fp ---
  var alloc_tail = function(result_var) {
    return `  %fp2 =l call $cell_rt_refresh_fp_checked(l %ctx)
  jnz %fp2, @ok, @exc
@ok
${sw("w", "%fp2", "%dest", result_var)}
  ret %fp2
@exc
  ret 0`
  }

  // --- Allocating tail without dest write ---
  var alloc_tail_nw = function() {
    return `  %fp2 =l call $cell_rt_refresh_fp_checked(l %ctx)
  jnz %fp2, @ok, @exc
@ok
  ret %fp2
@exc
  ret 0`
  }

  // ============================================================
  // Category A: Pure QBE helpers (no C calls)
  // ============================================================

  // move
  h[] = `export function $__move_ss(l %fp, l %dest, l %src) {
@entry
${sr("a", "%src")}
${sw("w", "%fp", "%dest", "%a")}
  ret
}`

  // int comparisons
  var int_ops = [
    ["eq_int", "ceqw"], ["ne_int", "cnew"], ["lt_int", "csltw"],
    ["le_int", "cslew"], ["gt_int", "csgtw"], ["ge_int", "csgew"]
  ]
  var i = 0
  while (i < length(int_ops)) {
    h[] = `export function $__${int_ops[i][0]}_ss(l %fp, l %dest, l %s1, l %s2) {
@entry
${sr("a", "%s1")}
${sr("b", "%s2")}
  %ia =l sar %a, 1
  %ib =l sar %b, 1
  %iaw =w copy %ia
  %ibw =w copy %ib
  %cr =w ${int_ops[i][1]} %iaw, %ibw
  %crext =l extuw %cr
  %sh =l shl %crext, 5
  %r =l or %sh, 3
${sw("w", "%fp", "%dest", "%r")}
  ret
}`
    i = i + 1
  }

  // bool comparisons
  h[] = `export function $__eq_bool_ss(l %fp, l %dest, l %s1, l %s2) {
@entry
${sr("a", "%s1")}
${sr("b", "%s2")}
  %cr =w ceql %a, %b
  %crext =l extuw %cr
  %sh =l shl %crext, 5
  %r =l or %sh, 3
${sw("w", "%fp", "%dest", "%r")}
  ret
}`

  h[] = `export function $__ne_bool_ss(l %fp, l %dest, l %s1, l %s2) {
@entry
${sr("a", "%s1")}
${sr("b", "%s2")}
  %cr =w cnel %a, %b
  %crext =l extuw %cr
  %sh =l shl %crext, 5
  %r =l or %sh, 3
${sw("w", "%fp", "%dest", "%r")}
  ret
}`

  // is_identical (same as eq_bool)
  h[] = `export function $__is_identical_ss(l %fp, l %dest, l %s1, l %s2) {
@entry
${sr("a", "%s1")}
${sr("b", "%s2")}
  %cr =w ceql %a, %b
  %crext =l extuw %cr
  %sh =l shl %crext, 5
  %r =l or %sh, 3
${sw("w", "%fp", "%dest", "%r")}
  ret
}`

  // is_int: (val & 1) == 0
  h[] = `export function $__is_int_ss(l %fp, l %dest, l %src) {
@entry
${sr("a", "%src")}
  %t =l and %a, 1
  %cr =w ceql %t, 0
  %crext =l extuw %cr
  %sh =l shl %crext, 5
  %r =l or %sh, 3
${sw("w", "%fp", "%dest", "%r")}
  ret
}`

  // is_null: (val & 31) == 7
  h[] = `export function $__is_null_ss(l %fp, l %dest, l %src) {
@entry
${sr("a", "%src")}
  %t =l and %a, 31
  %cr =w ceql %t, 7
  %crext =l extuw %cr
  %sh =l shl %crext, 5
  %r =l or %sh, 3
${sw("w", "%fp", "%dest", "%r")}
  ret
}`

  // is_bool: (val & 31) == 3
  h[] = `export function $__is_bool_ss(l %fp, l %dest, l %src) {
@entry
${sr("a", "%src")}
  %t =l and %a, 31
  %cr =w ceql %t, 3
  %crext =l extuw %cr
  %sh =l shl %crext, 5
  %r =l or %sh, 3
${sw("w", "%fp", "%dest", "%r")}
  ret
}`

  // is_num: (val & 1 == 0) || (val & 7 == 5)
  h[] = `export function $__is_num_ss(l %fp, l %dest, l %src) {
@entry
${sr("a", "%src")}
  %t1 =l and %a, 1
  %ii =w ceql %t1, 0
  %t2 =l and %a, 7
  %fi =w ceql %t2, 5
  %cr =w or %ii, %fi
  %crext =l extuw %cr
  %sh =l shl %crext, 5
  %r =l or %sh, 3
${sw("w", "%fp", "%dest", "%r")}
  ret
}`

  // ============================================================
  // Category B: Non-allocating C call helpers
  // ============================================================

  // Type checks via C (no ctx needed except is_proxy)
  var tc_ops = [
    ["is_text", "JS_IsText", false],
    ["is_array", "JS_IsArray", false],
    ["is_func", "JS_IsFunction", false],
    ["is_record", "JS_IsRecord", false],
    ["is_stone", "JS_IsStone", false],
    ["is_proxy", "cell_rt_is_proxy", true]
  ]
  var tc_name = null
  var tc_cfn = null
  var tc_ctx = null
  i = 0
  while (i < length(tc_ops)) {
    tc_name = tc_ops[i][0]
    tc_cfn = tc_ops[i][1]
    tc_ctx = tc_ops[i][2]
    if (tc_ctx) {
      h[] = `export function $__${tc_name}_ss(l %ctx, l %fp, l %dest, l %src) {
@entry
${sr("a", "%src")}
  %cr =w call $${tc_cfn}(l %ctx, l %a)
  %crext =l extuw %cr
  %sh =l shl %crext, 5
  %r =l or %sh, 3
${sw("w", "%fp", "%dest", "%r")}
  ret
}`
    } else {
      h[] = `export function $__${tc_name}_ss(l %fp, l %dest, l %src) {
@entry
${sr("a", "%src")}
  %cr =w call $${tc_cfn}(l %a)
  %crext =l extuw %cr
  %sh =l shl %crext, 5
  %r =l or %sh, 3
${sw("w", "%fp", "%dest", "%r")}
  ret
}`
    }
    i = i + 1
  }

  // Float comparisons: call qbe_float_cmp(ctx, op_id, a, b) → w, tag
  var fc_ops = [
    ["eq_float", 0], ["ne_float", 1], ["lt_float", 2],
    ["le_float", 3], ["gt_float", 4], ["ge_float", 5]
  ]
  i = 0
  while (i < length(fc_ops)) {
    h[] = `export function $__${fc_ops[i][0]}_ss(l %ctx, l %fp, l %dest, l %s1, l %s2) {
@entry
${sr("a", "%s1")}
${sr("b", "%s2")}
  %cr =w call $qbe_float_cmp(l %ctx, w ${fc_ops[i][1]}, l %a, l %b)
  %crext =l extuw %cr
  %sh =l shl %crext, 5
  %r =l or %sh, 3
${sw("w", "%fp", "%dest", "%r")}
  ret
}`
    i = i + 1
  }

  // Text comparisons: eq/ne via js_string_compare_value, others via cell_rt_*
  var txcmp_sv = [
    ["eq_text", "ceqw", 1], ["ne_text", "cnew", 1]
  ]
  i = 0
  while (i < length(txcmp_sv)) {
    h[] = `export function $__${txcmp_sv[i][0]}_ss(l %ctx, l %fp, l %dest, l %s1, l %s2) {
@entry
${sr("a", "%s1")}
${sr("b", "%s2")}
  %scmp =w call $js_string_compare_value(l %ctx, l %a, l %b, w ${txcmp_sv[i][2]})
  %cr =w ${txcmp_sv[i][1]} %scmp, 0
  %crext =l extuw %cr
  %sh =l shl %crext, 5
  %r =l or %sh, 3
${sw("w", "%fp", "%dest", "%r")}
  ret
}`
    i = i + 1
  }

  // lt/le/gt/ge_text via cell_rt_* (return tagged JSValue directly)
  var txcmp_rt = ["lt_text", "gt_text", "le_text", "ge_text"]
  i = 0
  while (i < length(txcmp_rt)) {
    h[] = `export function $__${txcmp_rt[i]}_ss(l %ctx, l %fp, l %dest, l %s1, l %s2) {
@entry
${sr("a", "%s1")}
${sr("b", "%s2")}
  %r =l call $cell_rt_${txcmp_rt[i]}(l %ctx, l %a, l %b)
${sw("w", "%fp", "%dest", "%r")}
  ret
}`
    i = i + 1
  }

  // eq_tol, ne_tol (return tagged JSValue directly) — needs tolerance (3rd value)
  var tol_ops = ["eq_tol", "ne_tol"]
  i = 0
  while (i < length(tol_ops)) {
    h[] = `export function $__${tol_ops[i]}_ss(l %ctx, l %fp, l %dest, l %s1, l %s2, l %s3) {
@entry
${sr("a", "%s1")}
${sr("b", "%s2")}
${sr("c", "%s3")}
  %r =l call $cell_rt_${tol_ops[i]}(l %ctx, l %a, l %b, l %c)
${sw("w", "%fp", "%dest", "%r")}
  ret
}`
    i = i + 1
  }

  // not: JS_ToBool + negate + tag
  h[] = `export function $__not_ss(l %ctx, l %fp, l %dest, l %src) {
@entry
${sr("a", "%src")}
  %bval =w call $JS_ToBool(l %ctx, l %a)
  %neg =w ceqw %bval, 0
  %nex =l extuw %neg
  %sh =l shl %nex, 5
  %r =l or %sh, 3
${sw("w", "%fp", "%dest", "%r")}
  ret
}`

  // and, or (return tagged JSValue directly)
  h[] = `export function $__and_ss(l %ctx, l %fp, l %dest, l %s1, l %s2) {
@entry
${sr("a", "%s1")}
${sr("b", "%s2")}
  %r =l call $cell_rt_and(l %ctx, l %a, l %b)
${sw("w", "%fp", "%dest", "%r")}
  ret
}`

  h[] = `export function $__or_ss(l %ctx, l %fp, l %dest, l %s1, l %s2) {
@entry
${sr("a", "%s1")}
${sr("b", "%s2")}
  %r =l call $cell_rt_or(l %ctx, l %a, l %b)
${sw("w", "%fp", "%dest", "%r")}
  ret
}`

  // Bitwise unary: bnot
  h[] = `export function $__bnot_ss(l %ctx, l %fp, l %dest, l %src) {
@entry
${sr("a", "%src")}
  %r =l call $qbe_bnot(l %ctx, l %a)
${sw("w", "%fp", "%dest", "%r")}
  ret
}`

  // Bitwise binary ops
  var bw_ops = [
    ["band", "qbe_bitwise_and"], ["bor", "qbe_bitwise_or"],
    ["bxor", "qbe_bitwise_xor"], ["bshl", "qbe_shift_shl"],
    ["bshr", "qbe_shift_sar"], ["bushr", "qbe_shift_shr"]
  ]
  i = 0
  while (i < length(bw_ops)) {
    h[] = `export function $__${bw_ops[i][0]}_ss(l %ctx, l %fp, l %dest, l %s1, l %s2) {
@entry
${sr("a", "%s1")}
${sr("b", "%s2")}
  %r =l call $${bw_ops[i][1]}(l %ctx, l %a, l %b)
${sw("w", "%fp", "%dest", "%r")}
  ret
}`
    i = i + 1
  }

  // ============================================================
  // Category C: Allocating helpers (return fp or 0)
  // ============================================================

  // Allocating binary ops: read 2 slots, call C, refresh, write dest
  var ab_ops = [
    ["add", "cell_rt_add"], ["sub", "qbe_float_sub"],
    ["mul", "qbe_float_mul"], ["div", "qbe_float_div"],
    ["mod", "qbe_float_mod"], ["pow", "qbe_float_pow"],
    ["concat", "JS_ConcatString"]
  ]
  i = 0
  while (i < length(ab_ops)) {
    h[] = `export function l $__${ab_ops[i][0]}_ss(l %ctx, l %fp, l %dest, l %s1, l %s2) {
@entry
${sr("a", "%s1")}
${sr("b", "%s2")}
  %r =l call $${ab_ops[i][1]}(l %ctx, l %a, l %b)
${alloc_tail("%r")}
}`
    i = i + 1
  }

  // Allocating unary: negate
  h[] = `export function l $__neg_ss(l %ctx, l %fp, l %dest, l %src) {
@entry
${sr("a", "%src")}
  %r =l call $qbe_float_neg(l %ctx, l %a)
${alloc_tail("%r")}
}`

  // Property access: load_field(ctx, fp, dest, obj_slot, name_ptr)
  h[] = `export function l $__load_field_ss(l %ctx, l %fp, l %dest, l %obj_slot, l %name) {
@entry
${sr("a", "%obj_slot")}
  %r =l call $cell_rt_load_field(l %ctx, l %a, l %name)
${alloc_tail("%r")}
}`

  // load_dynamic(ctx, fp, dest, obj_slot, key_slot)
  h[] = `export function l $__load_dynamic_ss(l %ctx, l %fp, l %dest, l %obj_slot, l %key_slot) {
@entry
${sr("a", "%obj_slot")}
${sr("b", "%key_slot")}
  %r =l call $cell_rt_load_dynamic(l %ctx, l %a, l %b)
${alloc_tail("%r")}
}`

  // load_index(ctx, fp, dest, arr_slot, idx_slot)
  h[] = `export function l $__load_index_ss(l %ctx, l %fp, l %dest, l %arr_slot, l %idx_slot) {
@entry
${sr("a", "%arr_slot")}
${sr("b", "%idx_slot")}
  %r =l call $cell_rt_load_index(l %ctx, l %a, l %b)
${alloc_tail("%r")}
}`

  // store_field(ctx, fp, obj_slot, val_slot, name_ptr) — no dest write
  h[] = `export function l $__store_field_ss(l %ctx, l %fp, l %obj_slot, l %val_slot, l %name) {
@entry
${sr("a", "%obj_slot")}
${sr("b", "%val_slot")}
  call $cell_rt_store_field(l %ctx, l %b, l %a, l %name)
${alloc_tail_nw()}
}`

  // store_dynamic(ctx, fp, obj_slot, val_slot, key_slot) — no dest write
  h[] = `export function l $__store_dynamic_ss(l %ctx, l %fp, l %obj_slot, l %val_slot, l %key_slot) {
@entry
${sr("a", "%obj_slot")}
${sr("b", "%val_slot")}
${sr("c", "%key_slot")}
  call $cell_rt_store_dynamic(l %ctx, l %b, l %a, l %c)
${alloc_tail_nw()}
}`

  // store_index(ctx, fp, obj_slot, val_slot, idx_slot) — no dest write
  h[] = `export function l $__store_index_ss(l %ctx, l %fp, l %obj_slot, l %val_slot, l %idx_slot) {
@entry
${sr("a", "%obj_slot")}
${sr("b", "%val_slot")}
${sr("c", "%idx_slot")}
  call $cell_rt_store_index(l %ctx, l %b, l %a, l %c)
${alloc_tail_nw()}
}`

  // frame(ctx, fp, dest, fn_slot, nargs)
  h[] = `export function l $__frame_ss(l %ctx, l %fp, l %dest, l %fn_slot, l %nargs) {
@entry
${sr("a", "%fn_slot")}
  %r =l call $cell_rt_frame(l %ctx, l %a, l %nargs)
${alloc_tail("%r")}
}`

  // goframe(ctx, fp, dest, fn_slot, nargs)
  h[] = `export function l $__goframe_ss(l %ctx, l %fp, l %dest, l %fn_slot, l %nargs) {
@entry
${sr("a", "%fn_slot")}
  %r =l call $cell_rt_goframe(l %ctx, l %a, l %nargs)
${alloc_tail("%r")}
}`

  // invoke(ctx, fp, frame_slot, result_slot) — two checks: exc + refresh
  h[] = `export function l $__invoke_ss(l %ctx, l %fp, l %frame_slot, l %result_slot) {
@entry
${sr("a", "%frame_slot")}
  %r =l call $cell_rt_invoke(l %ctx, l %a)
  %is_exc =w ceql %r, 15
  jnz %is_exc, @exc, @ok1
@ok1
  %fp2 =l call $cell_rt_refresh_fp_checked(l %ctx)
  jnz %fp2, @ok2, @exc
@ok2
${sw("w", "%fp2", "%result_slot", "%r")}
  ret %fp2
@exc
  ret 0
}`

  // function(ctx, fp, dest, fn_idx, arity)
  h[] = `export function l $__function_ss(l %ctx, l %fp, l %dest, l %fn_idx, l %arity) {
@entry
  %r =l call $cell_rt_make_function(l %ctx, l %fn_idx, l %fp, l %arity)
${alloc_tail("%r")}
}`

  // new_record(ctx, fp, dest)
  h[] = `export function l $__new_record_ss(l %ctx, l %fp, l %dest) {
@entry
  %r =l call $JS_NewObject(l %ctx)
${alloc_tail("%r")}
}`

  // new_array(ctx, fp, dest)
  h[] = `export function l $__new_array_ss(l %ctx, l %fp, l %dest) {
@entry
  %r =l call $JS_NewArray(l %ctx)
${alloc_tail("%r")}
}`

  // new_string(ctx, fp, dest, str_ptr)
  h[] = `export function l $__new_string_ss(l %ctx, l %fp, l %dest, l %str_ptr) {
@entry
  %r =l call $qbe_new_string(l %ctx, l %str_ptr)
${alloc_tail("%r")}
}`

  // new_float64(ctx, fp, dest, val) — val is a double
  h[] = `export function l $__new_float64_ss(l %ctx, l %fp, l %dest, d %val) {
@entry
  %r =l call $qbe_new_float64(l %ctx, d %val)
${alloc_tail("%r")}
}`

  // get_intrinsic(ctx, fp, dest, name_ptr)
  h[] = `export function l $__get_intrinsic_ss(l %ctx, l %fp, l %dest, l %name_ptr) {
@entry
  %r =l call $cell_rt_get_intrinsic(l %ctx, l %name_ptr)
${alloc_tail("%r")}
}`

  // push(ctx, fp, arr_slot, val_slot) — write back arr in case GC moved it
  h[] = `export function l $__push_ss(l %ctx, l %fp, l %arr_slot, l %val_slot) {
@entry
${sr("a", "%arr_slot")}
${sr("b", "%val_slot")}
  %r =l call $cell_rt_push(l %ctx, l %a, l %b)
  %fp2 =l call $cell_rt_refresh_fp_checked(l %ctx)
  jnz %fp2, @ok, @exc
@ok
${sw("w", "%fp2", "%arr_slot", "%r")}
  ret %fp2
@exc
  ret 0
}`

  // pop(ctx, fp, dest, arr_slot)
  h[] = `export function l $__pop_ss(l %ctx, l %fp, l %dest, l %arr_slot) {
@entry
${sr("a", "%arr_slot")}
  %r =l call $cell_rt_pop(l %ctx, l %a)
${alloc_tail("%r")}
}`

  // length(ctx, fp, dest, src)
  h[] = `export function l $__length_ss(l %ctx, l %fp, l %dest, l %src) {
@entry
${sr("a", "%src")}
  %r =l call $JS_CellLength(l %ctx, l %a)
${alloc_tail("%r")}
}`

  // delete_field(ctx, fp, dest, obj_slot, name_ptr)
  h[] = `export function l $__delete_field_ss(l %ctx, l %fp, l %dest, l %obj_slot, l %name) {
@entry
${sr("a", "%obj_slot")}
  %r =l call $cell_rt_delete_str(l %ctx, l %a, l %name)
${alloc_tail("%r")}
}`

  // delete_dynamic(ctx, fp, dest, obj_slot, key_slot)
  h[] = `export function l $__delete_dynamic_ss(l %ctx, l %fp, l %dest, l %obj_slot, l %key_slot) {
@entry
${sr("a", "%obj_slot")}
${sr("b", "%key_slot")}
  %r =l call $cell_rt_delete(l %ctx, l %a, l %b)
${alloc_tail("%r")}
}`

  // in(ctx, fp, dest, key_slot, obj_slot)
  h[] = `export function l $__in_ss(l %ctx, l %fp, l %dest, l %key_slot, l %obj_slot) {
@entry
${sr("a", "%key_slot")}
${sr("b", "%obj_slot")}
  %r =l call $cell_rt_in(l %ctx, l %a, l %b)
${alloc_tail("%r")}
}`

  // regexp(ctx, fp, dest, pat_ptr, flg_ptr)
  h[] = `export function l $__regexp_ss(l %ctx, l %fp, l %dest, l %pat, l %flg) {
@entry
  %r =l call $cell_rt_regexp(l %ctx, l %pat, l %flg)
${alloc_tail("%r")}
}`

  return h
}

var qbe_emit = function(ir, qbe, export_name) {
  var out = []
  var data_out = []
  var str_table = {}
  var str_id = 0
  var uid = 0

  // ============================================================
  // Output helpers
  // ============================================================

  var emit = function(s) {
    push(out, s)
  }

  var fresh = function() {
    uid = uid + 1
    return "u" + text(uid)
  }

  var sanitize = function(lbl) {
    var r = replace(lbl, ".", "_")
    r = replace(r, "-", "_")
    r = replace(r, " ", "_")
    r = replace(r, "/", "_")
    r = replace(r, "<", "")
    r = replace(r, ">", "")
    r = replace(r, "(", "")
    r = replace(r, ")", "")
    return r
  }

  // ============================================================
  // String interning — emit data section entries
  // ============================================================

  var intern_str = function(val) {
    if (str_table[val] != null) return str_table[val]
    var label = "$d_str_" + text(str_id)
    str_id = str_id + 1
    var escaped = replace(val, "\\", "\\\\")
    escaped = replace(escaped, "\"", "\\\"")
    escaped = replace(escaped, "\n", "\\n")
    escaped = replace(escaped, "\r", "\\r")
    escaped = replace(escaped, "\t", "\\t")
    var line = "data " + label + ' = ' + '{ b "' + escaped + '", b 0 }'
    push(data_out, line)
    str_table[val] = label
    return label
  }

  // ============================================================
  // Extract property name from mcode operand
  // ============================================================

  // prop_name inlined at each call site — closures have bytecode bugs with
  // early returns so we extract the property name inline via pn variable

  // ============================================================
  // Compile one function's instructions
  // ============================================================

  var compile_fn = function(fn, fn_idx, is_main) {
    var instrs = fn.instructions
    var nr_slots = fn.nr_slots
    var nr_args = fn.nr_args
    var disruption_pc = fn.disruption_pc != null ? fn.disruption_pc : 0
    var has_handler = disruption_pc > 0
    var name = is_main ? (export_name ? export_name : "cell_main") : "cell_fn_" + text(fn_idx)
    name = sanitize(name)
    var i = 0
    var instr = null
    var op = null
    var a1 = null
    var a2 = null
    var a3 = null
    var a4 = null
    var p = null
    var pn = null
    var sl = null
    var lbl = null
    var fop_id = 0
    var nr_elems = 0
    var ei = 0
    var elem_slot = 0
    var v = null
    var lhs = null
    var rhs = null
    var obj = null
    var chk = null
    var pat_label = null
    var flg_label = null
    var in_handler = false
    var tol = null
    var fn_arity = 0
    var arity_tmp = null

    // Function signature: (ctx, frame_ptr) → JSValue
    emit(`export function l $${name}(l %ctx, l %fp) {`)
    emit("@entry")

    // GC-safe slot access: every read/write goes through frame memory.
    // %fp may become stale after GC-triggering calls — use refresh_fp().
    var s_read = function(slot) {
      var t = fresh()
      emit(`  %${t} =l add %fp, ${text(slot * 8)}`)
      emit(`  %${t}v =l loadl %${t}`)
      return `%${t}v`
    }

    var s_write = function(slot, val) {
      var t = fresh()
      var sv = val
      if (!starts_with(val, "%")) {
        sv = `%${t}c`
        emit(`  ${sv} =l copy ${val}`)
      }
      emit(`  %${t} =l add %fp, ${text(slot * 8)}`)
      emit(`  storel ${sv}, %${t}`)
    }

    var needs_exc_ret = false

    var refresh_fp = function() {
      emit(`  %fp =l call $cell_rt_refresh_fp_checked(l %ctx)`)
      var exc = fresh()
      emit(`  %${exc} =w ceql %fp, 0`)
      if (has_handler && !in_handler) {
        emit(`  jnz %${exc}, @disruption_handler, @${exc}_ok`)
      } else {
        needs_exc_ret = true
        emit(`  jnz %${exc}, @_exc_ret, @${exc}_ok`)
      }
      emit(`@${exc}_ok`)
    }

    // Exception check after allocating helper call (helper returns fp or 0)
    var emit_exc_check = function() {
      var lbl = fresh()
      if (has_handler && !in_handler) {
        emit(`  jnz %fp, @${lbl}_ok, @disruption_handler`)
      } else {
        needs_exc_ret = true
        emit(`  jnz %fp, @${lbl}_ok, @_exc_ret`)
      }
      emit(`@${lbl}_ok`)
    }

    // Walk instructions
    var last_was_term = false
    i = 0
    while (i < length(instrs)) {
      instr = instrs[i]

      // Emit @disruption_handler at the right flat index
      // disruption_pc counts all entries (labels + instructions)
      if (has_handler && i == disruption_pc) {
        if (!last_was_term) {
          emit("  jmp @disruption_handler")
        }
        emit("@disruption_handler")
        emit("  call $cell_rt_clear_exception(l %ctx)")
        emit(`  %fp =l call $cell_rt_refresh_fp(l %ctx)`)
        last_was_term = false
        in_handler = true
      }
      i = i + 1

      // Labels are plain strings; skip nop pseudo-labels from streamline
      if (is_text(instr)) {
        if (starts_with(instr, "_nop_ur_") || starts_with(instr, "_nop_tc_")) continue
        lbl = sanitize(instr)
        if (!last_was_term) {
          emit(`  jmp @${lbl}`)
        }
        emit("@" + lbl)
        last_was_term = false
        continue
      }

      // Skip dead code: non-label instructions after a terminator are unreachable
      if (last_was_term) continue

      op = instr[0]
      a1 = instr[1]
      a2 = instr[2]
      a3 = instr[3]
      last_was_term = false

      // --- Constants ---

      if (op == "int") {
        s_write(a1, text(a2 * 2))
        continue
      }
      if (op == "null") {
        s_write(a1, text(qbe.js_null))
        continue
      }
      if (op == "true") {
        s_write(a1, text(qbe.js_true))
        continue
      }
      if (op == "false") {
        s_write(a1, text(qbe.js_false))
        continue
      }
      if (op == "access") {
        if (is_number(a2)) {
          if (is_integer(a2)) {
            s_write(a1, text(a2 * 2))
          } else {
            emit(`  %fp =l call $__new_float64_ss(l %ctx, l %fp, l ${text(a1)}, d d_${text(a2)})`)
            emit_exc_check()
          }
        } else if (is_text(a2)) {
          sl = intern_str(a2)
          emit(`  %fp =l call $__new_string_ss(l %ctx, l %fp, l ${text(a1)}, l ${sl})`)
          emit_exc_check()
        } else if (is_object(a2)) {
          if (a2.make == "intrinsic") {
            sl = intern_str(a2.name)
            emit(`  %fp =l call $__get_intrinsic_ss(l %ctx, l %fp, l ${text(a1)}, l ${sl})`)
            emit_exc_check()
          } else if (a2.kind == "number") {
            if (a2.number != null && is_integer(a2.number)) {
              s_write(a1, text(a2.number * 2))
            } else if (a2.number != null) {
              emit(`  %fp =l call $__new_float64_ss(l %ctx, l %fp, l ${text(a1)}, d d_${text(a2.number)})`)
              emit_exc_check()
            } else {
              s_write(a1, text(qbe.js_null))
            }
          } else if (a2.kind == "text") {
            sl = intern_str(a2.value)
            emit(`  %fp =l call $__new_string_ss(l %ctx, l %fp, l ${text(a1)}, l ${sl})`)
            emit_exc_check()
          } else if (a2.kind == "true") {
            s_write(a1, text(qbe.js_true))
          } else if (a2.kind == "false") {
            s_write(a1, text(qbe.js_false))
          } else if (a2.kind == "null") {
            s_write(a1, text(qbe.js_null))
          } else {
            s_write(a1, text(qbe.js_null))
          }
        } else {
          s_write(a1, text(qbe.js_null))
        }
        continue
      }

      // --- Movement ---

      if (op == "move") {
        emit(`  call $__move_ss(l %fp, l ${text(a1)}, l ${text(a2)})`)
        continue
      }

      // --- Generic arithmetic (VM dispatches int/float) ---

      if (op == "add") {
        emit(`  %fp =l call $__add_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        emit_exc_check()
        continue
      }
      if (op == "subtract") {
        emit(`  %fp =l call $__sub_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        emit_exc_check()
        continue
      }
      if (op == "multiply") {
        emit(`  %fp =l call $__mul_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        emit_exc_check()
        continue
      }
      if (op == "divide") {
        emit(`  %fp =l call $__div_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        emit_exc_check()
        continue
      }
      if (op == "modulo") {
        emit(`  %fp =l call $__mod_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        emit_exc_check()
        continue
      }
      if (op == "negate") {
        emit(`  %fp =l call $__neg_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)})`)
        emit_exc_check()
        continue
      }

      if (op == "pow") {
        emit(`  %fp =l call $__pow_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        emit_exc_check()
        continue
      }

      // --- String concat ---

      if (op == "concat") {
        emit(`  %fp =l call $__concat_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        emit_exc_check()
        continue
      }

      // --- Type checks — use qbe.cm macros (no GC, no refresh) ---

      if (op == "is_int") {
        emit(`  call $__is_int_ss(l %fp, l ${text(a1)}, l ${text(a2)})`)
        continue
      }
      if (op == "is_text") {
        emit(`  call $__is_text_ss(l %fp, l ${text(a1)}, l ${text(a2)})`)
        continue
      }
      if (op == "is_num") {
        emit(`  call $__is_num_ss(l %fp, l ${text(a1)}, l ${text(a2)})`)
        continue
      }
      if (op == "is_bool") {
        emit(`  call $__is_bool_ss(l %fp, l ${text(a1)}, l ${text(a2)})`)
        continue
      }
      if (op == "is_null") {
        emit(`  call $__is_null_ss(l %fp, l ${text(a1)}, l ${text(a2)})`)
        continue
      }
      if (op == "is_identical") {
        emit(`  call $__is_identical_ss(l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        continue
      }
      if (op == "is_array") {
        emit(`  call $__is_array_ss(l %fp, l ${text(a1)}, l ${text(a2)})`)
        continue
      }
      if (op == "is_func") {
        emit(`  call $__is_func_ss(l %fp, l ${text(a1)}, l ${text(a2)})`)
        continue
      }
      if (op == "is_record") {
        emit(`  call $__is_record_ss(l %fp, l ${text(a1)}, l ${text(a2)})`)
        continue
      }
      if (op == "is_stone") {
        emit(`  call $__is_stone_ss(l %fp, l ${text(a1)}, l ${text(a2)})`)
        continue
      }
      if (op == "is_proxy") {
        emit(`  call $__is_proxy_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)})`)
        continue
      }

      // --- Comparisons (int path, no GC) ---

      if (op == "eq_int") {
        emit(`  call $__eq_int_ss(l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        continue
      }
      if (op == "ne_int") {
        emit(`  call $__ne_int_ss(l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        continue
      }
      if (op == "lt_int") {
        emit(`  call $__lt_int_ss(l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        continue
      }
      if (op == "gt_int") {
        emit(`  call $__gt_int_ss(l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        continue
      }
      if (op == "le_int") {
        emit(`  call $__le_int_ss(l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        continue
      }
      if (op == "ge_int") {
        emit(`  call $__ge_int_ss(l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        continue
      }

      // --- Comparisons (float/text/bool) ---

      if (op == "eq_float") {
        emit(`  call $__eq_float_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        continue
      }
      if (op == "ne_float") {
        emit(`  call $__ne_float_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        continue
      }
      if (op == "lt_float") {
        emit(`  call $__lt_float_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        continue
      }
      if (op == "le_float") {
        emit(`  call $__le_float_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        continue
      }
      if (op == "gt_float") {
        emit(`  call $__gt_float_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        continue
      }
      if (op == "ge_float") {
        emit(`  call $__ge_float_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        continue
      }
      if (op == "eq_text") {
        emit(`  call $__eq_text_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        continue
      }
      if (op == "ne_text") {
        emit(`  call $__ne_text_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        continue
      }
      if (op == "lt_text" || op == "gt_text" || op == "le_text" || op == "ge_text") {
        emit(`  call $__${op}_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        continue
      }
      if (op == "eq_bool") {
        emit(`  call $__eq_bool_ss(l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        continue
      }
      if (op == "ne_bool") {
        emit(`  call $__ne_bool_ss(l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        continue
      }
      if (op == "eq_tol" || op == "ne_tol") {
        a4 = instr[4]
        emit(`  call $__${op}_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)}, l ${text(a4)})`)
        continue
      }

      // --- Boolean ops ---

      if (op == "not") {
        emit(`  call $__not_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)})`)
        continue
      }
      if (op == "and") {
        emit(`  call $__and_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        continue
      }
      if (op == "or") {
        emit(`  call $__or_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        continue
      }

      // --- Bitwise ops — use qbe.cm macros (no GC) ---

      if (op == "bitnot") {
        emit(`  call $__bnot_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)})`)
        continue
      }
      if (op == "bitand") {
        emit(`  call $__band_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        continue
      }
      if (op == "bitor") {
        emit(`  call $__bor_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        continue
      }
      if (op == "bitxor") {
        emit(`  call $__bxor_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        continue
      }
      if (op == "shl") {
        emit(`  call $__bshl_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        continue
      }
      if (op == "shr") {
        emit(`  call $__bshr_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        continue
      }
      if (op == "ushr") {
        emit(`  call $__bushr_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        continue
      }

      // --- Property access — runtime calls [G] ---

      if (op == "load_field") {
        pn = null
        if (is_text(a3)) {
          pn = a3
        } else if (is_object(a3)) {
          if (a3.name != null) {
            pn = a3.name
          } else if (a3.value != null) {
            pn = a3.value
          }
        }
        if (pn != null) {
          sl = intern_str(pn)
          emit(`  %fp =l call $__load_field_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${sl})`)
        } else {
          emit(`  %fp =l call $__load_dynamic_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        }
        emit_exc_check()
        continue
      }
      if (op == "load_index") {
        emit(`  %fp =l call $__load_index_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        emit_exc_check()
        continue
      }
      if (op == "load_dynamic") {
        pn = null
        if (is_text(a3)) {
          pn = a3
        } else if (is_object(a3)) {
          if (a3.name != null) {
            pn = a3.name
          } else if (a3.value != null) {
            pn = a3.value
          }
        }
        if (pn != null) {
          sl = intern_str(pn)
          emit(`  %fp =l call $__load_field_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${sl})`)
        } else {
          emit(`  %fp =l call $__load_dynamic_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        }
        emit_exc_check()
        continue
      }
      if (op == "store_field") {
        // IR: ["store_field", obj, val, prop]
        pn = null
        if (is_text(a3)) {
          pn = a3
        } else if (is_object(a3)) {
          if (a3.name != null) {
            pn = a3.name
          } else if (a3.value != null) {
            pn = a3.value
          }
        }
        if (pn != null) {
          sl = intern_str(pn)
          emit(`  %fp =l call $__store_field_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${sl})`)
        } else {
          emit(`  %fp =l call $__store_dynamic_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        }
        emit_exc_check()
        continue
      }
      if (op == "store_index") {
        // IR: ["store_index", obj, val, idx]
        emit(`  %fp =l call $__store_index_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        emit_exc_check()
        continue
      }
      if (op == "store_dynamic") {
        // IR: ["store_dynamic", obj, val, key]
        pn = null
        if (is_text(a3)) {
          pn = a3
        } else if (is_object(a3)) {
          if (a3.name != null) {
            pn = a3.name
          } else if (a3.value != null) {
            pn = a3.value
          }
        }
        if (pn != null) {
          sl = intern_str(pn)
          emit(`  %fp =l call $__store_field_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${sl})`)
        } else {
          emit(`  %fp =l call $__store_dynamic_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        }
        emit_exc_check()
        continue
      }

      // --- Closure access (no GC) ---

      if (op == "get") {
        // mcode: get(dest, slot, depth) — a2=slot, a3=depth
        p = fresh()
        emit(`  %${p} =l call $cell_rt_get_closure(l %ctx, l %fp, l ${text(a3)}, l ${text(a2)})`)
        s_write(a1, `%${p}`)
        continue
      }
      if (op == "put") {
        // mcode: put(val, slot, depth) — a2=slot, a3=depth
        v = s_read(a1)
        emit(`  call $cell_rt_put_closure(l %ctx, l %fp, l ${v}, l ${text(a3)}, l ${text(a2)})`)
        continue
      }

      // --- Control flow ---

      if (op == "jump") {
        emit(`  jmp @${sanitize(a1)}`)
        last_was_term = true
        continue
      }
      if (op == "jump_true") {
        v = s_read(a1)
        p = fresh()
        emit(`  %${p} =w call $JS_ToBool(l %ctx, l ${v})`)
        emit(`  jnz %${p}, @${sanitize(a2)}, @${p}_f`)
        emit(`@${p}_f`)
        continue
      }
      if (op == "jump_false") {
        v = s_read(a1)
        p = fresh()
        emit(`  %${p} =w call $JS_ToBool(l %ctx, l ${v})`)
        emit(`  jnz %${p}, @${p}_t, @${sanitize(a2)}`)
        emit(`@${p}_t`)
        continue
      }
      if (op == "jump_null") {
        v = s_read(a1)
        p = fresh()
        emit(`  %${p} =w ceql ${v}, ${text(qbe.js_null)}`)
        emit(`  jnz %${p}, @${sanitize(a2)}, @${p}_nn`)
        emit(`@${p}_nn`)
        continue
      }
      if (op == "jump_not_null") {
        v = s_read(a1)
        p = fresh()
        emit(`  %${p} =w cnel ${v}, ${text(qbe.js_null)}`)
        emit(`  jnz %${p}, @${sanitize(a2)}, @${p}_n`)
        emit(`@${p}_n`)
        continue
      }

      // --- Function calls [G] ---

      if (op == "frame") {
        emit(`  %fp =l call $__frame_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        emit_exc_check()
        continue
      }
      if (op == "setarg") {
        v = s_read(a1)
        lhs = s_read(a3)
        emit(`  call $cell_rt_setarg(l ${v}, l ${text(a2)}, l ${lhs})`)
        continue
      }
      if (op == "invoke") {
        emit(`  %fp =l call $__invoke_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)})`)
        emit_exc_check()
        continue
      }
      if (op == "tail_invoke") {
        emit(`  %fp =l call $__invoke_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)})`)
        emit_exc_check()
        continue
      }
      if (op == "goframe") {
        emit(`  %fp =l call $__goframe_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        emit_exc_check()
        continue
      }
      if (op == "goinvoke") {
        v = s_read(a1)
        p = fresh()
        emit(`  %${p} =l call $cell_rt_goinvoke(l %ctx, l ${v})`)
        chk = fresh()
        emit(`  %${chk} =w ceql %${p}, 15`)
        if (has_handler) {
          emit(`  jnz %${chk}, @disruption_handler, @${chk}_ok`)
          emit(`@${chk}_ok`)
          refresh_fp()
          emit(`  ret %${p}`)
        } else {
          needs_exc_ret = true
          emit(`  jnz %${chk}, @_exc_ret, @${chk}_ok`)
          emit(`@${chk}_ok`)
          emit(`  ret %${p}`)
        }
        last_was_term = true
        continue
      }

      // --- Function object creation [G] ---

      if (op == "function") {
        fn_arity = 0
        if (a2 >= 0 && a2 < length(ir.functions)) {
          fn_arity = ir.functions[a2].nr_args
        }
        emit(`  %fp =l call $__function_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(fn_arity)})`)
        emit_exc_check()
        continue
      }

      // --- Record/Array creation [G] ---

      if (op == "record") {
        emit(`  %fp =l call $__new_record_ss(l %ctx, l %fp, l ${text(a1)})`)
        emit_exc_check()
        continue
      }
      if (op == "array") {
        emit(`  %fp =l call $__new_array_ss(l %ctx, l %fp, l ${text(a1)})`)
        emit_exc_check()
        continue
      }

      // --- Array push/pop [G] ---

      if (op == "push") {
        emit(`  %fp =l call $__push_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)})`)
        emit_exc_check()
        continue
      }
      if (op == "pop") {
        emit(`  %fp =l call $__pop_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)})`)
        emit_exc_check()
        continue
      }

      // --- Length [G] ---

      if (op == "length") {
        emit(`  %fp =l call $__length_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)})`)
        emit_exc_check()
        continue
      }

      // --- Misc ---

      if (op == "return") {
        v = s_read(a1)
        emit(`  ret ${v}`)
        last_was_term = true
        continue
      }
      if (op == "disrupt") {
        emit(`  call $cell_rt_disrupt(l %ctx)`)
        if (has_handler && !in_handler) {
          emit("  jmp @disruption_handler")
        } else {
          emit(`  ret 15`)
        }
        last_was_term = true
        continue
      }
      if (op == "delete") {
        pn = null
        if (is_text(a3)) {
          pn = a3
        } else if (is_object(a3)) {
          if (a3.name != null) {
            pn = a3.name
          } else if (a3.value != null) {
            pn = a3.value
          }
        }
        if (pn != null) {
          sl = intern_str(pn)
          emit(`  %fp =l call $__delete_field_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${sl})`)
        } else {
          emit(`  %fp =l call $__delete_dynamic_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        }
        emit_exc_check()
        continue
      }

      // --- in [G] ---

      if (op == "in") {
        // IR: ["in", dest, key_slot, obj_slot]
        emit(`  %fp =l call $__in_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
        emit_exc_check()
        continue
      }

      // --- regexp [G] ---

      if (op == "regexp") {
        // IR: ["regexp", dest_slot, pattern_string, flags_string]
        pat_label = intern_str(a2)
        flg_label = intern_str(a3)
        emit(`  %fp =l call $__regexp_ss(l %ctx, l %fp, l ${text(a1)}, l ${pat_label}, l ${flg_label})`)
        emit_exc_check()
        continue
      }

      // --- Unknown opcode ---
      emit(`  # unknown: ${op}`)
    }

    // Emit @disrupt landing pad for arithmetic type-error branches
    if (!last_was_term) {
      emit("  jmp @disrupt")
    }
    emit("@disrupt")
    emit(`  call $cell_rt_disrupt(l %ctx)`)
    emit(`  ret 15`)

    // Shared exception return (for functions without disruption handler)
    if (needs_exc_ret) {
      emit("@_exc_ret")
      emit("  ret 15")
    }

    emit("}")
    emit("")
  }

  // ============================================================
  // Main: compile all functions then main
  // ============================================================

  var fn_bodies = []
  var fi = 0
  while (fi < length(ir.functions)) {
    out = []
    compile_fn(ir.functions[fi], fi, false)
    fn_bodies[] = text(out, "\n")
    fi = fi + 1
  }

  out = []
  compile_fn(ir.main, -1, true)
  fn_bodies[] = text(out, "\n")

  return {
    data: text(data_out, "\n"),
    functions: fn_bodies,
    helpers: emit_helpers(qbe)
  }
}

return qbe_emit
