// diff_ir.ce — mcode vs streamline diff
//
// Usage:
//   cell diff_ir <file>                Diff all functions
//   cell diff_ir --fn <N|name> <file>  Diff only one function
//   cell diff_ir --summary <file>      Counts only

var fd = use("fd")
var shop = use("internal/shop")

var pad_right = function(s, w) {
  var r = s
  while (length(r) < w) {
    r = r + " "
  }
  return r
}

var fmt_val = function(v) {
  if (is_null(v)) return "null"
  if (is_number(v)) return text(v)
  if (is_text(v)) return `"${v}"`
  if (is_object(v)) return text(v)
  if (is_logical(v)) return v ? "true" : "false"
  return text(v)
}

var run = function() {
  var fn_filter = null
  var show_summary = false
  var filename = null
  var i = 0
  var mcode_ir = null
  var opt_ir = null
  var source_text = null
  var source_lines = null
  var main_name = null
  var fi = 0
  var func = null
  var opt_func = null
  var fname = null

  while (i < length(args)) {
    if (args[i] == '--fn') {
      i = i + 1
      fn_filter = args[i]
    } else if (args[i] == '--summary') {
      show_summary = true
    } else if (args[i] == '--help' || args[i] == '-h') {
      log.console("Usage: cell diff_ir [--fn <N|name>] [--summary] <file>")
      log.console("")
      log.console("  --fn <N|name>   Filter to function by index or name")
      log.console("  --summary       Show counts only")
      return null
    } else if (!starts_with(args[i], '-')) {
      filename = args[i]
    }
    i = i + 1
  }

  if (!filename) {
    log.console("Usage: cell diff_ir [--fn <N|name>] [--summary] <file>")
    return null
  }

  mcode_ir = shop.mcode_file(filename)
  opt_ir = shop.compile_file(filename)

  source_text = text(fd.slurp(filename))
  source_lines = array(source_text, "\n")

  var get_source_line = function(line_num) {
    if (line_num < 1 || line_num > length(source_lines)) return null
    return source_lines[line_num - 1]
  }

  var fn_matches = function(index, name) {
    var match = null
    if (fn_filter == null) return true
    if (index >= 0 && fn_filter == text(index)) return true
    if (name != null) {
      match = search(name, fn_filter)
      if (match != null && match >= 0) return true
    }
    return false
  }

  var fmt_instr = function(instr) {
    var op = instr[0]
    var n = length(instr)
    var parts = []
    var j = 1
    var operands = null
    var line_str = null
    while (j < n - 2) {
      push(parts, fmt_val(instr[j]))
      j = j + 1
    }
    operands = text(parts, ", ")
    line_str = instr[n - 2] != null ? `:${text(instr[n - 2])}` : ""
    return pad_right(`${pad_right(op, 15)}${operands}`, 45) + line_str
  }

  var classify = function(before, after) {
    var bn = 0
    var an = 0
    var k = 0
    if (is_text(after) && starts_with(after, "_nop_")) return "eliminated"
    if (is_array(before) && is_array(after)) {
      if (before[0] != after[0]) return "rewritten"
      bn = length(before)
      an = length(after)
      if (bn != an) return "rewritten"
      k = 1
      while (k < bn - 2) {
        if (before[k] != after[k]) return "rewritten"
        k = k + 1
      }
      return "identical"
    }
    return "identical"
  }

  var total_eliminated = 0
  var total_rewritten = 0
  var total_funcs = 0

  var diff_function = function(mcode_func, opt_func, name, index) {
    var nr_args = mcode_func.nr_args != null ? mcode_func.nr_args : 0
    var nr_slots = mcode_func.nr_slots != null ? mcode_func.nr_slots : 0
    var m_instrs = mcode_func.instructions
    var o_instrs = opt_func.instructions
    var eliminated = 0
    var rewritten = 0
    var mi = 0
    var oi = 0
    var pc = 0
    var m_instr = null
    var o_instr = null
    var kind = null
    var last_line = null
    var instr_line = null
    var n = 0
    var src = null
    var annotation = null

    if (m_instrs == null) m_instrs = []
    if (o_instrs == null) o_instrs = []

    // First pass: count changes
    mi = 0
    oi = 0
    while (mi < length(m_instrs) && oi < length(o_instrs)) {
      m_instr = m_instrs[mi]
      o_instr = o_instrs[oi]

      if (is_text(m_instr)) {
        mi = mi + 1
        oi = oi + 1
        continue
      }

      if (is_text(o_instr) && starts_with(o_instr, "_nop_")) {
        if (is_array(m_instr)) {
          eliminated = eliminated + 1
        }
        mi = mi + 1
        oi = oi + 1
        continue
      }

      if (is_array(m_instr) && is_array(o_instr)) {
        kind = classify(m_instr, o_instr)
        if (kind == "rewritten") {
          rewritten = rewritten + 1
        }
      }
      mi = mi + 1
      oi = oi + 1
    }

    total_eliminated = total_eliminated + eliminated
    total_rewritten = total_rewritten + rewritten
    total_funcs = total_funcs + 1

    if (show_summary) {
      if (eliminated == 0 && rewritten == 0) {
        log.compile(`  ${pad_right(name + ":", 40)} 0 eliminated, 0 rewritten (unchanged)`)
      } else {
        log.compile(`  ${pad_right(name + ":", 40)} ${text(eliminated)} eliminated, ${text(rewritten)} rewritten`)
      }
      return null
    }

    if (eliminated == 0 && rewritten == 0) return null

    log.compile(`\n=== ${name} (args=${text(nr_args)}, slots=${text(nr_slots)}) ===`)
    log.compile(`  ${text(eliminated)} eliminated, ${text(rewritten)} rewritten`)

    // Second pass: show diffs
    mi = 0
    oi = 0
    pc = 0
    last_line = null
    while (mi < length(m_instrs) && oi < length(o_instrs)) {
      m_instr = m_instrs[mi]
      o_instr = o_instrs[oi]

      if (is_text(m_instr) && !starts_with(m_instr, "_nop_")) {
        mi = mi + 1
        oi = oi + 1
        continue
      }

      if (is_text(m_instr) && starts_with(m_instr, "_nop_")) {
        mi = mi + 1
        oi = oi + 1
        continue
      }

      if (is_text(o_instr) && starts_with(o_instr, "_nop_")) {
        if (is_array(m_instr)) {
          n = length(m_instr)
          instr_line = m_instr[n - 2]
          if (instr_line != last_line && instr_line != null) {
            src = get_source_line(instr_line)
            if (src != null) src = trim(src)
            if (last_line != null) log.compile("")
            if (src != null && length(src) > 0) {
              log.compile(`  --- line ${text(instr_line)}: ${src} ---`)
            }
            last_line = instr_line
          }
          log.compile(`  - ${pad_right(text(pc), 6)}${fmt_instr(m_instr)}`)
          log.compile(`  + ${pad_right(text(pc), 6)}${pad_right(o_instr, 45)}      (eliminated)`)
        }
        mi = mi + 1
        oi = oi + 1
        pc = pc + 1
        continue
      }

      if (is_array(m_instr) && is_array(o_instr)) {
        kind = classify(m_instr, o_instr)
        if (kind != "identical") {
          n = length(m_instr)
          instr_line = m_instr[n - 2]
          if (instr_line != last_line && instr_line != null) {
            src = get_source_line(instr_line)
            if (src != null) src = trim(src)
            if (last_line != null) log.compile("")
            if (src != null && length(src) > 0) {
              log.compile(`  --- line ${text(instr_line)}: ${src} ---`)
            }
            last_line = instr_line
          }

          annotation = ""
          if (kind == "rewritten") {
            if (o_instr[0] == "concat" && m_instr[0] != "concat") {
              annotation = "(specialized)"
            } else {
              annotation = "(rewritten)"
            }
          }

          log.compile(`  - ${pad_right(text(pc), 6)}${fmt_instr(m_instr)}`)
          log.compile(`  + ${pad_right(text(pc), 6)}${fmt_instr(o_instr)}  ${annotation}`)
        }
        pc = pc + 1
      }

      mi = mi + 1
      oi = oi + 1
    }

    return null
  }

  // Process functions
  main_name = mcode_ir.name != null ? mcode_ir.name : "<main>"

  if (mcode_ir.main != null && opt_ir.main != null) {
    if (fn_matches(-1, main_name)) {
      diff_function(mcode_ir.main, opt_ir.main, main_name, -1)
    }
  }

  if (mcode_ir.functions != null && opt_ir.functions != null) {
    fi = 0
    while (fi < length(mcode_ir.functions) && fi < length(opt_ir.functions)) {
      func = mcode_ir.functions[fi]
      opt_func = opt_ir.functions[fi]
      fname = func.name != null ? func.name : "<anonymous>"
      if (fn_matches(fi, fname)) {
        diff_function(func, opt_func, `[${text(fi)}] ${fname}`, fi)
      }
      fi = fi + 1
    }
  }

  if (show_summary) {
    log.compile(`\n  total: ${text(total_eliminated)} eliminated, ${text(total_rewritten)} rewritten across ${text(total_funcs)} functions`)
  }

  return null
}

run()
$stop()
