Skip to content

Commit 0b780a9

Browse files
committed
test: lift new-code coverage on Disassembler, GrobValue, GrobStruct, GrobFunction
Sonar new-code coverage was 64.2%, below the 80% gate. Disassembler: data-driven theory covers every OpCode dispatch arm, asserting the dispatch returns the documented instruction width and emits the opcode name. Width table mirrors the production dispatch so any future width drift fails loudly here. GrobValue: per-kind Equals/GetHashCode tests for Map and Function reference paths, +0.0/-0.0 hash agreement, full strict and try-accessor matrices, and ArgumentNullException tests for the five reference-typed From* factories. GrobStruct: null/empty TypeName guards, null field-name guards on GetField/SetField/TryGetField, missing-key behaviour, reflexive Equals and field-equality coverage. GrobFunction: null name, negative arity, and empty-name-allowed contract.
1 parent 0c70b03 commit 0b780a9

4 files changed

Lines changed: 827 additions & 0 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
namespace Grob.Core;
2+
3+
/// <summary>
4+
/// Base type for Grob runtime errors raised by the VM. The two-mode error
5+
/// model (D-284): the compiler/checker collect all errors; the VM stops on
6+
/// the first runtime error. Carries the error code from grob-error-codes.md
7+
/// and the source line from the chunk's per-instruction line array.
8+
/// </summary>
9+
public class GrobRuntimeException : Exception {
10+
/// <summary>The grob-error-codes.md identifier (e.g. <c>E5001</c>).</summary>
11+
public string Code { get; }
12+
13+
/// <summary>The source line attributed to the failing instruction.</summary>
14+
public int Line { get; }
15+
16+
/// <summary>
17+
/// Initialises a new <see cref="GrobRuntimeException"/> with the supplied
18+
/// error <paramref name="code"/>, source <paramref name="line"/>, and
19+
/// human-readable <paramref name="message"/>.
20+
/// </summary>
21+
public GrobRuntimeException(string code, int line, string message)
22+
: base(message) {
23+
Code = code;
24+
Line = line;
25+
}
26+
}
27+
28+
/// <summary>
29+
/// Arithmetic runtime error: integer overflow, division by zero, modulo by
30+
/// zero, math domain violations. Maps to the Grob <c>ArithmeticError</c>
31+
/// exception type (D-284).
32+
/// </summary>
33+
public sealed class GrobArithmeticException : GrobRuntimeException {
34+
/// <summary>
35+
/// Initialises a new <see cref="GrobArithmeticException"/> with the
36+
/// supplied error <paramref name="code"/>, source <paramref name="line"/>,
37+
/// and human-readable <paramref name="message"/>.
38+
/// </summary>
39+
public GrobArithmeticException(string code, int line, string message)
40+
: base(code, line, message) { }
41+
}

src/Grob.Vm/ValueStack.cs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using Grob.Core;
2+
3+
namespace Grob.Vm;
4+
5+
/// <summary>
6+
/// The VM operand stack: a fixed-capacity array of <see cref="GrobValue"/>
7+
/// slots. Pushing a primitive (Bool/Int/Float) is a 24-byte struct copy with
8+
/// no allocation (D-303, D-304).
9+
///
10+
/// Authority: grob-vm-architecture.md — value stack section.
11+
/// </summary>
12+
public sealed class ValueStack {
13+
/// <summary>
14+
/// Maximum simultaneous live values on the operand stack. Chosen to comfortably
15+
/// exceed Sprint 2's needs (no call frames yet) while leaving headroom for
16+
/// future locals and intermediate computation. The value-stack overflow
17+
/// path surfaces as a runtime error, not an unguarded array write.
18+
/// </summary>
19+
public const int Capacity = 16384;
20+
21+
private readonly GrobValue[] _values = new GrobValue[Capacity];
22+
private int _top;
23+
24+
/// <summary>Number of values currently on the stack.</summary>
25+
public int Count => _top;
26+
27+
/// <summary>
28+
/// Push <paramref name="value"/> onto the top of the stack. On overflow
29+
/// throws <see cref="GrobRuntimeException"/> carrying <paramref name="line"/>
30+
/// rather than an unguarded array write.
31+
/// </summary>
32+
public void Push(GrobValue value, int line) {
33+
if (_top == _values.Length)
34+
throw new GrobRuntimeException("E5903", line, "value stack overflow");
35+
_values[_top++] = value;
36+
}
37+
38+
/// <summary>
39+
/// Pop and return the top of the stack. Underflow is a compiler/VM bug,
40+
/// not a user-reachable runtime error — surfaces as
41+
/// <see cref="GrobInternalException"/>.
42+
/// </summary>
43+
public GrobValue Pop() {
44+
if (_top == 0)
45+
throw new GrobInternalException("value stack underflow");
46+
var value = _values[--_top];
47+
_values[_top] = default; // release reference slots for GC (D-304)
48+
return value;
49+
}
50+
51+
/// <summary>
52+
/// Read the value at <paramref name="distance"/> below the top without
53+
/// popping. <c>distance == 0</c> is the top.
54+
/// </summary>
55+
public GrobValue Peek(int distance = 0) {
56+
int index = _top - 1 - distance;
57+
if (index < 0)
58+
throw new GrobInternalException("value stack peek underflow");
59+
return _values[index];
60+
}
61+
62+
/// <summary>
63+
/// Snapshot the live region of the stack — used by the
64+
/// <c>#if DEBUG</c> trace hook to render the stack each iteration.
65+
/// </summary>
66+
internal ReadOnlySpan<GrobValue> AsSpan() => _values.AsSpan(0, _top);
67+
}

src/Grob.Vm/VirtualMachine.cs

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
using Grob.Core;
2+
3+
namespace Grob.Vm;
4+
5+
/// <summary>
6+
/// The Grob stack-based bytecode VM. Owns the operand stack and the
7+
/// fetch-decode-execute dispatch loop. Sprint 2 Increment B implements the
8+
/// subset of <see cref="OpCode"/> needed to execute hand-constructed chunks
9+
/// up to <c>print(2 + 3 * 4)</c> — see <see cref="Run"/> for the supported
10+
/// set; out-of-scope opcodes (control flow, calls, globals, structs, arrays,
11+
/// closures, exceptions, increments, properties, build-string, etc.) raise
12+
/// <see cref="GrobInternalException"/> until their owning increment lands.
13+
///
14+
/// Authority: grob-vm-architecture.md (dispatch loop, value stack, developer
15+
/// diagnostics) and grob-v1-requirements.md §3.3 (the OpCode set).
16+
/// </summary>
17+
public sealed class VirtualMachine {
18+
private readonly ValueStack _stack = new();
19+
private readonly TextWriter _out;
20+
private readonly TextWriter _trace;
21+
22+
/// <summary>
23+
/// Construct a VM whose <see cref="OpCode.Print"/> output goes to
24+
/// <paramref name="output"/>. <paramref name="trace"/> receives the
25+
/// <c>#if DEBUG</c> per-instruction trace (defaults to
26+
/// <see cref="TextWriter.Null"/>, which is also the only meaningful value
27+
/// in Release where the trace call is compiled out entirely).
28+
/// </summary>
29+
public VirtualMachine(TextWriter output, TextWriter? trace = null) {
30+
ArgumentNullException.ThrowIfNull(output);
31+
_out = output;
32+
_trace = trace ?? TextWriter.Null;
33+
}
34+
35+
/// <summary>The operand stack, exposed for tests to inspect post-run state.</summary>
36+
public ValueStack Stack => _stack;
37+
38+
/// <summary>
39+
/// Execute <paramref name="chunk"/> until <see cref="OpCode.Return"/>.
40+
/// Running off the end of the bytecode without a <see cref="OpCode.Return"/>
41+
/// is treated as a malformed chunk — it raises
42+
/// <see cref="GrobInternalException"/>, because the compiler always emits
43+
/// a terminating <c>Return</c> and hand-constructed test chunks must do
44+
/// the same.
45+
/// </summary>
46+
public void Run(Chunk chunk) {
47+
ArgumentNullException.ThrowIfNull(chunk);
48+
49+
int ip = 0;
50+
int line = 0;
51+
52+
try {
53+
while (true) {
54+
if (ip >= chunk.Count)
55+
throw new GrobInternalException(
56+
"execution ran past end of chunk without Return");
57+
58+
line = chunk.GetLine(ip);
59+
60+
#if DEBUG
61+
TraceInstruction(chunk, ip);
62+
#endif
63+
64+
byte instruction = chunk.ReadByte(ip);
65+
ip++;
66+
67+
switch ((OpCode)instruction) {
68+
// --- Constants and singletons ---
69+
case OpCode.Constant: {
70+
byte index = chunk.ReadByte(ip++);
71+
_stack.Push(chunk.ReadConstant(index), line);
72+
break;
73+
}
74+
case OpCode.ConstantLong: {
75+
int index = (chunk.ReadByte(ip) << 8) | chunk.ReadByte(ip + 1);
76+
ip += 2;
77+
_stack.Push(chunk.ReadConstant(index), line);
78+
break;
79+
}
80+
case OpCode.Nil: _stack.Push(GrobValue.Nil, line); break;
81+
case OpCode.True: _stack.Push(GrobValue.FromBool(true), line); break;
82+
case OpCode.False: _stack.Push(GrobValue.FromBool(false), line); break;
83+
84+
case OpCode.Pop: _stack.Pop(); break;
85+
case OpCode.PopN: {
86+
byte count = chunk.ReadByte(ip++);
87+
for (int i = 0; i < count; i++) _stack.Pop();
88+
break;
89+
}
90+
91+
// --- Integer arithmetic (checked; OverflowException → E5001) ---
92+
case OpCode.AddInt: {
93+
long b = _stack.Pop().AsInt();
94+
long a = _stack.Pop().AsInt();
95+
_stack.Push(GrobValue.FromInt(checked(a + b)), line);
96+
break;
97+
}
98+
case OpCode.SubtractInt: {
99+
long b = _stack.Pop().AsInt();
100+
long a = _stack.Pop().AsInt();
101+
_stack.Push(GrobValue.FromInt(checked(a - b)), line);
102+
break;
103+
}
104+
case OpCode.MultiplyInt: {
105+
long b = _stack.Pop().AsInt();
106+
long a = _stack.Pop().AsInt();
107+
_stack.Push(GrobValue.FromInt(checked(a * b)), line);
108+
break;
109+
}
110+
case OpCode.DivideInt: {
111+
long b = _stack.Pop().AsInt();
112+
long a = _stack.Pop().AsInt();
113+
if (b == 0L)
114+
throw new GrobArithmeticException("E5002", line, "integer division by zero");
115+
// long.MinValue / -1 overflows: caught below as E5001.
116+
_stack.Push(GrobValue.FromInt(checked(a / b)), line);
117+
break;
118+
}
119+
case OpCode.ModuloInt: {
120+
long b = _stack.Pop().AsInt();
121+
long a = _stack.Pop().AsInt();
122+
if (b == 0L)
123+
throw new GrobArithmeticException("E5003", line, "integer modulo by zero");
124+
_stack.Push(GrobValue.FromInt(checked(a % b)), line);
125+
break;
126+
}
127+
case OpCode.NegateInt: {
128+
long a = _stack.Pop().AsInt();
129+
_stack.Push(GrobValue.FromInt(checked(-a)), line);
130+
break;
131+
}
132+
133+
// --- Float arithmetic (D-273: x / 0.0 and x % 0.0 throw) ---
134+
case OpCode.AddFloat: {
135+
double b = _stack.Pop().AsFloat();
136+
double a = _stack.Pop().AsFloat();
137+
_stack.Push(GrobValue.FromFloat(a + b), line);
138+
break;
139+
}
140+
case OpCode.SubtractFloat: {
141+
double b = _stack.Pop().AsFloat();
142+
double a = _stack.Pop().AsFloat();
143+
_stack.Push(GrobValue.FromFloat(a - b), line);
144+
break;
145+
}
146+
case OpCode.MultiplyFloat: {
147+
double b = _stack.Pop().AsFloat();
148+
double a = _stack.Pop().AsFloat();
149+
_stack.Push(GrobValue.FromFloat(a * b), line);
150+
break;
151+
}
152+
case OpCode.DivideFloat: {
153+
double b = _stack.Pop().AsFloat();
154+
double a = _stack.Pop().AsFloat();
155+
if (b == 0.0)
156+
throw new GrobArithmeticException("E5004", line, "float division by zero");
157+
_stack.Push(GrobValue.FromFloat(a / b), line);
158+
break;
159+
}
160+
case OpCode.ModuloFloat: {
161+
double b = _stack.Pop().AsFloat();
162+
double a = _stack.Pop().AsFloat();
163+
if (b == 0.0)
164+
throw new GrobArithmeticException("E5005", line, "float modulo by zero");
165+
_stack.Push(GrobValue.FromFloat(a % b), line);
166+
break;
167+
}
168+
case OpCode.NegateFloat: {
169+
double a = _stack.Pop().AsFloat();
170+
_stack.Push(GrobValue.FromFloat(-a), line);
171+
break;
172+
}
173+
174+
// --- Strings ---
175+
case OpCode.Concat: {
176+
string b = _stack.Pop().AsString();
177+
string a = _stack.Pop().AsString();
178+
_stack.Push(GrobValue.FromString(string.Concat(a, b)), line);
179+
break;
180+
}
181+
182+
// --- Promotion ---
183+
case OpCode.IntToFloat: {
184+
long a = _stack.Pop().AsInt();
185+
_stack.Push(GrobValue.FromFloat(a), line);
186+
break;
187+
}
188+
189+
// --- I/O ---
190+
case OpCode.Print:
191+
_out.WriteLine(_stack.Pop().ToString());
192+
break;
193+
194+
// --- Top-level return ends this chunk's execution ---
195+
case OpCode.Return:
196+
return;
197+
198+
default:
199+
throw new GrobInternalException(
200+
$"opcode {(OpCode)instruction} not implemented in Sprint 2 Increment B dispatch loop");
201+
}
202+
}
203+
} catch (OverflowException) {
204+
// Centralised handler for `checked(...)` arithmetic: any int op
205+
// that overflows surfaces as E5001 carrying the failing line.
206+
throw new GrobArithmeticException("E5001", line, "integer overflow");
207+
}
208+
}
209+
210+
#if DEBUG
211+
/// <summary>
212+
/// D-306 per-instruction trace: renders the value stack and the
213+
/// about-to-execute instruction every iteration of the dispatch loop.
214+
/// Compiled into Debug builds only — entirely absent in Release so that
215+
/// D-302 benchmarks measure a branch-free dispatch path.
216+
/// </summary>
217+
private void TraceInstruction(Chunk chunk, int ip) {
218+
_trace.Write(" ");
219+
var span = _stack.AsSpan();
220+
for (int i = 0; i < span.Length; i++) {
221+
_trace.Write("[ ");
222+
_trace.Write(span[i].ToString());
223+
_trace.Write(" ]");
224+
}
225+
_trace.WriteLine();
226+
Disassembler.DisassembleInstruction(chunk, ip, _trace);
227+
}
228+
#endif
229+
}

0 commit comments

Comments
 (0)