CandleVM Memory
CandleVM keeps program state in registers, named globals, and heap-allocated objects. Registers and globals contain Values; values that refer to objects hold an ObjectRef.
Registers
Instructions read and write registers using one-byte indices. A register contains one Value; an unset register reads as Null. See the opcode reference for each instruction's register operands.
When a function is called, its register indices are relative to that function's register frame. Parameters start at register 1, followed by the captured values in a Closure call, as described in closures. Call and Ret create and leave these frames.
Two indices have special behavior inside a function:
- Register 0 reads as a ModuleRef to the current function's module. Writes to it are ignored. At the top level, register 0 is an ordinary register.
- Register 255 always reads as Null, and writes to it are ignored.
Registers hold object references used by the garbage collector to find live heap objects.
Globals
Globals are named values shared across function calls. LoadGlobal reads a global by name; an unknown name reads as Null. StoreGlobal creates or updates a writable global. Storing Null removes a writable global.
Built-in globals are read-only. Attempting to overwrite one raises ReadOnlyGlobal. Globals also keep any referenced heap objects alive during garbage collection.
Built-in read-only globals
The VM initializes these globals before executing bytecode:
| Global | Value | Meaning |
|---|---|---|
API_VERSION | Number, currently 1 | Version of the Candle ecosystem API. It matches the Candle version in the executable header. |
VM_VERSION | String | Version of the running CandleVM. |
DEBUG | Bool | Whether the VM is running with debug output enabled. |
The TYPE_* globals expose the Value type discriminators as Byte values. These numbers match the values returned by the Type opcode.
| Global | Value type | Discriminator |
|---|---|---|
TYPE_NULL | Null | 0 |
TYPE_BYTE | Byte | 1 |
TYPE_NUMBER | Number | 2 |
TYPE_BOOL | Bool | 3 |
TYPE_CHAR | Char | 4 |
TYPE_MODULE_REF | ModuleRef | 5 |
TYPE_FUNCTION_REF | FunctionRef | 6 |
TYPE_OBJECT_REF | ObjectRef | 7 |
The OBJECT_TYPE_* globals expose the Object type discriminators as Byte values. These numbers match the values returned by ObjectType.
| Global | Object type | Discriminator |
|---|---|---|
OBJECT_TYPE_ARRAY | Array | 0 |
OBJECT_TYPE_MAP | Map | 1 |
OBJECT_TYPE_STRING | String | 2 |
OBJECT_TYPE_ERROR | Error | 3 |
OBJECT_TYPE_BUFFER | Buffer | 4 |
OBJECT_TYPE_CLOSURE | Closure | 5 |
OBJECT_TYPE_NATIVE | Native | 6 |
Heap
The heap stores Objects, including Arrays, Maps, Strings, Errors, Buffers, Closures, and Native objects. A Value holds an object ID rather than the object itself.
Objects are created by opcodes such as NewArray, NewMap, NewString, NewBuffer, NewError, and NewClosure. Other operations that produce objects are documented in the opcode categories.
An object ID that no longer identifies a heap object raises InvalidHeapRef. IDs of collected objects can be reused, so a reference must remain reachable for as long as the object is needed. ObjectRef values cannot be loaded as immediate constants; this restriction is documented as ValueNotSerializable.
Garbage collector
CandleVM uses a tracing mark-and-sweep collector. Every heap allocation increments an allocation counter. When the counter reaches 1,024, the VM checks the threshold after these opcodes allocate an object and place its reference in the destination register:
The collection starts from every register and global value. It follows ObjectRef values inside arrays, map keys and values, and closure captures. Any other heap object is removed. The allocation counter resets after collection, and IDs from removed objects may be reused.
The threshold check is attached to the opcodes listed above, not to every operation that allocates an object. Allocations made by other opcodes still count toward the threshold; if they reach 1,024, collection waits until the next listed opcode performs its threshold check.