Skip to main content
AST transformations are the gold standard for source-level obfuscation. Because you operate on the parse tree rather than raw text, every transformation is guaranteed to produce syntactically valid output and can be made fully scope-aware. This page covers the core AST transformation techniques used in professional Lua and Luau obfuscation pipelines, from identifier renaming to control flow restructuring.

Scope-Aware Identifier Renaming

Identifier renaming is the most fundamental AST transform. A naive renamer that walks the AST and changes every Identifier node would break your code immediately, because it would rename globals, string contents, and field names indiscriminately. A proper renamer builds a scope tree and only renames local variables.

Building the Scope Tree

Each block-creating construct in Lua introduces a new scope: do ... end, function(...) ... end, for ... do ... end, while ... do ... end, and repeat ... until .... The scope tree builder walks the AST and creates a symbol table for each scope.

The Renamer Visitor

Once the scope tree is built, a second visitor walks the AST and applies renames. For each NameExpression, it walks up the scope chain to find which scope declared that name, then substitutes the renamed version.

Upvalue Handling

Closures capture references to variables from outer scopes. These are called upvalues. When you rename a local in an outer scope, every closure that captures it must use the same renamed name. The scope tree handles this naturally: all closures share the same symbol table reference for their parent scope, so a rename in the parent scope propagates to all child scopes automatically.
In the AST, the NameExpression for outer inside closure resolves to the same scope entry as the LocalStatement that declared it. The renamer replaces both with the same generated name.

Expression Decomposition

Complex expressions can be split into multiple statements using temporary variables. This breaks up readable arithmetic and makes data flow harder to trace.

Transform Pattern

AST Transform Steps

  1. Detect BinaryExpression nodes where either operand is itself a BinaryExpression
  2. Extract the left sub-expression into a new LocalStatement with a generated temporary name
  3. Extract the right sub-expression into another LocalStatement
  4. Replace the original operands with NameExpression nodes referencing the temporaries
Expression decomposition changes evaluation order. In Lua, expressions with side effects (like function calls) must be handled carefully to preserve the original left-to-right evaluation semantics.

Statement Reordering

Independent statements (those with no data dependencies between them) can be shuffled to make the logical flow harder to follow.

Detecting Data Dependencies

For each statement, compute two sets:
  • Writes: the set of variables assigned by this statement
  • Reads: the set of variables read by this statement
Two statements A and B are independent if B does not read any variable written by A, and A does not read any variable written by B.

Dependency Analysis Implementation

Function Inlining

Function inlining replaces a call to a small function with the function’s body directly at the call site. This removes the function from the source entirely, making call graphs harder to reconstruct.

Before and After

Advanced Inline with Scope Isolation

To avoid name conflicts, the inlined body should use fresh temporary names for all its locals:
The AST transform:
  1. Locate CallExpression nodes where the callee is a known local function
  2. Clone the function body AST
  3. Rename all parameters and locals in the cloned body to fresh names
  4. Replace the CallExpression with an immediately-invoked FunctionExpression
  5. Map the original call arguments to the renamed parameters

Function Outlining

Function outlining is the inverse of inlining. You extract a sequence of statements into a new anonymous function and replace the original statements with a call to that function.

Before and After

Outlining makes linear scripts appear to have many layers of abstraction. It also introduces closure boundaries that complicate static analysis.

Local-to-Upvalue Promotion

Wrapping the entire script body in a closure converts all top-level locals into upvalues of the inner function. This makes them invisible to global introspection tools and debuggers that scan the global environment.

The Wrapping Pattern

AST Transform

  1. Wrap the root Chunk body in a FunctionExpression with no parameters
  2. Wrap that function in a CallExpression (immediate invocation)
  3. Replace the original Chunk body with a single LocalStatement or AssignmentStatement that captures the result

Numeric For to While Transformation

Converting for loops to while loops removes a structured control flow construct and replaces it with a more verbose, manually-managed loop.

Transform Pattern

AST Transform Steps

  1. Detect NumericForStatement nodes
  2. Emit a LocalStatement for the counter variable initialized to the start expression
  3. Emit a WhileStatement with condition counter <= stop (or >= for negative step)
  4. Insert the original loop body into the WhileStatement’s body
  5. Append an AssignmentStatement at the end of the loop body to increment the counter by the step value
When the step expression is not a compile-time constant, the transform must emit a conditional increment or handle both positive and negative step directions.

Table Constructor Splitting

Table constructors can be split into a series of assignment statements. This destroys the visual grouping of related data and introduces intermediate states.

Transform Pattern

Optional Shuffling

After splitting, the assignment statements can be shuffled if they have no data dependencies:
The AST transform:
  1. Detect TableConstructor nodes with named fields
  2. Replace the constructor with an empty table {}
  3. For each field, emit an AssignmentStatement using FieldExpression indexing

String Concatenation Splitting

String literals can be split into runtime-assembled fragments to hide their contents from static analysis.

Transform Pattern

The AST transform replaces StringLiteral nodes with either:
  • A CallExpression to string.sub on a larger garbage-padded string
  • A chain of BinaryExpression nodes using .. with CallExpression nodes to string.char

Pipeline Ordering

Chain multiple AST transforms in a pipeline where each pass feeds its output AST into the next transform. The order matters significantly.
Recommended pipeline order:
1

Scope Analysis

Build the scope tree and symbol tables before any renaming occurs.
2

Identifier Renaming

Rename all local variables while scope information is clean and unmodified.
3

Expression Decomposition

Split complex expressions after renaming, so temporaries also get obfuscated names.
4

Statement Reordering

Shuffle independent statements after decomposition introduces new temporaries.
5

Control Flow Restructuring

Apply for-to-while transforms and function outlining after the basic structure is stable.
6

String Encoding

Replace string literals with runtime decoding expressions as a final pass.
Running expression decomposition before statement reordering is important because decomposition introduces new temporaries that create data dependencies, which the reordering pass must respect.

Next Steps

Now that you understand the core AST transformations, explore the tools and libraries that make them practical:

AST Tools

Discover parsers and obfuscation frameworks for Lua and Luau.

Variable Renaming

Deep dive into identifier renaming strategies and edge cases.

Control Flow

Learn additional control flow obfuscation techniques.