Step 01 · The runtime layer
A compiler produces code, but that code runs on top of services provided by the runtime:
- Allocate heap objects
- Move/free objects (garbage collection)
- Construct boxed values (strings, arrays, closures)
- Format values for
print - Raise structured errors (exceptions)
- Provide builtin functions and FFI bridges
These services live in a small library that the codegen calls into. Three places they show up:
- Compiler emits calls.
ml_alloc_stringbecomes an external symbol; the linker (or JIT) resolves it to a runtime function. - Compiler emits inline code that uses the runtime's invariants — reading the tag bits of a Value, indexing into an Object header, etc.
- Compiler emits metadata for the runtime to consume — stack maps that tell the GC where pointers live in each frame, unwind tables for exceptions, debug info for backtraces.
This lab implements (1) and (2). (3) — emitting stack maps — is the hard, language-specific work that production runtimes invest heavily in; we approximate it with an explicit root API that the host program pushes Values onto.
Why a tagged Value?
MiniLang is dynamically typed. Every variable can hold a number,
string, bool, nil, function, or array. The simplest representation is
a struct — { tag, payload } — but that's at least 16 bytes per
slot. A tagged 64-bit value gets us:
- Pointer-sized (fits in registers)
- Fast bit-test type checks
- 63-bit fixnums with no boxing
- Compatible with calling conventions for free
The trade-off is restricted integer range and a few bits of mental overhead — a worthwhile bargain for a scripting language.