Course Overview

What this Interpreters, Bytecode & Runtimes course covers, how to use it, prerequisites.

Course Overview

This course follows a program from source text to execution and opens every box on the way: lexers, parsers, abstract syntax trees, tree-walk interpreters, bytecode, stack and register virtual machines, call frames, closures, exceptions, garbage collection, runtime loading, JIT compilation — and then four production runtimes (the JVM, .NET, CPython and JavaScript engines), WebAssembly, debugging, security and advanced VM optimisation. You build a complete small language, MiniLang, twice: once as a tree-walk interpreter and once as a bytecode compiler plus stack virtual machine.

By the end of the course you will be able to:

Explain how a program moves from source text to execution, and distinguish compilers, interpreters, virtual machines, runtimes, bytecode and native machine code.

Build a lexer, a parser, an abstract syntax tree and a tree-walk interpreter.

Define a bytecode instruction set and implement a stack-based virtual machine with call frames, locals, globals, constants and closures.

Explain name resolution, scope, environments, closures, exceptions, modules, garbage collection and other runtime services.

Compare ahead-of-time compilation, interpretation, JIT compilation and tiered execution, and reason about inline caches, speculation and deoptimisation.

Read introductory JVM bytecode, .NET IL, CPython bytecode, V8 bytecode and WebAssembly.

Debug and profile interpreted and VM-based programs, and reason about sandboxing, bytecode validation and resource limits.

Distinguish portable language guarantees from implementation- and version-specific behaviour.

How Every Example Was Run

Every program in this course with a run environment named next to it was executed while the course was being built; the text under Output is exactly what it printed.

this course's reference implementation (TypeScript), run with Node.js 24 — every MiniLang program is run on both the tree-walk interpreter and the bytecode VM, and the build fails if their outputs differ Python / CPython bytecode

CPython 3.13.1 (dis output is specific to this version) Java / JVM bytecode

Eclipse Temurin JDK 21 (javac, java, javap).NET 10 SDK; IL read from the compiled method body at run time JavaScript / V8 bytecode

Node.js 24.18 (V8); --print-bytecode output has memory addresses removed binary modules assembled byte by byte and run by Node.js 24.18's WebAssembly engine Ubuntu 22.04 on WSL2, x86-64

Implementation-specific output is labelled as such. CPython bytecode changes between minor versions, javap output depends on the compiler version, V8's bytecode and tiers change between releases, and timings are one run on the build machine. Where a listing is *illustrative* rather than produced by a tool, the course says so.

The interactive visualizers in this course run the same MiniLang implementation in your browser, with instruction budgets and call-depth limits; nothing you type is sent to a server. Visualizers that model production runtimes (garbage collection, JIT) are labelled as simplified simulations.

How to Use This Course

Every lesson follows the same structure: objectives, prerequisites, a plain-language explanation, a precise technical explanation (one sub-section per topic), why it matters, key terms, a diagram or trace, the syntax or instruction format, a complete example with its real output, a line-by-line explanation, step-by-step runtime behaviour, misconceptions, bugs and failure modes, safer patterns, debugging, performance, security, portability caveats, real-world uses, best practices, three exercises (beginner, intermediate, advanced) with answers, a quiz with an explanation for every option, a summary and further reading.

When a concept has no source-language syntax, the lesson says "No dedicated source syntax; this is a runtime or architecture concept" and teaches it with an instruction format, a trace, a diagram or a command instead.

MiniLang at a Glance

MiniLang is the small, consistent teaching language used in every interpreter, compiler and VM lesson. Its implementation is a few hundred lines of TypeScript, shared by the course build and the visualizers.

Grammar (EBNF) The course starts with the specification's initial subset (let, print, expressions) and adds blocks, if, while, assignment, functions, calls, return and closures in later modules.

Lexical rules. Identifiers are letters, digits and _, not starting with a digit. Numbers are decimal digits with an optional fractional part (40, 2.5). Strings use double quotes with the escapes \n, \t, \" and \\. // starts a comment to the end of the line. Keywords: and else false fun if let nil or print return true while.

Precedence (lowest to highest): assignment (right-associative) → or → and → == != → < <= > >= → + - → * / → unary ! - (right-associative) → call. All binary operators are left-associative.

Semantics (both back ends). numbers (64-bit IEEE-754), strings, true/false, nil, functions, the native function str

nil and false are falsy; everything else — including 0 and "" — is truthy

== compares type and value with no conversion: "1" == 1 is false number + number, or string + string; anything else is a runtime type error Division by zero runtime error Division by zero. short-circuit; the result is one of the operand values integers without a decimal point (42), other numbers as JavaScript prints them, nil, true, <fn name> lexical; blocks create scopes; redeclaring a name in the same local scope is a compile error late-bound: a function may refer to a global defined later, as long as it exists when the code runs every run has an instruction/step budget (100,000 by default) and a call-depth limit (64)

Bytecode conventions (teaching VM). Each instruction is one opcode byte followed by its operands. A chunk has a constant pool (numbers, strings, functions) and a names table (global names). DEFINE_GLOBAL n pops the value on top of the stack and binds it to names[n]; GET_GLOBAL n pushes the value of names[n]. Local variables live in stack slots numbered from the first parameter (GET_LOCAL 0 is the first parameter). Jumps carry a 16-bit offset relative to the next instruction. The full instruction set is listed in Module 8.

program        = { declaration } ;
declaration    = funDecl | letDecl | statement ;
funDecl        = "fun", IDENTIFIER, "(", [ parameters ], ")", block ;
parameters     = IDENTIFIER, { ",", IDENTIFIER } ;
letDecl        = "let", IDENTIFIER, "=", expression, ";" ;
statement      = printStmt | ifStmt | whileStmt | returnStmt | block | expressionStmt ;
printStmt      = "print", "(", expression, ")", ";" ;
ifStmt         = "if", "(", expression, ")", statement, [ "else", statement ] ;
whileStmt      = "while", "(", expression, ")", statement ;
returnStmt     = "return", [ expression ], ";" ;
block          = "{", { declaration }, "}" ;
expressionStmt = expression, ";" ;
expression     = assignment ;
assignment     = IDENTIFIER, "=", assignment | logic_or ;
logic_or       = logic_and, { "or", logic_and } ;
logic_and      = equality, { "and", equality } ;
equality       = comparison, { ( "==" | "!=" ), comparison } ;
comparison     = term, { ( ">" | ">=" | "<" | "<=" ), term } ;
term           = factor, { ( "+" | "-" ), factor } ;
factor         = unary, { ( "*" | "/" ), unary } ;
unary          = ( "!" | "-" ), unary | call ;
call           = primary, { "(", [ arguments ], ")" } ;
argum
…

Prerequisites

Comfortable programming in at least one language (variables, functions, loops, recursion, basic data structures). Reading simple TypeScript, Python or Java helps; every example explains its syntax. The Memory & Program Execution course is useful background for the stack, heap and machine-code parts.