Introduction
In this post, we will examine how Luau bytecode is organized, how instructions operate on registers and constants, and how functions, closures, and optimizations are represented internally.
Prerequisite
I assume that you’re familiar with the Lua 5.1 bytecode structure, if you’re not, please refer to this.
How it works
First of all, make sure you’ve installed the luau-compile binary from the official Luau repository. You can compile it from source or download a pre-built binary for your platform. Once installed, you can compile a Luau script to bytecode using the command:
Now let's look at a simple example. Consider this basic Luau function:
When we compile this with luau-compile and examine the bytecode, we can see the instructions that the Luau VM will execute. understand the bytecode structure, we will avoid the use of the --binary option which gives us the raw representation of the bytecode (Note: you can use this site to generate a disassembly). Removing that option will force the compiler to generate a dissasembly (in short, parsed bytecode).
Let's break down what happens when our greet function is compiled. But before, we have to understand what makes up our bytecode.
Luau’s bytecode can be holded by a “Chunk” which can be divided into several components:
- Metadata (versioning)
- The string pool, each string in the program is stored there to maintain compatibility across the different protos (see below)
- The prototypes, or more likely the different functions
Each prototype contains the following:
- Metadata (stack size, params, upvalues, flags…)
- The instruction table
- The constant pool, to sum up, where all the constants (Booleans, Strings, Numbers and even nils) are stored
- Nested functions or prototypes
- Some debug info that we’re not gonna cover
Instructions
Instructions are the fundamental building blocks of Luau bytecode. Unlike Lua 5.1’s classic ABC instruction format, Luau introduces a more flexible operand encoding where the meaning of each operand depends heavily on the opcode itself.
An instruction generally operates on registers (R) and constants (K), but the same numeric operand may represent:
- A register index
- A constant index
- A jump offset
- A count (number of arguments, return values, etc.)
For example:
At first glance, this may seem ambiguous. In practice:
- R1 is the base register holding the function to call
- 1 is the argument count
- 0 is the expected return count
This overloading allows Luau to minimize instruction size while still supporting complex operations. As a result, reading Luau bytecode requires understanding each opcode’s operand contract, not just its mnemonic (the opcode name).
The Register Model
Luau is a register-based virtual machine (VM), but registers should not be confused with source-level variables.
Registers are temporary storage locations and are widely reused across the code. Their lifetime may span only a few instructions and they do not directly map to local variables.
For example:
This does not mean “assign name to a new variable”. Instead, it copies the value currently stored in R0 into R4 because the following instruction (CONCAT) requires a contiguous register range.
Constant Pool vs String Pool
Luau separates constants into two distinct storage:
String Pool
- Shared across the entire chunk
- Stores all strings used by the program
Constant Pool
- Local to each prototype
- Stores numbers, booleans, nil, and references to strings
- Indexed via K operands (e.g. K2)
This separation allows nested functions to reference the same string values without duplication, while still maintaining independent constant tables.
Function Prototypes and Closures
Every function in Luau, named, anonymous or top-level is represented as a prototype.
A prototype contains:
- Metadata (stack size, params, upvalues, flags…)
- Instruction list
- Constant pool
- Nested prototypes
- Optional debug metadata
When Luau encounters a function definition, it does not immediately create a callable function. Instead, it emits a prototype and later instantiates it using:
This instruction:
- Creates a new closure from prototype K0
- Captures required upvalues
- Stores the resulting function in R0
In Luau, upvalues are the mechanism that allows nested functions to capture variables from their enclosing scope.
Multiple closures may share the same prototype, but each closure has its own captured state. This distinction is essential to understanding how Luau represents functions internally.
Optimization Levels and Their Impact
Luau supports three optimization levels, each producing materially different bytecode.
Level 0:
- Minimal optimizations
- Bytecode resembles Lua 5.1
Level 1:
- Performs constant folding
- Reduces instruction count
Level 2:
- Enables aggressive optimizations
- Includes inlining and fastcalls.
- Alters instruction patterns
- Harder to analyze
Control Flow
High-level control structures such as if, while and for do not exist in bytecode. Instead, Luau relies on explicit instruction jumps.
Common control flow opcodes include:
- JUMP
- JUMPIF
- JUMPIFNOT
These instructions operate on instruction offsets rather than labels. At the bytecode level, control flow is linear and offset-based. Understanding this representation is a prerequisite for later reconstructing structured control flow.
Now that you know this, let’s analyze the function we compiled before:
Conclusion
At this stage, we have built a solid understanding of Luau bytecode as it exists at the virtual machine level. We have explored how chunks and prototypes are structured, how instructions operate on registers and constants, how functions and closures are represented, and how optimization levels influence the emitted bytecode. With this foundation, reading Luau disassemblies becomes a structured process rather than a guessing exercise. In the next part of this series, we will create our first “automated” parser in pure Luau and learn how to make our own virtual machine.
