1.3 Interpreters, Compilers, AOT, JIT, and VMs
Language implementation is not a binary choice between "compiled" and "interpreted." Real systems mix strategies. A Python implementation parses source into bytecode and interprets that bytecode. A Java implementation compiles source into class files, then a virtual machine interprets and JIT-compiles hot methods. A JavaScript engine may parse lazily, interpret first, collect feedback, compile hot functions, deoptimize when assumptions fail, and recompile later. A C compiler usually performs ahead-of-time native compilation, but the final program still depends on runtime libraries and the operating system loader.
The key question is not "is this language compiled?" The better question is: when is work performed, what representation is executed, what runtime services are required, and what information is available to optimize?
Interpreters
An interpreter executes a program representation directly. That representation might be source text, an AST, bytecode, or a custom IR. A tree-walking interpreter executes AST nodes recursively. A bytecode interpreter executes compact instructions in a loop. A threaded interpreter reduces dispatch overhead by using implementation tricks. The interpreter is often easier to build and easier to instrument than a full native-code backend.
Interpreters are valuable for REPLs, scripting, education, debugging, sandboxing, and highly dynamic languages. They can preserve runtime flexibility because they do not need to commit early to a static code layout. They can also produce excellent introspection: stack traces, dynamic hooks, runtime patching, and interactive evaluation.
The cost is execution overhead. If every addition, variable access, and function call flows through a dispatch loop, hot code may be much slower than optimized machine code. Interpreters can still be engineered carefully, but dispatch, dynamic checks, and boxed values often dominate runtime cost.
Ahead-of-Time Compilation
Ahead-of-time compilation performs most translation before the program runs. The output may be native machine code, bytecode, WebAssembly, or another deployable artifact. AOT is attractive when startup latency, predictable deployment, static analysis, and runtime simplicity matter. Command-line tools, kernels, embedded software, and many server programs benefit from AOT compilation.
AOT compilation has less runtime information. It may not know which branches are hot for a particular deployment, which object shapes dominate, which virtual calls are monomorphic in production, or which configuration values are common. Profile-guided optimization can feed real execution data back into AOT builds, but that adds a feedback loop to the build system.
Virtual Machines
A virtual machine defines an execution model that is not exactly the physical CPU. It may be stack-based, register-based, object-oriented, sandboxed, garbage-collected, or capability-based. The VM can provide bytecode verification, memory management, exception handling, dynamic loading, reflection, security boundaries, and profiling.
VMs decouple the language frontend from physical machines. A compiler can emit portable bytecode, then multiple VM implementations can run that bytecode on different platforms. This is the core idea behind systems such as the JVM, .NET CLR, many language-specific bytecode VMs, and WebAssembly runtimes. The price is a runtime layer that must be designed, optimized, debugged, and secured.
JIT Compilation and Deoptimization
Just-in-time compilation moves some compilation work to runtime. A JIT can observe actual execution: which functions are called often, which branches are usually taken, which object shapes appear, which types flow through dynamic operations. It can then optimize for reality rather than for a conservative guess.
JIT optimization is speculative. Suppose a JavaScript function has only seen numbers. The JIT may compile a fast numeric path. Later, the function receives a string. The optimized code's assumptions are now invalid. A robust JIT must deoptimize: it reconstructs a safe interpreter or baseline-compiled state and continues execution without violating language semantics. Deoptimization is one reason JITs are fascinating. They compile not only code, but also an escape route from compiled assumptions.
Choosing an Execution Strategy
No strategy dominates all others. An interpreter may be best for a configuration language embedded in an application. AOT may be best for a small command-line tool. A VM may be best for a portable teaching language. A JIT may be best for a long-running dynamic workload with stable hot paths. Hybrid designs are common: parse to AST, lower to bytecode, interpret cold code, baseline-compile warm code, optimize hot code, and deopt when assumptions fail.
The execution strategy affects the compiler architecture from the beginning. If you target a bytecode VM, your code generator emits VM instructions and your runtime must define stack behavior, call frames, constants, and heap objects. If you target native code, you must eventually face ABI, registers, instruction selection, relocation, and object files. If you plan to JIT, your IR must preserve enough metadata to reconstruct source-level state during deoptimization.
Cost Models, Not Slogans
An interpreter pays dispatch cost repeatedly but can start almost immediately and produce excellent source-level diagnostics. AOT compilation spends work before execution, usually improving steady-state speed and deployment predictability. A JIT starts with interpretation or baseline code, observes actual receiver types and branch frequencies, then specializes hot paths. A VM adds a portable execution contract: bytecode, object layout, garbage collection, and runtime services may be standardized even when hardware differs.
Consider a virtual method call in a loop. An AOT compiler may use conservative indirect dispatch. A JIT can observe that the receiver is Circle 99.9% of the time, install a guarded direct call, inline it, then deoptimize if a new receiver type appears. Deoptimization is not failure; it is the mechanism that keeps speculative optimization correct. The runtime must retain metadata to reconstruct interpreter or baseline frames when its assumption breaks.
Choosing Deliberately
- Optimize for the dominant constraint: command-line tool startup, server throughput, mobile download size, sandboxing, portability, or debugging are different products.
- Separate bytecode from a VM. Bytecode is a representation; a VM is an execution system that may interpret, JIT, or both.
- Benchmark warm and cold behavior separately. A JIT can be slower for a short script and dramatically faster for a long-lived service.