feat(a1): CLI (run/trace) + vm0.py --trace + per-scene differential test (A1 green)
C# VM is byte-identical to vm0.py across all 297 SC/SP scenes (offsets+halt+steps); RECOVER passes; SC0000 = 27994 steps / 186 lines matching the Python prototype. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 <file> | trace <out.json>"); 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<string, object>(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;
|
||||
|
||||
51
engine/Age.Engine.Tests/TraceDiffTests.cs
Normal file
51
engine/Age.Engine.Tests/TraceDiffTests.cs
Normal file
@@ -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<string>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
18
tools/vm0.py
18
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])
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user