Skip to main content
The Abstract Syntax Tree (AST) is the primary data structure in Full Moon that represents the hierarchical structure of Lua code. Every syntactic element in Lua is represented as a node in the AST.

Core AST Structure

From src/ast/mod.rs:51-73, the top-level structure is a Block:
/// A block of statements, such as in if/do/etc block
#[derive(Clone, Debug, Default, Display, PartialEq, Node, Visit)]
pub struct Block {
    stmts: Vec<(Stmt, Option<TokenReference>)>,
    last_stmt: Option<(LastStmt, Option<TokenReference>)>,
}

impl Block {
    /// An iterator over the statements in the block
    pub fn stmts(&self) -> impl Iterator<Item = &Stmt>
    
    /// The last statement of the block if one exists, such as `return foo`
    pub fn last_stmt(&self) -> Option<&LastStmt>
}
Notice that statements include an optional TokenReference for semicolons. This is part of Full Moon’s lossless parsing - even optional semicolons are tracked!

Major Node Types

Statements

From src/ast/mod.rs:438-507, the Stmt enum covers all Lua statements:
/// A statement that stands alone
pub enum Stmt {
    /// An assignment, such as `x = 1`
    Assignment(Assignment),
    /// A do block, `do end`
    Do(Do),
    /// A function call on its own, such as `call()`
    FunctionCall(FunctionCall),
    /// A function declaration, such as `function x() end`
    FunctionDeclaration(FunctionDeclaration),
    /// A generic for loop, such as `for index, value in pairs(list) do end`
    GenericFor(GenericFor),
    /// An if statement
    If(If),
    /// A local assignment, such as `local x = 1`
    LocalAssignment(LocalAssignment),
    /// A local function declaration
    LocalFunction(LocalFunction),
    /// A numeric for loop, such as `for index = 1, 10 do end`
    NumericFor(NumericFor),
    /// A repeat loop
    Repeat(Repeat),
    /// A while loop
    While(While),
    
    // Feature-specific variants...
}

Expressions

From src/ast/mod.rs:350-436, expressions represent values:
/// An expression, mostly useful for getting values
pub enum Expression {
    /// A binary operation, such as `1 + 3`
    BinaryOperator {
        lhs: Box<Expression>,
        binop: BinOp,
        rhs: Box<Expression>,
    },
    
    /// A statement in parentheses, such as `(#list)`
    Parentheses {
        contained: ContainedSpan,
        expression: Box<Expression>,
    },
    
    /// A unary operation, such as `#list`
    UnaryOperator {
        unop: UnOp,
        expression: Box<Expression>,
    },
    
    /// An anonymous function, such as `function() end`
    Function(Box<AnonymousFunction>),
    
    /// A function call, such as `call()`
    FunctionCall(FunctionCall),
    
    /// A table constructor, such as `{ 1, 2, 3 }`
    TableConstructor(TableConstructor),
    
    /// A number token, such as `3.3`
    Number(TokenReference),
    
    /// A string token, such as `"hello"`
    String(TokenReference),
    
    /// A symbol, such as `true`
    Symbol(TokenReference),
    
    /// A more complex value, such as `call().x`
    Var(Var),
}
Notice how leaf nodes (like Number, String, Symbol) use TokenReference instead of plain values - this preserves formatting and comments!

Detailed Node Examples

If Statement

From src/ast/mod.rs:974-1096:
/// An if statement
pub struct If {
    if_token: TokenReference,
    condition: Expression,
    then_token: TokenReference,
    block: Block,
    else_if: Option<Vec<ElseIf>>,
    else_token: Option<TokenReference>,
    r#else: Option<Block>,
    end_token: TokenReference,
}

impl If {
    /// The `if` token
    pub fn if_token(&self) -> &TokenReference
    
    /// The condition of the if statement
    pub fn condition(&self) -> &Expression
    
    /// The `then` token
    pub fn then_token(&self) -> &TokenReference
    
    /// The block inside the initial if statement
    pub fn block(&self) -> &Block
    
    /// If there are `elseif` conditions, returns a vector of them
    pub fn else_if(&self) -> Option<&Vec<ElseIf>>
    
    /// The code inside an `else` block if one exists
    pub fn else_block(&self) -> Option<&Block>
}
Key insight: Every keyword token (if, then, else, end) is stored separately, allowing precise formatting control.

Numeric For Loop

From src/ast/mod.rs:588-772:
/// A numeric for loop, such as `for index = 1, 10 do end`
pub struct NumericFor {
    for_token: TokenReference,
    index_variable: TokenReference,
    equal_token: TokenReference,
    start: Expression,
    start_end_comma: TokenReference,
    end: Expression,
    end_step_comma: Option<TokenReference>,
    step: Option<Expression>,
    do_token: TokenReference,
    block: Block,
    end_token: TokenReference,
}
Even the commas are preserved as TokenReference objects!

Table Constructor

From src/ast/mod.rs:231-280:
/// A table being constructed, such as `{ 1, 2, 3 }` or `{ a = 1 }`
pub struct TableConstructor {
    braces: ContainedSpan,
    fields: Punctuated<Field>,
}

/// Fields of a [`TableConstructor`]
pub enum Field {
    /// A key in the format of `[expression] = value`
    ExpressionKey {
        brackets: ContainedSpan,
        key: Expression,
        equal: TokenReference,
        value: Expression,
    },
    
    /// A key in the format of `name = value`
    NameKey {
        key: TokenReference,
        equal: TokenReference,
        value: Expression,
    },
    
    /// A field with no key, just a value (such as `"a"` in `{ "a" }`)
    NoKey(Expression),
}

Working with AST Nodes

Accessing Node Data

use full_moon::parse;
use full_moon::ast::Stmt;

let ast = parse("local x = 1")?;

for stmt in ast.nodes().stmts() {
    match stmt {
        Stmt::LocalAssignment(local) => {
            println!("Found local assignment");
            for name in local.names() {
                println!("Variable: {}", name.token());
            }
        }
        _ => {}
    }
}

Builder Pattern

Most AST nodes support a builder pattern for construction (src/ast/mod.rs:608-771):
let numeric_for = NumericFor::new(
    TokenReference::basic_symbol("i"),
    Expression::Number(TokenReference::basic_symbol("1")),
    Expression::Number(TokenReference::basic_symbol("10")),
)
.with_step(Some(Expression::Number(
    TokenReference::basic_symbol("2")
)))
.with_block(Block::new());

Punctuated Sequences

Many AST nodes use the Punctuated type for comma-separated lists:
use full_moon::ast::punctuated::Punctuated;

// Function arguments: foo(a, b, c)
// Parameters: function f(x, y, z)
// Name lists: local a, b, c

pub struct Punctuated<T> {
    // Internal implementation preserves separators
}
This allows tracking the exact formatting of commas and spacing in lists.

ContainedSpan

From src/ast/span.rs, paired tokens (parentheses, brackets, braces) use ContainedSpan:
pub struct ContainedSpan {
    // Stores both the opening and closing tokens
    // Example: `(` and `)` in `(expression)`
}
This preserves:
  • Spacing inside parentheses: ( x ) vs (x)
  • Comments between brackets: { -- comment\n }

Node Traits

All AST nodes implement several derive macros from src/ast/mod.rs:52:
  • Clone - Nodes can be duplicated
  • Debug - Pretty printing for debugging
  • Display - Convert back to Lua code
  • PartialEq - Structural equality
  • Node - Common node interface
  • Visit - Visitor pattern support

Luau-Specific Nodes

When the luau feature is enabled, additional nodes are available:
#[cfg(feature = "luau")]
pub enum Stmt {
    // ... standard statements ...
    
    /// A type declaration, such as `type Meters = number`
    TypeDeclaration(TypeDeclaration),
    
    /// An exported type declaration
    ExportedTypeDeclaration(ExportedTypeDeclaration),
    
    /// A compound assignment, such as `x += 1`
    CompoundAssignment(CompoundAssignment),
}

AST Traversal Example

use full_moon::parse;
use full_moon::ast::Stmt;

fn count_function_calls(code: &str) -> usize {
    let ast = parse(code).unwrap();
    let mut count = 0;
    
    for stmt in ast.nodes().stmts() {
        if matches!(stmt, Stmt::FunctionCall(_)) {
            count += 1;
        }
    }
    
    count
}

let code = "print('hello')\nwarn('world')";
assert_eq!(count_function_calls(code), 2);
For more advanced traversal, use the Visitor pattern which provides callbacks for every node type.

AST Modification

Because AST nodes derive Clone, you can create modified versions:
let original_if = If::new(condition);
let modified_if = original_if
    .with_block(new_block)
    .with_else(Some(else_block));
The builder methods return new instances, making AST transformations functional and safe.

Key Design Principles

  1. Immutability: AST nodes are typically used immutably with builder patterns for modifications
  2. Exhaustive: Every syntactic element has a corresponding AST node
  3. Token-based: Leaf nodes use TokenReference to preserve formatting
  4. Type-safe: Rust’s type system ensures AST validity
  5. Feature-gated: Language-specific nodes (Luau, Lua 5.2+) are behind feature flags

Next Steps

Tokenization

Learn how source code becomes tokens

Visitors

Traverse and transform AST nodes

Build docs developers (and LLMs) love