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:

  1. Compiler emits calls. ml_alloc_string becomes an external symbol; the linker (or JIT) resolves it to a runtime function.
  2. Compiler emits inline code that uses the runtime's invariants — reading the tag bits of a Value, indexing into an Object header, etc.
  3. 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.