The earlier Lua lessons used scripts from the outside and defined what the game host would let them do. The computability lesson then showed how one program can execute another—and why no perfect termination checker can replace a work budget. Now look inside the interpreter. The same small expression has to become operations a virtual machine can execute, and those operations still need limits and useful errors.
Section: An interpreter is a pipeline.
When Lua reads total equals 5 + 2, it does not ask the C P U, central processing unit, to execute the characters + and equals directly. Several layers turn text into actions:
a lexer groups characters into tokens such as name, equals, integer, plus, integer;
a parser checks that the token order follows the language grammar;
a compiler turns the meaning into compact bytecode instructions;
a virtual machine executes those bytecodes using values, a stack, and an instruction pointer.
Real Lua is carefully optimized. Our tiny V M, virtual machine, is intentionally smaller so every moving part fits on one page.
These stages are common, but they are not the definition of every interpreter. One interpreter may walk a syntax tree directly; another may compile to bytecode first; a just-in-time compiler may later translate hot bytecode into native machine code. The language’s meaning can stay the same while its implementation changes.
That gives us an important abstraction boundary:
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
Do not explain Lua semantics by accident from one current bytecode layout. The source rule is the contract; tokens, syntax trees, bytecodes, stacks, and native instructions are replaceable representations used to carry it out.
The same pipeline can be viewed from the host’s side. Lua 5.4 typically recognizes names and numbers with a lexer, checks expressions and statements with a parser, compiles them to bytecode, and executes those instructions in its V M. The V M maintains Lua values, call frames, and its own program counter; the host supplies functions that may be called during execution:
Some hosts cache bytecode; others compile source each time it loads. LuaJIT may additionally translate hot paths into native machine code. Identify the runtime before assuming its internal representation matches this teaching V M.
Section: Bytecode names small operations.
The lab uses an enum instead of raw bytes:
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
Constant(2) means “copy constant number two onto the value stack.” Add pops two integers and pushes their sum. JumpIfFalse(9) changes the V M instruction pointer when the popped condition is false.
An enum makes this teaching V M easy to inspect. A full game-scripting interpreter may encode opcodes and operands more densely after profiling shows that bytecode size or dispatch speed matters.
That 9 is not decoration. Jump operands are positions in the bytecode vector, so they only mean anything once you can see the whole program. Here is the lab’s example, if (5 + 2) greater than 6 then print(1) else print(0), after compilation. The constants are collected into a pool, and the instructions refer to them by index:
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
Now the operands read as addresses within this listing. JumpIfFalse(9) sends a false condition to index 9, which is exactly where the else branch begins. Jump(11) carries the then branch past the else branch to the Halt. The if/else structure you wrote in Lua has become two jumps and a layout — the compiler kept the meaning and threw away the shape.

Section: The V M has its own instruction pointer.
The C P U has an instruction pointer such as r i p. The V M also needs one, but it points into the bytecode vector:
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
A jump replaces that default next location. Bounds checks prevent corrupt bytecode from jumping outside the program.
Section: Model the V M as state plus one transition function.
At any instant, this tiny machine is completely described by a small state:
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
One interpreter-loop iteration reads the current instruction and changes that state. Constant advances the pointer and grows the stack. Add removes two values and adds one. Jump changes the pointer. Print moves a value to observable output. Halt ends the transition loop.
This state-machine view is more reliable than imagining that the V M “somehow runs Lua.” It lets you state an invariant for every instruction: required stack inputs, resulting stack height, legal jump targets, possible error, and instruction-pointer change.
Section: The value stack carries temporary results.
In brief: The pool stores fixed constants. The stack stores temporary values. Adding two values removes both and leaves one result.
Indices 0 to 4 of that listing compute (5 + 2) greater than 6. Watching the stack through those five steps shows where the intermediate values live:
Add pops the right side first because it was pushed last. This last-in, first-out order is the same basic idea as a call stack, although a real V M may use separate regions or registers for different jobs.
Section: Keep dynamic types explicit.
Alternative: A register-based V M names temporary slots directly. This stack V M instead makes each instruction use the values at the top.
Lua values can have several types. Our tiny subset has two:
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
pop_integer checks the variant. Adding a Boolean produces a useful V M error rather than reinterpreting its bytes as a number. Dynamic typing means the check happens at run time; it does not mean “types do not exist.”
A larger Lua V M also needs an environment that maps names to values. Functions capture environments through closures, tables hold references to other values, and garbage collection decides when unreachable heap objects can be reclaimed. Those features enlarge the state, but they do not change the basic model: each operation checks its inputs and moves the machine from one valid state to the next.
Section: Separate four kinds of failure.
Keeping stages separate produces clearer errors:
If all four become “script failed,” the host cannot tell whether the author wrote invalid Lua, the compiler produced invalid code, or a valid script exceeded the game tool’s resource policy.
Section: Budgets belong inside the execution loop.
Every instruction consumes one step:
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
The test program Jump(0) would loop forever without this guard. This miniature rule connects directly to the instruction hook used by the mlua host earlier in the chapter.
Section: Run the original lab.
The executable’s sample expression takes 5 + 2, compares it with 6, and prints 1 for true. The test command then checks two failure paths: a jump outside the program and a loop that exhausts its instruction budget.
Terminal window
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
For a much larger interpreter-building journey, Build a Lua Interpreter in Rust develops tokens, bytecode, values, tables, control flow, functions, and closures over many stages. This book’s V M and wording are original and deliberately smaller; the linked book is useful further reading when you want to build a language implementation rather than embed an existing Lua runtime.