# crono-vm crono-vm is a register-based bytecode virtual machine written in C++. It is the execution target for [crono-sharp](https://crono.rac.so/sharp.md), but can also be embedded directly via its C API. Source lives at [github.com/Racso/crono-lang](https://github.com/Racso/crono-lang). --- ## Design overview Instructions are 32-bit fixed-width words. The opcode occupies the low 8 bits of the first word; operands are packed as 8-bit fields within the same word or in subsequent 32-bit words. The instruction pointer is a `uint32_t*` into a flat bytecode buffer. Dispatch uses GCC computed goto (label-as-value) on the low 8 bits of the current word — no switch, no bounds check. Each call frame has a fixed register count (`maxRegs`, encoded in the function header word). Registers are slots in a flat `std::vector` stack. Global variables occupy the bottom of that same stack. --- ## Bytecode format A `.crono` bytecode file starts with a 24-byte header: | Field | Type | Value | |------------------|------------|-------------------| | magic | 8 bytes | `CRONO_VM` | | version | uint32\_t | 1 | | entryPoint | uint32\_t | word index of `_main` | | metadataOffset | uint64\_t | offset to debug metadata (0 if stripped) | The instruction stream begins immediately after the header. Constants (strings, numbers) are stored in a separate constant pool at the end of the module, alongside the exception table, class table, and extension requests. --- ## Value types crono-vm is dynamically typed at the VM level (static typing is enforced by the crono-sharp compiler). Every value is a tagged union: | Type | Storage | Notes | |------------|----------------|-------| | NIL | — | Default value | | BOOL | bool | Only `nil` and `false` are falsey; `0` is truthy | | INT | int32\_t | | | LONG | int64\_t | | | FLOAT | float | | | DOUBLE | double | | | OBJ | vm::Object\* | Heap-allocated, GC-tracked | | ADDRESS | int | Absolute word index into bytecode; represents a function | Heap objects (`OBJ`) carry an `ObjType` tag: | ObjType | Description | |------------------|-------------| | STRING | Interned. Identical literals share one object. | | NATIVE | Wraps a C++ `std::function` callable from bytecode. | | CLASS | Class descriptor — field count, base class id, method offsets. | | INSTANCE | Class instance — reference to class + field values. | | ARRAY | Fixed-length array of Values. | | BUFFER | Raw byte buffer (uint8\_t\[\]). | | MODULE | Loaded native extension (DLL handle). | | DYNAMIC\_FUNCTION | FFI symbol (void\* to a C function). | | NATIVE\_OBJECT | Opaque C++ object with optional GC tracer. | | SYNC\_HANDLE | Orbit synchronisation primitive (payload + ready flag). | --- ## Instruction set All instructions are 32-bit. Operands are 8-bit register indices packed into the same word (or follow as additional words for wide operands). A function's entry point is a header word encoding `[isVariadic:1 | arity:15 | maxRegs:16]`; `CALL` and `ORBIT_SPAWN` target this word. ### Core operations | Opcode | Operation | |--------|-----------| | NONE / WIDE | No-op | | BREAKPOINT | Pause execution (debug mode); no-op otherwise | | RETURN | Return `reg[0]` to caller; pop frame | | SET\_LOCAL / GET\_GLOBAL / SET\_GLOBAL (and wide variants) | Register ↔ global slot transfers | | LOAD\_CONSTANT\_WIDE | Load constant pool entry into register | ### Arithmetic and logic Standard `ADD`, `SUB`, `MUL`, `DIV`, `MOD`, `AND`, `OR`, `XOR`, `LSHIFT`, `RSHIFT`, `NEG`, `NOT`, `BITWISE_NOT`. All are dynamic (dispatch on runtime type). Typed integer variants (`ADD_INT`, `SUB_INT`, etc.) skip dynamic dispatch and operate directly on `int32_t` — generated by the compiler when types are statically known. `INC_INT` / `DEC_INT` are in-place. `ADD` on two strings is string concatenation. ### Comparisons and branches `SET_BOOL_EQ / NE / LT / GT / LE / GE` write a BOOL result to a destination register. `_INT` variants operate on raw `int32_t`. `BRANCH_LT_INT` and siblings are 3-word superinstructions that jump directly to a true or false target, avoiding a separate `JUMP_IF`. `JUMP` is an unconditional jump to an absolute word index. `JUMP_IF` takes a slot in a second word and jumps if that value is truthy. ### Calls and concurrency | Opcode | Description | |--------|-------------| | CALL | Call a function (bytecode, native, or dynamic FFI). Arg slots follow in packed words. | | GET\_VAR\_COUNT / GET\_VAR\_ARG | Access variadic arguments. | | ORBIT\_SPAWN | Spawn a cooperative coroutine. Returns a SyncHandle. | | ORBIT\_SPAWN\_METHOD | Same, but for an instance method. | | ORBIT\_YIELD | Cooperative yield to the next ready orbit. | | LAUNCH\_NATIVE | Run a native function on a real OS thread. Returns a SyncHandle. | ### Objects and arrays | Opcode | Description | |--------|-------------| | INSTANTIATE\_CLASS | Allocate a new class instance; fields initialised to NIL. | | BUILTIN\_CONSTRUCT | Invoke a built-in factory by type index. | | GET\_FIELD\_REG / SET\_FIELD\_REG | Field access by integer slot index (fast path). | | INVOKE\_METHOD | Call a method by slot index. | | INVOKE\_METHOD\_BY\_NAME | Call a method by name string (requires Metadata). | | GET\_FIELD\_BY\_NAME / SET\_FIELD\_BY\_NAME | Field access by name (requires Metadata). | | NEW\_ARRAY | Allocate an array of a given size (all NIL). | | LOAD\_ELEMENT / STORE\_ELEMENT | Array and buffer element access. | | ARRAY\_LENGTH | Length of array or buffer. | | IS\_INSTANCE / INSTANCE\_OF | Runtime type checks. | ### Errors | Opcode | Description | |--------|-------------| | THROW\_ERROR | Raise an error from a register. Walks the exception table; unwinds frames if unhandled. | | GET\_ERROR | Copy the current error value to a register and clear the error flag. | | GC\_COLLECT | Force a garbage collection cycle. | --- ## Execution model ### Call frames Each frame tracks: instruction pointer, base register slot, function start offset, argument count, and the caller's result register. `RETURN` reads `reg[0]`, trims the stack back to the caller's base, and writes the return value to the caller's result slot. ### Error handling `THROW_ERROR` walks a flat `ExceptionTable` in the module, matching the current program counter against `[startPC, endPC)` ranges. On a match, the IP jumps to `handlerPC`. If no handler is found, frames are popped until one is found or the stack empties — at which point `CronoError::Runtime` is thrown to the host. `GET_ERROR` retrieves the caught value. ### Cooperative concurrency (orbits) The VM is single-threaded with cooperative multitasking. `ORBIT_SPAWN` enqueues a new coroutine; scheduling is round-robin. A coroutine suspends when `ORBIT_YIELD` is called or when it awaits a `SyncHandle`. `LAUNCH_NATIVE` runs a function on a real OS thread; when it completes, it enqueues a wake event that resumes the waiting orbit. --- ## Memory and garbage collector crono-vm uses a tri-color mark-and-sweep GC. - **Allocation**: `Memory::allocate()` — bumps `bytesAllocated`, links object into a singly-linked list. - **Trigger**: automatic when `bytesAllocated > nextGC` (initial threshold: 1 MB). After each collection, `nextGC = bytesAllocated * 2`. Can also be triggered explicitly via `GC_COLLECT`. - **Roots**: all live stack values, all constants, plus any values registered via `markRootsCallback` (used by the orbit scheduler). - **String interning**: all strings are interned — identical literals share a single `vm::String` object. - **NativeObject tracing**: extensions that store GC-managed values inside a `NativeObject` must provide a `trace` callback; the VM calls it during mark phase. --- ## Native extensions Extensions are shared libraries (`.so` / `.dll`) that export: ```c void crono_setup(CronoRegistry* registry); // required void crono_init(CronoVM* vm); // optional ``` `CronoRegistry` provides callbacks to register functions and types. Extensions are loaded at runtime via `crono_load_extension(vm, path)` or declared at compile time in the module's extension list. Native functions have the signature: ```cpp Value fn(Memory* memory, int argCount, Value* args); ``` Args are pointers into the VM stack — valid only for the duration of the call. `memory` may be `nullptr` when called from a `LAUNCH_NATIVE` background thread. ### Dynamic FFI `DynamicFunction` objects hold a raw `void*` symbol resolved by `FFI::getSymbol` (via `dlFindSymbol`). `FFI::call` wraps dyncall to invoke arbitrary C functions at runtime. Supported argument types: INT, LONG, DOUBLE, FLOAT, BOOL, OBJ (as pointer). --- ## C API crono-vm exposes a stable C API (`crono_api.h`, all symbols `extern "C"`): ```c // Lifecycle CronoVM* crono_create(); void crono_destroy(CronoVM* vm); // Run a .crono source string (compile + execute) int crono_run(CronoVM* vm, const char* source); // Call a named function, read a global, access instance fields int crono_call(CronoVM* vm, const char* func, 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); // Register a native function int crono_register_function(CronoVM* vm, const char* name, const char** param_types, int param_count, CronoNativeCallback callback); // Extensions and instances int crono_load_extension(CronoVM* vm, const char* path); int crono_create_instance(CronoVM* vm, const char* class_name, CronoValue* result); // Error const char* crono_get_last_error(CronoVM* vm); // Debugger (step, frame inspection) 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); // Buffer access int crono_get_buffer_info(CronoVM* vm, CronoValue buffer, void** ptr, int* size); ``` `CronoValue` is a tagged C struct holding type + union of `bool`, `int`, `double`, `const char*`, `void*`, or a native object. `CronoStepResult` is `OK=0`, `DONE=1`, `ERROR=2`, `PAUSED=3`. --- ## Known limitations - **LONG and DOUBLE not in C API**: `ValueType::LONG` and `ValueType::DOUBLE` have no `CronoValueType` counterpart. Values of these types are silently dropped to `CRONO_VAL_NIL` when crossing the C boundary. - **WIDE opcode is a no-op**: declared and dispatched, but the handler does nothing. No prefix semantics implemented. - **CATCH vs FINALLY not distinguished**: `ExceptionEntry::Type` exists but both values follow the same code path in the runtime. - **Dynamic dispatch requires Metadata**: `INVOKE_METHOD_BY_NAME`, `GET_FIELD_BY_NAME`, `SET_FIELD_BY_NAME` all throw at runtime if metadata was stripped. Production builds without metadata cannot use dynamic-typed variables. - **FFI return type heuristic**: the return type for `DynamicFunction` calls is inferred from whether any argument is float/double — unreliable for functions with no float args that return a double. - **Orbit deadlock detection is basic**: detected only when all orbits are suspended and no live `LAUNCH_NATIVE` threads remain. --- ## See also - [crono-sharp](https://crono.rac.so/sharp.md) — the language that compiles to crono-vm - [Source repository](https://github.com/Racso/crono-lang) - [Interactive reference](https://crono.rac.so/vm)