Virtual Machine
Matchbox 0.2 runs bytecode on a stack-based virtual machine. The VM stores the operand stack, service table, instruction pointer, stack pointer, frame pointer, module pointer, and globals array.
Runtime
The VM starts by loading the top-level function from constant pool slot 0 and setting the instruction pointer to that function's bytecode. It then reads one opcode at a time, advances the instruction pointer, performs the operation, and continues until hlt or an unknown/default opcode stops execution.
Built-in functions are implemented internally as services. A reqs instruction reads a service opcode, calls the registered native function with its arguments on the stack, removes those arguments, and pushes the returned value when the service produces one.
Stack
The stack has a fixed capacity of 1024 values. Most expression instructions consume and produce stack values. For example, add pops two integers and pushes their sum, while neg updates the top stack value in place.
Before entering the top-level function or a called function, the VM checks whether that function's recorded maximum stack use would exceed the fixed stack capacity. If it would, Matchbox reports stack overflow and exits.
The stack stores Value entries, not raw integers. A value may currently hold a Boolean, float, integer, or pointer internally, even though int is the only source-language type exposed in 0.2. The opcode decides which field is meaningful. Arithmetic instructions pop values and read their integer field. ldc copies a value from the constant pool onto the stack. reg pops a value from the stack and appends it to the globals array. ldg copies a global value back onto the stack.
This means the same physical stack carries user values, temporary expression results, function arguments, call metadata, frame-local values, and return values. Bytecode keeps those roles separate by convention: an add instruction expects two integer operands at the top of the stack, while call expects its arguments to have already been pushed and then adds the frame metadata it needs.
Call Frames
Calls use the same value stack for arguments, return metadata, locals, and temporaries. Before jumping into a callee, the VM pushes the argument count, the return instruction pointer, and the previous frame pointer. It then sets the frame pointer to the current stack pointer and starts executing the callee's bytecode.
Parameters are addressed at negative frame offsets, while locals are addressed at non-negative frame offsets. On ret, the VM restores the caller frame, removes the caller's arguments, and pushes 0. On retv, it preserves the explicit return value instead.
Values
Runtime values are stored in a tagged-by-context union that can hold a Boolean, float, integer, or pointer internally. The current language surface exposes int as the only user-declarable type in 0.2, but the runtime also uses pointer values for function objects and has room for additional internal value forms.
Because values do not carry a runtime tag in the union itself, the compiler and VM rely on instruction context. Arithmetic opcodes read operands as integers, calls read function constants as pointers, and services interpret their arguments according to service metadata.
The main value flows are:
| Flow | What happens |
|---|---|
| Literal to stack | push creates an integer Value, or ldc copies an existing Value from the constant pool. |
| Stack to global | reg or stg stores a stack value in the VM globals array. |
| Global to stack | ldg copies a global value onto the stack. |
| Local to stack | ldl copies a frame-relative value onto the stack. |
| Stack to local | stl stores the top stack value at a frame-relative position. |
| Stack to service | reqs passes a pointer to the service arguments already sitting on the stack. |
| Stack to return value | retv preserves the top stack value while restoring the caller frame, then pushes that value for the caller. |
Size
Source-level int values use the C int type in the parser, compiler, and value representation. On the supported build, that means signed 32-bit integer behavior. Bytecode is stored as bytes, immediate operands are 8-bit or 16-bit, and larger integer literals are stored in the module constant pool.
The VM stack is fixed at 1024 Value entries. Code objects and value arrays grow dynamically as bytecode, constants, and globals are appended.
Signed vs. Unsigned Integers
The lexer recognizes unsigned integer type tokens such as uint, uint8, uint16, uint32, and uint64, but 0.2 accepts only int as a source-level type. Unsigned integer declarations are therefore reserved syntax rather than supported type annotations.
Bitwise operators operate on the current signed integer representation. Shifts use the C operators in the VM: lsl uses <<, and lsr currently uses >> on the integer value. Portable programs should avoid depending on right-shifting negative values.
Overflow
Stack overflow is checked explicitly before running a function. Arithmetic overflow, division by zero, invalid shift counts, left-shifting negative values, and exponent results outside the integer range are not guarded by the VM in 0.2 and are documented as outside the language's promised behavior.
The pow instruction computes with the C math library and stores the result back into an integer value. Programs should use non-negative exponents whose results fit in int.