// =============================================================================
// Var-hoist scan microbenchmark (issue #325)
//
// Run with:
// moon bench -p benchmarks -f varhoist_scan_benchmark.mbt --target js --release
//
// Isolated in its own file so `moon bench -f` can target it without running the
// multi-second execution benchmarks in benchmarks.mbt.
// =============================================================================
///|
/// Parse a body-shaped source into a statement list. Aborts (always fatal) on
/// parse failure so a broken workload cannot be timed.
fn parse_body_or_abort(src : String) -> Array[@ast.Stmt] {
@parser.parse(src).stmts catch {
_ => abort("var_hoist_scan: parse failed for body workload")
}
}
///|
/// Isolated cost of the per-call var-hoist re-walk that issue #325 removes.
///
/// What it measures: @static_semantics.collect_var_declared_names(body) — the
/// exact recursive AST descent BytecodeFrame::hoist_env_var_bindings runs on
/// *every* bytecode function call (compiler/bytecode_vm.mbt) — versus simply
/// iterating the precomputed name array the Stage-7 fix stores on
/// BytecodeFunction and reads at runtime.
/// What it answers: how much work the fix eliminates from the per-call hoist
/// path. Multiply the per-scan delta by call volume to size the real win: a
/// hot helper (e.g. fib(30) ≈ 2.7M calls) pays this on every invocation.
/// What it excludes: define_compiled_binding (identical in both designs) and the
/// local_index_from_names filter (a small linear scan moved to lowering in the
/// fix). Excluding them is conservative — it understates the win.
///
/// The body is fixed across iterations on purpose: the real scenario is the
/// *same* function body re-walked on each call, so a fixed AST is honest here,
/// not a JIT-deflation artifact (collect_var_declared_names allocates a fresh
/// out array and re-pushes every name on each call regardless of caching).
test "frontend/var_hoist_scan" (b : @bench.T) {
let small_body = parse_body_or_abort(VARHOIST_SMALL_BODY)
let nested_body = parse_body_or_abort(VARHOIST_NESTED_BODY)
// Precomputed name arrays — what the optimized VM iterates per call.
let small_names = @static_semantics.collect_var_declared_names(small_body)
let nested_names = @static_semantics.collect_var_declared_names(nested_body)
if small_names.length() != 2 {
abort("var_hoist_scan: expected 2 small names, got \{small_names.length()}")
}
if nested_names.length() != 9 {
abort(
"var_hoist_scan: expected 9 nested names, got \{nested_names.length()}",
)
}
b.bench(name="small: re-walk (current)", fn() {
b.keep(@static_semantics.collect_var_declared_names(small_body))
})
b.bench(name="small: iterate precomputed (fix)", fn() {
let mut n = 0
for _name in small_names {
n = n + 1
}
b.keep(n)
})
b.bench(name="nested: re-walk (current)", fn() {
b.keep(@static_semantics.collect_var_declared_names(nested_body))
})
b.bench(name="nested: iterate precomputed (fix)", fn() {
let mut n = 0
for _name in nested_names {
n = n + 1
}
b.keep(n)
})
}