Skip to content
40 changes: 35 additions & 5 deletions src/Grob.Core/Chunk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@ namespace Grob.Core;

/// <summary>
/// A compiled bytecode chunk: instruction bytes, constant pool, and per-instruction
/// source line numbers.
/// source positions (line + column).
///
/// Write surface (<c>WriteByte</c>, <c>AddConstant</c>) is used by the compiler
/// (Sprint 2 Increment D) and by hand-constructed test chunks (Increment A/B).
/// Source columns are 1-based; <c>0</c> is the sentinel used when a chunk byte
/// has no meaningful column origin (synthetic prologue, hand-built test bytecode
/// that did not supply one, etc.).
/// </summary>
public sealed class Chunk {
private readonly List<byte> _code = [];
private readonly List<GrobValue> _constants = [];
private readonly List<int> _lines = []; // parallel to _code: source line per byte
private readonly List<int> _lines = []; // parallel to _code: source line per byte
private readonly List<int> _columns = []; // parallel to _code: source column per byte (0 = unknown)

// ----- Read surface (Disassembler and VM) -----

Expand All @@ -29,17 +33,43 @@ public sealed class Chunk {
/// <summary>Source line number for the instruction byte at <paramref name="offset"/>.</summary>
public int GetLine(int offset) => _lines[offset];

/// <summary>
/// Source column number for the instruction byte at <paramref name="offset"/>.
/// Returns <c>0</c> when no column was supplied at write time. Columns are
/// 1-based when present.
/// </summary>
public int GetColumn(int offset) => _columns[offset];

// ----- Write surface (Compiler / hand-constructed tests) -----

/// <summary>Append a raw byte attributed to <paramref name="line"/>.</summary>
public void WriteByte(byte value, int line) {
/// <summary>
/// Append a raw byte attributed to <paramref name="line"/>. Column is
/// recorded as <c>0</c> ("unknown"); prefer the <see cref="WriteByte(byte, int, int)"/>
/// overload from the compiler so runtime errors can point at the exact
/// column.
/// </summary>
public void WriteByte(byte value, int line) => WriteByte(value, line, 0);

/// <summary>
/// Append a raw byte attributed to <paramref name="line"/> and
/// <paramref name="column"/> (1-based; <c>0</c> means "unknown").
/// </summary>
public void WriteByte(byte value, int line, int column) {
_code.Add(value);
_lines.Add(line);
_columns.Add(column);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/// <summary>Append an opcode byte attributed to <paramref name="line"/>.</summary>
public void WriteOpCode(OpCode opCode, int line) =>
WriteByte((byte)opCode, line);
WriteByte((byte)opCode, line, 0);

/// <summary>
/// Append an opcode byte attributed to <paramref name="line"/> and
/// <paramref name="column"/> (1-based; <c>0</c> means "unknown").
/// </summary>
public void WriteOpCode(OpCode opCode, int line, int column) =>
WriteByte((byte)opCode, line, column);

/// <summary>
/// Add a value to the constant pool and return its index.
Expand Down
26 changes: 26 additions & 0 deletions src/Grob.Core/GrobArithmeticException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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"/>. <see cref="GrobRuntimeException.Column"/>
/// is recorded as <c>0</c>.
/// </summary>
public GrobArithmeticException(string code, int line, string message)
: base(code, line, message) { }

/// <summary>
/// Initialises a new <see cref="GrobArithmeticException"/> with the
/// supplied error <paramref name="code"/>, source <paramref name="line"/>,
/// 1-based <paramref name="column"/>, and human-readable
/// <paramref name="message"/>.
/// </summary>
public GrobArithmeticException(string code, int line, int column, string message)
: base(code, line, column, message) { }
}
47 changes: 47 additions & 0 deletions src/Grob.Core/GrobRuntimeException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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 position (line + column) from the chunk's per-instruction
/// position arrays. <see cref="Column"/> is <c>0</c> when no column was
/// recorded for the failing instruction.
/// </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>
/// The 1-based source column attributed to the failing instruction.
/// <c>0</c> indicates the chunk byte was written without a column
/// (e.g. hand-built test bytecode or synthetic prologue).
/// </summary>
public int Column { 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"/>. <see cref="Column"/> is
/// recorded as <c>0</c> ("unknown"); prefer the four-argument constructor
/// when a column is available.
/// </summary>
public GrobRuntimeException(string code, int line, string message)
: this(code, line, 0, message) { }

/// <summary>
/// Initialises a new <see cref="GrobRuntimeException"/> with the supplied
/// error <paramref name="code"/>, source <paramref name="line"/>,
/// 1-based <paramref name="column"/>, and human-readable
/// <paramref name="message"/>.
/// </summary>
public GrobRuntimeException(string code, int line, int column, string message)
: base(message) {
Code = code;
Line = line;
Column = column;
}
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);
}
Loading
Loading