C VM Assembler

Cortex-VM

A fast, extensible 64-bit virtual machine and assembler built as a compiler backend target.

C VM Assembler ISA Design
GitHub → Blog Post →
64-bit word-addressed ISA
64 registers
~400M instructions/sec
201 passing tests

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. r1r3 are pc, sp, and ra. Fourteen callee-saved registers (s0s13), fourteen argument/return registers (a0a13), and thirty-two temporaries (t0t31). 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, remu and their immediate variants. mulh produces 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.