01 — JIT vs AOT

In cp-16 we built minilangc, an ahead-of-time compiler: it produced a .ll text file, then shelled out to llc and clang to assemble and link an executable. The compiler and the program are different processes, separated by time and by the filesystem.

In cp-17 we build mldyn, a just-in-time runner. The compiler and the program live in the same process. The flow is:

source ──► tokens ──► AST ──► llvm::Module ──► machine code ──► call()
                              (in memory)      (in memory)

There is no .ll, no .o, no clang invocation. We link against LLVM's libraries (libLLVMCore, libLLVMOrcJIT, …) and the IRBuilder hands us a Module object that ORC compiles in-process to executable memory pages.

Why JIT for a dynamic language?

Dynamic languages — Python, Ruby, JavaScript, Lua — discover types and shapes at runtime. An AOT compiler must therefore choose: either generate slow generic code, or refuse to compile until annotations are added. A JIT can defer codegen, observe what actually happens (type feedback, hot paths), then emit specialised code. That's how V8, HotSpot, LuaJIT, PyPy and Truffle all earn their speed.

cp-17 doesn't implement specialisation — that would take a tracing/IC infrastructure — but it lays the groundwork:

  • IR built programmatically, so we could vary it per call site.
  • A runtime symbol table that JIT'd code can call back into.
  • A ml_record_int_arg hook that demonstrates type feedback: every function entry tells the host "this argument was an int". A real system would consult this table on the next compile to decide whether to generate a fast int-only version.

What we keep from cp-16

The frontend. Lexer, parser, diagnostics, spans — all of it carries over. The language grew a string literal and a print_str statement, but the parser infrastructure (recursive descent, sync, span-bearing diagnostics) is the same code we built in cp-15 and reused in cp-16. Frontends are stable; backends are where the interesting variation lives.