// Copyright 2026 The moon-diff Authors.
// Licensed under the Apache License, Version 2.0.
//
// Benchmark harness for moon-diff. Run with:
//
// moon run src/bench
//
// It generates a synthetic corpus (words drawn from a fixed 10-word
// vocabulary, then edited with a target edit ratio) and exercises three diff
// strategies: Myers O(ND), LCS backtrack, and token-level diff. For each
// workload it runs the diff `reps` times and prints a `RESULT` line of the
// form:
//
// RESULT||||
//
// Wall-clock timing is done by the caller (see docs/benchmark.py), which
// timestamps each RESULT line as it streams from this program's stdout. This
// keeps the MoonBit side free of any clock dependency and lets the same
// workload be compared apples-to-apples against Python's `difflib`.
//
// `ratio` is an integer percentage (0..100); all generation uses a 31-bit LCG
// (same constants as the Python reference) so both sides produce identical
// corpora.
let vocab: Array[String] = [
"alpha", "beta", "gamma", "delta", "epsilon", "zeta", "theta", "kappa",
"lambda", "sigma",
]
// 31-bit LCG (same constants as the Python reference).
fn lcg(state: Int64) -> Int64 {
(state * 1103515245L + 12345L) & 0x7fffffffL
}
fn gen_old(n: Int, seed: Int64) -> Array[String] {
let mut state = seed
let out: Array[String] = []
for i = 0; i < n; i = i + 1 {
state = lcg(state)
out.push(vocab[(state % 10L).to_int()])
}
out
}
// `ratio` is an integer percentage (0..100).
fn gen_new(old: Array[String], ratio: Int, seed: Int64) -> Array[String] {
let mut state = seed
let r = ratio.to_int64()
let out: Array[String] = []
for i = 0; i < old.length(); i = i + 1 {
state = lcg(state)
if (state % 1000L) < r {
state = lcg(state)
let tok = vocab[(state % 10L).to_int()]
state = lcg(state)
let is_delete = (state % 2L) == 1L
if !is_delete {
out.push(tok)
}
state = lcg(state)
if (state % 1000L) < (r * 3L / 10L) {
out.push(tok)
}
} else {
out.push(old[i])
}
}
out
}
// One RESULT line per strategy so the caller can time each algorithm
// independently (Myers O(ND) is MoonBit's fast path; the plain LCS `diff` and
// the token-level `diff_tokens` are included for completeness and allocate an
// O(N*M) table).
//
// `old`/`new` are arrays of vocabulary words (so the line-level strategies
// `myers_diff`/`diff` operate word-by-word), while `old_str`/`new_str` are the
// same corpora joined with spaces so the token-level `diff_tokens` sees real
// word tokens.
fn run_strategy(
strategy: String,
n: Int,
ratio: Int,
reps: Int,
old: Array[String],
new: Array[String],
old_str: String,
new_str: String
) -> Unit {
let mut sink = 0
for r = 0; r < reps; r = r + 1 {
if strategy == "myers" {
sink = sink + @diff.myers_diff(old, new).length()
} else if strategy == "lcs" {
sink = sink + @diff.diff(old, new).length()
} else {
sink = sink + @diff.diff_tokens(old_str, new_str).length()
}
}
println(
"RESULT|" + strategy + "|" + n.to_string() + "|" + ratio.to_string() +
"|" + reps.to_string() + "|" + sink.to_string(),
)
}
fn run_one(n: Int, ratio: Int, reps: Int) -> Unit {
let old = gen_old(n, 1234567L)
let new = gen_new(old, ratio, 987654321L)
let old_str = old.join(" ")
let new_str = new.join(" ")
run_strategy("myers", n, ratio, reps, old, new, old_str, new_str)
run_strategy("lcs", n, ratio, reps, old, new, old_str, new_str)
run_strategy("tokens", n, ratio, reps, old, new, old_str, new_str)
}
fn main {
// Warm up the runtime so the first timed strategy is not penalised by
// one-off startup costs.
run_one(20, 10, 1)
// Sizes are kept modest because the LCS-backed strategies (`diff`,
// `diff_tokens`) allocate an O(N*M) table; `myers_diff` is O(ND) and could
// go much larger.
run_one(500, 10, 200)
run_one(1500, 10, 40)
run_one(3000, 10, 10)
run_one(1500, 50, 40)
println("DONE")
}