// streamline.cm — mcode IR optimizer
// Composed of independent passes, each a separate function.
// Optional `log` parameter enables structured observability.

var streamline = function(ir, log) {
  // IR verification support — verifier module passed via ir._verify_mod
  // (streamline's use() is use_basic from bootstrap, which can't load source)
  var verify_fn = null
  var verifier = null
  if (ir._verify && ir._verify_mod) {
    verifier = ir._verify_mod
    verify_fn = function(func, pass_name) {
      var errs = verifier.verify_all(func, pass_name)
      var i = 0
      while (i < length(errs)) {
        print(`[verify_ir] ${errs[i]}\n`)
        i = i + 1
      }
      if (length(errs) > 0) {
        print(`[verify_ir] ${text(length(errs))} errors after ${pass_name}\n`)
      }
    }
  }

  // Type constants
  var T_UNKNOWN = "unknown"
  var T_INT = "int"
  var T_FLOAT = "float"
  var T_NUM = "num"
  var T_TEXT = "text"
  var T_BOOL = "bool"
  var T_NULL = "null"
  var T_ARRAY = "array"
  var T_RECORD = "record"
  var T_FUNCTION = "function"
  var T_BLOB = "blob"

  var numeric_ops = {
    add: true, subtract: true, multiply: true,
    divide: true, modulo: true, pow: true
  }
  var bool_result_ops = {
    eq_int: true, ne_int: true, lt_int: true, gt_int: true,
    le_int: true, ge_int: true,
    eq_float: true, ne_float: true, lt_float: true, gt_float: true,
    le_float: true, ge_float: true,
    eq_text: true, ne_text: true, lt_text: true, gt_text: true,
    le_text: true, ge_text: true,
    eq_bool: true, ne_bool: true,
    eq_tol: true, ne_tol: true,
    not: true, and: true, or: true,
    is_int: true, is_text: true, is_num: true,
    is_bool: true, is_null: true, is_identical: true,
    is_array: true, is_func: true, is_record: true, is_stone: true
  }
  var type_check_map = {
    is_int: T_INT, is_text: T_TEXT, is_num: T_NUM,
    is_bool: T_BOOL, is_null: T_NULL,
    is_array: T_ARRAY, is_func: T_FUNCTION,
    is_record: T_RECORD
  }

  // simplify_algebra dispatch tables
  var self_true_ops = {
    eq_int: true, eq_float: true, eq_text: true, eq_bool: true,
    is_identical: true,
    le_int: true, le_float: true, le_text: true,
    ge_int: true, ge_float: true, ge_text: true
  }
  var self_false_ops = {
    ne_int: true, ne_float: true, ne_text: true, ne_bool: true,
    lt_int: true, lt_float: true, lt_text: true,
    gt_int: true, gt_float: true, gt_text: true
  }
  var no_clear_ops = {
    int: true, access: true, true: true, false: true, move: true, null: true,
    jump: true, jump_true: true, jump_false: true, jump_not_null: true,
    return: true, disrupt: true,
    store_field: true, store_index: true, store_dynamic: true,
    push: true, setarg: true, invoke: true, tail_invoke: true
  }

  // --- Logging support ---

  var ir_stats = null
  var time_mod = null
  if (log != null) {
    ir_stats = use("ir_stats")
    time_mod = use("time")
  }

  var run_pass = function(func, pass_name, pass_fn) {
    var before = null
    var after = null
    var t0 = null
    var t1 = null
    var ms = null
    var changed = false
    var result = null
    if (log == null) {
      return pass_fn()
    }
    before = ir_stats.detailed_stats(func)
    t0 = time_mod.number()
    result = pass_fn()
    t1 = time_mod.number()
    after = ir_stats.detailed_stats(func)
    ms = (t1 - t0) * 1000
    changed = before.instr != after.instr ||
              before.nop != after.nop ||
              before.guard != after.guard
    log.passes[] = {
      pass: pass_name,
      fn: func.name,
      ms: ms,
      before: before,
      after: after,
      changed: changed,
      changes: {
        nops_added: after.nop - before.nop,
        guards_removed: before.guard - after.guard
      }
    }
    return result
  }

  // --- Shared helpers ---

  var access_value_type = function(val) {
    if (is_number(val)) {
      if (is_integer(val)) {
        return T_INT
      }
      return T_FLOAT
    }
    if (is_text(val)) {
      return T_TEXT
    }
    return T_UNKNOWN
  }

  // track_types reuses write_rules table; move handled specially
  var track_types = function(slot_types, instr) {
    var op = instr[0]
    var rule = null
    var src_type = null
    var typ = null
    if (op == "move") {
      src_type = slot_types[instr[2]]
      slot_types[instr[1]] = src_type != null ? src_type : T_UNKNOWN
      return null
    }
    rule = write_rules[op]
    if (rule != null) {
      typ = rule[1]
      if (typ == null) {
        typ = access_value_type(instr[2])
      }
      slot_types[instr[rule[0]]] = typ
    }
    return null
  }

  var slot_is = function(slot_types, slot, typ) {
    var known = slot_types[slot]
    if (known == null) {
      return false
    }
    if (known == typ) {
      return true
    }
    if (typ == T_NUM && (known == T_INT || known == T_FLOAT)) {
      return true
    }
    return false
  }

  var merge_backward = function(backward_types, slot, typ) {
    var existing = null
    if (!is_number(slot)) {
      return null
    }
    existing = backward_types[slot]
    if (existing == null) {
      backward_types[slot] = typ
    } else if (existing != typ && existing != T_UNKNOWN) {
      if ((existing == T_INT || existing == T_FLOAT) && typ == T_NUM) {
        backward_types[slot] = T_NUM
      } else if (existing == T_NUM && (typ == T_INT || typ == T_FLOAT)) {
        // Keep wider T_NUM
      } else if ((existing == T_INT && typ == T_FLOAT) || (existing == T_FLOAT && typ == T_INT)) {
        backward_types[slot] = T_NUM
      } else {
        backward_types[slot] = T_UNKNOWN
      }
    }
    return null
  }

  var seed_params = function(slot_types, param_types, nr_args) {
    var j = 1
    while (j <= nr_args) {
      if (param_types[j] != null) {
        slot_types[j] = param_types[j]
      }
      j = j + 1
    }
    return null
  }

  var seed_writes = function(slot_types, write_types) {
    var k = 0
    while (k < length(write_types)) {
      if (write_types[k] != null) {
        slot_types[k] = write_types[k]
      }
      k = k + 1
    }
    return null
  }

  // =========================================================
  // Pass: infer_param_types — backward type inference
  // Scans typed operators to infer immutable parameter types.
  // Uses data-driven dispatch: each rule is [pos1, type1] or
  // [pos1, type1, pos2, type2] for operand positions to merge.
  // =========================================================
  var param_rules = {
    add: [2, T_NUM, 3, T_NUM],
    subtract: [2, T_NUM, 3, T_NUM], multiply: [2, T_NUM, 3, T_NUM],
    divide: [2, T_NUM, 3, T_NUM], modulo: [2, T_NUM, 3, T_NUM],
    pow: [2, T_NUM, 3, T_NUM], negate: [2, T_NUM],
    bitand: [2, T_INT, 3, T_INT], bitor: [2, T_INT, 3, T_INT],
    bitxor: [2, T_INT, 3, T_INT], shl: [2, T_INT, 3, T_INT],
    shr: [2, T_INT, 3, T_INT], ushr: [2, T_INT, 3, T_INT],
    bitnot: [2, T_INT],
    concat: [2, T_TEXT, 3, T_TEXT],
    not: [2, T_BOOL], and: [2, T_BOOL, 3, T_BOOL], or: [2, T_BOOL, 3, T_BOOL],
    store_index: [1, T_ARRAY, 2, T_INT], store_field: [1, T_RECORD],
    push: [1, T_ARRAY],
    load_index: [2, T_ARRAY, 3, T_INT], load_field: [2, T_RECORD],
    pop: [2, T_ARRAY]
  }

  var infer_param_types = function(func) {
    var instructions = func.instructions
    var nr_args = func.nr_args != null ? func.nr_args : 0
    var num_instr = 0
    var backward_types = null
    var param_types = null
    var i = 0
    var j = 0
    var instr = null
    var bt = null
    var rule = null

    if (instructions == null || nr_args == 0) {
      return array(func.nr_slots)
    }

    num_instr = length(instructions)
    backward_types = array(func.nr_slots)
    i = 0
    while (i < num_instr) {
      instr = instructions[i]
      if (is_array(instr)) {
        rule = param_rules[instr[0]]
        if (rule != null) {
          merge_backward(backward_types, instr[rule[0]], rule[1])
          if (length(rule) > 2) {
            merge_backward(backward_types, instr[rule[2]], rule[3])
          }
        }
      }
      i = i + 1
    }

    param_types = array(func.nr_slots)
    j = 1
    while (j <= nr_args) {
      bt = backward_types[j]
      if (bt != null && bt != T_UNKNOWN) {
        param_types[j] = bt
      }
      j = j + 1
    }
    return param_types
  }

  // =========================================================
  // Pass: infer_slot_write_types — slot write-type invariance
  // Scans all instructions to find non-parameter slots where
  // every write produces the same type. These types persist
  // across label join points.
  // Uses data-driven dispatch: each rule is [dest_pos, type].
  // =========================================================
  var write_rules = {
    int: [1, T_INT], true: [1, T_BOOL], false: [1, T_BOOL],
    null: [1, T_NULL], access: [1, null],
    array: [1, T_ARRAY], record: [1, T_RECORD],
    function: [1, T_FUNCTION], length: [1, T_INT],
    bitnot: [1, T_INT], bitand: [1, T_INT], bitor: [1, T_INT],
    bitxor: [1, T_INT], shl: [1, T_INT], shr: [1, T_INT], ushr: [1, T_INT],
    negate: [1, T_NUM], concat: [1, T_TEXT],
    eq: [1, T_BOOL], ne: [1, T_BOOL], lt: [1, T_BOOL],
    le: [1, T_BOOL], gt: [1, T_BOOL], ge: [1, T_BOOL], in: [1, T_BOOL],
    add: [1, T_NUM], subtract: [1, T_NUM], multiply: [1, T_NUM],
    divide: [1, T_NUM], modulo: [1, T_NUM], pow: [1, T_NUM],
    move: [1, T_UNKNOWN], load_field: [1, T_UNKNOWN],
    load_index: [1, T_UNKNOWN], load_dynamic: [1, T_UNKNOWN],
    pop: [1, T_UNKNOWN], get: [1, T_UNKNOWN],
    invoke: [2, T_UNKNOWN], tail_invoke: [2, T_UNKNOWN],
    eq_int: [1, T_BOOL], ne_int: [1, T_BOOL], lt_int: [1, T_BOOL],
    gt_int: [1, T_BOOL], le_int: [1, T_BOOL], ge_int: [1, T_BOOL],
    eq_float: [1, T_BOOL], ne_float: [1, T_BOOL], lt_float: [1, T_BOOL],
    gt_float: [1, T_BOOL], le_float: [1, T_BOOL], ge_float: [1, T_BOOL],
    eq_text: [1, T_BOOL], ne_text: [1, T_BOOL], lt_text: [1, T_BOOL],
    gt_text: [1, T_BOOL], le_text: [1, T_BOOL], ge_text: [1, T_BOOL],
    eq_bool: [1, T_BOOL], ne_bool: [1, T_BOOL],
    eq_tol: [1, T_BOOL], ne_tol: [1, T_BOOL],
    not: [1, T_BOOL], and: [1, T_BOOL], or: [1, T_BOOL],
    is_int: [1, T_BOOL], is_text: [1, T_BOOL], is_num: [1, T_BOOL],
    is_bool: [1, T_BOOL], is_null: [1, T_BOOL], is_identical: [1, T_BOOL],
    is_array: [1, T_BOOL], is_func: [1, T_BOOL],
    is_record: [1, T_BOOL], is_stone: [1, T_BOOL]
  }

  var infer_slot_write_types = function(func) {
    var instructions = func.instructions
    var nr_args = func.nr_args != null ? func.nr_args : 0
    var num_instr = 0
    var write_types = null
    var i = 0
    var k = 0
    var instr = null
    var slot = 0
    var typ = null
    var rule = null
    var cw_keys = null

    if (instructions == null) {
      return array(func.nr_slots)
    }

    num_instr = length(instructions)
    write_types = array(func.nr_slots)
    i = 0
    while (i < num_instr) {
      instr = instructions[i]
      if (is_array(instr)) {
        rule = write_rules[instr[0]]
        if (rule != null) {
          slot = instr[rule[0]]
          typ = rule[1]
          if (typ == null) {
            typ = access_value_type(instr[2])
          }
          if (slot > 0 && slot > nr_args) {
            merge_backward(write_types, slot, typ)
          }
        }
      }
      i = i + 1
    }

    // Closure-written slots can have any type at runtime — mark unknown
    if (func.closure_written != null) {
      cw_keys = array(func.closure_written)
      k = 0
      while (k < length(cw_keys)) {
        slot = number(cw_keys[k])
        if (slot >= 0 && slot < length(write_types)) {
          write_types[slot] = T_UNKNOWN
        }
        k = k + 1
      }
    }

    // Filter to only slots with known (non-unknown) types
    k = 0
    while (k < length(write_types)) {
      if (write_types[k] == T_UNKNOWN) {
        write_types[k] = null
      }
      k = k + 1
    }
    return write_types
  }

  // =========================================================
  // Pass: eliminate_type_checks — language-level type narrowing
  // Eliminates is_<type>/jump pairs when type is known.
  // Reduces load_dynamic/store_dynamic to field/index forms.
  // =========================================================
  var eliminate_type_checks = function(func, param_types, write_types, log) {
    var instructions = func.instructions
    var nr_args = func.nr_args != null ? func.nr_args : 0
    var num_instr = 0
    var base_types = null
    var slot_types = null
    var nc = 0
    var i = 0
    var j = 0
    var instr = null
    var op = null
    var dest = 0
    var src = 0
    var checked_type = null
    var next = null
    var next_op = null
    var target_label = null
    var src_known = null
    var jlen = 0
    var events = null
    var old_op = null

    if (instructions == null || length(instructions) == 0) {
      return {}
    }

    if (log != null && log.events != null) {
      events = log.events
    }

    num_instr = length(instructions)

    // Pre-compute base types: params + write-invariant types
    base_types = array(func.nr_slots)
    j = 1
    while (j <= nr_args) {
      if (param_types[j] != null) {
        base_types[j] = param_types[j]
      }
      j = j + 1
    }
    j = 0
    while (j < length(write_types)) {
      if (write_types[j] != null) {
        base_types[j] = write_types[j]
      }
      j = j + 1
    }

    slot_types = array(base_types)

    i = 0
    while (i < num_instr) {
      instr = instructions[i]

      if (is_text(instr)) {
        slot_types = array(base_types)
        i = i + 1
        continue
      }

      if (!is_array(instr)) {
        i = i + 1
        continue
      }

      op = instr[0]

      // Type-check + jump elimination
      if (type_check_map[op] != null && i + 1 < num_instr) {
        dest = instr[1]
        src = instr[2]
        checked_type = type_check_map[op]
        next = instructions[i + 1]

        if (is_array(next)) {
          next_op = next[0]

          if (next_op == "jump_false" && next[1] == dest) {
            target_label = next[2]
            if (slot_is(slot_types, src, checked_type)) {
              nc = nc + 1
              instructions[i] = "_nop_tc_" + text(nc)
              nc = nc + 1
              instructions[i + 1] = "_nop_tc_" + text(nc)
              if (events != null) {
                events[] = {
                  event: "rewrite",
                  pass: "eliminate_type_checks",
                  rule: "known_type_eliminates_guard",
                  at: i,
                  before: [instr, next],
                  after: [instructions[i], instructions[i + 1]],
                  why: {slot: src, known_type: slot_types[src], checked_type: checked_type}
                }
              }
              slot_types[dest] = T_BOOL
              i = i + 2
              continue
            }
            src_known = slot_types[src]
            if (src_known != null && src_known != T_UNKNOWN && src_known != checked_type) {
              if (checked_type == T_NUM && (src_known == T_INT || src_known == T_FLOAT)) {
                nc = nc + 1
                instructions[i] = "_nop_tc_" + text(nc)
                nc = nc + 1
                instructions[i + 1] = "_nop_tc_" + text(nc)
                if (events != null) {
                  events[] = {
                    event: "rewrite",
                    pass: "eliminate_type_checks",
                    rule: "num_subsumes_int_float",
                    at: i,
                    before: [instr, next],
                    after: [instructions[i], instructions[i + 1]],
                    why: {slot: src, known_type: src_known, checked_type: checked_type}
                  }
                }
                slot_types[dest] = T_BOOL
                i = i + 2
                continue
              }
              if ((checked_type == T_INT || checked_type == T_FLOAT) && src_known == T_NUM) {
                // T_NUM could be int or float — not a mismatch, keep check
                slot_types[dest] = T_BOOL
                slot_types[src] = checked_type
                i = i + 2
                continue
              }
              nc = nc + 1
              instructions[i] = "_nop_tc_" + text(nc)
              jlen = length(next)
              instructions[i + 1] = ["jump", target_label, next[jlen - 2], next[jlen - 1]]
              if (events != null) {
                events[] = {
                  event: "rewrite",
                  pass: "eliminate_type_checks",
                  rule: "incompatible_type_forces_jump",
                  at: i,
                  before: [instr, next],
                  after: [instructions[i], instructions[i + 1]],
                  why: {slot: src, known_type: src_known, checked_type: checked_type}
                }
              }
              slot_types[dest] = T_UNKNOWN
              i = i + 2
              continue
            }
            slot_types[dest] = T_BOOL
            slot_types[src] = checked_type
            i = i + 2
            continue
          }

          if (next_op == "jump_true" && next[1] == dest) {
            target_label = next[2]
            if (slot_is(slot_types, src, checked_type)) {
              nc = nc + 1
              instructions[i] = "_nop_tc_" + text(nc)
              jlen = length(next)
              instructions[i + 1] = ["jump", target_label, next[jlen - 2], next[jlen - 1]]
              if (events != null) {
                events[] = {
                  event: "rewrite",
                  pass: "eliminate_type_checks",
                  rule: "known_type_eliminates_guard",
                  at: i,
                  before: [instr, next],
                  after: [instructions[i], instructions[i + 1]],
                  why: {slot: src, known_type: slot_types[src], checked_type: checked_type}
                }
              }
              slot_types[dest] = T_BOOL
              i = i + 2
              continue
            }
            src_known = slot_types[src]
            if (src_known != null && src_known != T_UNKNOWN && src_known != checked_type) {
              if (checked_type == T_NUM && (src_known == T_INT || src_known == T_FLOAT)) {
                nc = nc + 1
                instructions[i] = "_nop_tc_" + text(nc)
                jlen = length(next)
                instructions[i + 1] = ["jump", target_label, next[jlen - 2], next[jlen - 1]]
                if (events != null) {
                  events[] = {
                    event: "rewrite",
                    pass: "eliminate_type_checks",
                    rule: "num_subsumes_int_float",
                    at: i,
                    before: [instr, next],
                    after: [instructions[i], instructions[i + 1]],
                    why: {slot: src, known_type: src_known, checked_type: checked_type}
                  }
                }
                slot_types[dest] = T_BOOL
                i = i + 2
                continue
              }
              if ((checked_type == T_INT || checked_type == T_FLOAT) && src_known == T_NUM) {
                // T_NUM could be int or float — not a mismatch, keep check
                slot_types[dest] = T_BOOL
                i = i + 2
                continue
              }
              nc = nc + 1
              instructions[i] = "_nop_tc_" + text(nc)
              nc = nc + 1
              instructions[i + 1] = "_nop_tc_" + text(nc)
              if (events != null) {
                events[] = {
                  event: "rewrite",
                  pass: "eliminate_type_checks",
                  rule: "incompatible_type_forces_jump",
                  at: i,
                  before: [instr, next],
                  after: [instructions[i], instructions[i + 1]],
                  why: {slot: src, known_type: src_known, checked_type: checked_type}
                }
              }
              slot_types[dest] = T_BOOL
              i = i + 2
              continue
            }
            slot_types[dest] = T_BOOL
            i = i + 2
            continue
          }
        }

        slot_types[dest] = T_BOOL
        i = i + 1
        continue
      }

      // Dynamic access reduction
      if (op == "load_dynamic") {
        old_op = op
        if (slot_is(slot_types, instr[3], T_TEXT)) {
          instr[0] = "load_field"
          if (events != null) {
            events[] = {
              event: "rewrite",
              pass: "eliminate_type_checks",
              rule: "dynamic_to_field",
              at: i, before: old_op, after: instr[0],
              why: {slot: instr[3], known_type: slot_types[instr[3]]}
            }
          }
        } else if (slot_is(slot_types, instr[3], T_INT)) {
          instr[0] = "load_index"
          if (events != null) {
            events[] = {
              event: "rewrite",
              pass: "eliminate_type_checks",
              rule: "dynamic_to_index",
              at: i, before: old_op, after: instr[0],
              why: {slot: instr[3], known_type: slot_types[instr[3]]}
            }
          }
        }
        slot_types[instr[1]] = T_UNKNOWN
        i = i + 1
        continue
      }
      if (op == "store_dynamic") {
        old_op = op
        if (slot_is(slot_types, instr[3], T_TEXT)) {
          instr[0] = "store_field"
          if (events != null) {
            events[] = {
              event: "rewrite",
              pass: "eliminate_type_checks",
              rule: "dynamic_to_field",
              at: i, before: old_op, after: instr[0],
              why: {slot: instr[3], known_type: slot_types[instr[3]]}
            }
          }
        } else if (slot_is(slot_types, instr[3], T_INT)) {
          instr[0] = "store_index"
          if (events != null) {
            events[] = {
              event: "rewrite",
              pass: "eliminate_type_checks",
              rule: "dynamic_to_index",
              at: i, before: old_op, after: instr[0],
              why: {slot: instr[3], known_type: slot_types[instr[3]]}
            }
          }
        }
        i = i + 1
        continue
      }

      track_types(slot_types, instr)
      i = i + 1
    }

    return slot_types
  }

  // =========================================================
  // Pass: simplify_algebra — algebraic identity & comparison
  // Tracks known constant values. Rewrites identity ops to
  // moves or constants. Folds same-slot comparisons.
  // =========================================================
  var simplify_algebra = function(func, log) {
    var instructions = func.instructions
    var num_instr = 0
    var slot_values = null
    var nc = 0
    var i = 0
    var instr = null
    var op = null
    var ilen = 0
    var sv = null
    var events = null

    if (instructions == null || length(instructions) == 0) {
      return null
    }

    if (log != null && log.events != null) {
      events = log.events
    }

    num_instr = length(instructions)
    slot_values = array(func.nr_slots)

    i = 0
    while (i < num_instr) {
      instr = instructions[i]

      if (is_text(instr)) {
        slot_values = array(func.nr_slots)
        i = i + 1
        continue
      }
      if (!is_array(instr)) {
        i = i + 1
        continue
      }

      op = instr[0]
      ilen = length(instr)

      // Track known constant values
      if (op == "int") {
        slot_values[instr[1]] = instr[2]
      } else if (op == "access" && is_number(instr[2])) {
        slot_values[instr[1]] = instr[2]
      } else if (op == "true") {
        slot_values[instr[1]] = true
      } else if (op == "false") {
        slot_values[instr[1]] = false
      } else if (op == "move") {
        sv = slot_values[instr[2]]
        if (sv != null) {
          slot_values[instr[1]] = sv
        } else {
          slot_values[instr[1]] = null
        }
      }

      // Same-slot comparisons
      if (is_number(instr[2]) && instr[2] == instr[3]) {
        if (self_true_ops[op] == true) {
          instructions[i] = ["true", instr[1], instr[ilen - 2], instr[ilen - 1]]
          if (events != null) {
            events[] = {
              event: "rewrite", pass: "simplify_algebra",
              rule: "self_eq", at: i,
              before: instr, after: instructions[i],
              why: {op: op, slot: instr[2]}
            }
          }
          slot_values[instr[1]] = true
          i = i + 1
          continue
        }
        if (self_false_ops[op] == true) {
          instructions[i] = ["false", instr[1], instr[ilen - 2], instr[ilen - 1]]
          if (events != null) {
            events[] = {
              event: "rewrite", pass: "simplify_algebra",
              rule: "self_ne", at: i,
              before: instr, after: instructions[i],
              why: {op: op, slot: instr[2]}
            }
          }
          slot_values[instr[1]] = false
          i = i + 1
          continue
        }
      }

      // Clear value tracking for dest-producing ops (not reads-only)
      if (op == "invoke" || op == "tail_invoke") {
        slot_values[instr[2]] = null
      } else if (no_clear_ops[op] != true) {
        if (is_number(instr[1])) {
          slot_values[instr[1]] = null
        }
      }

      i = i + 1
    }

    return null
  }

  // =========================================================
  // Pass: simplify_booleans — not+jump fusion, double-not
  // =========================================================
  var simplify_booleans = function(func, log) {
    var instructions = func.instructions
    var num_instr = 0
    var nc = 0
    var i = 0
    var instr = null
    var next = null
    var next_op = null
    var nlen = 0
    var events = null

    if (instructions == null || length(instructions) == 0) {
      return null
    }

    if (log != null && log.events != null) {
      events = log.events
    }

    num_instr = length(instructions)
    i = 0
    while (i < num_instr) {
      instr = instructions[i]
      if (!is_array(instr) || instr[0] != "not" || i + 1 >= num_instr) {
        i = i + 1
        continue
      }

      next = instructions[i + 1]
      if (!is_array(next)) {
        i = i + 1
        continue
      }

      next_op = next[0]
      nlen = length(next)

      // not d, x; jump_false d, label → jump_true x, label
      if (next_op == "jump_false" && next[1] == instr[1]) {
        nc = nc + 1
        instructions[i] = "_nop_bl_" + text(nc)
        instructions[i + 1] = ["jump_true", instr[2], next[2], next[nlen - 2], next[nlen - 1]]
        if (events != null) {
          events[] = {
            event: "rewrite", pass: "simplify_booleans",
            rule: "not_jump_false_fusion", at: i,
            before: [instr, next],
            after: [instructions[i], instructions[i + 1]]
          }
        }
        i = i + 2
        continue
      }

      // not d, x; jump_true d, label → jump_false x, label
      if (next_op == "jump_true" && next[1] == instr[1]) {
        nc = nc + 1
        instructions[i] = "_nop_bl_" + text(nc)
        instructions[i + 1] = ["jump_false", instr[2], next[2], next[nlen - 2], next[nlen - 1]]
        if (events != null) {
          events[] = {
            event: "rewrite", pass: "simplify_booleans",
            rule: "not_jump_true_fusion", at: i,
            before: [instr, next],
            after: [instructions[i], instructions[i + 1]]
          }
        }
        i = i + 2
        continue
      }

      // not d1, x; not d2, d1 → move d2, x
      if (next_op == "not" && next[2] == instr[1]) {
        nc = nc + 1
        instructions[i] = "_nop_bl_" + text(nc)
        instructions[i + 1] = ["move", next[1], instr[2], next[nlen - 2], next[nlen - 1]]
        if (events != null) {
          events[] = {
            event: "rewrite", pass: "simplify_booleans",
            rule: "double_not", at: i,
            before: [instr, next],
            after: [instructions[i], instructions[i + 1]]
          }
        }
        i = i + 2
        continue
      }

      i = i + 1
    }

    return null
  }

  // =========================================================
  // Pass: eliminate_moves — move a, a → nop
  // =========================================================
  var eliminate_moves = function(func, log) {
    var instructions = func.instructions
    var num_instr = 0
    var nc = 0
    var i = 0
    var instr = null
    var events = null

    if (instructions == null || length(instructions) == 0) {
      return null
    }

    if (log != null && log.events != null) {
      events = log.events
    }

    num_instr = length(instructions)
    i = 0
    while (i < num_instr) {
      instr = instructions[i]
      if (is_array(instr) && instr[0] == "move" && instr[1] == instr[2]) {
        nc = nc + 1
        instructions[i] = "_nop_mv_" + text(nc)
        if (events != null) {
          events[] = {
            event: "rewrite", pass: "eliminate_moves",
            rule: "self_move", at: i,
            before: instr, after: instructions[i]
          }
        }
      }
      i = i + 1
    }

    return null
  }

  // =========================================================
  // Pass: eliminate_unreachable — nop code after return/disrupt
  // =========================================================
  var eliminate_unreachable = function(func) {
    var instructions = func.instructions
    var num_instr = 0
    var nc = 0
    var after_return = false
    var i = 0
    var instr = null

    if (instructions == null || length(instructions) == 0) {
      return null
    }

    num_instr = length(instructions)
    i = 0
    while (i < num_instr) {
      instr = instructions[i]
      if (is_text(instr)) {
        if (!starts_with(instr, "_nop_")) {
          after_return = false
        }
      } else if (is_array(instr)) {
        if (after_return) {
          nc = nc + 1
          instructions[i] = "_nop_ur_" + text(nc)
        } else if (instr[0] == "return") {
          after_return = true
        }
      }
      i = i + 1
    }

    return null
  }

  // =========================================================
  // Pass: eliminate_dead_jumps — jump to next label → nop
  // =========================================================
  var eliminate_dead_jumps = function(func, log) {
    var instructions = func.instructions
    var num_instr = 0
    var nc = 0
    var i = 0
    var j = 0
    var instr = null
    var target_label = null
    var peek = null
    var events = null

    if (instructions == null || length(instructions) == 0) {
      return null
    }

    if (log != null && log.events != null) {
      events = log.events
    }

    num_instr = length(instructions)
    i = 0
    while (i < num_instr) {
      instr = instructions[i]
      if (is_array(instr) && instr[0] == "jump") {
        target_label = instr[1]
        j = i + 1
        while (j < num_instr) {
          peek = instructions[j]
          if (is_text(peek)) {
            if (peek == target_label) {
              nc = nc + 1
              instructions[i] = "_nop_dj_" + text(nc)
              if (events != null) {
                events[] = {
                  event: "rewrite", pass: "eliminate_dead_jumps",
                  rule: "jump_to_next", at: i,
                  before: instr, after: instructions[i],
                  why: {label: target_label}
                }
              }
            }
            break
          }
          if (is_array(peek)) {
            break
          }
          j = j + 1
        }
      }
      i = i + 1
    }

    return null
  }

  // =========================================================
  // Pass: compress_slots — linear-scan register allocation
  // Reuses slots with non-overlapping live ranges to reduce
  // nr_slots. Mirrors mcode_compress_regs from mach.c.
  // Works across all functions for captured-slot tracking.
  // =========================================================

  // Which instruction positions hold slot references (special cases)
  var slot_idx_special = {
    get: [1], put: [1],
    access: [1], int: [1], function: [1], regexp: [1],
    true: [1], false: [1], null: [1],
    record: [1], array: [1],
    invoke: [1, 2], tail_invoke: [1, 2],
    goinvoke: [1],
    setarg: [1, 3],
    frame: [1, 2], goframe: [1, 2],
    jump: [], disrupt: [],
    jump_true: [1], jump_false: [1], jump_not_null: [1],
    return: [1]
  }

  var get_slot_refs = function(instr) {
    var special = slot_idx_special[instr[0]]
    var result = null
    var j = 0
    var limit = 0
    if (special != null) return special
    result = []
    limit = length(instr) - 2
    j = 1
    while (j < limit) {
      if (is_number(instr[j])) result[] = j
      j = j + 1
    }
    return result
  }

  var compress_one_fn = function(func, captured_slots) {
    var instructions = func.instructions
    var nr_slots = func.nr_slots
    var nr_args = func.nr_args != null ? func.nr_args : 0
    var n = 0
    var pinned = 0
    var first_ref = null
    var last_ref = null
    var i = 0
    var j = 0
    var k = 0
    var s = 0
    var instr = null
    var refs = null
    var op = null
    var target = null
    var tpos = 0
    var changed = false
    var label_map = null
    var live_slots = null
    var live_first = null
    var live_last = null
    var cnt = 0
    var key_s = 0
    var key_f = 0
    var key_l = 0
    var remap = null
    var pool = null
    var next_phys = 0
    var active_phys = null
    var active_last = null
    var phys = 0
    var mi = 0
    var new_max = 0
    var old_val = 0
    var new_active_phys = null
    var new_active_last = null
    var new_pool = null

    if (instructions == null || !is_number(nr_slots) || nr_slots <= 1) return null
    n = length(instructions)
    pinned = 1 + nr_args

    // Step 1: build live ranges
    first_ref = array(nr_slots, -1)
    last_ref = array(nr_slots, -1)

    // Pin this + args
    k = 0
    while (k < pinned) {
      first_ref[k] = 0
      last_ref[k] = n
      k = k + 1
    }

    // Scan instructions for slot references
    i = 0
    while (i < n) {
      instr = instructions[i]
      if (is_array(instr)) {
        refs = get_slot_refs(instr)
        j = 0
        while (j < length(refs)) {
          s = instr[refs[j]]
          if (is_number(s) && s >= 0 && s < nr_slots) {
            if (first_ref[s] < 0) first_ref[s] = i
            last_ref[s] = i
          }
          j = j + 1
        }
      }
      i = i + 1
    }

    // Pin captured slots (AFTER scan so last_ref isn't overwritten)
    if (captured_slots != null) {
      k = 0
      while (k < length(captured_slots)) {
        s = captured_slots[k]
        if (s >= 0 && s < nr_slots) {
          if (first_ref[s] < 0) first_ref[s] = 0
          last_ref[s] = n
        }
        k = k + 1
      }
    }

    // Step 1b: extend for backward jumps (loops)
    label_map = {}
    i = 0
    while (i < n) {
      instr = instructions[i]
      if (is_text(instr) && !starts_with(instr, "_nop_")) {
        label_map[instr] = i
      }
      i = i + 1
    }

    changed = true
    while (changed) {
      changed = false
      i = 0
      while (i < n) {
        instr = instructions[i]
        if (!is_array(instr)) {
          i = i + 1
          continue
        }
        op = instr[0]
        target = null
        if (op == "jump") {
          target = instr[1]
        } else if (op == "jump_true" || op == "jump_false" || op == "jump_not_null") {
          target = instr[2]
        }
        if (target == null || !is_text(target)) {
          i = i + 1
          continue
        }
        tpos = label_map[target]
        if (tpos == null || tpos >= i) {
          i = i + 1
          continue
        }
        // Backward jump: extend slots live into loop
        s = pinned
        while (s < nr_slots) {
          if (first_ref[s] >= 0 && first_ref[s] < tpos && last_ref[s] >= tpos && last_ref[s] < i) {
            last_ref[s] = i
            changed = true
          }
          s = s + 1
        }
        i = i + 1
      }
    }

    // Step 2: sort live intervals by first_ref
    live_slots = []
    live_first = []
    live_last = []
    s = pinned
    while (s < nr_slots) {
      if (first_ref[s] >= 0) {
        live_slots[] = s
        live_first[] = first_ref[s]
        live_last[] = last_ref[s]
      }
      s = s + 1
    }

    cnt = length(live_slots)
    i = 1
    while (i < cnt) {
      key_s = live_slots[i]
      key_f = live_first[i]
      key_l = live_last[i]
      j = i - 1
      while (j >= 0 && (live_first[j] > key_f || (live_first[j] == key_f && live_slots[j] > key_s))) {
        live_slots[j + 1] = live_slots[j]
        live_first[j + 1] = live_first[j]
        live_last[j + 1] = live_last[j]
        j = j - 1
      }
      live_slots[j + 1] = key_s
      live_first[j + 1] = key_f
      live_last[j + 1] = key_l
      i = i + 1
    }

    // Linear-scan allocation
    remap = array(nr_slots)
    s = 0
    while (s < nr_slots) {
      remap[s] = s
      s = s + 1
    }

    pool = []
    next_phys = pinned
    active_phys = []
    active_last = []

    i = 0
    while (i < cnt) {
      // Expire intervals whose last < live_first[i]
      new_active_phys = []
      new_active_last = []
      j = 0
      while (j < length(active_phys)) {
        if (active_last[j] < live_first[i]) {
          pool[] = active_phys[j]
        } else {
          new_active_phys[] = active_phys[j]
          new_active_last[] = active_last[j]
        }
        j = j + 1
      }
      active_phys = new_active_phys
      active_last = new_active_last

      // Pick lowest available physical register
      if (length(pool) > 0) {
        mi = 0
        j = 1
        while (j < length(pool)) {
          if (pool[j] < pool[mi]) mi = j
          j = j + 1
        }
        phys = pool[mi]
        new_pool = []
        j = 0
        while (j < length(pool)) {
          if (j != mi) new_pool[] = pool[j]
          j = j + 1
        }
        pool = new_pool
      } else {
        phys = next_phys
        next_phys = next_phys + 1
      }

      remap[live_slots[i]] = phys
      active_phys[] = phys
      active_last[] = live_last[i]
      i = i + 1
    }

    // Compute new nr_slots
    new_max = pinned
    s = 0
    while (s < nr_slots) {
      if (first_ref[s] >= 0 && remap[s] >= new_max) {
        new_max = remap[s] + 1
      }
      s = s + 1
    }

    if (new_max >= nr_slots) return null

    // Step 3: apply remap to instructions
    i = 0
    while (i < n) {
      instr = instructions[i]
      if (is_array(instr)) {
        refs = get_slot_refs(instr)
        j = 0
        while (j < length(refs)) {
          old_val = instr[refs[j]]
          if (is_number(old_val) && old_val >= 0 && old_val < nr_slots) {
            instr[refs[j]] = remap[old_val]
          }
          j = j + 1
        }
      }
      i = i + 1
    }

    func.nr_slots = new_max
    return remap
  }

  var compress_slots = function(ir) {
    if (ir == null || ir.main == null) return null
    var functions = ir.functions != null ? ir.functions : []
    var func_count = length(functions)
    var parent_of = null
    var captured = null
    var remaps = null
    var remap_sizes = null
    var instrs = null
    var instr = null
    var child_idx = 0
    var parent_slot = 0
    var level = 0
    var ancestor = 0
    var caps = null
    var found = false
    var anc_remap = null
    var old_slot = 0
    var fi = 0
    var i = 0
    var j = 0
    var k = 0

    // Build parent_of: parent_of[i] = parent index, func_count = main
    parent_of = array(func_count, -1)

    // Scan main for function instructions
    if (ir.main != null && ir.main.instructions != null) {
      instrs = ir.main.instructions
      i = 0
      while (i < length(instrs)) {
        instr = instrs[i]
        if (is_array(instr) && instr[0] == "function") {
          child_idx = instr[2]
          if (child_idx >= 0 && child_idx < func_count) {
            parent_of[child_idx] = func_count
          }
        }
        i = i + 1
      }
    }

    // Scan each function for function instructions
    fi = 0
    while (fi < func_count) {
      instrs = functions[fi].instructions
      if (instrs != null) {
        i = 0
        while (i < length(instrs)) {
          instr = instrs[i]
          if (is_array(instr) && instr[0] == "function") {
            child_idx = instr[2]
            if (child_idx >= 0 && child_idx < func_count) {
              parent_of[child_idx] = fi
            }
          }
          i = i + 1
        }
      }
      fi = fi + 1
    }

    // Build captured slots per function
    captured = array(func_count + 1)
    i = 0
    while (i < func_count + 1) {
      captured[i] = []
      i = i + 1
    }

    fi = 0
    while (fi < func_count) {
      instrs = functions[fi].instructions
      if (instrs != null) {
        i = 0
        while (i < length(instrs)) {
          instr = instrs[i]
          if (is_array(instr) && (instr[0] == "get" || instr[0] == "put")) {
            parent_slot = instr[2]
            level = instr[3]
            ancestor = fi
            j = 0
            while (j < level && ancestor >= 0) {
              ancestor = parent_of[ancestor]
              j = j + 1
            }
            if (ancestor >= 0) {
              caps = captured[ancestor]
              found = false
              k = 0
              while (k < length(caps)) {
                if (caps[k] == parent_slot) {
                  found = true
                  k = length(caps)
                }
                k = k + 1
              }
              if (!found) caps[] = parent_slot
            }
          }
          i = i + 1
        }
      }
      fi = fi + 1
    }

    // Compress each function and save remap tables
    remaps = array(func_count + 1)
    remap_sizes = array(func_count + 1, 0)

    fi = 0
    while (fi < func_count) {
      remap_sizes[fi] = functions[fi].nr_slots
      remaps[fi] = compress_one_fn(functions[fi], captured[fi])
      fi = fi + 1
    }

    if (ir.main != null) {
      remap_sizes[func_count] = ir.main.nr_slots
      remaps[func_count] = compress_one_fn(ir.main, captured[func_count])
    }

    // Fix get/put parent_slot references using ancestor remap tables
    fi = 0
    while (fi < func_count) {
      instrs = functions[fi].instructions
      if (instrs != null) {
        i = 0
        while (i < length(instrs)) {
          instr = instrs[i]
          if (is_array(instr) && (instr[0] == "get" || instr[0] == "put")) {
            level = instr[3]
            ancestor = fi
            j = 0
            while (j < level && ancestor >= 0) {
              ancestor = parent_of[ancestor]
              j = j + 1
            }
            if (ancestor >= 0 && remaps[ancestor] != null) {
              anc_remap = remaps[ancestor]
              old_slot = instr[2]
              if (old_slot >= 0 && old_slot < remap_sizes[ancestor]) {
                instr[2] = anc_remap[old_slot]
              }
            }
          }
          i = i + 1
        }
      }
      fi = fi + 1
    }

    return null
  }

  // =========================================================
  // Pre-pass: mark closure-written slots
  // Scans child functions for 'put' instructions and annotates
  // the target ancestor func with closure_written[slot] = true.
  // =========================================================
  var mark_closure_writes = function(ir) {
    var functions = ir.functions != null ? ir.functions : []
    var fc = length(functions)
    var parent_of = array(fc, -1)
    var instrs = null
    var instr = null
    var fi = 0
    var i = 0
    var j = 0
    var level = 0
    var anc = 0
    var slot = 0
    var target = null

    if (fc == 0) {
      return null
    }

    // Build parent_of map
    if (ir.main != null && ir.main.instructions != null) {
      instrs = ir.main.instructions
      i = 0
      while (i < length(instrs)) {
        instr = instrs[i]
        if (is_array(instr) && instr[0] == "function") {
          if (instr[2] >= 0 && instr[2] < fc) {
            parent_of[instr[2]] = fc
          }
        }
        i = i + 1
      }
    }
    fi = 0
    while (fi < fc) {
      instrs = functions[fi].instructions
      if (instrs != null) {
        i = 0
        while (i < length(instrs)) {
          instr = instrs[i]
          if (is_array(instr) && instr[0] == "function") {
            if (instr[2] >= 0 && instr[2] < fc) {
              parent_of[instr[2]] = fi
            }
          }
          i = i + 1
        }
      }
      fi = fi + 1
    }

    // Scan for 'put' instructions and mark ancestor slots
    fi = 0
    while (fi < fc) {
      instrs = functions[fi].instructions
      if (instrs != null) {
        i = 0
        while (i < length(instrs)) {
          instr = instrs[i]
          if (is_array(instr) && instr[0] == "put") {
            slot = instr[2]
            level = instr[3]
            anc = fi
            j = 0
            while (j < level && anc >= 0) {
              anc = parent_of[anc]
              j = j + 1
            }
            if (anc >= 0) {
              if (anc == fc) {
                target = ir.main
              } else {
                target = functions[anc]
              }
              if (target != null) {
                if (target.closure_written == null) {
                  target.closure_written = {}
                }
                target.closure_written[text(slot)] = true
              }
            }
          }
          i = i + 1
        }
      }
      fi = fi + 1
    }
    return null
  }

  // =========================================================
  // Compose all passes
  // =========================================================
  var optimize_function = function(func, log) {
    var param_types = null
    var write_types = null
    var slot_types = null
    if (func.instructions == null || length(func.instructions) == 0) {
      return null
    }
    run_pass(func, "infer_param_types", function() {
      param_types = infer_param_types(func)
      return param_types
    })
    if (verify_fn) verify_fn(func, "after infer_param_types")
    run_pass(func, "infer_slot_write_types", function() {
      write_types = infer_slot_write_types(func)
      return write_types
    })
    if (verify_fn) verify_fn(func, "after infer_slot_write_types")
    run_pass(func, "eliminate_type_checks", function() {
      slot_types = eliminate_type_checks(func, param_types, write_types, log)
      return slot_types
    })
    if (verify_fn) verify_fn(func, "after eliminate_type_checks")
    if (log != null && log.type_deltas != null && slot_types != null) {
      log.type_deltas[] = {
        fn: func.name,
        param_types: param_types,
        slot_types: slot_types
      }
    }
    run_pass(func, "simplify_algebra", function() {
      return simplify_algebra(func, log)
    })
    if (verify_fn) verify_fn(func, "after simplify_algebra")
    run_pass(func, "simplify_booleans", function() {
      return simplify_booleans(func, log)
    })
    if (verify_fn) verify_fn(func, "after simplify_booleans")
    run_pass(func, "eliminate_moves", function() {
      return eliminate_moves(func, log)
    })
    if (verify_fn) verify_fn(func, "after eliminate_moves")
    run_pass(func, "eliminate_unreachable", function() {
      return eliminate_unreachable(func)
    })
    if (verify_fn) verify_fn(func, "after eliminate_unreachable")
    run_pass(func, "eliminate_dead_jumps", function() {
      return eliminate_dead_jumps(func, log)
    })
    if (verify_fn) verify_fn(func, "after eliminate_dead_jumps")
    return null
  }

  // Pre-pass: mark slots written by child closures via 'put' instructions.
  // Without this, infer_slot_write_types would assume those slots keep their
  // initial type (e.g. T_NULL from 'var x = null'), causing the type-check
  // eliminator to mis-optimize comparisons on closure-written variables.
  mark_closure_writes(ir)

  // Process main function
  if (ir.main != null) {
    optimize_function(ir.main, log)
  }

  // Process all sub-functions
  var fi = 0
  if (ir.functions != null) {
    fi = 0
    while (fi < length(ir.functions)) {
      optimize_function(ir.functions[fi], log)
      fi = fi + 1
    }
  }

  // Compress slots across all functions (must run after per-function passes)
  compress_slots(ir)

  return ir
}

return streamline
