From 6b3d90182e54b9ebfd948aaf6fed9ba0170e779d Mon Sep 17 00:00:00 2001 From: gamer147 Date: Tue, 7 Jul 2026 13:18:30 -0400 Subject: [PATCH] Wire Sys4ScriptProvider into CLI run/play/sweep; validate real subroutine execution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integration: ADDILL executes ADDILLSUB + CALCREVISE and returns to its own exit. Sweep (execution on): 284/297 exit clean, 13 STEP-LIMIT (input/state-gated ADV scenes that now spin headless once subroutine global-writes drive their loops — state divergence, not a call-script bug; 0 depth-cap, 0 unresolved, 0 crashes). Removed the dead _halted field (halt propagates via FrameOutcome). Co-Authored-By: Claude Opus 4.8 --- engine/Age.Cli/Program.cs | 13 +++-- .../CallScriptIntegrationTests.cs | 52 +++++++++++++++++++ engine/Age.Engine/Vm/GameSession.cs | 5 +- engine/Age.Engine/Vm/VirtualMachine.cs | 5 +- 4 files changed, 65 insertions(+), 10 deletions(-) create mode 100644 engine/Age.Engine.Tests/CallScriptIntegrationTests.cs diff --git a/engine/Age.Cli/Program.cs b/engine/Age.Cli/Program.cs index a0951f2..92fc970 100644 --- a/engine/Age.Cli/Program.cs +++ b/engine/Age.Cli/Program.cs @@ -5,13 +5,16 @@ using Age.Engine.Sys4; using Age.Engine.Vm; var table = OpcodeTableJson.Load(Paths.OpcodesJson); +// call-script execution: resolves ids -> scripts. Product paths pass this so subroutines run; +// `trace` stays provider-less on purpose (the base-ISA offset oracle). +var provider = Sys4ScriptProvider.Load(table); 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()); + var vm = new VirtualMachine(script, table, new CaptureHost(), null, provider); 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}"); @@ -94,7 +97,7 @@ if (args[0] == "play") foreach (var name in scenes) { var script = Sys4Loader.Load(scripts[name.ToUpperInvariant()], table); - var r = session.RunScene(script, table, new CaptureHost()); + var r = session.RunScene(script, table, new CaptureHost(), null, provider); totalLines += r.Emitted.Count; Console.WriteLine($" {name,-14} {r.Emitted.Count,4} lines, {r.Steps,7} steps (halt: {r.Halt})"); } @@ -118,7 +121,7 @@ if (args[0] == "sweep") var bootSession = new GameSession(); foreach (var s in new[] { "SKINIT.BIN", "ITINIT.BIN", "EBINIT.BIN", "CGINIT.BIN", "MPINIT.BIN", "AFINIT.BIN", "CCINIT.BIN", "STINIT.BIN", "STINIT2.BIN" }) - bootSession.RunScene(Sys4Loader.Load(scripts[s], table), table, new CaptureHost()); + bootSession.RunScene(Sys4Loader.Load(scripts[s], table), table, new CaptureHost(), null, provider); baseline = bootSession.ToJson(); Console.WriteLine($"[boot] baseline = {bootSession.Globals.Count} globals; running {names.Count} scenes from it."); } @@ -137,7 +140,7 @@ if (args[0] == "sweep") { var session = Fresh(); if (seeded) foreach (var (k, v) in seeds) session.Seed(k, v); - return session.RunScene(Sys4Loader.Load(scripts[name], table), table, new CaptureHost()).Emitted.Count; + return session.RunScene(Sys4Loader.Load(scripts[name], table), table, new CaptureHost(), null, provider).Emitted.Count; } if (seeds.Count > 0) @@ -159,7 +162,7 @@ if (args[0] == "sweep") foreach (var name in names) { var session = Fresh(); - var r = session.RunScene(Sys4Loader.Load(scripts[name], table), table, new CaptureHost()); + var r = session.RunScene(Sys4Loader.Load(scripts[name], table), table, new CaptureHost(), null, provider); var halt = r.Halt ?? "null"; haltDist[halt] = haltDist.GetValueOrDefault(halt) + 1; totalLines += r.Emitted.Count; diff --git a/engine/Age.Engine.Tests/CallScriptIntegrationTests.cs b/engine/Age.Engine.Tests/CallScriptIntegrationTests.cs new file mode 100644 index 0000000..b8b8647 --- /dev/null +++ b/engine/Age.Engine.Tests/CallScriptIntegrationTests.cs @@ -0,0 +1,52 @@ +using Age.Engine.Hosting; +using Age.Engine.Sys4; +using Age.Engine.Vm; +using Xunit; + +public class CallScriptIntegrationTests +{ + private sealed class NullHost : IHost + { + public int CallScripts; + public void ShowText(int o, string t) { } + public void CallScript(long id) => CallScripts++; + public void OnStub(int op) { } + public void WaitForInput() { } + public void CreateTexture(int s, int w, int h) { } + public void SetTexture(long r, int s) { } + public void DrawTexture(int s, int sx, int sy, int w, int h, int dx, int dy) { } + public (int Width, int Height) GetTextureSize(int s) => (0, 0); + public void PlayBgm(long id) { } + public void PlayVoice(long id) { } + } + + [Fact] + public void RealScriptExecutesRealSubroutinesAndReturns() + { + // ADDILL.BIN unconditionally call-scripts ADDILLSUB then CALCREVISE at entry, then exits. + // With execution on, both subroutines load, run, and return, so ADDILL reaches its own exit. + var t = OpcodeTableJson.Load(Paths.OpcodesJson); + var provider = Sys4ScriptProvider.Load(t); + var script = Sys4Loader.Load(Paths.Scripts()["ADDILL.BIN"], t); + var host = new NullHost(); + var vm = new VirtualMachine(script, t, host, null, provider); + vm.Run(); + Assert.Equal(2, host.CallScripts); // ADDILLSUB + CALCREVISE both dispatched + Assert.Equal("exit", vm.HaltReason); // subroutines returned; ADDILL reached its own exit + } + + [Fact] + public void BunkiTopLevelRetReturnsCleanlyAsSubroutine() + { + // BUNKI.BIN ends with a top-level `ret` (empty intra-call stack). Called as a subroutine it + // must return to the caller, not underflow-halt. Drive it directly. + var t = OpcodeTableJson.Load(Paths.OpcodesJson); + var provider = Sys4ScriptProvider.Load(t); + var bunki = provider.GetById(0x143); // BUNKI.BIN + Assert.NotNull(bunki); + var vm = new VirtualMachine(bunki!, t, new NullHost(), null, provider); + vm.Run(); + // Reaching a frame-return at the top = clean "exit"; never "ret-underflow". + Assert.NotEqual("ret-underflow", vm.HaltReason); + } +} diff --git a/engine/Age.Engine/Vm/GameSession.cs b/engine/Age.Engine/Vm/GameSession.cs index 76c6770..0507c8e 100644 --- a/engine/Age.Engine/Vm/GameSession.cs +++ b/engine/Age.Engine/Vm/GameSession.cs @@ -24,9 +24,10 @@ public sealed class GameSession public void SeedString(int addr, string value) => GlobalStrings[addr] = value; /// Run one scene: seed a fresh VM from session state, execute, merge final state back. - public SceneResult RunScene(Script script, OpcodeTable table, IHost host, VmOptions? options = null) + public SceneResult RunScene(Script script, OpcodeTable table, IHost host, + VmOptions? options = null, IScriptProvider? provider = null) { - var vm = new VirtualMachine(script, table, host, options); + var vm = new VirtualMachine(script, table, host, options, provider); foreach (var kv in Globals) vm.Globals[kv.Key] = kv.Value; foreach (var kv in GlobalStrings) vm.GlobalStrings[kv.Key] = kv.Value; diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs index 46dc109..57b05bc 100644 --- a/engine/Age.Engine/Vm/VirtualMachine.cs +++ b/engine/Age.Engine/Vm/VirtualMachine.cs @@ -17,7 +17,6 @@ public sealed class VirtualMachine private readonly IScriptProvider? _provider; private ExecFrame _cur = null!; private int _depth; - private bool _halted; public Dictionary Globals { get; } = new(); public Dictionary GlobalStrings { get; } = new(); @@ -111,11 +110,11 @@ public sealed class VirtualMachine int pc = frame.Pc; while (pc >= 0 && pc < frame.Script.Instructions.Count) { - if (Steps >= _o.MaxSteps) { HaltReason ??= "STEP-LIMIT"; _halted = true; outcome = FrameOutcome.Halted; break; } + if (Steps >= _o.MaxSteps) { HaltReason ??= "STEP-LIMIT"; outcome = FrameOutcome.Halted; break; } Steps++; int next = Step(frame.Script.Instructions[pc], pc); if (next == FRAME_RETURN) { outcome = FrameOutcome.Returned; break; } - if (next == HALT) { _halted = true; outcome = FrameOutcome.Halted; break; } + if (next == HALT) { outcome = FrameOutcome.Halted; break; } pc = next; } _cur = prev; _depth--;