> ## Documentation Index
> Fetch the complete documentation index at: https://starcryptdocs.starsolenterprise.com/lua-docs/llms.txt
> Use this file to discover all available pages before exploring further.

# AST Transformation Techniques for Lua/Luau

> Apply scope-aware identifier renaming, expression splitting, statement reordering, function inlining, and other semantic-preserving AST transforms to obfuscate Lua code.

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.

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
function buildScopeTree(node, parentScope)
  local scope = {
    parent = parentScope,
    symbols = {},      -- name -> renamedName
    children = {}
  }

  if node.type == "LocalStatement" or node.type == "LocalFunctionStatement" then
    for _, ident in ipairs(node.names) do
      scope.symbols[ident.name] = generateRandomName()
    end
  end

  if node.type == "FunctionExpression" or node.type == "LocalFunctionStatement"
     or node.type == "FunctionStatement" then
    for _, param in ipairs(node.parameters) do
      scope.symbols[param.name] = generateRandomName()
    end
  end

  if node.type == "NumericForStatement" or node.type == "GenericForStatement" then
    for _, ident in ipairs(node.names) do
      scope.symbols[ident.name] = generateRandomName()
    end
  end

  -- Recurse into child nodes, passing this scope
  for _, child in ipairs(node.children or {}) do
    table.insert(scope.children, buildScopeTree(child, scope))
  end

  return scope
end
```

### 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.

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
function renameVisitor(node, scope)
  if node.type == "NameExpression" then
    local current = scope
    while current do
      if current.symbols[node.name] then
        node.name = current.symbols[node.name]
        break
      end
      current = current.parent
    end
    -- If not found in any scope, it's a global: leave it unchanged
  end

  -- When entering a new scope block, switch to that child scope
  for _, child in ipairs(node.children or {}) do
    renameVisitor(child, findChildScope(node, scope) or scope)
  end
end
```

### 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.

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local outer = 10
local function closure()
  return outer + 1   -- 'outer' is an upvalue
end
```

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

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Before
local x = (a + b) * (c - d)

-- After
local _t1 = a + b
local _t2 = c - d
local x = _t1 * _t2
```

### 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

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
function decomposeExpressions(node)
  if node.type == "LocalStatement" then
    for i, value in ipairs(node.values) do
      if isComplexExpression(value) then
        local temps = {}
        local newValue = extractSubExpressions(value, temps)

        -- Insert temporaries before this statement
        for _, temp in ipairs(temps) do
          insertBefore(node, temp)
        end

        node.values[i] = newValue
      end
    end
  end
end
```

> [!WARNING]
>
> 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.

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Before: clear sequential logic
local a = 1
local b = 2
local c = 3
local d = 4
local e = 5

-- After: shuffled but semantically equivalent
local c = 3
local a = 1
local e = 5
local b = 2
local d = 4
```

### Dependency Analysis Implementation

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
function getReadsAndWrites(node)
  local reads, writes = {}, {}

  if node.type == "LocalStatement" or node.type == "AssignmentStatement" then
    for _, name in ipairs(node.names) do
      if name.type == "Identifier" then
        table.insert(writes, name.name)
      end
    end
    for _, value in ipairs(node.values) do
      collectNames(value, reads)
    end
  end

  return reads, writes
end

function canReorder(stmtA, stmtB)
  local readsA, writesA = getReadsAndWrites(stmtA)
  local readsB, writesB = getReadsAndWrites(stmtB)

  -- Check for intersection between writes of A and reads of B
  for _, w in ipairs(writesA) do
    for _, r in ipairs(readsB) do
      if w == r then return false end
    end
  end

  -- Check for intersection between writes of B and reads of A
  for _, w in ipairs(writesB) do
    for _, r in ipairs(readsA) do
      if w == r then return false end
    end
  end

  return true
end
```

## 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

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Before
local function addOne(x)
  return x + 1
end

local result = addOne(5)

-- After
local result = (function(x)
  return x + 1
end)(5)
```

### Advanced Inline with Scope Isolation

To avoid name conflicts, the inlined body should use fresh temporary names for all its locals:

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Before
local function helper(a, b)
  local temp = a * b
  return temp + 1
end

local x = 2
local y = 3
local result = helper(x, y)

-- After
local x = 2
local y = 3
local result = (function(_a, _b)
  local _temp = _a * _b
  return _temp + 1
end)(x, y)
```

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

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Before: flat sequence
local a = 1
local b = 2
local c = a + b
local d = c * 2
local e = d - 1
local f = e / 3
local g = f + 10
local h = g ^ 2

-- After: split into outlined functions
local a = 1
local b = 2

local _t1 = (function()
  local c = a + b
  local d = c * 2
  return d
end)()

local _t2 = (function()
  local e = _t1 - 1
  local f = e / 3
  return f
end)()

local h = (function()
  local g = _t2 + 10
  return g ^ 2
end)()
```

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

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Before
local secret = "hidden value"
local function process(data)
  return data .. secret
end

-- After
local result = (function()
  local secret = "hidden value"
  local function process(data)
    return data .. secret
  end
  -- expose only what is needed
  return process
end)()
```

### 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

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Before
for i = 1, 10, 2 do
  print(i)
end

-- After
local _i = 1
while _i <= 10 do
  print(_i)
  _i = _i + 2
end
```

### 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

> [!NOTE]
>
> 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

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Before
local t = {a = 1, b = 2, c = 3}

-- After
local t = {}
t.a = 1
t.b = 2
t.c = 3
```

### Optional Shuffling

After splitting, the assignment statements can be shuffled if they have no data dependencies:

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local t = {}
t.c = 3
t.a = 1
t.b = 2
```

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

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Before
local msg = "hello world"

-- After: using string.sub with garbage padding
local msg = string.sub("xxhello worldyy", 3, 13)

-- After: using multiple char calls
local msg = string.char(104) .. string.char(101) .. string.char(108)
            .. string.char(108) .. string.char(111) .. string.char(32)
            .. string.char(119) .. string.char(111) .. string.char(114)
            .. string.char(108) .. string.char(100)
```

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

> [!TIP]
>
> 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.

> [!NOTE]
>
> 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](/lua-docs/lua-docs/obfuscation/ast-tools)**
>
> Discover parsers and obfuscation frameworks for Lua and Luau.

> **[Variable Renaming](/lua-docs/lua-docs/obfuscation/variable-renaming)**
>
> Deep dive into identifier renaming strategies and edge cases.

> **[Control Flow](/lua-docs/lua-docs/obfuscation/control-flow)**
>
> Learn additional control flow obfuscation techniques.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.