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

# Abstract Syntax Tree (AST) Obfuscation: Overview

> Learn what a Lua/Luau Abstract Syntax Tree is, how parsers produce it, and why AST-based transformations are the most reliable approach to source-level obfuscation.

Abstract Syntax Tree (AST) obfuscation is the most robust and precise method for transforming Lua and Luau source code. Instead of manipulating raw text with fragile regular expressions, you work on a structured tree that represents the exact semantic meaning of your code. This page explains what an AST is, how parsers construct it from source text, and why AST-based transforms are the foundation of professional obfuscation pipelines.

## What Is an AST

An Abstract Syntax Tree is a hierarchical data structure that represents the syntactic structure of source code. Every node in the tree corresponds to a syntactic construct in the language: a function declaration, a variable assignment, a binary operation, a string literal, and so on. The tree captures the relationships between these constructs, such as which statements belong inside which function body, or which expressions are operands of which operator.

Consider this simple Lua snippet:

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local x = 1 + 2
print(x)
```

The AST for this code has a root `Chunk` node containing two `Statement` children. The first is a `LocalStatement` with an `Identifier` named `x` and a value that is a `BinaryExpression` with operator `+`, left operand `NumberLiteral{1}`, and right operand `NumberLiteral{2}`. The second is a `CallExpression` with a `NameExpression` callee `print` and a single argument that is a `NameExpression` referencing `x`.

This tree representation is fundamentally different from the raw text. The AST knows that `x` in `print(x)` is a variable reference, not a string or keyword. It knows that `1 + 2` is an addition operation with two numeric operands. It knows the scope boundaries created by `local` declarations. This semantic awareness is what makes AST transforms so powerful.

## Why AST-Based Obfuscation Beats Text Replacement

Text-based obfuscation using regular expressions or string replacement is brittle and error-prone. AST-based obfuscation is precise, scope-aware, and guaranteed to produce syntactically valid output.

> **Text Replacement**
>
> * Breaks on multi-line strings containing keywords
> * Cannot distinguish variables from string literals
> * Ignores scope boundaries
> * Fails on comments and whitespace edge cases
> * Produces invalid syntax when reordering statements

> **AST Transforms**
>
> * Operates on semantic structure, not characters
> * Knows the difference between identifiers and strings
> * Full scope and upvalue analysis
> * Comments and formatting are handled by the serializer
> * Every transformation preserves syntactic correctness

For example, a naive text replacement that renames `x` to `a` would break this code:

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local x = 10
local s = "the variable x is unused"
print(s)
```

A text replacer might turn the string literal into `"the variable a is unused"`, corrupting the program's behavior. An AST-based renamer knows that `x` inside the string literal is a `StringLiteral` node, not a `NameExpression`, and leaves it untouched.

## The Lua AST Structure

Lua and Luau parsers produce ASTs with a well-defined set of node types. Understanding these node types is essential for writing custom transforms.

### Top-Level and Statement Nodes

The root of every Lua AST is a `Chunk` node, which contains a list of statements. Each statement is one of the following types:

| Node Type | Represents |
| - | - |
| `LocalStatement` | `local name = value` declarations |
| `AssignmentStatement` | Regular assignments like `a = b` |
| `DoStatement` | `do ... end` blocks |
| `WhileStatement` | `while condition do ... end` loops |
| `RepeatStatement` | `repeat ... until condition` loops |
| `IfStatement` | `if ... then ... else ... end` conditionals |
| `NumericForStatement` | `for i = start, stop, step do ... end` |
| `GenericForStatement` | `for k, v in pairs(t) do ... end` |
| `FunctionStatement` | Global function declarations |
| `LocalFunctionStatement` | `local function name(...)` declarations |
| `ReturnStatement` | `return expr` |
| `BreakStatement` | `break` |
| `GotoStatement` | `goto label` (Lua 5.2+) |
| `LabelStatement` | `::label::` (Lua 5.2+) |

### Expression Nodes

Expressions appear as values on the right-hand side of assignments, as conditions in control flow, as arguments to functions, and in many other positions. The main expression node types are:

| Node Type | Represents |
| - | - |
| `NumberLiteral` | Numeric constants like `42` or `3.14` |
| `StringLiteral` | String constants like `"hello"` |
| `NilLiteral` | `nil` |
| `TrueExpression` / `FalseExpression` | Boolean literals |
| `VarArgExpression` | `...` in vararg functions |
| `NameExpression` | Variable references like `x` or `print` |
| `IndexExpression` | Table indexing like `t[field]` |
| `FieldExpression` | Dot indexing like `t.field` |
| `MethodCallExpression` | Method calls like `obj:method(args)` |
| `CallExpression` | Function calls like `func(args)` |
| `UnaryExpression` | Unary operations like `-x` or `not b` |
| `BinaryExpression` | Binary operations like `a + b` or `x == y` |
| `TableConstructor` | Table literals like `{1, 2, a = 3}` |
| `FunctionExpression` | Anonymous functions like `function(...) ... end` |

### Annotated Code Sample

Here is a slightly more complex function with prose annotations mapping each syntactic element to its AST node type:

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local function calculate(a, b)
  -- LocalFunctionStatement
  --   name: Identifier "calculate"
  --   parameters: [Identifier "a", Identifier "b"]
  --   body: Chunk

  local sum = a + b
  -- LocalStatement
  --   names: [Identifier "sum"]
  --   values: [BinaryExpression
  --     op: "+"
  --     left: NameExpression "a"
  --     right: NameExpression "b"]

  if sum > 10 then
  -- IfStatement
  --   condition: BinaryExpression { op: ">", left: NameExpression "sum", right: NumberLiteral 10 }
  --   then: Chunk

    return sum * 2
    -- ReturnStatement
    --   values: [BinaryExpression { op: "*", left: NameExpression "sum", right: NumberLiteral 2 }]

  else
    return sum
    -- ReturnStatement
    --   values: [NameExpression "sum"]
  end
end
```

Every token in the source maps to a specific node type, and the nesting of the tree reflects the nesting of blocks and expressions in the code.

## How a Parser Produces the AST

Parsing is a two-phase process: lexing (tokenization) followed by syntactic analysis.

### Phase 1: Lexing

The lexer scans the raw source text character by character and produces a stream of tokens. Each token has a type and a value. For the input `local x = 1 + 2`, the lexer produces:

```text theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
Keyword("local"), Identifier("x"), Operator("="), Number("1"), Operator("+"), Number("2")
```

The lexer discards whitespace and comments (or attaches them as trivia to adjacent tokens, depending on the parser), and recognizes keywords, identifiers, operators, string literals, and numeric literals.

### Phase 2: Parsing

The parser consumes the token stream and builds the AST using a recursive-descent algorithm. It matches token sequences against the grammar rules of Lua. For `local x = 1 + 2`, the parser performs roughly these steps:

1. See `Keyword("local")` — begin a `LocalStatement`
2. Read `Identifier("x")` — add to the names list
3. See `Operator("=")` — expect value expressions next
4. Read `Number("1")` — create a `NumberLiteral` node
5. See `Operator("+")` — create a `BinaryExpression` with the previous `NumberLiteral` as left operand
6. Read `Number("2")` — set as the right operand of the `BinaryExpression`
7. Finish the `LocalStatement` with names `["x"]` and values `[BinaryExpression]`

### JSON-Like Pseudo-AST

Here is a simplified JSON-like representation of the AST for a small Lua function:

```json theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
{
  "type": "Chunk",
  "body": [
    {
      "type": "LocalFunctionStatement",
      "name": { "type": "Identifier", "name": "greet" },
      "parameters": [
        { "type": "Identifier", "name": "name" }
      ],
      "body": {
        "type": "Chunk",
        "body": [
          {
            "type": "LocalStatement",
            "names": [
              { "type": "Identifier", "name": "message" }
            ],
            "values": [
              {
                "type": "BinaryExpression",
                "operator": "..",
                "left": { "type": "StringLiteral", "value": "Hello, " },
                "right": { "type": "NameExpression", "name": "name" }
              }
            ]
          },
          {
            "type": "CallExpression",
            "base": { "type": "NameExpression", "name": "print" },
            "arguments": [
              { "type": "NameExpression", "name": "message" }
            ]
          }
        ]
      }
    }
  ]
}
```

This structure is what tools like [luaparse](/lua-docs/lua-docs/obfuscation/ast-tools) or [full-moon](/lua-docs/lua-docs/obfuscation/ast-tools) emit when you parse Lua source code.

## The AST Visitor Pattern

The visitor pattern is the standard technique for traversing and transforming an AST. A visitor is a function that receives a node, performs some action, and recursively visits the node's children.

### Counting String Literals

Here is pseudocode for a visitor that counts all string literals in an AST:

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
function countStrings(node, counter)
  counter = counter or 0

  if node.type == "StringLiteral" then
    counter = counter + 1
  end

  -- Recurse into child nodes
  for _, child in ipairs(node.children or {}) do
    counter = countStrings(child, counter)
  end

  return counter
end
```

You invoke this visitor on the root `Chunk` node, and it walks the entire tree, incrementing the counter every time it encounters a `StringLiteral`.

### Renaming Local Variables

Here is a more complex visitor that renames all variables declared in `LocalStatement` nodes:

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
function renameLocals(node, scope)
  scope = scope or {}

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

  if node.type == "NameExpression" then
    local newName = scope[node.name]
    if newName then
      node.name = newName
    end
  end

  for _, child in ipairs(node.children or {}) do
    renameLocals(child, scope)
  end
end
```

This visitor maintains a scope table mapping original names to renamed names. When it sees a `LocalStatement`, it generates a new random name for each declared identifier and records the mapping. When it sees a `NameExpression`, it looks up the name in the scope table and applies the rename if found.

> [!NOTE]
>
> Real-world renamers need proper scope analysis to handle shadowing and upvalues correctly. The pseudocode above is simplified. See [AST Transformations](/lua-docs/lua-docs/obfuscation/ast-transformations) for a complete scope-aware renaming implementation.

## AST Serialization

After you transform the AST, you must convert it back into valid Lua source text. This process is called serialization, pretty-printing, or code generation. A serializer traverses the AST depth-first and emits source text for each node type:

* A `LocalStatement` with names `[a, b]` and values `[1, 2]` emits `local a, b = 1, 2`
* A `BinaryExpression` with operator `+` emits the left operand, then `+`, then the right operand
* A `FunctionExpression` emits `function`, then parameters in parentheses, then the body, then `end`

Most AST libraries include a built-in printer or serializer. When building custom pipelines, you can either use the library's printer or write your own for fine-grained control over formatting.

> [!WARNING]
>
> AST-based obfuscation is not something you implement from scratch in ten minutes. A production-quality pipeline requires a full Lua parser, scope analysis, and a serializer. Use established libraries like [luaparse](/lua-docs/lua-docs/obfuscation/ast-tools) or [full-moon](/lua-docs/lua-docs/obfuscation/ast-tools) rather than building your own parser.

## AST vs. CST

> **What is the difference between an AST and a CST?**
>
> A **Concrete Syntax Tree (CST)** includes every token from the source file, including whitespace, comments, and parentheses that may be semantically redundant. A CST preserves the exact surface structure of the code.
>
> An **Abstract Syntax Tree (AST)** strips away unneeded syntax and represents only the semantic structure. Parentheses around expressions are folded into the tree structure. Whitespace and comments are typically discarded or stored as metadata.
>
> Most obfuscation tools work on ASTs because the goal is to transform semantics, not preserve formatting. However, some tools use CSTs when they need fine-grained control over output formatting or when they want to preserve comments during minification.

## Next Steps

Now that you understand what an AST is and how parsers produce it, you can explore the specific transformations you can apply:

> **[AST Transformations](/lua-docs/lua-docs/obfuscation/ast-transformations)**
>
> Learn scope-aware renaming, expression splitting, statement reordering, and function inlining.

> **[AST Tools](/lua-docs/lua-docs/obfuscation/ast-tools)**
>
> Discover the parsers and libraries available for Lua and Luau.

> **[Variable Renaming](/lua-docs/lua-docs/obfuscation/variable-renaming)**
>
> Dive deeper into identifier renaming strategies.


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