> ## 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 Libraries and Tools for Lua/Luau Obfuscation

> An overview of the parsers, AST libraries, and obfuscation frameworks available for Lua 5.x and Luau that you can use to build or extend your own obfuscation pipeline.

Building a Lua parser from scratch is a significant undertaking that requires implementing a lexer, recursive-descent parser, and AST data structures for every Lua version you want to support. Fortunately, several battle-tested libraries and tools exist for both standard Lua and Luau that handle parsing, AST manipulation, and serialization. This page surveys the most important options and shows how to integrate them into an obfuscation workflow.

## Standard Lua AST Libraries

These libraries target Lua 5.1, 5.2, and 5.3. They are suitable for general Lua obfuscation, game modding, and embedded scripting environments.

### luaparse (JavaScript/Node.js)

[luaparse](https://github.com/oxyc/luaparse) is the most widely used Lua parser in the JavaScript ecosystem. It supports Lua 5.1, 5.2, and 5.3 and produces a JSON-compatible AST. It is an excellent choice if you want to build obfuscation tooling in Node.js.

```bash theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
npm install luaparse
```

```javascript theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
const luaparse = require('luaparse');

const code = `
local function greet(name)
  local message = "Hello, " .. name
  return message
end
`;

const ast = luaparse.parse(code, {
  locations: true,
  ranges: true,
  comments: true
});

console.log(JSON.stringify(ast, null, 2));
```

The AST output includes location information, ranges, and comments, which is useful for source map generation or preserving comments during minification. The AST structure follows the node types described in [AST Overview](/lua-docs/lua-docs/obfuscation/ast-overview).

### metalua (Lua)

metalua is a historical Lua-based parser and AST library designed for macro programming and metaprogramming. It includes a complete Lua parser written in Lua itself and provides utilities for AST traversal and transformation.

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local parser = require("metalua.compiler").new()
local ast = parser:src_to_ast([[local x = 1 + 2]])
-- ast is a Lua table representing the AST
```

While metalua is less actively maintained than luaparse, it remains useful when you need a self-hosted Lua parser (a parser written in Lua that parses Lua).

### lua-parser (LuaRocks)

Several community Lua parsers are available on LuaRocks and GitHub. Search for packages like `lua-parser` or `lua-ast` to find implementations in various languages. Quality varies, so evaluate based on:

* Which Lua versions are supported
* Whether the AST includes location information
* Whether a serializer/printer is included
* Test coverage and maintenance activity

### LPeg-Based Parsers

[LPeg](http://www.inf.puc-rio.br/~roberto/lpeg/) is a pattern-matching library for Lua based on Parsing Expression Grammars (PEGs). It enables writing grammar-based parsers in pure Lua. While building a full Lua parser with LPeg is complex, it is feasible and gives you complete control over the AST structure.

Here is a tiny LPeg grammar snippet for parsing Lua identifiers:

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local lpeg = require("lpeg")
local P, R, S, C = lpeg.P, lpeg.R, lpeg.S, lpeg.C

-- Lua identifier: letter or underscore, followed by letters, digits, or underscores
local identifier = C((R("AZ", "az") + P("_")) * (R("AZ", "az", "09") + P("_"))^0)

-- Test
local input = "myVariable123 _private x"
for match in lpeg.match(identifier * (P(" ") * identifier)^0, input) do
  print(match)
end
```

A complete LPeg-based Lua parser would expand this into a full grammar covering all Lua syntactic constructs. Libraries like `lua-parser` (the LPeg-based one) provide this out of the box.

## Luau-Specific Parsers

Luau is Roblox's fork of Lua 5.1 with additional type syntax and optimizations. Standard Lua parsers often choke on Luau-specific syntax, so you need a Luau-aware parser for Roblox-related obfuscation.

### luau-ast (Official Luau Repo)

The [Luau open-source repository](https://github.com/luau-lang/luau) includes an official C++ parser that exposes an AST. You can access it by building the Luau CLI tools from source or by linking against the Luau library in a C++ project.

```bash theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
git clone https://github.com/luau-lang/luau.git
cd luau
mkdir build && cd build
cmake ..
make luau
```

The Luau CLI includes `luau-analyze` for type checking and `luau-compile` for compilation. For AST-based obfuscation, you would use the C++ AST API directly or pipe the output through a custom tool.

### full-moon (Rust)

[full-moon](https://github.com/Kampfkarren/full-moon) is a Lua 5.1 and Luau parser written in Rust. Its key differentiator is **lossless CST output**: it preserves whitespace, comments, and every token exactly as it appeared in the source. This makes it ideal for building obfuscation tools where you need fine-grained control over output formatting.

```bash theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
cargo add full-moon
```

```rust theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
use full_moon::parse;
use full_moon::ast::*;
use full_moon::visitors::Visitor;

struct CountIdentifiers;

impl Visitor for CountIdentifiers {
    fn visit_name(&mut self, name: &Name) {
        println!("Found identifier: {}", name);
    }
}

fn main() {
    let ast = parse(r#"
        local function greet(name)
            return "Hello, " .. name
        end
    "#).unwrap();

    let mut counter = CountIdentifiers;
    counter.visit_ast(&ast);
}
```

full-moon's visitor API allows you to walk the tree, inspect nodes, and build new AST fragments. Because it outputs a CST, you can serialize back to source with original formatting intact, or strip formatting entirely for minification.

### darklua (Rust)

darklua is a Lua/Luau source transformation tool written in Rust, specifically designed for bundling and obfuscation. It supports configurable transforms via a JSON configuration file and is one of the most production-ready open-source options for Lua/Luau obfuscation.

## Building an Obfuscation Pipeline with darklua

darklua provides a declarative configuration system where you enable transforms by name in a JSON file. It handles parsing, transformation, and serialization automatically.

### Installation

```bash theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
# Using Cargo
cargo install darklua

# Or download a prebuilt binary from GitHub releases
```

### Configuration File

Create a `darklua.json` file that enables multiple obfuscation rules:

```json theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
{
  "rules": [
    {
      "rule": "remove_comments"
    },
    {
      "rule": "remove_spaces"
    },
    {
      "rule": "convert_index_to_field",
      "properties": {
        "include_arguments": true
      }
    },
    {
      "rule": "inject_global_value",
      "identifier": "__DARKLUA__",
      "value": true
    },
    {
      "rule": "rename_variables",
      "include_functions": true,
      "name_generator": "mangled",
      "mangle_length": 6
    },
    {
      "rule": "encode_strings",
      "method": "base64"
    }
  ],
  "bundle": {
    "require_mode": "path"
  }
}
```

### Running darklua

```bash theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
darklua process input.lua output.lua --config darklua.json
```

The `process` command reads `input.lua`, applies all enabled rules in order, and writes the obfuscated output to `output.lua`. Available rules include:

| Rule | Description |
| - | - |
| `remove_comments` | Strips all comments from the source |
| `remove_spaces` | Minifies whitespace |
| `rename_variables` | Renames local variables and functions |
| `encode_strings` | Encodes string literals using base64 or other methods |
| `convert_index_to_field` | Converts `t["key"]` to `t.key` where possible |
| `inject_global_value` | Injects a global variable for runtime detection |
| `remove_unused_if_branch` | Removes dead code branches |

## Building a Custom Pipeline in Node.js with luaparse

For maximum control over your obfuscation logic, you can build a custom pipeline in Node.js using luaparse for parsing and a custom printer for serialization.

### Step 1: Parse Source to AST

```javascript theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
const luaparse = require('luaparse');

const source = `
local function calculate(a, b)
  local sum = a + b
  return sum * 2
end
`;

const ast = luaparse.parse(source, {
  locations: false,
  comments: false
});
```

### Step 2: Walk and Transform the AST

```javascript theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
function walkAndRename(node, scope) {
  scope = scope || {};

  if (node.type === 'LocalStatement') {
    node.names.forEach(ident => {
      const oldName = ident.name;
      const newName = '_' + Math.random().toString(36).substring(2, 8);
      scope[oldName] = newName;
      ident.name = newName;
    });
  }

  if (node.type === 'NameExpression') {
    if (scope[node.name]) {
      node.name = scope[node.name];
    }
  }

  // Recurse into child arrays
  for (const key in node) {
    const child = node[key];
    if (Array.isArray(child)) {
      child.forEach(c => {
        if (c && typeof c === 'object' && c.type) {
          walkAndRename(c, { ...scope });
        }
      });
    } else if (child && typeof child === 'object' && child.type) {
      walkAndRename(child, { ...scope });
    }
  }
}

walkAndRename(ast);
```

### Step 3: Serialize Back to Lua Source

luaparse does not include a built-in printer, so you need a custom serializer. Here is a minimal printer for the core node types:

```javascript theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
function printNode(node) {
  switch (node.type) {
    case 'Chunk':
      return node.body.map(printNode).join('\n');

    case 'LocalStatement':
      const names = node.names.map(n => n.name).join(', ');
      const values = node.values.map(printNode).join(', ');
      return `local ${names} = ${values}`;

    case 'AssignmentStatement':
      const targets = node.variables.map(printNode).join(', ');
      const vals = node.init.map(printNode).join(', ');
      return `${targets} = ${vals}`;

    case 'BinaryExpression':
      return `${printNode(node.left)} ${node.operator} ${printNode(node.right)}`;

    case 'NumberLiteral':
      return node.value.toString();

    case 'StringLiteral':
      return `"${node.value}"`;

    case 'NameExpression':
      return node.name;

    case 'CallExpression':
      const args = node.arguments.map(printNode).join(', ');
      return `${printNode(node.base)}(${args})`;

    case 'FunctionDeclaration':
      const params = node.parameters.map(p => p.name).join(', ');
      const body = node.body.map(printNode).join('\n  ');
      return `function(${params})\n  ${body}\nend`;

    case 'ReturnStatement':
      const ret = node.arguments.map(printNode).join(', ');
      return `return ${ret}`;

    default:
      return '';
  }
}

const obfuscated = printNode(ast);
console.log(obfuscated);
```

### Complete Pipeline Script

Here is a complete \~40-line Node.js script that parses, renames identifiers, and prints the result:

```javascript theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
const luaparse = require('luaparse');

const source = `local x = 1 + 2\nprint(x)`;
const ast = luaparse.parse(source);

function rename(node, scope) {
  scope = scope || {};
  if (node.type === 'LocalStatement') {
    node.names.forEach(id => {
      const n = '_' + Math.random().toString(36).slice(2, 6);
      scope[id.name] = n; id.name = n;
    });
  }
  if (node.type === 'NameExpression' && scope[node.name]) {
    node.name = scope[node.name];
  }
  for (const k in node) {
    const c = node[k];
    if (Array.isArray(c)) c.forEach(x => x?.type && rename(x, {...scope}));
    else if (c?.type) rename(c, {...scope});
  }
}

rename(ast);

function printNode(n) {
  if (n.type === 'Chunk') return n.body.map(printNode).join('\n');
  if (n.type === 'LocalStatement') return `local ${n.names.map(x=>x.name).join(',')}=${n.values.map(printNode).join(',')}`;
  if (n.type === 'BinaryExpression') return `${printNode(n.left)}${n.operator}${printNode(n.right)}`;
  if (n.type === 'NumberLiteral') return n.value;
  if (n.type === 'StringLiteral') return `"${n.value}"`;
  if (n.type === 'NameExpression') return n.name;
  if (n.type === 'CallExpression') return `${printNode(n.base)}(${n.arguments.map(printNode).join(',')})`;
  return '';
}

console.log(printNode(ast));
```

## Testing Your Obfuscated Output

Every obfuscation pipeline must include verification that the transformed code behaves identically to the original.

**1. Run Original Test Suite**

Execute your full test suite against the unmodified source to establish a baseline.

**2. Obfuscate and Execute**

Run the same test suite against the obfuscated output using `lua` or `luau` CLI:

```bash theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
lua obfuscated_output.lua
luau obfuscated_output.lua
```

**3. Compare Outputs**

Diff the stdout, stderr, and any file outputs between the original and obfuscated runs. They must match exactly.

**4. Fuzz with Random Inputs**

If your code accepts inputs, generate random test cases and verify both versions produce identical results.

```bash theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
# Quick verification with lua -e
lua -e "dofile('obfuscated_output.lua')"

# Or pipe through luau
luau --fflags=true obfuscated_output.lua
```

> [!WARNING]
>
> Always version-control the original source. The obfuscated output is a build artifact, not the canonical source. Obfuscation is a one-way transform: you cannot recover the original from the obfuscated output.

## Accordion: Common Questions

> **Can I use LuaJIT's built-in parser?**
>
> LuaJIT includes a built-in parser as part of its compiler pipeline, but it is not exposed as a public API for AST extraction. The parser is tightly coupled to LuaJIT's bytecode generation and does not produce a standalone AST that you can manipulate. For tooling purposes, use luaparse, full-moon, or darklua instead.

> **Is there a ready-made Lua obfuscator built on AST transforms?**
>
> darklua is the most production-ready open-source option that uses AST-level transforms internally. It supports both Lua and Luau and provides configurable rules via JSON. For the Roblox ecosystem, Prometheus is a community obfuscator that also uses AST-level transforms internally, though it is not open source. If you need full control, building a custom pipeline with luaparse or full-moon is the recommended approach.

> **Which parser should I choose for Luau?**
>
> If you need lossless output (preserving comments and formatting), use full-moon. If you need a CLI-driven workflow with built-in obfuscation rules, use darklua. If you are building a C++ tool and need the official parser, use luau-ast from the Luau repository.

## Next Steps

With your parser and pipeline chosen, you can now implement the specific obfuscation techniques:

> **[AST Transformations](/lua-docs/lua-docs/obfuscation/ast-transformations)**
>
> Learn the core AST transforms: renaming, splitting, reordering, and inlining.

> **[AST Overview](/lua-docs/lua-docs/obfuscation/ast-overview)**
>
> Understand the structure and node types of the Lua AST.

> **[Bytecode Obfuscation](/lua-docs/lua-docs/obfuscation/bytecode)**
>
> Explore bytecode-level obfuscation for deeper protection.


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