-
Notifications
You must be signed in to change notification settings - Fork 0
test: lift new-code coverage on Disassembler, GrobValue, GrobStruct, … #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
0b780a9
d92368d
b36b1bd
23f3696
3ed929c
48e7d75
2c6bf81
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| namespace Grob.Core; | ||
|
|
||
| /// <summary> | ||
| /// Arithmetic runtime error: integer overflow, division by zero, modulo by | ||
| /// zero, math domain violations. Maps to the Grob <c>ArithmeticError</c> | ||
| /// exception type (D-284). | ||
| /// </summary> | ||
| public sealed class GrobArithmeticException : GrobRuntimeException { | ||
| /// <summary> | ||
| /// Initialises a new <see cref="GrobArithmeticException"/> with the | ||
| /// supplied error <paramref name="code"/>, source <paramref name="line"/>, | ||
| /// and human-readable <paramref name="message"/>. | ||
| /// </summary> | ||
| public GrobArithmeticException(string code, int line, string message) | ||
| : base(code, line, message) { } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| namespace Grob.Core; | ||
|
|
||
| /// <summary> | ||
| /// Base type for Grob runtime errors raised by the VM. The two-mode error | ||
| /// model (D-284): the compiler/checker collect all errors; the VM stops on | ||
| /// the first runtime error. Carries the error code from grob-error-codes.md | ||
| /// and the source line from the chunk's per-instruction line array. | ||
| /// </summary> | ||
| public class GrobRuntimeException : Exception { | ||
| /// <summary>The grob-error-codes.md identifier (e.g. <c>E5001</c>).</summary> | ||
| public string Code { get; } | ||
|
|
||
| /// <summary>The source line attributed to the failing instruction.</summary> | ||
| public int Line { get; } | ||
|
|
||
| /// <summary> | ||
| /// Initialises a new <see cref="GrobRuntimeException"/> with the supplied | ||
| /// error <paramref name="code"/>, source <paramref name="line"/>, and | ||
| /// human-readable <paramref name="message"/>. | ||
| /// </summary> | ||
| public GrobRuntimeException(string code, int line, string message) | ||
| : base(message) { | ||
| Code = code; | ||
| Line = line; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| using Grob.Core; | ||
|
|
||
| namespace Grob.Vm; | ||
|
|
||
| /// <summary> | ||
| /// The VM operand stack: a fixed-capacity array of <see cref="GrobValue"/> | ||
| /// slots. Pushing a primitive (Bool/Int/Float) is a 24-byte struct copy with | ||
| /// no allocation (D-303, D-304). | ||
| /// | ||
| /// Authority: grob-vm-architecture.md — value stack section. | ||
| /// </summary> | ||
| public sealed class ValueStack { | ||
| /// <summary> | ||
| /// Maximum simultaneous live values on the operand stack. Chosen to comfortably | ||
| /// exceed Sprint 2's needs (no call frames yet) while leaving headroom for | ||
| /// future locals and intermediate computation. The value-stack overflow | ||
| /// path surfaces as a runtime error, not an unguarded array write. | ||
| /// </summary> | ||
| public const int Capacity = 16384; | ||
|
|
||
| private readonly GrobValue[] _values = new GrobValue[Capacity]; | ||
| private int _top; | ||
|
|
||
| /// <summary>Number of values currently on the stack.</summary> | ||
| public int Count => _top; | ||
|
|
||
| /// <summary> | ||
| /// Push <paramref name="value"/> onto the top of the stack. On overflow | ||
| /// throws <see cref="GrobRuntimeException"/> carrying <paramref name="line"/> | ||
| /// rather than an unguarded array write. | ||
| /// </summary> | ||
| public void Push(GrobValue value, int line) { | ||
| if (_top == _values.Length) | ||
| throw new GrobRuntimeException("E5903", line, "value stack overflow"); | ||
| _values[_top++] = value; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Pop and return the top of the stack. Underflow is a compiler/VM bug, | ||
| /// not a user-reachable runtime error — surfaces as | ||
| /// <see cref="GrobInternalException"/>. | ||
| /// </summary> | ||
| public GrobValue Pop() { | ||
| if (_top == 0) | ||
| throw new GrobInternalException("value stack underflow"); | ||
| var value = _values[--_top]; | ||
| _values[_top] = default; // release reference slots for GC (D-304) | ||
| return value; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Read the value at <paramref name="distance"/> below the top without | ||
| /// popping. <c>distance == 0</c> is the top. Negative distances and | ||
| /// distances past the bottom of the live region are compiler/VM bugs | ||
| /// and surface as <see cref="GrobInternalException"/>. | ||
| /// </summary> | ||
| public GrobValue Peek(int distance = 0) { | ||
| if (distance < 0) | ||
| throw new GrobInternalException("value stack peek with negative distance"); | ||
| int index = _top - 1 - distance; | ||
| if (index < 0) | ||
| throw new GrobInternalException("value stack peek underflow"); | ||
| return _values[index]; | ||
|
kwakker35 marked this conversation as resolved.
|
||
| } | ||
|
|
||
| /// <summary> | ||
| /// Logically empty the stack. Sets the top pointer to zero without | ||
| /// clearing slots — the next <see cref="Push"/> will overwrite, and | ||
| /// any reference values left over are released by the per-<see cref="Pop"/> | ||
| /// slot clear once <see cref="Pop"/> is invoked. Used by | ||
|
kwakker35 marked this conversation as resolved.
Outdated
|
||
| /// <c>VirtualMachine.Run</c> to start each invocation from a clean | ||
| /// operand stack regardless of any leftovers from a prior | ||
| /// exception-terminated run. | ||
| /// </summary> | ||
| internal void Reset() { | ||
| if (_top > 0) | ||
| Array.Clear(_values, 0, _top); // release reference slots for GC (D-304) | ||
| _top = 0; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Snapshot the live region of the stack — used by the | ||
| /// <c>#if DEBUG</c> trace hook to render the stack each iteration. | ||
| /// </summary> | ||
| internal ReadOnlySpan<GrobValue> AsSpan() => _values.AsSpan(0, _top); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,234 @@ | ||
| using Grob.Core; | ||
|
|
||
| namespace Grob.Vm; | ||
|
|
||
| /// <summary> | ||
| /// The Grob stack-based bytecode VM. Owns the operand stack and the | ||
| /// fetch-decode-execute dispatch loop. Sprint 2 Increment B implements the | ||
| /// subset of <see cref="OpCode"/> needed to execute hand-constructed chunks | ||
| /// up to <c>print(2 + 3 * 4)</c> — see <see cref="Run"/> for the supported | ||
| /// set; out-of-scope opcodes (control flow, calls, globals, structs, arrays, | ||
| /// closures, exceptions, increments, properties, build-string, etc.) raise | ||
| /// <see cref="GrobInternalException"/> until their owning increment lands. | ||
| /// | ||
| /// Authority: grob-vm-architecture.md (dispatch loop, value stack, developer | ||
| /// diagnostics) and grob-v1-requirements.md §3.3 (the OpCode set). | ||
| /// </summary> | ||
| public sealed class VirtualMachine { | ||
| private readonly ValueStack _stack = new(); | ||
| private readonly TextWriter _out; | ||
| private readonly TextWriter _trace; | ||
|
Check warning on line 20 in src/Grob.Vm/VirtualMachine.cs
|
||
|
|
||
| /// <summary> | ||
| /// Construct a VM whose <see cref="OpCode.Print"/> output goes to | ||
| /// <paramref name="output"/>. <paramref name="trace"/> receives the | ||
| /// <c>#if DEBUG</c> per-instruction trace (defaults to | ||
| /// <see cref="TextWriter.Null"/>, which is also the only meaningful value | ||
| /// in Release where the trace call is compiled out entirely). | ||
| /// </summary> | ||
| public VirtualMachine(TextWriter output, TextWriter? trace = null) { | ||
| ArgumentNullException.ThrowIfNull(output); | ||
| _out = output; | ||
| _trace = trace ?? TextWriter.Null; | ||
| } | ||
|
|
||
| /// <summary>The operand stack, exposed for tests to inspect post-run state.</summary> | ||
| public ValueStack Stack => _stack; | ||
|
|
||
| /// <summary> | ||
| /// Execute <paramref name="chunk"/> until <see cref="OpCode.Return"/>. | ||
| /// Running off the end of the bytecode without a <see cref="OpCode.Return"/> | ||
| /// is treated as a malformed chunk — it raises | ||
| /// <see cref="GrobInternalException"/>, because the compiler always emits | ||
| /// a terminating <c>Return</c> and hand-constructed test chunks must do | ||
| /// the same. | ||
| /// </summary> | ||
| public void Run(Chunk chunk) { | ||
|
Check warning on line 46 in src/Grob.Vm/VirtualMachine.cs
|
||
| ArgumentNullException.ThrowIfNull(chunk); | ||
|
|
||
| // Defensive: a prior Run that terminated by exception may have left | ||
| // values on the operand stack. Start every invocation clean so the | ||
| // VM behaves the same on the Nth chunk as on the first. | ||
| _stack.Reset(); | ||
|
|
||
| int ip = 0; | ||
| int line = 0; | ||
|
kwakker35 marked this conversation as resolved.
|
||
|
|
||
| try { | ||
| while (true) { | ||
| if (ip >= chunk.Count) | ||
| throw new GrobInternalException( | ||
| "execution ran past end of chunk without Return"); | ||
|
|
||
| line = chunk.GetLine(ip); | ||
|
|
||
| #if DEBUG | ||
| TraceInstruction(chunk, ip); | ||
| #endif | ||
|
|
||
| byte instruction = chunk.ReadByte(ip); | ||
| ip++; | ||
|
|
||
| switch ((OpCode)instruction) { | ||
| // --- Constants and singletons --- | ||
| case OpCode.Constant: { | ||
| byte index = chunk.ReadByte(ip++); | ||
| _stack.Push(chunk.ReadConstant(index), line); | ||
| break; | ||
| } | ||
| case OpCode.ConstantLong: { | ||
| int index = (chunk.ReadByte(ip) << 8) | chunk.ReadByte(ip + 1); | ||
| ip += 2; | ||
| _stack.Push(chunk.ReadConstant(index), line); | ||
|
kwakker35 marked this conversation as resolved.
|
||
| break; | ||
| } | ||
| case OpCode.Nil: _stack.Push(GrobValue.Nil, line); break; | ||
| case OpCode.True: _stack.Push(GrobValue.FromBool(true), line); break; | ||
| case OpCode.False: _stack.Push(GrobValue.FromBool(false), line); break; | ||
|
|
||
| case OpCode.Pop: _stack.Pop(); break; | ||
| case OpCode.PopN: { | ||
| byte count = chunk.ReadByte(ip++); | ||
| for (int i = 0; i < count; i++) _stack.Pop(); | ||
| break; | ||
| } | ||
|
|
||
| // --- Integer arithmetic (checked; OverflowException → E5001) --- | ||
| case OpCode.AddInt: { | ||
| long b = _stack.Pop().AsInt(); | ||
| long a = _stack.Pop().AsInt(); | ||
| _stack.Push(GrobValue.FromInt(checked(a + b)), line); | ||
| break; | ||
| } | ||
| case OpCode.SubtractInt: { | ||
| long b = _stack.Pop().AsInt(); | ||
| long a = _stack.Pop().AsInt(); | ||
| _stack.Push(GrobValue.FromInt(checked(a - b)), line); | ||
| break; | ||
| } | ||
| case OpCode.MultiplyInt: { | ||
| long b = _stack.Pop().AsInt(); | ||
| long a = _stack.Pop().AsInt(); | ||
| _stack.Push(GrobValue.FromInt(checked(a * b)), line); | ||
| break; | ||
| } | ||
| case OpCode.DivideInt: { | ||
| long b = _stack.Pop().AsInt(); | ||
| long a = _stack.Pop().AsInt(); | ||
| if (b == 0L) | ||
| throw new GrobArithmeticException("E5002", line, "integer division by zero"); | ||
| // long.MinValue / -1 overflows: caught below as E5001. | ||
| _stack.Push(GrobValue.FromInt(checked(a / b)), line); | ||
| break; | ||
| } | ||
| case OpCode.ModuloInt: { | ||
| long b = _stack.Pop().AsInt(); | ||
| long a = _stack.Pop().AsInt(); | ||
| if (b == 0L) | ||
| throw new GrobArithmeticException("E5003", line, "integer modulo by zero"); | ||
| _stack.Push(GrobValue.FromInt(checked(a % b)), line); | ||
| break; | ||
| } | ||
| case OpCode.NegateInt: { | ||
| long a = _stack.Pop().AsInt(); | ||
| _stack.Push(GrobValue.FromInt(checked(-a)), line); | ||
| break; | ||
| } | ||
|
|
||
| // --- Float arithmetic (D-273: x / 0.0 and x % 0.0 throw) --- | ||
| case OpCode.AddFloat: { | ||
| double b = _stack.Pop().AsFloat(); | ||
| double a = _stack.Pop().AsFloat(); | ||
| _stack.Push(GrobValue.FromFloat(a + b), line); | ||
| break; | ||
| } | ||
| case OpCode.SubtractFloat: { | ||
| double b = _stack.Pop().AsFloat(); | ||
| double a = _stack.Pop().AsFloat(); | ||
| _stack.Push(GrobValue.FromFloat(a - b), line); | ||
| break; | ||
| } | ||
| case OpCode.MultiplyFloat: { | ||
| double b = _stack.Pop().AsFloat(); | ||
| double a = _stack.Pop().AsFloat(); | ||
| _stack.Push(GrobValue.FromFloat(a * b), line); | ||
| break; | ||
| } | ||
| case OpCode.DivideFloat: { | ||
| double b = _stack.Pop().AsFloat(); | ||
| double a = _stack.Pop().AsFloat(); | ||
| if (b == 0.0) | ||
|
Check warning on line 160 in src/Grob.Vm/VirtualMachine.cs
|
||
|
github-code-quality[bot] marked this conversation as resolved.
Fixed
github-code-quality[bot] marked this conversation as resolved.
Fixed
github-code-quality[bot] marked this conversation as resolved.
Fixed
github-code-quality[bot] marked this conversation as resolved.
Fixed
kwakker35 marked this conversation as resolved.
Dismissed
|
||
| throw new GrobArithmeticException("E5004", line, "float division by zero"); | ||
| _stack.Push(GrobValue.FromFloat(a / b), line); | ||
| break; | ||
| } | ||
| case OpCode.ModuloFloat: { | ||
| double b = _stack.Pop().AsFloat(); | ||
| double a = _stack.Pop().AsFloat(); | ||
| if (b == 0.0) | ||
|
Check warning on line 168 in src/Grob.Vm/VirtualMachine.cs
|
||
|
github-code-quality[bot] marked this conversation as resolved.
Fixed
github-code-quality[bot] marked this conversation as resolved.
Fixed
github-code-quality[bot] marked this conversation as resolved.
Fixed
github-code-quality[bot] marked this conversation as resolved.
Fixed
kwakker35 marked this conversation as resolved.
Dismissed
|
||
| throw new GrobArithmeticException("E5005", line, "float modulo by zero"); | ||
| _stack.Push(GrobValue.FromFloat(a % b), line); | ||
| break; | ||
| } | ||
| case OpCode.NegateFloat: { | ||
| double a = _stack.Pop().AsFloat(); | ||
| _stack.Push(GrobValue.FromFloat(-a), line); | ||
| break; | ||
| } | ||
|
|
||
| // --- Strings --- | ||
| case OpCode.Concat: { | ||
| string b = _stack.Pop().AsString(); | ||
| string a = _stack.Pop().AsString(); | ||
| _stack.Push(GrobValue.FromString(string.Concat(a, b)), line); | ||
| break; | ||
| } | ||
|
|
||
| // --- Promotion --- | ||
| case OpCode.IntToFloat: { | ||
| long a = _stack.Pop().AsInt(); | ||
| _stack.Push(GrobValue.FromFloat(a), line); | ||
| break; | ||
| } | ||
|
|
||
| // --- I/O --- | ||
| case OpCode.Print: | ||
| _out.WriteLine(_stack.Pop().ToString()); | ||
| break; | ||
|
|
||
| // --- Top-level return ends this chunk's execution --- | ||
| case OpCode.Return: | ||
| return; | ||
|
|
||
| default: | ||
| throw new GrobInternalException( | ||
| $"opcode {(OpCode)instruction} not implemented in Sprint 2 Increment B dispatch loop"); | ||
| } | ||
| } | ||
| } catch (OverflowException) { | ||
| // Centralised handler for `checked(...)` arithmetic: any int op | ||
| // that overflows surfaces as E5001 carrying the failing line. | ||
| throw new GrobArithmeticException("E5001", line, "integer overflow"); | ||
| } | ||
| } | ||
|
|
||
| #if DEBUG | ||
| /// <summary> | ||
| /// D-306 per-instruction trace: renders the value stack and the | ||
| /// about-to-execute instruction every iteration of the dispatch loop. | ||
| /// Compiled into Debug builds only — entirely absent in Release so that | ||
| /// D-302 benchmarks measure a branch-free dispatch path. | ||
| /// </summary> | ||
| private void TraceInstruction(Chunk chunk, int ip) { | ||
| _trace.Write(" "); | ||
| var span = _stack.AsSpan(); | ||
| for (int i = 0; i < span.Length; i++) { | ||
| _trace.Write("[ "); | ||
| _trace.Write(span[i].ToString()); | ||
| _trace.Write(" ]"); | ||
| } | ||
| _trace.WriteLine(); | ||
| Disassembler.DisassembleInstruction(chunk, ip, _trace); | ||
| } | ||
| #endif | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.