Understanding the AST structure and node types in full-moon
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.
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!
From src/ast/mod.rs:438-507, the Stmt enum covers all Lua statements:
/// A statement that stands alonepub 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...}
From src/ast/mod.rs:350-436, expressions represent values:
/// An expression, mostly useful for getting valuespub 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!
/// An if statementpub 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.
/// 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),}
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()); } } _ => {} }}
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, cpub struct Punctuated<T> { // Internal implementation preserves separators}
This allows tracking the exact formatting of commas and spacing in lists.
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),}