Wire Sys4ScriptProvider into CLI run/play/sweep; validate real subroutine execution
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 <noreply@anthropic.com>
This commit is contained in:
@@ -5,13 +5,16 @@ using Age.Engine.Sys4;
|
|||||||
using Age.Engine.Vm;
|
using Age.Engine.Vm;
|
||||||
|
|
||||||
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
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 <file> | trace <out.json>"); return 1; }
|
if (args.Length == 0) { Console.WriteLine("usage: run <file> | trace <out.json>"); return 1; }
|
||||||
|
|
||||||
if (args[0] == "run")
|
if (args[0] == "run")
|
||||||
{
|
{
|
||||||
var script = Sys4Loader.Load(args[1], table);
|
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();
|
vm.Run();
|
||||||
Console.WriteLine($"{Path.GetFileName(args[1])}: {vm.Steps} steps, {vm.Emitted.Count} show-text (halt: {vm.HaltReason})");
|
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}");
|
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)
|
foreach (var name in scenes)
|
||||||
{
|
{
|
||||||
var script = Sys4Loader.Load(scripts[name.ToUpperInvariant()], table);
|
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;
|
totalLines += r.Emitted.Count;
|
||||||
Console.WriteLine($" {name,-14} {r.Emitted.Count,4} lines, {r.Steps,7} steps (halt: {r.Halt})");
|
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();
|
var bootSession = new GameSession();
|
||||||
foreach (var s in new[] { "SKINIT.BIN", "ITINIT.BIN", "EBINIT.BIN", "CGINIT.BIN", "MPINIT.BIN",
|
foreach (var s in new[] { "SKINIT.BIN", "ITINIT.BIN", "EBINIT.BIN", "CGINIT.BIN", "MPINIT.BIN",
|
||||||
"AFINIT.BIN", "CCINIT.BIN", "STINIT.BIN", "STINIT2.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();
|
baseline = bootSession.ToJson();
|
||||||
Console.WriteLine($"[boot] baseline = {bootSession.Globals.Count} globals; running {names.Count} scenes from it.");
|
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();
|
var session = Fresh();
|
||||||
if (seeded) foreach (var (k, v) in seeds) session.Seed(k, v);
|
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)
|
if (seeds.Count > 0)
|
||||||
@@ -159,7 +162,7 @@ if (args[0] == "sweep")
|
|||||||
foreach (var name in names)
|
foreach (var name in names)
|
||||||
{
|
{
|
||||||
var session = Fresh();
|
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";
|
var halt = r.Halt ?? "null";
|
||||||
haltDist[halt] = haltDist.GetValueOrDefault(halt) + 1;
|
haltDist[halt] = haltDist.GetValueOrDefault(halt) + 1;
|
||||||
totalLines += r.Emitted.Count;
|
totalLines += r.Emitted.Count;
|
||||||
|
|||||||
52
engine/Age.Engine.Tests/CallScriptIntegrationTests.cs
Normal file
52
engine/Age.Engine.Tests/CallScriptIntegrationTests.cs
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,9 +24,10 @@ public sealed class GameSession
|
|||||||
public void SeedString(int addr, string value) => GlobalStrings[addr] = value;
|
public void SeedString(int addr, string value) => GlobalStrings[addr] = value;
|
||||||
|
|
||||||
/// <summary>Run one scene: seed a fresh VM from session state, execute, merge final state back.</summary>
|
/// <summary>Run one scene: seed a fresh VM from session state, execute, merge final state back.</summary>
|
||||||
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 Globals) vm.Globals[kv.Key] = kv.Value;
|
||||||
foreach (var kv in GlobalStrings) vm.GlobalStrings[kv.Key] = kv.Value;
|
foreach (var kv in GlobalStrings) vm.GlobalStrings[kv.Key] = kv.Value;
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ public sealed class VirtualMachine
|
|||||||
private readonly IScriptProvider? _provider;
|
private readonly IScriptProvider? _provider;
|
||||||
private ExecFrame _cur = null!;
|
private ExecFrame _cur = null!;
|
||||||
private int _depth;
|
private int _depth;
|
||||||
private bool _halted;
|
|
||||||
|
|
||||||
public Dictionary<int, long> Globals { get; } = new();
|
public Dictionary<int, long> Globals { get; } = new();
|
||||||
public Dictionary<int, string> GlobalStrings { get; } = new();
|
public Dictionary<int, string> GlobalStrings { get; } = new();
|
||||||
@@ -111,11 +110,11 @@ public sealed class VirtualMachine
|
|||||||
int pc = frame.Pc;
|
int pc = frame.Pc;
|
||||||
while (pc >= 0 && pc < frame.Script.Instructions.Count)
|
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++;
|
Steps++;
|
||||||
int next = Step(frame.Script.Instructions[pc], pc);
|
int next = Step(frame.Script.Instructions[pc], pc);
|
||||||
if (next == FRAME_RETURN) { outcome = FrameOutcome.Returned; break; }
|
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;
|
pc = next;
|
||||||
}
|
}
|
||||||
_cur = prev; _depth--;
|
_cur = prev; _depth--;
|
||||||
|
|||||||
Reference in New Issue
Block a user