Skip to main content
The visitor pattern is Full Moon’s primary mechanism for traversing and transforming Abstract Syntax Trees. It provides a clean, type-safe way to process every node in a Lua AST.

Why Visitors?

Manually traversing an AST requires matching on every node type and recursing into child nodes. The visitor pattern automates this:
  • Automatic traversal: Visit all nodes without manual recursion
  • Type safety: Compiler ensures you handle all node types
  • Separation of concerns: Analysis logic separate from traversal logic
  • Composability: Multiple visitors can be combined

Two Visitor Traits

Full Moon provides two visitor traits (src/visitors.rs:28-158):

1. Visitor (Immutable)

/// A trait that implements functions to listen for specific nodes/tokens.
/// Unlike [`VisitorMut`], nodes/tokens passed are immutable.
pub trait Visitor {
    /// Visit the nodes of an [`Ast`](crate::ast::Ast)
    fn visit_ast(&mut self, ast: &Ast) where Self: Sized {
        ast.nodes().visit(self);
        ast.eof().visit(self);
    }
    
    // Node visitors
    fn visit_block(&mut self, _node: &Block) { }
    fn visit_block_end(&mut self, _node: &Block) { }
    
    fn visit_if(&mut self, _node: &If) { }
    fn visit_if_end(&mut self, _node: &If) { }
    
    fn visit_function_call(&mut self, _node: &FunctionCall) { }
    fn visit_function_call_end(&mut self, _node: &FunctionCall) { }
    
    // ... many more node types ...
    
    // Token visitors
    fn visit_token(&mut self, _token: &Token) { }
    fn visit_identifier(&mut self, _token: &Token) { }
    fn visit_number(&mut self, _token: &Token) { }
    fn visit_string_literal(&mut self, _token: &Token) { }
    // ... more token types ...
}

2. VisitorMut (Mutable)

/// A trait that implements functions to listen for specific nodes/tokens.
/// Unlike [`Visitor`], nodes/tokens passed are mutable.
pub trait VisitorMut {
    /// Visit the nodes of an [`Ast`](crate::ast::Ast)
    fn visit_ast(&mut self, ast: Ast) -> Ast where Self: Sized {
        let eof = ast.eof().to_owned();
        let nodes = ast.nodes.visit_mut(self);
        
        Ast {
            nodes,
            eof: self.visit_eof(eof),
        }
    }
    
    // Node visitors (return modified nodes)
    fn visit_block(&mut self, node: Block) -> Block { node }
    fn visit_block_end(&mut self, node: Block) -> Block { node }
    
    fn visit_if(&mut self, node: If) -> If { node }
    fn visit_if_end(&mut self, node: If) -> If { node }
    
    // ... many more node types ...
}
Each visitor method has an _end variant that’s called after visiting child nodes. This enables post-order traversal logic.

Example: Finding Local Variables

From src/visitors.rs:36-51, here’s a visitor that collects all local variable names:
use full_moon::ast;
use full_moon::visitors::*;

/// A visitor that logs every local assignment made
#[derive(Default)]
struct LocalVariableVisitor {
    names: Vec<String>,
}

impl Visitor for LocalVariableVisitor {
    fn visit_local_assignment(&mut self, local_assignment: &ast::LocalAssignment) {
        self.names.extend(
            local_assignment.names()
                .iter()
                .map(|name| name.token().to_string())
        );
    }
}

// Usage
let mut visitor = LocalVariableVisitor::default();
visitor.visit_ast(&full_moon::parse("local x = 1; local y, z = 2, 3")?);
assert_eq!(visitor.names, vec!["x", "y", "z"]);

Example: Counting Function Calls

use full_moon::visitors::*;
use full_moon::ast::*;

#[derive(Default)]
struct FunctionCallCounter {
    count: usize,
}

impl Visitor for FunctionCallCounter {
    fn visit_function_call(&mut self, _call: &FunctionCall) {
        self.count += 1;
    }
}

let code = r#"
    print("hello")
    warn("world")
    foo.bar:baz()
"#;

let ast = full_moon::parse(code)?;
let mut counter = FunctionCallCounter::default();
counter.visit_ast(&ast);
assert_eq!(counter.count, 3);

Example: Finding All Comments

use full_moon::visitors::*;
use full_moon::tokenizer::*;

#[derive(Default)]
struct CommentCollector {
    comments: Vec<String>,
}

impl Visitor for CommentCollector {
    fn visit_single_line_comment(&mut self, token: &Token) {
        if let TokenType::SingleLineComment { comment } = token.token_type() {
            self.comments.push(comment.to_string());
        }
    }
    
    fn visit_multi_line_comment(&mut self, token: &Token) {
        if let TokenType::MultiLineComment { comment, .. } = token.token_type() {
            self.comments.push(comment.to_string());
        }
    }
}

let code = r#"
    -- This is a comment
    local x = 1  -- inline comment
    --[[ Block comment ]]
"#;

let ast = full_moon::parse(code)?;
let mut collector = CommentCollector::default();
collector.visit_ast(&ast);
assert_eq!(collector.comments.len(), 3);

Transforming ASTs with VisitorMut

Example: Renaming Variables

use full_moon::visitors::*;
use full_moon::tokenizer::*;
use full_moon::ast::*;
use std::collections::HashMap;

struct VariableRenamer {
    renames: HashMap<String, String>,
}

impl VisitorMut for VariableRenamer {
    fn visit_local_assignment(&mut self, local: LocalAssignment) -> LocalAssignment {
        let names = local.names().clone();
        let new_names = names.into_pairs()
            .map(|pair| {
                pair.map(|name| {
                    let old_name = name.token().to_string();
                    if let Some(new_name) = self.renames.get(&old_name) {
                        TokenReference::new(
                            name.leading_trivia().cloned().collect(),
                            Token::new(TokenType::Identifier {
                                identifier: new_name.clone().into(),
                            }),
                            name.trailing_trivia().cloned().collect(),
                        )
                    } else {
                        name
                    }
                })
            })
            .collect();
        
        local.with_names(new_names)
    }
}

// Usage
let code = "local x = 1";
let ast = full_moon::parse(code)?;

let mut renamer = VariableRenamer {
    renames: [("x".to_string(), "y".to_string())].into(),
};

let new_ast = renamer.visit_ast(ast);
assert_eq!(new_ast.to_string(), "local y = 1");

Example: Adding Print Statements

use full_moon::visitors::*;
use full_moon::ast::*;

struct PrintInjector;

impl VisitorMut for PrintInjector {
    fn visit_function_body(&mut self, body: FunctionBody) -> FunctionBody {
        let mut block = body.block().clone();
        
        // Add print("Function called") at the start
        let print_stmt = /* construct a function call statement */;
        
        let mut stmts: Vec<_> = block.stmts_with_semicolon()
            .cloned()
            .collect();
        stmts.insert(0, (print_stmt, None));
        
        block = block.with_stmts(stmts);
        body.with_block(block)
    }
}

Available Visitor Methods

From src/visitors.rs:242-336, Full Moon provides visitors for:

AST Nodes

visit_anonymous_call(&mut self, _node: &FunctionArgs)
visit_anonymous_function(&mut self, _node: &AnonymousFunction)
visit_assignment(&mut self, _node: &Assignment)
visit_block(&mut self, _node: &Block)
visit_call(&mut self, _node: &Call)
visit_do(&mut self, _node: &Do)
visit_else_if(&mut self, _node: &ElseIf)
visit_expression(&mut self, _node: &Expression)
visit_field(&mut self, _node: &Field)
visit_function_body(&mut self, _node: &FunctionBody)
visit_function_call(&mut self, _node: &FunctionCall)
visit_function_declaration(&mut self, _node: &FunctionDeclaration)
visit_generic_for(&mut self, _node: &GenericFor)
visit_if(&mut self, _node: &If)
visit_index(&mut self, _node: &Index)
visit_local_assignment(&mut self, _node: &LocalAssignment)
visit_local_function(&mut self, _node: &LocalFunction)
visit_method_call(&mut self, _node: &MethodCall)
visit_numeric_for(&mut self, _node: &NumericFor)
visit_parameter(&mut self, _node: &Parameter)
visit_repeat(&mut self, _node: &Repeat)
visit_return(&mut self, _node: &Return)
visit_stmt(&mut self, _node: &Stmt)
visit_table_constructor(&mut self, _node: &TableConstructor)
visit_var(&mut self, _node: &Var)
visit_while(&mut self, _node: &While)
// ... and more

Tokens

visit_token(&mut self, _token: &Token)
visit_identifier(&mut self, _token: &Token)
visit_number(&mut self, _token: &Token)
visit_string_literal(&mut self, _token: &Token)
visit_whitespace(&mut self, _token: &Token)
visit_single_line_comment(&mut self, _token: &Token)
visit_multi_line_comment(&mut self, _token: &Token)
visit_symbol(&mut self, _token: &Token)

Feature-Gated Visitors

#[cfg(feature = "luau")]
visit_type_declaration(&mut self, _node: &TypeDeclaration)
visit_exported_type_declaration(&mut self, _node: &ExportedTypeDeclaration)
visit_if_expression(&mut self, _node: &IfExpression)
visit_interpolated_string(&mut self, _node: &InterpolatedString)
// ... more Luau types

#[cfg(any(feature = "lua52", feature = "luajit"))]
visit_goto(&mut self, _node: &Goto)
visit_label(&mut self, _node: &Label)

#[cfg(feature = "lua54")]
visit_attribute(&mut self, _node: &Attribute)

Visit Trait Implementation

From src/visitors.rs:162-173, the Visit trait enables the traversal:
#[doc(hidden)]
pub trait Visit: Sealed {
    fn visit<V: Visitor>(&self, visitor: &mut V);
}

#[doc(hidden)]
pub trait VisitMut: Sealed
where
    Self: Sized,
{
    fn visit_mut<V: VisitorMut>(self, visitor: &mut V) -> Self;
}
These traits are implemented for:
  • All AST nodes (via derive macros)
  • Vec<T> where T: Visit
  • Option<T> where T: Visit
  • Box<T> where T: Visit
  • Tuples (A, B) where both implement Visit

Traversal Order

Visitors use depth-first, pre-order traversal by default:
  1. Call visit_*() on the node
  2. Recursively visit all child nodes
  3. Call visit_*_end() on the node
Example:
if condition then
    print("hello")
end
Visit order:
  1. visit_if()
  2. visit_expression() (the condition)
  3. visit_block()
  4. visit_function_call() (print)
  5. visit_block_end()
  6. visit_if_end()

Combining Multiple Visitors

You can run multiple visitors sequentially:
let ast = full_moon::parse(code)?;

let mut counter = FunctionCallCounter::default();
counter.visit_ast(&ast);

let mut collector = CommentCollector::default();
collector.visit_ast(&ast);

let mut renamer = VariableRenamer { /* ... */ };
let new_ast = renamer.visit_ast(ast);
Or create a composite visitor:
struct CompositeVisitor {
    counter: FunctionCallCounter,
    collector: CommentCollector,
}

impl Visitor for CompositeVisitor {
    fn visit_function_call(&mut self, call: &FunctionCall) {
        self.counter.visit_function_call(call);
    }
    
    fn visit_single_line_comment(&mut self, token: &Token) {
        self.collector.visit_single_line_comment(token);
    }
}

Performance Tips

  1. Selective visiting: Only implement methods for nodes you care about
  2. Early termination: Store a flag and check it to skip unnecessary work
  3. Avoid cloning: Use Visitor (immutable) for analysis, not transformation
  4. Batch modifications: Collect changes first, apply them in one pass

Common Patterns

Pattern: Collecting Information

#[derive(Default)]
struct InfoCollector {
    data: Vec<SomeData>,
}

impl Visitor for InfoCollector {
    fn visit_some_node(&mut self, node: &SomeNode) {
        self.data.push(extract_info(node));
    }
}

Pattern: Validation

struct Validator {
    errors: Vec<String>,
}

impl Visitor for Validator {
    fn visit_some_node(&mut self, node: &SomeNode) {
        if !is_valid(node) {
            self.errors.push(format!("Invalid node at {:?}", node));
        }
    }
}

Pattern: Transformation

struct Transformer {
    changes: usize,
}

impl VisitorMut for Transformer {
    fn visit_some_node(&mut self, node: SomeNode) -> SomeNode {
        if should_transform(&node) {
            self.changes += 1;
            transform_node(node)
        } else {
            node
        }
    }
}

Advanced: Stateful Visitors

Track context while traversing:
struct ScopeTracker {
    scope_depth: usize,
    variables: HashMap<String, usize>,
}

impl Visitor for ScopeTracker {
    fn visit_function_body(&mut self, _body: &FunctionBody) {
        self.scope_depth += 1;
    }
    
    fn visit_function_body_end(&mut self, _body: &FunctionBody) {
        self.scope_depth -= 1;
    }
    
    fn visit_local_assignment(&mut self, local: &LocalAssignment) {
        for name in local.names() {
            self.variables.insert(
                name.token().to_string(),
                self.scope_depth,
            );
        }
    }
}
The _end methods are perfect for maintaining scope or state that should be restored after visiting a subtree.

Limitations

  1. No early exit: Visitors must traverse the entire tree
  2. Single dispatch: Can’t easily coordinate between parent and child visits
  3. Cloning overhead: VisitorMut clones nodes during transformation
For more control, consider manual AST traversal:
fn custom_traverse(expr: &Expression) {
    match expr {
        Expression::BinaryOperator { lhs, rhs, .. } => {
            custom_traverse(lhs);
            custom_traverse(rhs);
        }
        Expression::FunctionCall(call) => {
            // Custom logic here
        }
        _ => {}
    }
}

Next Steps

AST Structure

Understand the nodes you’re visiting

Lossless Parsing

Learn how visitors preserve formatting

Build docs developers (and LLMs) love