Skip to main content
Loretta provides SyntaxFactory to create syntax nodes, tokens, and trivia programmatically. This allows you to build Lua code from scratch or construct nodes for tree transformations.

Understanding SyntaxFactory

SyntaxFactory is a static class that provides factory methods for creating all syntax elements:
using Loretta.CodeAnalysis.Lua;
using Loretta.CodeAnalysis.Lua.Syntax;

Creating Simple Tokens

Start with the building blocks - tokens:
// Create keyword tokens
var localKeyword = SyntaxFactory.Token(SyntaxKind.LocalKeyword);
var functionKeyword = SyntaxFactory.Token(SyntaxKind.FunctionKeyword);

// Create punctuation
var equals = SyntaxFactory.Token(SyntaxKind.EqualsToken);
var comma = SyntaxFactory.Token(SyntaxKind.CommaToken);

Creating Identifiers

Identifiers require special methods:
// Create an identifier token
var identifier = SyntaxFactory.Identifier("myVariable");

// Create an identifier name (a node)
var identifierName = SyntaxFactory.IdentifierName("myVariable");

Creating Literals

For literal values:
// Numeric literals
var numberToken = SyntaxFactory.Literal(42);
var floatToken = SyntaxFactory.Literal(3.14);

// String literals
var stringToken = SyntaxFactory.Literal("hello");

// Boolean literals (keywords)
var trueToken = SyntaxFactory.Token(SyntaxKind.TrueKeyword);
var falseToken = SyntaxFactory.Token(SyntaxKind.FalseKeyword);

Building Expressions

Literal Expressions

// Create a numeric literal expression
var numberExpr = SyntaxFactory.LiteralExpression(
    SyntaxKind.NumericLiteralExpression,
    SyntaxFactory.Literal(10)
);

// Create a string literal expression
var stringExpr = SyntaxFactory.LiteralExpression(
    SyntaxKind.StringLiteralExpression,
    SyntaxFactory.Literal("hello")
);

Binary Expressions

// Create: x + 10
var addition = SyntaxFactory.BinaryExpression(
    SyntaxKind.AddExpression,
    SyntaxFactory.IdentifierName("x"),
    SyntaxFactory.Token(SyntaxKind.PlusToken),
    SyntaxFactory.LiteralExpression(
        SyntaxKind.NumericLiteralExpression,
        SyntaxFactory.Literal(10)
    )
);

Function Calls

// Create: print("hello")
var printCall = SyntaxFactory.FunctionCallExpression(
    SyntaxFactory.IdentifierName("print"),
    SyntaxFactory.FunctionArgumentList(
        SyntaxFactory.SeparatedList<ExpressionSyntax>(new[] {
            SyntaxFactory.LiteralExpression(
                SyntaxKind.StringLiteralExpression,
                SyntaxFactory.Literal("hello")
            )
        })
    )
);

Member Access

// Create: string.format
var memberAccess = SyntaxFactory.MemberAccessExpression(
    SyntaxFactory.IdentifierName("string"),
    SyntaxFactory.Token(SyntaxKind.DotToken),
    SyntaxFactory.IdentifierName("format")
);

// Create: string.format("x = %d", x)
var formatCall = SyntaxFactory.FunctionCallExpression(
    memberAccess,
    SyntaxFactory.FunctionArgumentList(
        SyntaxFactory.SeparatedList<ExpressionSyntax>(new[] {
            SyntaxFactory.LiteralExpression(
                SyntaxKind.StringLiteralExpression,
                SyntaxFactory.Literal("x = %d")
            ),
            SyntaxFactory.IdentifierName("x")
        })
    )
);

Building Statements

Local Variable Declaration

// Create: local x = 10
var localDecl = SyntaxFactory.LocalVariableDeclarationStatement(
    SyntaxFactory.SeparatedList(new[] {
        SyntaxFactory.LocalDeclarationName(
            SyntaxFactory.IdentifierName("x")
        )
    }),
    SyntaxFactory.SeparatedList<ExpressionSyntax>(new[] {
        SyntaxFactory.LiteralExpression(
            SyntaxKind.NumericLiteralExpression,
            SyntaxFactory.Literal(10)
        )
    })
);

Assignment Statement

// Create: x = 20
var assignment = SyntaxFactory.AssignmentStatement(
    SyntaxFactory.SeparatedList<PrefixExpressionSyntax>(new[] {
        SyntaxFactory.IdentifierName("x")
    }),
    SyntaxFactory.SeparatedList<ExpressionSyntax>(new[] {
        SyntaxFactory.LiteralExpression(
            SyntaxKind.NumericLiteralExpression,
            SyntaxFactory.Literal(20)
        )
    })
);

Function Declaration

// Create: function add(a, b) return a + b end
var functionDecl = SyntaxFactory.FunctionDeclarationStatement(
    SyntaxFactory.Token(SyntaxKind.FunctionKeyword),
    SyntaxFactory.FunctionName(SyntaxFactory.IdentifierName("add")),
    SyntaxFactory.ParameterList(
        SyntaxFactory.SeparatedList<ParameterSyntax>(new[] {
            SyntaxFactory.NamedParameter(SyntaxFactory.Identifier("a")),
            SyntaxFactory.NamedParameter(SyntaxFactory.Identifier("b"))
        })
    ),
    SyntaxFactory.StatementList(
        SyntaxFactory.SingletonList<StatementSyntax>(
            SyntaxFactory.ReturnStatement(
                SyntaxFactory.SeparatedList<ExpressionSyntax>(new[] {
                    SyntaxFactory.BinaryExpression(
                        SyntaxKind.AddExpression,
                        SyntaxFactory.IdentifierName("a"),
                        SyntaxFactory.Token(SyntaxKind.PlusToken),
                        SyntaxFactory.IdentifierName("b")
                    )
                })
            )
        )
    ),
    SyntaxFactory.Token(SyntaxKind.EndKeyword)
);

Working with Lists

Loretta uses SyntaxList and SeparatedSyntaxList for collections:

SyntaxList

For lists without separators (like statement lists):
// Create a list of statements
var statements = SyntaxFactory.List(new StatementSyntax[] {
    localDecl,
    assignment
});

// Or use SingletonList for a single item
var singleStatement = SyntaxFactory.SingletonList<StatementSyntax>(localDecl);

SeparatedSyntaxList

For lists with separators (like argument lists):
// Create a separated list
var arguments = SyntaxFactory.SeparatedList<ExpressionSyntax>(new[] {
    SyntaxFactory.LiteralExpression(
        SyntaxKind.NumericLiteralExpression,
        SyntaxFactory.Literal(1)
    ),
    SyntaxFactory.LiteralExpression(
        SyntaxKind.NumericLiteralExpression,
        SyntaxFactory.Literal(2)
    )
});

// Separators (commas) are added automatically

Formatting Generated Code

Code created with SyntaxFactory has no whitespace by default. Use NormalizeWhitespace to add formatting:
// Without normalization
var ugly = localDecl.ToFullString();
// Output: "localx=10"

// With normalization
var pretty = localDecl.NormalizeWhitespace().ToFullString();
// Output: "local x = 10"

Custom Formatting

Control formatting with options:
// Use tabs instead of spaces
var formatted = localDecl.NormalizeWhitespace(
    indentation: "\t",
    eol: "\n"
);

Adding Trivia

Add comments, whitespace, and line breaks:
// Create trivia
var comment = SyntaxFactory.Comment("-- This is a comment");
var newLine = SyntaxFactory.EndOfLine("\n");
var space = SyntaxFactory.Whitespace(" ");

// Add to tokens
var tokenWithComment = SyntaxFactory.Token(
    SyntaxFactory.TriviaList(comment, newLine),
    SyntaxKind.LocalKeyword,
    SyntaxFactory.TriviaList(space)
);

// Add to nodes
var nodeWithTrivia = localDecl
    .WithLeadingTrivia(comment, newLine)
    .WithTrailingTrivia(newLine);

ToFullString vs ToString

Two methods convert nodes to text:
var node = localDecl
    .WithLeadingTrivia(SyntaxFactory.Comment("-- comment"))
    .WithTrailingTrivia(SyntaxFactory.EndOfLine("\n"));

// ToString() excludes trivia
var withoutTrivia = node.ToString();
// Output: "local x = 10"

// ToFullString() includes trivia
var withTrivia = node.ToFullString();
// Output: "-- comment\nlocal x = 10\n"
Always use ToFullString() when generating complete code files. Use ToString() when you only need the node’s text without surrounding whitespace or comments.

Complete Example: Generating a Lua Module

Here’s a complete example that generates a Lua module:
using Loretta.CodeAnalysis.Lua;
using Loretta.CodeAnalysis.Lua.Syntax;

public static class ModuleGenerator
{
    public static string GenerateModule(string moduleName, 
        IEnumerable<(string name, int value)> constants)
    {
        var statements = new List<StatementSyntax>();

        // Create: local M = {}
        statements.Add(
            SyntaxFactory.LocalVariableDeclarationStatement(
                SyntaxFactory.SeparatedList(new[] {
                    SyntaxFactory.LocalDeclarationName(
                        SyntaxFactory.IdentifierName("M")
                    )
                }),
                SyntaxFactory.SeparatedList<ExpressionSyntax>(new[] {
                    SyntaxFactory.TableConstructorExpression(
                        SyntaxFactory.Token(SyntaxKind.OpenBraceToken),
                        default,
                        SyntaxFactory.Token(SyntaxKind.CloseBraceToken)
                    )
                })
            )
        );

        // Add constants: M.NAME = value
        foreach (var (name, value) in constants)
        {
            statements.Add(
                SyntaxFactory.AssignmentStatement(
                    SyntaxFactory.SeparatedList<PrefixExpressionSyntax>(new[] {
                        SyntaxFactory.MemberAccessExpression(
                            SyntaxFactory.IdentifierName("M"),
                            SyntaxFactory.Token(SyntaxKind.DotToken),
                            SyntaxFactory.IdentifierName(name)
                        )
                    }),
                    SyntaxFactory.SeparatedList<ExpressionSyntax>(new[] {
                        SyntaxFactory.LiteralExpression(
                            SyntaxKind.NumericLiteralExpression,
                            SyntaxFactory.Literal(value)
                        )
                    })
                )
            );
        }

        // Create: return M
        statements.Add(
            SyntaxFactory.ReturnStatement(
                SyntaxFactory.SeparatedList<ExpressionSyntax>(new[] {
                    SyntaxFactory.IdentifierName("M")
                })
            )
        );

        // Create compilation unit
        var compilationUnit = SyntaxFactory.CompilationUnit(
            SyntaxFactory.StatementList(SyntaxFactory.List(statements)),
            SyntaxFactory.Token(SyntaxKind.EndOfFileToken)
        );

        // Normalize and return
        return compilationUnit.NormalizeWhitespace().ToFullString();
    }
}

// Usage
var code = ModuleGenerator.GenerateModule(
    "Constants",
    new[] {
        ("VERSION", 1),
        ("MAX_SIZE", 100),
        ("TIMEOUT", 30)
    }
);

Console.WriteLine(code);
Output:
local M = {}
M.VERSION = 1
M.MAX_SIZE = 100
M.TIMEOUT = 30
return M

Builder Pattern

For complex node construction, consider using a builder pattern:
public class LocalVariableBuilder
{
    private readonly List<string> _names = new();
    private readonly List<ExpressionSyntax> _values = new();

    public LocalVariableBuilder AddVariable(string name, ExpressionSyntax? value = null)
    {
        _names.Add(name);
        if (value != null)
            _values.Add(value);
        return this;
    }

    public LocalVariableDeclarationStatementSyntax Build()
    {
        var names = SyntaxFactory.SeparatedList(
            _names.Select(n => SyntaxFactory.LocalDeclarationName(
                SyntaxFactory.IdentifierName(n)
            ))
        );

        var values = _values.Count > 0
            ? SyntaxFactory.SeparatedList(_values)
            : default;

        return SyntaxFactory.LocalVariableDeclarationStatement(names, values);
    }
}

// Usage
var decl = new LocalVariableBuilder()
    .AddVariable("x", SyntaxFactory.LiteralExpression(
        SyntaxKind.NumericLiteralExpression,
        SyntaxFactory.Literal(10)
    ))
    .AddVariable("y", SyntaxFactory.LiteralExpression(
        SyntaxKind.NumericLiteralExpression,
        SyntaxFactory.Literal(20)
    ))
    .Build();

Next Steps

Build docs developers (and LLMs) love