// =============================================================================
// Call param-binding microbenchmark (issues #362 / #363)
//
// Run with:
//   moon bench -p benchmarks -f call_param_binding_benchmark.mbt --target js --release
//
// Measures the per-call double-binding cost the #362 fast path targets:
//   1. env round-trip: def_parameter × N + has+get × N per local (current path)
//   2. direct array write: locals[i] = args[j] × N (fast path)
//   3. end-to-end JS: 5-param function called 100K times (regression guard for #362)
//
// The "double-binding": params bound into func_env by bind_simple_params
// (closure_conversion.mbt:1618), then read back into BytecodeFrame.locals by
// BytecodeFrame::BytecodeFrame (bytecode_vm.mbt:28-32).
// =============================================================================

///|
/// 5-param function called 100K times.
/// Last value: add5(99999,99999,99999,99999,99999) = 5 * 99999 = 499995.
const CALL_5PARAM_100K_SRC : String =
  #|function add5(a,b,c,d,e){return a+b+c+d+e}
  #|var r=0
  #|for(var i=0;i<100000;i++){r=add5(i,i,i,i,i)}
  #|r

///|
/// Replicates local_index_from_names (compiler/bytecode_ir.mbt:617) used by
/// string_array_contains inside BytecodeFrame::BytecodeFrame (bytecode_vm.mbt:28).
fn find_param_index(params : Array[String], name : String) -> Int? {
  for i, p in params {
    if p == name {
      return Some(i)
    }
  }
  None
}

///|
/// Isolated cost of the per-call double-binding: env Map round-trip vs direct array write.
///
/// What it measures:
///   "env round-trip (current)": def_parameter × N into func_env
///     (bind_simple_params, closure_conversion.mbt:1618), then linear-scan +
///     has + get × N locals back out (BytecodeFrame ctor, bytecode_vm.mbt:28-32)
///   "direct array write (fast path)": locals[i] = args[j] × N with a linear
///     param-index lookup; zero env Map operations on the param-to-local path
///
/// What it answers: how much Map overhead the #362 fast path eliminates per call.
/// What it excludes: Environment::new is on both paths; the fast path still
///   creates func_env for this/new.target/arguments bindings.
///
/// All 5 params are in local_names — the common !captured && !refs_arguments
/// case that the fast path covers (see plan_bytecode_locals, bytecode_lower.mbt:1331).
test "call/param_binding_5params" (b : @bench.T) {
  let closure = @runtime.Environment::new()
  let args : Array[@runtime.Value] = [
    @runtime.Value::Number(1.0),
    @runtime.Value::Number(2.0),
    @runtime.Value::Number(3.0),
    @runtime.Value::Number(4.0),
    @runtime.Value::Number(5.0),
  ]
  let params : Array[String] = ["a", "b", "c", "d", "e"]
  let local_names : Array[String] = ["a", "b", "c", "d", "e"]

  b.bench(name="env round-trip (current)", fn() {
    try {
      // bind_simple_params: def_parameter × N (closure_conversion.mbt:1623)
      let func_env = @runtime.Environment::new(parent=Some(closure))
      for i, param in params {
        let v = if i < args.length() {
          args[i]
        } else {
          @runtime.Value::Undefined
        }
        func_env.def_parameter(param, v)
      }
      // BytecodeFrame ctor: linear-scan + has + get × N (bytecode_vm.mbt:28-32)
      let locals = Array::make(local_names.length(), @runtime.Value::Undefined)
      for idx, name in local_names {
        if find_param_index(params, name) is Some(_) && func_env.has(name) {
          locals[idx] = func_env.get(name)
        }
      }
      b.keep(locals)
    } catch {
      _ => abort("env round-trip: unexpected raise")
    }
  })

  b.bench(name="direct array write (fast path)", fn() {
    let locals = Array::make(local_names.length(), @runtime.Value::Undefined)
    for idx, name in local_names {
      match find_param_index(params, name) {
        Some(j) =>
          locals[idx] = if j < args.length() {
            args[j]
          } else {
            @runtime.Value::Undefined
          }
        None => ()
      }
    }
    b.keep(locals)
  })
}

///|
/// End-to-end JS call benchmark: 5-param bytecode function called 100K times.
///
/// What it measures: full @lib.run() cost including interpreter startup,
///   JS compilation, and 100K bytecode function calls each paying the
///   double-binding cost.
/// What it answers: is the per-call double-binding cost measurable at 100K
///   call volume? Run before and after #362 to quantify the end-to-end speedup.
/// What it excludes: startup cost is included; isolated per-call cost is in
///   call/param_binding_5params above.
///
/// Expected result: "499995"
test "call/e2e_5param_100k" (b : @bench.T) {
  try {
    let (_, got) = @lib.run(CALL_5PARAM_100K_SRC)
    if got != "499995" {
      abort("[call/e2e_5param_100k] expected \"499995\", got \"\{got}\"")
    }
  } catch {
    e => abort("[call/e2e_5param_100k] pre-check raised: \{e}")
  }
  b.bench(fn() {
    try {
      let (_, v) = @lib.run(CALL_5PARAM_100K_SRC)
      b.keep(v)
    } catch {
      _ => abort("run raised")
    }
  })
}