Cortex-VM
A fast, extensible 64-bit virtual machine and assembler built as a compiler backend target.
- word-addressed ISA
- 64-bit
- registers
- 64
- instructions/sec
- ~400M
- passing tests
- 201
Overview
Cortex-VM is a 64-bit virtual machine and assembler I built from scratch in C as a dedicated backend target for Marrow-Lang. Rather than targeting LLVM or x86 directly, I designed the entire ISA myself — a decision that kept the full system understandable, made the calling convention compiler-friendly, and gave me complete control over every design tradeoff.
The VM reaches around 400 million instructions per second at -O3 -march=native on modern hardware — roughly comparable to a Lua interpreter, and more than fast enough for a language runtime.
Design Principles
- Word addressing only. All registers and memory words are 64 bits. Offsets and addresses are word indices, not byte offsets. This eliminates alignment edge cases and makes code generation straightforward.
- No implicit side effects. No condition codes, no flags register, no hidden state outside the register file. Dataflow analysis is trivial.
- Flat calling convention. Up to 13 arguments and 13 return values pass in registers. No implicit stack frames, no hidden pointer arguments — easy to target from a compiler.
- Embeddable by design. Builds as a static library (
libcortex-vm.a) with a three-function API from day one.
The Register File
64 registers, each 64 bits wide. r0 (zero) is hardwired. r1–r3 are pc, sp, and ra. Fourteen callee-saved registers (s0–s13), fourteen argument/return registers (a0–a13), and thirty-two temporaries (t0–t31). Having 32 temporaries means register pressure is rarely the bottleneck for most function bodies.
Instruction Set
All instructions are 64 bits wide. Seven base formats: R-type (register ALU), I-type (immediate ALU / jump), S-type (store), L-type (load), B-type (branch), and SYS (halt, syscall, nop, break). Calls and returns use a single jmp instruction that writes the return address into a register — no implicit link register behavior.
jmp ra, zero, target ; call — save return address in ra, jump to target
jmp zero, ra, 0 ; return — jump to address in ra
Extensions
Two optional extensions, auto-detected by the assembler and flagged in the binary header:
- M extension — integer multiply/divide:
mul,mulh,div,divu,rem,remuand their immediate variants.mulhproduces the upper 64 bits of a 128-bit product via__int128_t. - F extension — 64-bit IEEE 754 floats. Reuses the same register file (no separate FP registers). Adds arithmetic, conversion, and branch instructions. Float immediates in instructions are 32-bit single-precision, widened at execution.
Assembler & Disassembler
The assembler is single-pass with two-pass label resolution for forward references. It supports decimal, hex (0xFF), binary (0b1010), octal (0o17), and character literals. The .data section accepts strings, integers, and 64-bit float literals.
The disassembler is a full round-trip tool: assemble a program, disassemble the binary, re-assemble, and get a functionally equivalent binary. 42 pytest tests verify this fidelity.
Library API
// Assemble source to binary, optionally write to file
uint64_t *cortexAssemble(const char *source, const char *outputPath);
// Assemble and execute immediately
int cortexExecSource(const char *source);
// Execute a pre-assembled binary
int cortexExecBinary(const uint64_t *binary, size_t wordCount);
The compile-once-run-many workflow via cortexAssemble was designed specifically for the Marrow-Lang compiler pipeline.