Refactor VM to ExecFrame + tag emitted lines with source script (no behavior change)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@ if (args[0] == "run")
|
|||||||
var vm = new VirtualMachine(script, table, new CaptureHost());
|
var vm = new VirtualMachine(script, table, new CaptureHost());
|
||||||
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}");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
namespace Age.Engine.Model;
|
namespace Age.Engine.Model;
|
||||||
public sealed class Script
|
public sealed class Script
|
||||||
{
|
{
|
||||||
|
public string Name { get; init; } = "";
|
||||||
public required ScriptHeader Header { get; init; }
|
public required ScriptHeader Header { get; init; }
|
||||||
public required IReadOnlyList<Instruction> Instructions { get; init; }
|
public required IReadOnlyList<Instruction> Instructions { get; init; }
|
||||||
public required IReadOnlyDictionary<int, int> IndexByOffset { get; init; }
|
public required IReadOnlyDictionary<int, int> IndexByOffset { get; init; }
|
||||||
|
|||||||
@@ -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 header = new ScriptHeader(fields[0], fields[1], fields[2], fields[3], fields[4], fields[5]);
|
||||||
var (instrs, idxByOff, strings) = DecodeCode(dw, fields, nbody, table);
|
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<Instruction>, Dictionary<int, int>, Dictionary<int, string>)
|
private static (List<Instruction>, Dictionary<int, int>, Dictionary<int, string>)
|
||||||
|
|||||||
15
engine/Age.Engine/Vm/ExecFrame.cs
Normal file
15
engine/Age.Engine/Vm/ExecFrame.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using Age.Engine.Model;
|
||||||
|
namespace Age.Engine.Vm;
|
||||||
|
|
||||||
|
/// <summary>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.</summary>
|
||||||
|
internal sealed class ExecFrame
|
||||||
|
{
|
||||||
|
public readonly Script Script;
|
||||||
|
public int Pc; // entry instruction index
|
||||||
|
public readonly Frame Locals = new();
|
||||||
|
public readonly List<int> CallStack = new(); // intra-script `call` (op 0x8f) returns
|
||||||
|
public readonly Dictionary<int, int> EmitSeen = new();
|
||||||
|
public ExecFrame(Script script, int pc) { Script = script; Pc = pc; }
|
||||||
|
}
|
||||||
@@ -64,4 +64,4 @@ public sealed class GameSession
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>The observable result of running one scene into a <see cref="GameSession"/>.</summary>
|
/// <summary>The observable result of running one scene into a <see cref="GameSession"/>.</summary>
|
||||||
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);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ public sealed class VirtualMachine
|
|||||||
{
|
{
|
||||||
private const long NoJump = 0xFFFFFFFF;
|
private const long NoJump = 0xFFFFFFFF;
|
||||||
private const int HALT = int.MinValue;
|
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,
|
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;
|
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 OpcodeTable _t;
|
||||||
private readonly IHost _host;
|
private readonly IHost _host;
|
||||||
private readonly VmOptions _o;
|
private readonly VmOptions _o;
|
||||||
private readonly Frame _fr = new();
|
private readonly IScriptProvider? _provider;
|
||||||
private readonly List<int> _callstack = new();
|
private ExecFrame _cur = null!;
|
||||||
private readonly Dictionary<int, int> _emitSeen = new();
|
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();
|
||||||
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 string? HaltReason { get; private set; }
|
||||||
public long Steps { get; private set; }
|
public long Steps { get; private set; }
|
||||||
|
|
||||||
public VirtualMachine(Script s, OpcodeTable t, IHost host, VmOptions? o = null)
|
public VirtualMachine(Script s, OpcodeTable t, IHost host, VmOptions? o = null, IScriptProvider? provider = null)
|
||||||
{ _s = s; _t = t; _host = host; _o = o ?? new VmOptions(); }
|
{ _s = s; _t = t; _host = host; _o = o ?? new VmOptions(); _provider = provider; }
|
||||||
|
|
||||||
private static long Gi(Dictionary<int, long> d, int k) => d.TryGetValue(k, out var v) ? v : 0;
|
private static long Gi(Dictionary<int, long> d, int k) => d.TryGetValue(k, out var v) ? v : 0;
|
||||||
private static string Gs(Dictionary<int, string> d, int k) => d.TryGetValue(k, out var v) ? v : "";
|
private static string Gs(Dictionary<int, string> d, int k) => d.TryGetValue(k, out var v) ? v : "";
|
||||||
@@ -38,9 +40,9 @@ public sealed class VirtualMachine
|
|||||||
T_IMM => op.Value,
|
T_IMM => op.Value,
|
||||||
T_GINT or T_GFLOAT => Gi(Globals, (int)op.Value),
|
T_GINT or T_GFLOAT => Gi(Globals, (int)op.Value),
|
||||||
T_GPTR => Gi(Globals, (int)Gi(Globals, (int)op.Value)),
|
T_GPTR => Gi(Globals, (int)Gi(Globals, (int)op.Value)),
|
||||||
T_LINT => Gi(_fr.I, (int)op.Value),
|
T_LINT => Gi(_cur.Locals.I, (int)op.Value),
|
||||||
T_LFLOAT => Gi(_fr.F, (int)op.Value),
|
T_LFLOAT => Gi(_cur.Locals.F, (int)op.Value),
|
||||||
T_LPTR => Gi(Globals, (int)Gi(_fr.P, (int)op.Value)),
|
T_LPTR => Gi(Globals, (int)Gi(_cur.Locals.P, (int)op.Value)),
|
||||||
_ => 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_GINT: case T_GFLOAT: Globals[(int)op.Value] = val; break;
|
||||||
case T_GPTR: Globals[(int)Gi(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_LINT: _cur.Locals.I[(int)op.Value] = val; break;
|
||||||
case T_LFLOAT: _fr.F[(int)op.Value] = val; break;
|
case T_LFLOAT: _cur.Locals.F[(int)op.Value] = val; break;
|
||||||
case T_LPTR: Globals[(int)Gi(_fr.P, (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
|
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_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)
|
switch (op.Type)
|
||||||
{
|
{
|
||||||
case T_GSTR: GlobalStrings[(int)op.Value] = val; break;
|
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
|
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_IMM or T_GINT or T_GFLOAT or T_GSTR or T_GPTR => op.Value,
|
||||||
T_LINT => Gi(_fr.I, (int)op.Value),
|
T_LINT => Gi(_cur.Locals.I, (int)op.Value),
|
||||||
T_LPTR => Gi(_fr.P, (int)op.Value),
|
T_LPTR => Gi(_cur.Locals.P, (int)op.Value),
|
||||||
_ => op.Value,
|
_ => op.Value,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,24 +87,39 @@ public sealed class VirtualMachine
|
|||||||
{
|
{
|
||||||
switch (dst.Type)
|
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;
|
case T_GPTR: Globals[(int)dst.Value] = addr; break;
|
||||||
default: Write(dst, Gi(Globals, (int)addr)); break;
|
default: Write(dst, Gi(Globals, (int)addr)); break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum FrameOutcome { Returned, Halted, RanOff }
|
||||||
|
|
||||||
public void Run(int entryOffset = 0)
|
public void Run(int entryOffset = 0)
|
||||||
{
|
{
|
||||||
int pc = _s.IndexByOffset.TryGetValue(entryOffset, out var idx) ? idx : 0;
|
var top = new ExecFrame(_s, _s.IndexByOffset.TryGetValue(entryOffset, out var idx) ? idx : 0);
|
||||||
while (pc >= 0 && pc < _s.Instructions.Count)
|
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++;
|
Steps++;
|
||||||
int next = Step(_s.Instructions[pc], pc);
|
int next = Step(frame.Script.Instructions[pc], pc);
|
||||||
if (next == HALT) return;
|
if (next == FRAME_RETURN) { outcome = FrameOutcome.Returned; break; }
|
||||||
|
if (next == HALT) { _halted = true; outcome = FrameOutcome.Halted; break; }
|
||||||
pc = next;
|
pc = next;
|
||||||
}
|
}
|
||||||
HaltReason ??= "pc-out-of-range";
|
_cur = prev; _depth--;
|
||||||
|
return outcome;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int Step(Instruction ins, int pc)
|
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 "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 "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 "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 "jmp": return _cur.Script.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 "call": _cur.CallStack.Add(pc + 1); return _cur.Script.IndexByOffset.GetValueOrDefault((int)a[0].Value, pc + 1);
|
||||||
case "ret":
|
case "ret":
|
||||||
if (_callstack.Count > 0) { int r = _callstack[^1]; _callstack.RemoveAt(_callstack.Count - 1); return r; }
|
if (_cur.CallStack.Count > 0) { int r = _cur.CallStack[^1]; _cur.CallStack.RemoveAt(_cur.CallStack.Count - 1); return r; }
|
||||||
HaltReason = "ret-underflow"; return HALT;
|
return FRAME_RETURN; // empty intra-call stack => return from the script frame
|
||||||
case "jcc":
|
case "jcc":
|
||||||
{
|
{
|
||||||
long tgt = Read(a[0]) != 0 ? a[1].Value : a[2].Value;
|
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":
|
||||||
case "exit-script": HaltReason = "exit"; return HALT;
|
case "exit-script": return FRAME_RETURN;
|
||||||
case "call-script": _host.CallScript(a.Count > 0 ? Read(a[0]) : 0); return pc + 1;
|
case "call-script": _host.CallScript(a.Count > 0 ? Read(a[0]) : 0); return pc + 1; // stub (executes in Task 3)
|
||||||
case "show-text":
|
case "show-text":
|
||||||
foreach (var o in a)
|
foreach (var o in a)
|
||||||
{
|
{
|
||||||
if (o.Type != T_STR) continue;
|
if (o.Type != T_STR) continue;
|
||||||
int off = (int)o.Value;
|
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; }
|
if (c > _o.EmitCap) { HaltReason = $"LOOP:line@0x{off:x}×{c}"; return HALT; }
|
||||||
string text = _s.GetString(off);
|
string text = _cur.Script.GetString(off);
|
||||||
Emitted.Add((off, text));
|
Emitted.Add((off, text, _cur.Script.Name));
|
||||||
_host.ShowText(off, text);
|
_host.ShowText(off, text);
|
||||||
}
|
}
|
||||||
return pc + 1;
|
return pc + 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user