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 );
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 );
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:
Call visit_*() on the node
Recursively visit all child nodes
Call visit_*_end() on the node
Example:
if condition then
print ( "hello" )
end
Visit order:
visit_if()
visit_expression() (the condition)
visit_block()
visit_function_call() (print)
visit_block_end()
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 );
}
}
Selective visiting : Only implement methods for nodes you care about
Early termination : Store a flag and check it to skip unnecessary work
Avoid cloning : Use Visitor (immutable) for analysis, not transformation
Batch modifications : Collect changes first, apply them in one pass
Common Patterns
#[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 ));
}
}
}
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
No early exit : Visitors must traverse the entire tree
Single dispatch : Can’t easily coordinate between parent and child visits
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