Skip to main content

Introduction

Syntax nodes are the building blocks of Loretta’s syntax trees. They represent the structural elements of Lua code, from entire files down to individual expressions and statements.

Syntax Node Hierarchy

All syntax nodes in Loretta inherit from the base class LuaSyntaxNode, which provides common functionality for working with the syntax tree.
LuaSyntaxNode (base class)
├── CompilationUnitSyntax (root node)
├── StatementSyntax (base for all statements)
│   ├── LocalVariableDeclarationStatementSyntax
│   ├── AssignmentStatementSyntax
│   ├── IfStatementSyntax
│   └── ...
├── ExpressionSyntax (base for all expressions)
│   ├── BinaryExpressionSyntax
│   ├── LiteralExpressionSyntax
│   ├── IdentifierNameSyntax
│   └── ...
├── TypeSyntax (base for type annotations)
│   ├── SimpleTypeNameSyntax
│   ├── FunctionTypeSyntax
│   ├── TableTypeSyntax
│   └── ...
└── Other supporting nodes

CompilationUnitSyntax - The Root Node

Every syntax tree starts with a CompilationUnitSyntax node, which represents the entire Lua source file:
var tree = LuaSyntaxTree.ParseText(sourceCode);
var root = tree.GetCompilationUnitRoot();

// Access the statements in the file
foreach (var statement in root.Statements.Statements)
{
    Console.WriteLine($"Statement kind: {statement.Kind()}");
}
Key properties:
  • Statements - A StatementListSyntax containing all top-level statements
  • EndOfFileToken - The end-of-file token marking the end of the file

Common Properties and Methods

All syntax nodes inherit these members from LuaSyntaxNode:
  • Parent - Gets the parent node in the tree
  • ChildNodes() - Gets all child nodes
  • Ancestors() - Gets all ancestor nodes
  • Descendants() - Gets all descendant nodes

Position and Location

  • Span - The text span this node covers
  • FullSpan - The span including leading and trailing trivia
  • GetLocation() - Gets the location information for this node

Trivia (Whitespace and Comments)

  • GetLeadingTrivia() - Gets whitespace/comments before this node
  • GetTrailingTrivia() - Gets whitespace/comments after this node

Token Access

  • GetFirstToken() - Gets the first token in this node
  • GetLastToken() - Gets the last token in this node
  • FindToken(position) - Finds a token at a specific position

Kind and Type

  • Kind() - Returns the SyntaxKind enum value for this node
  • IsKind(kind) - Tests if the node is of a specific kind

Tokens vs. Nodes vs. Trivia

Loretta’s syntax tree consists of three fundamental elements:

Syntax Nodes

Represent structural elements like statements and expressions. Nodes contain other nodes and tokens.
var ifStmt = (IfStatementSyntax)node;
Console.WriteLine($"Condition: {ifStmt.Condition}");
Console.WriteLine($"Body has {ifStmt.Body.Statements.Count} statements");

Syntax Tokens

Represent individual keywords, identifiers, operators, and literals.
var ifKeyword = ifStmt.IfKeyword;
Console.WriteLine($"Token text: {ifKeyword.Text}");
Console.WriteLine($"Token kind: {ifKeyword.Kind()}");

Syntax Trivia

Represent whitespace, comments, and other non-semantic content.
var trivia = node.GetLeadingTrivia();
foreach (var triviaItem in trivia)
{
    if (triviaItem.Kind() == SyntaxKind.SingleLineCommentTrivia)
    {
        Console.WriteLine($"Comment: {triviaItem.ToFullString()}");
    }
}

Traversing the Syntax Tree

You can walk the syntax tree in several ways:

Using Visitors

class MyVisitor : LuaSyntaxVisitor
{
    public override void VisitLocalVariableDeclarationStatement(
        LocalVariableDeclarationStatementSyntax node)
    {
        foreach (var name in node.Names)
        {
            Console.WriteLine($"Variable: {name.Name}");
        }
        base.VisitLocalVariableDeclarationStatement(node);
    }
}

var visitor = new MyVisitor();
visitor.Visit(root);

Using Walkers

class MyWalker : LuaSyntaxWalker
{
    public override void Visit(LuaSyntaxNode node)
    {
        Console.WriteLine($"{node.Kind()}: {node}");
        base.Visit(node);
    }
}

var walker = new MyWalker();
walker.Visit(root);

Manual Traversal

void TraverseNode(LuaSyntaxNode node, int depth = 0)
{
    var indent = new string(' ', depth * 2);
    Console.WriteLine($"{indent}{node.Kind()}");
    
    foreach (var child in node.ChildNodes())
    {
        TraverseNode(child, depth + 1);
    }
}

TraverseNode(root);

Using LINQ

// Find all function declarations
var functions = root.DescendantNodes()
    .OfType<FunctionDeclarationStatementSyntax>();

// Find all identifiers named "foo"
var fooIdentifiers = root.DescendantNodes()
    .OfType<IdentifierNameSyntax>()
    .Where(id => id.Name == "foo");

Immutability

Syntax nodes are immutable. To modify a tree, you must create new nodes:
// Create a new node with a different body
var newIfStatement = ifStatement.WithBody(newBody);

// Or use Update for multiple changes
var updated = ifStatement.Update(
    ifStatement.IfKeyword,
    newCondition,  // changed
    ifStatement.ThenKeyword,
    newBody,       // changed
    ifStatement.ElseIfClauses,
    ifStatement.ElseClause,
    ifStatement.EndKeyword,
    ifStatement.SemicolonToken
);

Working with Positions

Every node knows its position in the source text:
var node = root.FindNode(TextSpan.FromBounds(10, 20));
Console.WriteLine($"Node at position 10-20: {node.Kind()}");

var token = root.FindToken(15);
Console.WriteLine($"Token at position 15: {token.Text}");

See Also

Build docs developers (and LLMs) love