Skip to main content

Overview

Full Moon’s AST nodes are immutable by default, using a builder pattern for modifications. Each node has with_* methods that return new instances with updated values.

Builder Pattern Basics

All AST nodes use the builder pattern for modifications:
use full_moon::ast::*;

// Create a new return statement
let return_stmt = Return::new()
    .with_token(TokenReference::basic_symbol("return "))
    .with_returns(Punctuated::new());

Why Immutability?

Immutable AST nodes ensure thread safety and make it easier to reason about transformations. When you “modify” a node, you create a new instance with the changes applied.

Creating New Nodes

Building a Local Assignment

use full_moon::ast::*;
use full_moon::tokenizer::TokenReference;

// Create: local x = 1
let name = TokenReference::new(
    vec![], // leading trivia
    Token::new(TokenType::Identifier { 
        identifier: "x".into() 
    }),
    vec![], // trailing trivia
);

let value = Expression::Number(
    TokenReference::basic_symbol("1")
);

let local_assignment = LocalAssignment::new(Punctuated::from_iter([name]))
    .with_expressions(Punctuated::from_iter([value]));

Building a Function

use full_moon::ast::*;

// Create: function add(a, b) return a + b end
let params = Punctuated::from_iter([
    Parameter::Name(TokenReference::basic_symbol("a")),
    Parameter::Name(TokenReference::basic_symbol("b")),
]);

let body = FunctionBody::new()
    .with_parameters(params)
    .with_block(/* block with return statement */);

let func = AnonymousFunction::new()
    .with_body(body);

Modifying Existing Nodes

Updating a Block

use full_moon::parse;
use full_moon::ast::*;

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

// Get existing statements
let stmts: Vec<_> = block.stmts_with_semicolon().cloned().collect();

// Create a new block with modified statements
let new_block = Block::new()
    .with_stmts(stmts);

Changing Loop Parameters

Modify a numeric for loop:
use full_moon::ast::*;

// Original: for i = 1, 10 do end
let numeric_for = NumericFor::new(
    TokenReference::basic_symbol("i"),
    Expression::Number(TokenReference::basic_symbol("1")),
    Expression::Number(TokenReference::basic_symbol("10")),
);

// Change to: for i = 1, 100 do end
let modified = numeric_for
    .with_end(Expression::Number(TokenReference::basic_symbol("100")));

// Add step: for i = 1, 100, 2 do end
let with_step = modified
    .with_step(Some(Expression::Number(TokenReference::basic_symbol("2"))))
    .with_end_step_comma(Some(TokenReference::basic_symbol(", ")));

Working with Punctuated Lists

Many AST nodes contain Punctuated<T> for comma-separated lists:
use full_moon::ast::punctuated::Punctuated;
use full_moon::tokenizer::TokenReference;

// Create a punctuated list: a, b, c
let mut list = Punctuated::new();

list.push(Pair::new(
    TokenReference::basic_symbol("a"),
    Some(TokenReference::basic_symbol(", ")),
));

list.push(Pair::new(
    TokenReference::basic_symbol("b"),
    Some(TokenReference::basic_symbol(", ")),
));

list.push(Pair::End(
    TokenReference::basic_symbol("c"),
));

// Iterate over values
for value in list.iter() {
    println!("Value: {}", value);
}
The last element in a Punctuated list uses Pair::End (no comma), while others use Pair::Punctuated (with comma).

Modifying Table Constructors

use full_moon::ast::*;

// Create: { x = 1, y = 2 }
let mut fields = Punctuated::new();

fields.push(Pair::Punctuated(
    Field::NameKey {
        key: TokenReference::basic_symbol("x"),
        equal: TokenReference::basic_symbol(" = "),
        value: Expression::Number(TokenReference::basic_symbol("1")),
    },
    TokenReference::basic_symbol(", "),
));

fields.push(Pair::End(
    Field::NameKey {
        key: TokenReference::basic_symbol("y"),
        equal: TokenReference::basic_symbol(" = "),
        value: Expression::Number(TokenReference::basic_symbol("2")),
    },
));

let table = TableConstructor::new()
    .with_fields(fields);

Preserving Formatting

Using Token Trivia

Full Moon preserves whitespace and comments through “trivia”:
use full_moon::tokenizer::*;

// Create a token with leading/trailing whitespace
let token = TokenReference::new(
    vec![Token::new(TokenType::Whitespace { characters: "  ".into() })],
    Token::new(TokenType::Identifier { identifier: "x".into() }),
    vec![Token::new(TokenType::Whitespace { characters: " ".into() })],
);

Cloning Formatting

use full_moon::tokenizer::TokenReference;

// Clone formatting from an existing token
let original = TokenReference::basic_symbol("original");

// Create new token with same trivia
let leading = original.leading_trivia().cloned().collect();
let trailing = original.trailing_trivia().cloned().collect();

let new_token = TokenReference::new(
    leading,
    Token::new(TokenType::Identifier { identifier: "new".into() }),
    trailing,
);

Complex Transformations

Wrapping Expressions

Wrap an expression in parentheses:
use full_moon::ast::*;
use full_moon::ast::span::ContainedSpan;

fn wrap_in_parens(expr: Expression) -> Expression {
    Expression::Parentheses {
        contained: ContainedSpan::new(
            TokenReference::basic_symbol("("),
            TokenReference::basic_symbol(")"),
        ),
        expression: Box::new(expr),
    }
}

Converting Statements

use full_moon::ast::*;

// Convert local assignment to regular assignment
fn local_to_assignment(local: LocalAssignment) -> Option<Assignment> {
    // Extract variables and expressions
    let vars = local.names().iter().map(|name| {
        Var::Name(name.clone())
    }).collect();
    
    let exprs = local.expressions().clone();
    
    Some(Assignment::new(
        Punctuated::from_iter(vars),
        exprs,
    ))
}

Luau-Specific Features

When the luau feature is enabled, you can work with type annotations:
#[cfg(feature = "luau")]
use full_moon::ast::*;

// Add type annotation to function parameter
let type_specifier = TypeSpecifier::new(
    TokenReference::basic_symbol(": "),
    TypeInfo::Basic(TokenReference::basic_symbol("number")),
);

let body = FunctionBody::new()
    .with_type_specifiers(vec![Some(type_specifier)]);

Type Assertions

#[cfg(feature = "luau")]
use full_moon::ast::*;

// Create: value :: string
let assertion = TypeAssertion::new(
    TokenReference::basic_symbol("::"),
    TypeInfo::Basic(TokenReference::basic_symbol("string")),
);

let expr = Expression::TypeAssertion {
    expression: Box::new(/* original expression */),
    type_assertion: assertion,
};

Best Practices

1

Always Preserve Trivia

When creating new tokens, consider copying trivia from similar existing tokens to maintain formatting.
2

Use Builder Methods

Chain with_* methods for clean, readable transformations.
3

Test Round-Trip

After modifying an AST, convert it to string and parse again to verify validity.

Common Patterns

Adding a Statement to a Block

use full_moon::ast::*;

fn add_statement(block: Block, stmt: Stmt) -> Block {
    let mut stmts: Vec<_> = block.stmts_with_semicolon()
        .cloned()
        .collect();
    
    stmts.push((stmt, None)); // No semicolon
    
    block.with_stmts(stmts)
}

Replacing All Occurrences

Use visitors for systematic replacements (see Visitors Walkthrough).

Next Steps

Visitors

Learn to traverse and modify ASTs with visitors

Examples

See real-world AST transformation examples

Build docs developers (and LLMs) love