fig. 0.0 — overview
§0
architecture
crono-vm
register-based bytecode VM · computed goto
32
bit words
8
opcode bits
24B
header
v1
format
Register-based VM — not stack-based. Instructions are 32-bit fixed-width words. OpCode in the low 8 bits of the first word; operands packed as 8-bit fields in the same or subsequent words. IP is a uint32_t* into a flat std::vector<uint32_t>. Dispatch via computed goto (GCC label-as-value) on the low 8 bits.
fig. 1.0 — globals / locals / constants
§1
load · store
globals, locals & constants
register load/store opcodes
opcode
encoding
operation
SET_GLOBAL
[op:8, slot:8, type:2, src:8]
write value to global slot (old format)
GET_GLOBAL
[op:8, dst:8, globalSlot:16]
copy global → local reg
SET_GLOBAL_REG
[op:8, globalSlot:16, src:8]
copy local reg → global
GET_GLOBAL_WIDE
[op:8, local:8, global:16]
copy global → local (wide index)
SET_GLOBAL_WIDE
[op:8, local:8, global:16]
copy local → global (wide index)
LOAD_CONSTANT_WIDE
[op:8, local:8, constIdx:16]
load constant table entry → local
SET_LOCAL
[op:8, dst:8, type:2, slot:8]
write decoded operand → local reg (old format)
fig. 2.0 — dynamic arithmetic + unary
§2
arithmetic · logic
dynamic arith + unary
dynamically typed · ADD supports string concat
opcode
encoding
operation
ADD
[op:8, dst:8, left:8, right:8]
dst = left + right; string concat via addValues
SUB
[op:8, dst:8, left:8, right:8]
dst = left - right
MUL
[op:8, dst:8, left:8, right:8]
dst = left * right
DIV
[op:8, dst:8, left:8, right:8]
dst = left / right
MOD
[op:8, dst:8, left:8, right:8]
dst = left % right
NEG
[op:8, dst:8, src:8, _:8]
arithmetic negate
NOT
[op:8, dst:8, src:8, _:8]
logical NOT (isFalsey) — nil and false are falsey
BITWISE_NOT
[op:8, dst:8, src:8, _:8]
bitwise NOT
fig. 3.0 — dynamic comparison
§3
comparison · calls
dynamic comparison
dispatch via compareValues → BOOL
opcode
encoding
operation
SET_BOOL_EQ
[op:8, dst:8, left:8, right:8]
dst = (left == right)
SET_BOOL_NE
[op:8, dst:8, left:8, right:8]
dst = (left != right)
SET_BOOL_LT
[op:8, dst:8, left:8, right:8]
dst = (left < right)
SET_BOOL_GT
[op:8, dst:8, left:8, right:8]
dst = (left > right)
SET_BOOL_LE
[op:8, dst:8, left:8, right:8]
dst = (left <= right)
SET_BOOL_GE
[op:8, dst:8, left:8, right:8]
dst = (left >= right)
Dynamic dispatch via compareValues. All six produce BOOL values.
fig. 4.0 — dynamic dispatch
§4
objects · concurrency
dynamic dispatch
name-based field & method access
opcode
encoding
operation
INVOKE_METHOD_BY_NAME
[op:8,dst:8,obj:8,_:8] [nameConstIdx:16,argc:8,_:8] [argSlots…]
lookup method by name string; falls back to callable field
GET_FIELD_BY_NAME
[op:8,dst:8,obj:8,_:8] [nameConstIdx:16,_:16]
field lookup by name
SET_FIELD_BY_NAME
[op:8,src:8,obj:8,_:8] [nameConstIdx:16,_:16]
field store by name
⚠ All three require Metadata at runtime. Throw if metadata == nullptr. Production builds that strip metadata cannot use these opcodes.
fig. 5.0 — memory & GC
§5
integration
memory & GC
tri-color mark-and-sweep
allocator
Memory::allocate<T>() — bumps bytesAllocated, links into object list via Object::next
algorithm
tri-color mark-and-sweep (incremental via grayStack)
trigger
bytesAllocated > nextGC (initial 1 MB); also GC_COLLECT opcode or DEBUG_STRESS_GC
nextGC
bytesAllocated * 2 after each collection
roots
stack [0, liveTop), all constants, markRootsCallback (orbit stacks)
strings
interned in Memory::strings (unordered_multimap keyed by hash); copyString/takeString return canonical ptr
constants
only STRING objects accepted by Memory::addConstant
raw memory
allocateRaw/deallocateRaw for Array::elements and Buffer::data — not GC-tracked
NativeObject tracing: if NativeObject::trace != nullptr, called with (ptr, Memory*) during traceReferences. Call crono_mark_value inside tracer to keep values alive.
fig. 0.1 — instruction word format
§6
architecture
instruction word
32-bit fixed-width encoding
All instructions are 32-bit words. OpCode occupies the low 8 bits of the first word. Remaining 24 bits and any subsequent words carry operands packed as 8-bit fields. The IP is a uint32_t* advanced by one or more words per instruction.
word size
32 bits
opcode field
bits [0, 7] of word 0 (uint8_t)
IP type
uint32_t* into flat std::vector<uint32_t>
function header
[isVariadic:1 | arity:15 | maxRegs:16]
isVariadic
bit 31 of function header word
arity
bits [16, 30] of function header word
arg packing
4 arg slots per subsequent word (8 bits each)
fig. 1.1 — control flow
§7
control
control flow
jumps · yield · debug · no-ops
opcode
encoding
operation
JUMP
[op:8, target:24]
unconditional jump; ip = bytecode.data() + target
JUMP_IF
[op:8, target:24] [w2]
jump if second word's slot is truthy
RETURN
[op:8, _:24]
return reg[0] to caller; pops frame; signals orbit completion if in spawned orbit
BREAKPOINT
[op:8, _:24]
returns PAUSED in debug mode; no-op otherwise
NONE
[op:8, _:24]
no-op
GC_COLLECT
[op:8, _:24]
force GC collection
WIDE
[op:8, _:24]
no-op placeholder; prefix handling not implemented
fig. 2.1 — bitwise ops
§8
bitwise
bitwise ops
dynamically typed · same encoding as ADD/SUB
opcode
encoding
operation
AND
[op:8, dst:8, left:8, right:8]
bitwise AND
OR
[op:8, dst:8, left:8, right:8]
bitwise OR
XOR
[op:8, dst:8, left:8, right:8]
bitwise XOR
LSHIFT
[op:8, dst:8, left:8, right:8]
left shift
RSHIFT
[op:8, dst:8, left:8, right:8]
right shift
All bitwise ops are dynamically typed — no type enforcement at the opcode level. Same [op:8, dst:8, left:8, right:8] encoding as ADD/SUB/MUL/DIV.
fig. 3.1 — INT-typed comparison
§9
comparison (typed)
INT comparison
typed fast path · skips dynamic dispatch
opcode
encoding
operation
SET_BOOL_EQ_INT
[op:8, dst:8, left:8, right:8]
dst = (left.i32 == right.i32)
SET_BOOL_NE_INT
[op:8, dst:8, left:8, right:8]
dst = (left.i32 != right.i32)
SET_BOOL_LT_INT
[op:8, dst:8, left:8, right:8]
dst = (left.i32 < right.i32)
SET_BOOL_GT_INT
[op:8, dst:8, left:8, right:8]
dst = (left.i32 > right.i32)
SET_BOOL_LE_INT
[op:8, dst:8, left:8, right:8]
dst = (left.i32 <= right.i32)
SET_BOOL_GE_INT
[op:8, dst:8, left:8, right:8]
dst = (left.i32 >= right.i32)
Typed fast path for int32_t comparisons. Operands are read as .i32 directly — no dynamic dispatch through compareValues.
fig. 4.1 — arrays & buffers
§10
objects
arrays & buffers
heap-allocated sequence types + type checks
opcode
encoding
operation
NEW_ARRAY
[op:8, dst:8, sizeReg:8, _:8]
allocate array of sizeReg elements (all NIL); sizeReg must be INT; negative raises error
LOAD_ELEMENT
[op:8, dst:8, obj:8, index:8]
dst = obj[index]; ARRAY → Value, BUFFER → INT byte
STORE_ELEMENT
[op:8, obj:8, index:8, value:8]
obj[index] = value; BUFFER only accepts INT (truncated to uint8_t)
ARRAY_LENGTH
[op:8, dst:8, obj:8, _:8]
dst = length/count; works on ARRAY and BUFFER
IS_INSTANCE
[op:8, dst:8, src:8, _:8]
dst = (src.isObj && src.objType == INSTANCE)
INSTANCE_OF
[op:8, dst:8, src:8, classReg:8]
dst = (src instanceof classReg); walks baseClassId chain
fig. 5.1 — module system
§11
integration
module system
ModuleData · Metadata · extensions
ModuleData is the bytecode module container. Metadata is optional and stripped in production builds — its absence disables name-based dynamic dispatch.
classes
ClassesTable — vector<ClassData> (fieldCount, baseClassId, methodOffsets)
constants
ConstantsTable — variant of monostate/bool/int32/int64/float/double/string/ConstantAddress
exceptions
ExceptionTable (flat list of ExceptionEntry)
extensions
vector<ExtensionRequest>
meta.lines
vector<int> — word index → source line
meta.symbols
unordered_map<int, FunctionSymbols> keyed by functionStart
meta.globalNames
map<string, int> — name → absolute stack slot
resolution
.crono files by path; supports * and ** wildcards; base via ModuleLoader::setScriptDirectory
Extensions export crono_setup(CronoRegistry*) and optionally crono_init(CronoVM*). Loaded via crono_load_extension(vm, path) or declared in ModuleData::extensions at compile time.
fig. 0.2 — value types (scalar)
§12
type system
value types
ValueType enum · types_core.hpp
NIL
default-constructed Value
BOOL
bool
INT
int32_t
LONG
int64_t
FLOAT
float
DOUBLE
double
OBJ
vm::Object*
heap-allocated, GC-managed
ADDRESS
int
absolute word index; typeOf() returns "function"
VOID_TYPE
declared but unused; no constructor; not produced by typeOf()
Value is a tagged union (value.hpp). typeOf(Value) returns a human-readable string; returns typeName for NATIVE_OBJECT.
fig. 1.2 — error model
§13
errors
error model
THROW_ERROR · GET_ERROR · ExceptionTable
opcode
encoding
operation
THROW_ERROR
[op:8, src:8, _:16]
raise error from register; walks ExceptionTable; unwinds frames; throws CronoError::Runtime if unhandled
GET_ERROR
[op:8, dst:8, _:16]
copy current error value to dst; clear error flag
raiseError walks the flat ExceptionTable from ModuleData, comparing errorPC against [startPC, endPC) of each ExceptionEntry. On match: IP jumps to handlerPC. On no match: frames popped until one matches or stack exhausted.
ExceptionEntry has types CATCH and FINALLY — the runtime does NOT differentiate them. Both redirect IP to handlerPC identically.
fig. 2.2 — INT arithmetic
§14
arithmetic (typed)
INT arithmetic
typed int32_t operations
opcode
encoding
operation
ADD_INT
[op:8, dst:8, left:8, right:8]
dst = left.i32 + right.i32
SUB_INT
[op:8, dst:8, left:8, right:8]
dst = left.i32 - right.i32
MUL_INT
[op:8, dst:8, left:8, right:8]
dst = left.i32 * right.i32
DIV_INT
[op:8, dst:8, left:8, right:8]
dst = left.i32 / right.i32 — raises error on divisor == 0
MOD_INT
[op:8, dst:8, left:8, right:8]
dst = left.i32 % right.i32 — error on mod 0; result sign follows divisor
INC_INT
[op:8, dst:8, _:16]
dst.i32++ (in-place)
DEC_INT
[op:8, dst:8, _:16]
dst.i32-- (in-place)
fig. 3.2 — call + variadic
§15
calls
call + variadic
CALL · GET_VAR_COUNT · GET_VAR_ARG
opcode
encoding
operation
CALL
[op:8, callee:8, result:8, argc:8] [argSlots…]
call callee; result → result reg; supports ADDRESS, NATIVE, DYNAMIC_FUNCTION
GET_VAR_COUNT
[op:8, dst:8, _:16]
dst = actual_argc - fixed_arity (number of extra variadic args)
GET_VAR_ARG
[op:8, dst:8, indexReg:8, _:8]
dst = vararg[index]
isVariadic
bit 31 of function header word
arity
bits [16, 30] of function header word
vararg layout
reg[arity .. arity+varCount-1]
fig. 4.2 — orbit scheduler
§16
concurrency
orbit scheduler
single-threaded cooperative multitasking
opcode
encoding
operation
ORBIT_SPAWN
[op:8, callee:8, dst:8, argc:8] [argSlots…]
spawn cooperative coroutine; dst = SyncHandle*; native callee runs sync and returns pre-signaled handle; bytecode callee enqueued
ORBIT_SPAWN_METHOD
[op:8, dst:8, obj:8, methodSlot:8] [argc:8, _:24] [argSlots…]
spawn method as orbit; receiver pushed as arg[0]
ORBIT_YIELD
[op:8, _:24]
cooperative yield to next ready orbit; no-op if no other orbits
scheduling
round-robin; readyOrbits queue
suspension
native sets memory->pendingSuspend
resumption
memory->enqueueWake (thread-safe)
deadlock
detected when all orbits suspended and no LAUNCH_NATIVE threads in flight
fig. 5.2 — C API
§17
integration
C API
crono_api.h · extern "C" · CRONO_API
// Lifecycle CronoVM* crono_create(); void crono_destroy(CronoVM* vm); // Execution int crono_run(CronoVM* vm, const char* source); int crono_call(CronoVM* vm, const char* func_name, int argc, const CronoValue* argv, CronoValue* result); int crono_get_global(CronoVM* vm, const char* name, CronoValue* result); int crono_get_field(CronoVM* vm, CronoValue instance, const char* name, CronoValue* result); int crono_set_field(CronoVM* vm, CronoValue instance, const char* name, CronoValue value); // Registration int crono_register_function(CronoVM* vm, const char* name, const char** param_types, int param_count, CronoNativeCallback callback); // Extensions int crono_load_extension(CronoVM* vm, const char* path); int crono_create_instance(CronoVM* vm, const char* class_name, CronoValue* result); // Error / Debug const char* crono_get_last_error(CronoVM* vm); CronoStepResult crono_step(CronoVM* vm); int crono_get_current_line(CronoVM* vm); int crono_get_frame_count(CronoVM* vm); void crono_get_frame_info(CronoVM* vm, int index, CronoFrameInfo* info); int crono_get_stack_value(CronoVM* vm, int index, CronoValue* result); int crono_get_buffer_info(CronoVM* vm, CronoValue buf, void** ptr, int* size); void crono_mark_value(void* gc_ctx, CronoValue val);
CronoStepResult
OK=0, DONE=1, ERROR=2, PAUSED=3
CronoValueType
NIL, BOOL, INT, FLOAT, STRING, POINTER, BUFFER, NATIVE_OBJECT, ERROR
LONG / DOUBLE
not exposed — LONG drops to NIL, DOUBLE maps to FLOAT
fig. 0.3 — object types (heap)
§18
type system
object types
ObjType enum · heap-allocated · types_core.hpp
STRING
vm::String
interned; char* chars, int length, uint32_t hash
NATIVE
vm::Native
wraps NativeFn (std::function<Value(Memory*, int, Value*)>)
CLASS
vm::Class
classId, baseClassId (-1=no base), fieldCount, methodOffsets
INSTANCE
vm::Instance
vm::Class* cls, std::vector<Value> fields
ARRAY
vm::Array
int length, Value* elements (raw heap)
MODULE
vm::Module
vm::String* name, void* handle (DLL handle)
DYNAMIC_FUNCTION
vm::DynamicFunction
vm::String* name, void* func_ptr (FFI symbol)
BUFFER
vm::Buffer
int count, uint8_t* data (raw bytes)
NATIVE_OBJECT
vm::NativeObject
void* ptr, const char* typeName, destructor, tracer
SYNC_HANDLE
vm::SyncHandle
Value payload, bool ready; used by orbit scheduler
fig. 1.3 — execution model
§19
runtime
execution model
CallFrame · stack layout · falsey values
struct CallFrame { uint32_t* ip; // instruction pointer int slots; // absolute stack index of reg[0] int functionStart; // bytecode word index of function header int argCount; // total args passed int resultLocal; // caller's slot for return value (-1 = discard) };
_main frame
GLOBAL_FRAME_START = -1
RETURN
reads reg[0]; trims stack to callerStackBase; writes to caller.reg[resultLocal] if != -1
globals
occupy stack [0, globalsEnd); indexed by Metadata::globalNames
falsey
only nil and false (BOOL); 0 (INT) is truthy
globalNames
map<string, int> — name → absolute stack slot (Metadata only)
fig. 2.3 — branch superinstructions
§20
superinstructions
branch superinstr.
3-word INT branch — trueTarget or falseTarget
opcode
condition
shape
BRANCH_LT_INT
left.i32 < right.i32
3 words
BRANCH_LE_INT
left.i32 <= right.i32
3 words
BRANCH_GT_INT
left.i32 > right.i32
3 words
BRANCH_GE_INT
left.i32 >= right.i32
3 words
BRANCH_EQ_INT
left.i32 == right.i32
3 words
BRANCH_NE_INT
left.i32 != right.i32
3 words
All six use the same 3-word encoding: [op:8, left:8, right:8, _:8] [_:8, trueTarget:16, …] [_:8, falseTarget:16, …]. Operands are INT only. Jumps to trueTarget or falseTarget (absolute bytecode word indices).
fig. 3.3 — classes by index
§21
objects
classes by index
index-based field & method access
opcode
encoding
operation
INSTANTIATE_CLASS
[op:8, dst:8, classReg:8, _:8]
allocate new instance; fields initialised to NIL
BUILTIN_CONSTRUCT
[op:8, dst:8, typeId:8, _:8]
invoke builtin factory by typeId (indexes builtinFactories from getStandardNativeTypes())
GET_FIELD_REG
[op:8, dst:8, obj:8, fieldSlot:8]
dst = instance.fields[fieldSlot] (integer index)
SET_FIELD_REG
[op:8, src:8, obj:8, fieldSlot:8]
instance.fields[fieldSlot] = src
INVOKE_METHOD
[op:8,dst:8,obj:8,methodSlot:8] [argc:8,_:24] [argSlots…]
call instance.cls.methodOffsets[methodSlot]; receiver pushed as arg[0]
fig. 4.3 — OS threads & FFI
§22
concurrency · FFI
OS threads & FFI
LAUNCH_NATIVE · dyncall · DynamicFunction
opcode
encoding
operation
LAUNCH_NATIVE
[op:8, callee:8, dst:8, argc:8] [argSlots…]
run NATIVE object on OS thread; dst = SyncHandle*; result enqueued via memory->enqueueWake; threads joined on VM::~VM
FFI::call
FFI::call(void* func, vector<Value> args, FFIResultType) wraps dyncall
arg types
INT, LONG, DOUBLE, FLOAT, BOOL, OBJ (as pointer)
FFIResultType
INTEGER (→ int32_t), DOUBLE, POINTER (→ vm::Object*), VOID_TYPE
return heuristic
any float/double arg → DOUBLE, else INTEGER (unreliable)
DynamicFunction
holds raw void* symbol; FFI::getSymbol wraps dlFindSymbol
⚠ LAUNCH_NATIVE is only callable on NATIVE objects. memory may be nullptr when called from background thread context.
fig. 5.3 — known limitations
§23
limitations
known limitations
8 documented issues · source-verified
1 — C API types
LONG / DOUBLE not in C API — internalToC drops LONG silently to CRONO_VAL_NIL; DOUBLE maps to FLOAT. 64-bit ints/doubles lose data across the C boundary.
2 — VOID_TYPE dead
ValueType::VOID_TYPE is declared but has no constructor, accessor, or typeOf branch. Not produced anywhere at runtime.
3 — WIDE no-op
lbl_WIDE does nothing. No prefix handling in deserializer or VM loop. Listed in opcodes.def but non-functional.
4 — dynamic needs meta
INVOKE_METHOD_BY_NAME, GET_FIELD_BY_NAME, SET_FIELD_BY_NAME throw if metadata == nullptr. Cannot use in production builds that strip metadata.
5 — FFI heuristic
Return type inferred from args: any float/double → DOUBLE, else INTEGER. Unreliable for float-returning functions with no float args.
6 — CATCH ≡ FINALLY
ExceptionEntry CATCH vs FINALLY not distinguished at runtime — both redirect to handlerPC identically.
7 — NativeObject trace
NativeObject fields not auto-traced. Provide NativeObject::trace or GC may collect values stored inside native objects.
8 — constant strings only
Memory::addConstant rejects non-string objects at runtime. Only primitives and strings may appear in the constant pool.