diff --git a/engine/Age.Cli/Program.cs b/engine/Age.Cli/Program.cs index 83fa4f4..f86a0f6 100644 --- a/engine/Age.Cli/Program.cs +++ b/engine/Age.Cli/Program.cs @@ -1,2 +1,37 @@ -// See https://aka.ms/new-console-template for more information -Console.WriteLine("Hello, World!"); +using System.Text.Json; +using System.Text.RegularExpressions; +using Age.Engine.Hosting; +using Age.Engine.Sys4; +using Age.Engine.Vm; + +var table = OpcodeTableJson.Load(Paths.OpcodesJson); + +if (args.Length == 0) { Console.WriteLine("usage: run | trace "); return 1; } + +if (args[0] == "run") +{ + var script = Sys4Loader.Load(args[1], table); + var vm = new VirtualMachine(script, table, new CaptureHost()); + vm.Run(); + Console.WriteLine($"{Path.GetFileName(args[1])}: {vm.Steps} steps, {vm.Emitted.Count} show-text (halt: {vm.HaltReason})"); + foreach (var (off, text) in vm.Emitted.Take(20)) Console.WriteLine($" [{off:x}] {text}"); + return 0; +} + +if (args[0] == "trace") +{ + var scene = new Regex(@"^S[CP]\d{4}\.BIN$"); + var scripts = Paths.Scripts(); + var trace = new SortedDictionary(StringComparer.Ordinal); + foreach (var name in scripts.Keys.Where(n => scene.IsMatch(n)).OrderBy(n => n, StringComparer.Ordinal)) + { + var vm = new VirtualMachine(Sys4Loader.Load(scripts[name], table), table, new CaptureHost()); + vm.Run(); + trace[name] = new { offsets = vm.Emitted.Select(e => e.Offset).ToArray(), halt = vm.HaltReason, steps = vm.Steps }; + } + File.WriteAllText(args[1], JsonSerializer.Serialize(trace, + new JsonSerializerOptions { Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping })); + Console.WriteLine($"trace: {trace.Count} scenes -> {args[1]}"); + return 0; +} +Console.WriteLine("unknown command"); return 1; diff --git a/engine/Age.Engine.Tests/TraceDiffTests.cs b/engine/Age.Engine.Tests/TraceDiffTests.cs new file mode 100644 index 0000000..a326e0c --- /dev/null +++ b/engine/Age.Engine.Tests/TraceDiffTests.cs @@ -0,0 +1,51 @@ +using System.Text.Json; +using System.Text.RegularExpressions; +using Age.Engine.Hosting; +using Age.Engine.Sys4; +using Age.Engine.Vm; +using Xunit; + +public class TraceDiffTests +{ + private static readonly Regex Scene = new(@"^S[CP]\d{4}\.BIN$"); + + [Fact] + public void CsTraceMatchesVm0PerScene() + { + string refPath = Path.Combine(Paths.Build, "vm0-trace.json"); + Assert.True(File.Exists(refPath), + "prerequisite: run `py -3.11 -X utf8 tools/vm0.py --trace build/vm0-trace.json`"); + + using var doc = JsonDocument.Parse(File.ReadAllText(refPath)); + var expected = doc.RootElement; + var table = OpcodeTableJson.Load(Paths.OpcodesJson); + var scripts = Paths.Scripts(); + + var mismatches = new List(); + foreach (var name in scripts.Keys.Where(n => Scene.IsMatch(n)).OrderBy(n => n, StringComparer.Ordinal)) + { + var script = Sys4Loader.Load(scripts[name], table); + var vm = new VirtualMachine(script, table, new CaptureHost()); + vm.Run(); + var offsets = vm.Emitted.Select(e => e.Offset).ToArray(); + + if (!expected.TryGetProperty(name, out var exp)) { mismatches.Add($"{name}: absent in vm0 trace"); continue; } + var expOffsets = exp.GetProperty("offsets").EnumerateArray().Select(x => x.GetInt32()).ToArray(); + string expHalt = exp.GetProperty("halt").GetString() ?? ""; + long expSteps = exp.GetProperty("steps").GetInt64(); + + if (!offsets.SequenceEqual(expOffsets)) + mismatches.Add($"{name}: offsets differ (cs {offsets.Length} vs vm0 {expOffsets.Length}; first diff at {FirstDiff(offsets, expOffsets)})"); + else if (vm.HaltReason != expHalt) mismatches.Add($"{name}: halt cs='{vm.HaltReason}' vs vm0='{expHalt}'"); + else if (vm.Steps != expSteps) mismatches.Add($"{name}: steps cs={vm.Steps} vs vm0={expSteps}"); + } + Assert.True(mismatches.Count == 0, "scene mismatches:\n" + string.Join("\n", mismatches.Take(20))); + } + + private static int FirstDiff(int[] a, int[] b) + { + int n = Math.Min(a.Length, b.Length); + for (int i = 0; i < n; i++) if (a[i] != b[i]) return i; + return n; + } +} diff --git a/tools/vm0.py b/tools/vm0.py index b7de699..9546386 100644 --- a/tools/vm0.py +++ b/tools/vm0.py @@ -410,6 +410,22 @@ def run_sweep(limit=None): return 0 +def run_trace(out_path): + """Dump per-scene emitted show-text offsets + halt + steps for the differential test.""" + oracle = load_oracle() + scripts = paths.scripts() + names = sorted(n for n in scripts if SCENE_RE.match(n)) + trace = {} + for name in names: + r = run_scene(name, scripts[name], oracle) + vm = r["vm"] + trace[name] = {"offsets": [off for off, _ in vm.text], + "halt": vm.halt_reason, "steps": vm.steps} + Path(out_path).write_text(json.dumps(trace, ensure_ascii=False), encoding="utf-8") + print(f"trace: {len(trace)} scenes -> {out_path}") + return 0 + + def main(argv=None): argv = argv if argv is not None else sys.argv[1:] if not argv or argv[0] == "--test": @@ -418,6 +434,8 @@ def main(argv=None): return run_sweep(limit=int(argv[1]) if len(argv) > 1 else None) if argv[0] == "--scene": return run_one_scene(argv[1]) + if argv[0] == "--trace": + return run_trace(argv[1]) return run_file(argv[0])