// =============================================================================
// Closure-conversion baseline benchmarks
//
// These @bench.T tests group the interpreter workloads most relevant before
// changing function and closure execution. They intentionally do not replace
// the stable regression/component tests in benchmarks.mbt; the baseline/*
// names give closure-conversion work a focused, separately runnable set:
//
// moon bench -p dowdiness/js_engine/benchmarks --target js -f baseline.mbt
//
// Correctness is checked once before each timed region with expect(). The
// timed section always keeps the run() result so the compiler cannot eliminate
// the work being measured.
// =============================================================================
///|
using @runtime {type ExecContext, type Value}
///|
/// Loop-only arithmetic workload.
///
/// Category: closure-conversion baseline
/// Exercises: variable reads/writes, numeric addition, comparison, update
/// expressions, and statement dispatch in a large loop.
/// Excludes: function calls, closure creation, object/property lookup,
/// builtin method dispatch, and array allocation.
///
/// Expected result: "500000500000" (sum 1..1,000,000)
const BASELINE_ARITHMETIC_LOOP_SRC : String =
#|var sum = 0;
#|for (var i = 1; i <= 1000000; i++) {
#| sum = sum + i;
#|}
#|sum;
///|
/// Counter factory expression used by the opt-in closure-conversion prototype
/// benchmark. The MoonBit harness performs the outer loop so this isolates the
/// compiled function-expression and compiled function-body call path without
/// requiring whole-program statement compilation yet.
const COMPILED_COUNTER_FACTORY_EXPR : String =
#|(function(start) {
#| var count = start;
#| return function() {
#| count = count + 1;
#| return count;
#| };
#|})
///|
/// Closure-conversion loop microbenchmark with a braced body that contains no
/// lexical declarations. This isolates the cost of creating a block
/// Environment for loop bodies whose semantics do not require one.
const COMPILED_BLOCK_BODY_LOOP_SRC : String =
#|(function() {
#| var sum = 0;
#| for (var i = 0; i < 20000; i++) {
#| sum = sum + i;
#| }
#| return sum;
#|})()
///|
/// Same workload as COMPILED_BLOCK_BODY_LOOP_SRC, but without a braced loop
/// body. Comparing the two isolates block-body environment overhead.
const COMPILED_BARE_BODY_LOOP_SRC : String =
#|(function() {
#| var sum = 0;
#| for (var i = 0; i < 20000; i++)
#| sum = sum + i;
#| return sum;
#|})()
///|
fn keep_run_result(b : @bench.T, src : String) -> Unit {
try {
let (_, v) = @lib.run(src)
b.keep(v)
} catch {
_ => abort("run raised")
}
}
///|
fn parse_single_expr(src : String) -> @ast.Expr raise Error {
let prog = @parser.parse(src)
match prog.stmts {
[ExprStmt(expr, _)] => expr
_ => abort("expected a single expression statement")
}
}
///|
fn default_exec_ctx() -> ExecContext {
{ strict: false, current_generator: None, }
}
///|
fn run_compiled_counter_factory() -> Value raise Error {
let interp = @interpreter.new_interpreter()
let compiled = @compiler.compile_expr_to_closure(
parse_single_expr(COMPILED_COUNTER_FACTORY_EXPR),
)
let factory = compiled(interp, default_exec_ctx(), interp.global)
let counters : Array[Value] = []
for i = 0; i < 1000; i = i + 1 {
counters.push(
interp.call_value(
factory,
Value::Undefined,
[Value::Number(i.to_double())],
@token.Loc::default(),
),
)
}
let mut total = 0.0
for counter in counters {
match
interp.call_value(counter, Value::Undefined, [], @token.Loc::default()) {
Value::Number(n) => total += n
other => abort("compiled counter returned \{other}")
}
}
Value::Number(total)
}
///|
fn compile_bytecode_closure_factory() -> @compiler.BytecodeProgram raise Error {
let prog = @parser.parse(CLOSURE_FACTORY_SRC)
@compiler.compile_script_to_bytecode(prog.stmts)
}
///|
fn expect_value_string(name : String, actual : Value, want : String) -> Unit {
let got = actual.to_string()
guard got == want else {
abort(
"[\{name}] bench correctness check failed\n expected: \{want}\n got: \{got}",
)
}
}
///|
/// Recursive function calls: fib(30).
///
/// What it measures: function invocation and return through the full
/// interpreter pipeline: call_value(), environment allocation, statement
/// execution, expression evaluation, and recursive lookup of fib.
/// What it answers: What is the current call/return cost before closure
/// conversion changes the function representation?
/// What it excludes: closures that capture variables, object property access,
/// builtin calls, and array operations.
///
/// fib(30) is intentionally expensive, so this baseline uses fewer outer
/// runs than the default @bench.T count while keeping the same workload.
test "baseline/exec/fibonacci_30" (b : @bench.T) {
expect("baseline/exec/fibonacci_30", FIBONACCI_SRC, "832040")
b.bench(fn() { keep_run_result(b, FIBONACCI_SRC) }, count=3)
}
///|
/// Closure factory: 1,000 closures created, then called once each.
///
/// What it measures: closure environment allocation, captured-variable lookup
/// through the parent chain, and call overhead for both factory and returned
/// functions.
/// What it answers: How expensive is the current closure representation before
/// closure conversion can remove or simplify captured environments?
/// What it excludes: deep scope chains beyond two levels, class methods,
/// Function.prototype.bind, and prototype/property descriptor behavior.
test "baseline/exec/closure_factory" (b : @bench.T) {
expect("baseline/exec/closure_factory", CLOSURE_FACTORY_SRC, "500500")
b.bench(fn() { keep_run_result(b, CLOSURE_FACTORY_SRC) })
}
///|
/// Opt-in closure-conversion prototype counter factory.
///
/// What it measures: the compiled function-expression and compiled function
/// body path for the same captured-variable shape used by closure_factory.
/// What it answers: whether the prototype call path is viable before wiring
/// whole-program statement compilation into `pipeline/exec/evaluate`.
/// What it excludes: JavaScript-level arrays, for-loops, function declarations,
/// and member/computed calls, which still require statement compilation work.
test "baseline/closure_legacy/compiled_counter_factory" (b : @bench.T) {
let precheck = run_compiled_counter_factory() catch {
_ => abort("compiled counter factory precheck raised")
}
expect_value_string(
"baseline/closure_legacy/compiled_counter_factory", precheck, "500500",
)
b.bench(fn() {
let result = run_compiled_counter_factory() catch {
_ => abort("compiled counter factory raised")
}
b.keep(result)
})
}
///|
/// Opt-in closure-conversion script-level closure factory.
///
/// What it measures: evaluating a pre-parsed, pre-compiled script closure for
/// the same JavaScript source as baseline/exec/closure_factory, including the
/// same script setup envelope as Interpreter::run.
/// What it answers: whether whole-program closure conversion can move the
/// primary closure_factory target, excluding frontend parse/compile cost.
/// What it excludes: lexer/parser cost and closure-conversion compilation cost.
test "baseline/closure_legacy/closure_factory" (b : @bench.T) {
let prog = @parser.parse(CLOSURE_FACTORY_SRC) catch {
_ => abort("parse raised")
}
let compiled = @compiler.compile_script_to_closure(prog.stmts)
let precheck_interp = @interpreter.new_interpreter()
let got = compiled(precheck_interp) catch {
_ => abort("compiled program raised")
}
expect_value_string("baseline/closure_legacy/closure_factory", got, "500500")
b.bench(fn() {
let interp = @interpreter.new_interpreter()
let result = compiled(interp) catch {
_ => abort("compiled closure_factory raised")
}
b.keep(result)
})
}
///|
/// Opt-in bytecode/IR prototype script-level closure factory.
///
/// What it measures: evaluating a pre-parsed, pre-compiled bytecode program for
/// the same JavaScript source as baseline/exec/closure_factory.
/// What it answers: whether the first bytecode skeleton can run the primary
/// closure-factory shape without changing the default interpreter.
test "baseline/bytecode/closure_factory" (b : @bench.T) {
let compiled = compile_bytecode_closure_factory() catch {
_ => abort("bytecode compile raised")
}
let got = compiled.run(@interpreter.new_interpreter()) catch {
_ => abort("bytecode precheck raised")
}
expect_value_string("baseline/bytecode/closure_factory", got, "500500")
b.bench(fn() {
let result = compiled.run(@interpreter.new_interpreter()) catch {
_ => abort("bytecode closure_factory raised")
}
b.keep(result)
})
}
///|
/// Property chain access in a 10,000-iteration JS loop.
///
/// What it measures: own-property lookup cost on a stable five-segment chain
/// while the interpreter executes loop control and arithmetic around it.
/// What it answers: Whether closure-conversion work accidentally regresses
/// unrelated object/property hot paths.
/// What it excludes: prototype chain traversal, accessors, computed keys,
/// Proxy traps, and builtin dispatch.
test "baseline/exec/property_chain" (b : @bench.T) {
expect("baseline/exec/property_chain", PROPERTY_CHAIN_SRC, "420000")
b.bench(fn() { keep_run_result(b, PROPERTY_CHAIN_SRC) })
}
///|
/// Simple arithmetic loop with no function calls.
///
/// What it measures: baseline expression and statement execution without call
/// frames or closures, giving a control workload for comparison against the
/// function-heavy benchmarks above.
/// What it answers: Did closure-conversion work improve calls specifically,
/// or did it also change the generic loop/expression evaluator cost?
/// What it excludes: function invocation, closure capture, property access,
/// arrays, objects, and builtins.
test "baseline/exec/arithmetic_loop" (b : @bench.T) {
expect(
"baseline/exec/arithmetic_loop",
BASELINE_ARITHMETIC_LOOP_SRC,
"500000500000",
)
b.bench(fn() { keep_run_result(b, BASELINE_ARITHMETIC_LOOP_SRC) })
}
///|
/// Isolate closure-conversion block-body environment overhead.
///
/// The two programs differ only in whether the `for` body is a braced block.
/// The braced body has no `let`, `const`, class, or function declaration, so a
/// gap here points at avoidable block Environment allocation in the compiled
/// statement path rather than JavaScript work performed by the loop body.
test "isolate/closure_legacy/block_body_env" (b : @bench.T) {
let block_prog = @parser.parse(COMPILED_BLOCK_BODY_LOOP_SRC) catch {
_ => abort("block-body loop parse raised")
}
let bare_prog = @parser.parse(COMPILED_BARE_BODY_LOOP_SRC) catch {
_ => abort("bare-body loop parse raised")
}
let block_compiled = @compiler.compile_script_to_closure(block_prog.stmts)
let bare_compiled = @compiler.compile_script_to_closure(bare_prog.stmts)
let block_precheck = block_compiled(@interpreter.new_interpreter()) catch {
_ => abort("block-body loop precheck raised")
}
let bare_precheck = bare_compiled(@interpreter.new_interpreter()) catch {
_ => abort("bare-body loop precheck raised")
}
expect_value_string(
"isolate/closure_legacy/block_body_env block", block_precheck, "199990000",
)
expect_value_string(
"isolate/closure_legacy/block_body_env bare", bare_precheck, "199990000",
)
b.bench(name="braced body", fn() {
let result = block_compiled(@interpreter.new_interpreter()) catch {
_ => abort("compiled block-body loop raised")
}
b.keep(result)
})
b.bench(name="bare body", fn() {
let result = bare_compiled(@interpreter.new_interpreter()) catch {
_ => abort("compiled bare-body loop raised")
}
b.keep(result)
})
}
///|
/// Fixed-configuration CLI counterpart for CSV baseline snapshots.
///
/// The existing CLI runner already covers fib(30), closure_factory, and
/// property_chain. This adds the loop-only control workload to `--all --csv`
/// without changing the established benchmark names.
fn bench_arithmetic_loop() -> BenchResult raise {
verify(BASELINE_ARITHMETIC_LOOP_SRC, "500000500000")
run_bench(
"exec/arithmetic_loop",
Component,
{ warmup: 2, iterations: 8, group_size: 1, },
fn() raise { let _ = @lib.run(BASELINE_ARITHMETIC_LOOP_SRC) },
)
}