From 3f9117363cd4952608a3286bee7afeef2890c23a Mon Sep 17 00:00:00 2001 From: gamer147 Date: Tue, 7 Jul 2026 13:11:50 -0400 Subject: [PATCH] Refactor VM to ExecFrame + tag emitted lines with source script (no behavior change) Co-Authored-By: Claude Opus 4.8 --- engine/Age.Cli/Program.cs | 2 +- engine/Age.Engine/Model/Script.cs | 1 + engine/Age.Engine/Sys4/Sys4Loader.cs | 2 +- engine/Age.Engine/Vm/ExecFrame.cs | 15 +++++ engine/Age.Engine/Vm/GameSession.cs | 2 +- engine/Age.Engine/Vm/VirtualMachine.cs | 85 +++++++++++++++----------- 6 files changed, 70 insertions(+), 37 deletions(-) create mode 100644 engine/Age.Engine/Vm/ExecFrame.cs diff --git a/engine/Age.Cli/Program.cs b/engine/Age.Cli/Program.cs index 6ee938d..a0951f2 100644 --- a/engine/Age.Cli/Program.cs +++ b/engine/Age.Cli/Program.cs @@ -14,7 +14,7 @@ if (args[0] == "run") 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}"); + foreach (var (off, text, _) in vm.Emitted.Take(20)) Console.WriteLine($" [{off:x}] {text}"); return 0; } diff --git a/engine/Age.Engine/Model/Script.cs b/engine/Age.Engine/Model/Script.cs index 870e03d..e002dfc 100644 --- a/engine/Age.Engine/Model/Script.cs +++ b/engine/Age.Engine/Model/Script.cs @@ -1,6 +1,7 @@ namespace Age.Engine.Model; public sealed class Script { + public string Name { get; init; } = ""; public required ScriptHeader Header { get; init; } public required IReadOnlyList Instructions { get; init; } public required IReadOnlyDictionary IndexByOffset { get; init; } diff --git a/engine/Age.Engine/Sys4/Sys4Loader.cs b/engine/Age.Engine/Sys4/Sys4Loader.cs index ac41fb4..ab09af8 100644 --- a/engine/Age.Engine/Sys4/Sys4Loader.cs +++ b/engine/Age.Engine/Sys4/Sys4Loader.cs @@ -22,7 +22,7 @@ public static class Sys4Loader var header = new ScriptHeader(fields[0], fields[1], fields[2], fields[3], fields[4], fields[5]); var (instrs, idxByOff, strings) = DecodeCode(dw, fields, nbody, table); - return new Script { Header = header, Instructions = instrs, IndexByOffset = idxByOff, Strings = strings }; + return new Script { Name = name, Header = header, Instructions = instrs, IndexByOffset = idxByOff, Strings = strings }; } private static (List, Dictionary, Dictionary) diff --git a/engine/Age.Engine/Vm/ExecFrame.cs b/engine/Age.Engine/Vm/ExecFrame.cs new file mode 100644 index 0000000..361267d --- /dev/null +++ b/engine/Age.Engine/Vm/ExecFrame.cs @@ -0,0 +1,15 @@ +using Age.Engine.Model; +namespace Age.Engine.Vm; + +/// One script activation: the running script, its instruction cursor, its local slots, +/// its intra-script call/ret stack, and its per-script loop-guard map. Globals live on the VM and +/// are shared across frames; everything here is per-call and discarded on return. +internal sealed class ExecFrame +{ + public readonly Script Script; + public int Pc; // entry instruction index + public readonly Frame Locals = new(); + public readonly List CallStack = new(); // intra-script `call` (op 0x8f) returns + public readonly Dictionary EmitSeen = new(); + public ExecFrame(Script script, int pc) { Script = script; Pc = pc; } +} diff --git a/engine/Age.Engine/Vm/GameSession.cs b/engine/Age.Engine/Vm/GameSession.cs index 0698e79..76c6770 100644 --- a/engine/Age.Engine/Vm/GameSession.cs +++ b/engine/Age.Engine/Vm/GameSession.cs @@ -64,4 +64,4 @@ public sealed class GameSession } /// The observable result of running one scene into a . -public sealed record SceneResult(IReadOnlyList<(int Offset, string Text)> Emitted, string? Halt, long Steps); +public sealed record SceneResult(IReadOnlyList<(int Offset, string Text, string Script)> Emitted, string? Halt, long Steps); diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs index 9f3e3fd..a560941 100644 --- a/engine/Age.Engine/Vm/VirtualMachine.cs +++ b/engine/Age.Engine/Vm/VirtualMachine.cs @@ -6,6 +6,7 @@ public sealed class VirtualMachine { private const long NoJump = 0xFFFFFFFF; private const int HALT = int.MinValue; + private const int FRAME_RETURN = int.MinValue + 1; private const int T_IMM = 0, T_STR = 2, T_GINT = 3, T_GFLOAT = 4, T_GSTR = 5, T_GPTR = 6, T_LINT = 9, T_LFLOAT = 10, T_LSTR = 11, T_LPTR = 12; @@ -13,18 +14,19 @@ public sealed class VirtualMachine private readonly OpcodeTable _t; private readonly IHost _host; private readonly VmOptions _o; - private readonly Frame _fr = new(); - private readonly List _callstack = new(); - private readonly Dictionary _emitSeen = new(); + private readonly IScriptProvider? _provider; + private ExecFrame _cur = null!; + private int _depth; + private bool _halted; public Dictionary Globals { get; } = new(); public Dictionary GlobalStrings { get; } = new(); - public List<(int Offset, string Text)> Emitted { get; } = new(); + public List<(int Offset, string Text, string Script)> Emitted { get; } = new(); public string? HaltReason { get; private set; } public long Steps { get; private set; } - public VirtualMachine(Script s, OpcodeTable t, IHost host, VmOptions? o = null) - { _s = s; _t = t; _host = host; _o = o ?? new VmOptions(); } + public VirtualMachine(Script s, OpcodeTable t, IHost host, VmOptions? o = null, IScriptProvider? provider = null) + { _s = s; _t = t; _host = host; _o = o ?? new VmOptions(); _provider = provider; } private static long Gi(Dictionary d, int k) => d.TryGetValue(k, out var v) ? v : 0; private static string Gs(Dictionary d, int k) => d.TryGetValue(k, out var v) ? v : ""; @@ -38,9 +40,9 @@ public sealed class VirtualMachine T_IMM => op.Value, T_GINT or T_GFLOAT => Gi(Globals, (int)op.Value), T_GPTR => Gi(Globals, (int)Gi(Globals, (int)op.Value)), - T_LINT => Gi(_fr.I, (int)op.Value), - T_LFLOAT => Gi(_fr.F, (int)op.Value), - T_LPTR => Gi(Globals, (int)Gi(_fr.P, (int)op.Value)), + T_LINT => Gi(_cur.Locals.I, (int)op.Value), + T_LFLOAT => Gi(_cur.Locals.F, (int)op.Value), + T_LPTR => Gi(Globals, (int)Gi(_cur.Locals.P, (int)op.Value)), _ => op.Value, }; @@ -50,17 +52,17 @@ public sealed class VirtualMachine { case T_GINT: case T_GFLOAT: Globals[(int)op.Value] = val; break; case T_GPTR: Globals[(int)Gi(Globals, (int)op.Value)] = val; break; - case T_LINT: _fr.I[(int)op.Value] = val; break; - case T_LFLOAT: _fr.F[(int)op.Value] = val; break; - case T_LPTR: Globals[(int)Gi(_fr.P, (int)op.Value)] = val; break; + case T_LINT: _cur.Locals.I[(int)op.Value] = val; break; + case T_LFLOAT: _cur.Locals.F[(int)op.Value] = val; break; + case T_LPTR: Globals[(int)Gi(_cur.Locals.P, (int)op.Value)] = val; break; } } private string ReadStr(Operand op) => op.Type switch { - T_STR => _s.GetString((int)op.Value), + T_STR => _cur.Script.GetString((int)op.Value), T_GSTR => Gs(GlobalStrings, (int)op.Value), - T_LSTR => Gs(_fr.S, (int)op.Value), + T_LSTR => Gs(_cur.Locals.S, (int)op.Value), _ => "", }; @@ -69,15 +71,15 @@ public sealed class VirtualMachine switch (op.Type) { case T_GSTR: GlobalStrings[(int)op.Value] = val; break; - case T_LSTR: _fr.S[(int)op.Value] = val; break; + case T_LSTR: _cur.Locals.S[(int)op.Value] = val; break; } } private long BaseAddr(Operand op) => op.Type switch { T_IMM or T_GINT or T_GFLOAT or T_GSTR or T_GPTR => op.Value, - T_LINT => Gi(_fr.I, (int)op.Value), - T_LPTR => Gi(_fr.P, (int)op.Value), + T_LINT => Gi(_cur.Locals.I, (int)op.Value), + T_LPTR => Gi(_cur.Locals.P, (int)op.Value), _ => op.Value, }; @@ -85,24 +87,39 @@ public sealed class VirtualMachine { switch (dst.Type) { - case T_LPTR: _fr.P[(int)dst.Value] = addr; break; + case T_LPTR: _cur.Locals.P[(int)dst.Value] = addr; break; case T_GPTR: Globals[(int)dst.Value] = addr; break; default: Write(dst, Gi(Globals, (int)addr)); break; } } + private enum FrameOutcome { Returned, Halted, RanOff } + public void Run(int entryOffset = 0) { - int pc = _s.IndexByOffset.TryGetValue(entryOffset, out var idx) ? idx : 0; - while (pc >= 0 && pc < _s.Instructions.Count) + var top = new ExecFrame(_s, _s.IndexByOffset.TryGetValue(entryOffset, out var idx) ? idx : 0); + var outcome = RunFrame(top); + if (outcome == FrameOutcome.RanOff) HaltReason ??= "pc-out-of-range"; + else if (outcome == FrameOutcome.Returned) HaltReason ??= "exit"; + // Halted: HaltReason already set by the halting op. + } + + private FrameOutcome RunFrame(ExecFrame frame) + { + var prev = _cur; _cur = frame; _depth++; + var outcome = FrameOutcome.RanOff; + int pc = frame.Pc; + while (pc >= 0 && pc < frame.Script.Instructions.Count) { - if (Steps >= _o.MaxSteps) { HaltReason ??= "STEP-LIMIT"; return; } + if (Steps >= _o.MaxSteps) { HaltReason ??= "STEP-LIMIT"; _halted = true; outcome = FrameOutcome.Halted; break; } Steps++; - int next = Step(_s.Instructions[pc], pc); - if (next == HALT) return; + 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; } pc = next; } - HaltReason ??= "pc-out-of-range"; + _cur = prev; _depth--; + return outcome; } private int Step(Instruction ins, int pc) @@ -139,28 +156,28 @@ public sealed class VirtualMachine case "bit-reset": Write(a[0], Read(a[0]) & ~Read(a[1])); return pc + 1; case "check-bit": Write(a[0], (Read(a[1]) >> (int)(Read(a[2]) & 31)) & 1); return pc + 1; case "copy-to-global": Write(a[0], Read(a[1])); return pc + 1; - case "jmp": return _s.IndexByOffset.GetValueOrDefault((int)a[0].Value, pc + 1); - case "call": _callstack.Add(pc + 1); return _s.IndexByOffset.GetValueOrDefault((int)a[0].Value, pc + 1); + case "jmp": return _cur.Script.IndexByOffset.GetValueOrDefault((int)a[0].Value, pc + 1); + case "call": _cur.CallStack.Add(pc + 1); return _cur.Script.IndexByOffset.GetValueOrDefault((int)a[0].Value, pc + 1); case "ret": - if (_callstack.Count > 0) { int r = _callstack[^1]; _callstack.RemoveAt(_callstack.Count - 1); return r; } - HaltReason = "ret-underflow"; return HALT; + if (_cur.CallStack.Count > 0) { int r = _cur.CallStack[^1]; _cur.CallStack.RemoveAt(_cur.CallStack.Count - 1); return r; } + return FRAME_RETURN; // empty intra-call stack => return from the script frame case "jcc": { long tgt = Read(a[0]) != 0 ? a[1].Value : a[2].Value; - return tgt == NoJump ? pc + 1 : _s.IndexByOffset.GetValueOrDefault((int)tgt, pc + 1); + return tgt == NoJump ? pc + 1 : _cur.Script.IndexByOffset.GetValueOrDefault((int)tgt, pc + 1); } case "exit": - case "exit-script": HaltReason = "exit"; return HALT; - case "call-script": _host.CallScript(a.Count > 0 ? Read(a[0]) : 0); return pc + 1; + case "exit-script": return FRAME_RETURN; + case "call-script": _host.CallScript(a.Count > 0 ? Read(a[0]) : 0); return pc + 1; // stub (executes in Task 3) case "show-text": foreach (var o in a) { if (o.Type != T_STR) continue; int off = (int)o.Value; - _emitSeen.TryGetValue(off, out var c); c++; _emitSeen[off] = c; + _cur.EmitSeen.TryGetValue(off, out var c); c++; _cur.EmitSeen[off] = c; if (c > _o.EmitCap) { HaltReason = $"LOOP:line@0x{off:x}×{c}"; return HALT; } - string text = _s.GetString(off); - Emitted.Add((off, text)); + string text = _cur.Script.GetString(off); + Emitted.Add((off, text, _cur.Script.Name)); _host.ShowText(off, text); } return pc + 1;