Skip to content
16 changes: 16 additions & 0 deletions src/Grob.Core/GrobArithmeticException.cs
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) { }
}
26 changes: 26 additions & 0 deletions src/Grob.Core/GrobRuntimeException.cs
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;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
86 changes: 86 additions & 0 deletions src/Grob.Vm/ValueStack.cs
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];
Comment thread
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
Comment thread
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);
}
234 changes: 234 additions & 0 deletions src/Grob.Vm/VirtualMachine.cs
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

View workflow job for this annotation

GitHub Actions / SonarCloud

Remove this unread private field '_trace' or refactor the code to use its value.

Check warning on line 20 in src/Grob.Vm/VirtualMachine.cs

View workflow job for this annotation

GitHub Actions / SonarCloud

Remove this unread private field '_trace' or refactor the code to use its value.

Check failure on line 20 in src/Grob.Vm/VirtualMachine.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unread private field '_trace' or refactor the code to use its value.

See more on https://sonarcloud.io/project/issues?id=grob-lang_grob&issues=AZ5hKa1_cVnEFZITPmNr&open=AZ5hKa1_cVnEFZITPmNr&pullRequest=41

/// <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

View workflow job for this annotation

GitHub Actions / SonarCloud

Refactor this method to reduce its Cognitive Complexity from 21 to the 15 allowed.

Check warning on line 46 in src/Grob.Vm/VirtualMachine.cs

View workflow job for this annotation

GitHub Actions / SonarCloud

Refactor this method to reduce its Cognitive Complexity from 21 to the 15 allowed.

Check failure on line 46 in src/Grob.Vm/VirtualMachine.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 21 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=grob-lang_grob&issues=AZ5hKa1_cVnEFZITPmNs&open=AZ5hKa1_cVnEFZITPmNs&pullRequest=41
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;
Comment thread
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);
Comment thread
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

View workflow job for this annotation

GitHub Actions / SonarCloud

Do not check floating point equality with exact values, use a range instead.

Check warning on line 160 in src/Grob.Vm/VirtualMachine.cs

View workflow job for this annotation

GitHub Actions / SonarCloud

Do not check floating point equality with exact values, use a range instead.

Check warning on line 160 in src/Grob.Vm/VirtualMachine.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not check floating point equality with exact values, use a range instead.

See more on https://sonarcloud.io/project/issues?id=grob-lang_grob&issues=AZ5hKa1_cVnEFZITPmNt&open=AZ5hKa1_cVnEFZITPmNt&pullRequest=41
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
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

View workflow job for this annotation

GitHub Actions / SonarCloud

Do not check floating point equality with exact values, use a range instead.

Check warning on line 168 in src/Grob.Vm/VirtualMachine.cs

View workflow job for this annotation

GitHub Actions / SonarCloud

Do not check floating point equality with exact values, use a range instead.

Check warning on line 168 in src/Grob.Vm/VirtualMachine.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not check floating point equality with exact values, use a range instead.

See more on https://sonarcloud.io/project/issues?id=grob-lang_grob&issues=AZ5hKa1_cVnEFZITPmNu&open=AZ5hKa1_cVnEFZITPmNu&pullRequest=41
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
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
}
Loading
Loading