Skip to main content

Parsing Lua Code

Loretta provides a robust parser that converts Lua source code into syntax trees. The parser handles error recovery, allowing you to get a syntax tree even from invalid code.

Basic Parsing

The simplest way to parse Lua code is using LuaSyntaxTree.ParseText:
using Loretta.CodeAnalysis.Lua;

var code = "local x = 10";
var tree = LuaSyntaxTree.ParseText(code);

var root = tree.GetRoot();
Console.WriteLine(root.ToFullString());

ParseText Methods

LuaSyntaxTree.ParseText has two main overloads:

Parsing from String

public static SyntaxTree ParseText(
    string text,
    LuaParseOptions? options = null,
    string path = "",
    Encoding? encoding = null,
    CancellationToken cancellationToken = default)
Example:
var code = "print('Hello, World!')";
var tree = LuaSyntaxTree.ParseText(
    text: code,
    options: new LuaParseOptions(LuaSyntaxOptions.Lua54),
    path: "script.lua"
);

Parsing from SourceText

public static SyntaxTree ParseText(
    SourceText text,
    LuaParseOptions? options = null,
    string path = "",
    CancellationToken cancellationToken = default)
Example:
using Loretta.CodeAnalysis.Text;

var code = "local x = 10";
var sourceText = SourceText.From(code, Encoding.UTF8);
var tree = LuaSyntaxTree.ParseText(sourceText);

SourceText

SourceText is an abstraction over source code that provides efficient access to text content and line information. It’s particularly useful for:
  • Large files: Efficient handling of large source files
  • Encoding: Preserving encoding information
  • Line mapping: Fast line/column lookups
  • Incremental parsing: Reusing parsed trees when text changes

Creating SourceText

using Loretta.CodeAnalysis.Text;

// From string
var sourceText = SourceText.From("local x = 10");

// From string with encoding
var sourceTextWithEncoding = SourceText.From(
    "local x = 10",
    Encoding.UTF8
);

// From stream
using var stream = File.OpenRead("script.lua");
var sourceTextFromStream = SourceText.From(stream, Encoding.UTF8);

Working with SourceText

var sourceText = SourceText.From("local x = 10\nprint(x)");

// Get length
Console.WriteLine($"Length: {sourceText.Length}");

// Get a character at position
char ch = sourceText[6]; // 'x'

// Get line information
var lines = sourceText.Lines;
Console.WriteLine($"Line count: {lines.Count}");

// Get line and column from position
var linePosition = sourceText.Lines.GetLinePosition(6);
Console.WriteLine($"Position 6 is at line {linePosition.Line}, column {linePosition.Character}");

// Get text span for a line
var firstLine = sourceText.Lines[0];
var firstLineText = sourceText.ToString(firstLine.Span);
Console.WriteLine($"First line: {firstLineText}");

LuaParseOptions

LuaParseOptions controls how the parser interprets Lua syntax. It wraps LuaSyntaxOptions to specify which Lua version or dialect to parse.
public sealed class LuaParseOptions
{
    public LuaSyntaxOptions SyntaxOptions { get; }
}

Creating Parse Options

using Loretta.CodeAnalysis.Lua;

// Use default options (accepts all syntax)
var defaultOptions = LuaParseOptions.Default;

// Create options for a specific Lua version
var lua54Options = new LuaParseOptions(LuaSyntaxOptions.Lua54);
var luauOptions = new LuaParseOptions(LuaSyntaxOptions.Luau);
var gmodOptions = new LuaParseOptions(LuaSyntaxOptions.GMod);

Changing Options

var options = new LuaParseOptions(LuaSyntaxOptions.Lua51);

// Create new options with different syntax options
var newOptions = options.WithSyntaxOptions(LuaSyntaxOptions.Lua54);

LuaSyntaxOptions and LuaParseOptions

LuaParseOptions contains LuaSyntaxOptions, which defines what language features are accepted:
var syntaxOptions = LuaSyntaxOptions.Lua54;
var parseOptions = new LuaParseOptions(syntaxOptions);

var tree = LuaSyntaxTree.ParseText(
    "local x <const> = 10", // Lua 5.4 constant attribute
    parseOptions
);

// The tree will parse without errors because Lua 5.4 supports attributes
var diagnostics = tree.GetDiagnostics();
Console.WriteLine($"Errors: {diagnostics.Count()}");
See Lua Versions for more details on syntax options.

Parsing with Different Lua Versions

Lua 5.1

var code = "local x = 10";
var tree = LuaSyntaxTree.ParseText(
    code,
    new LuaParseOptions(LuaSyntaxOptions.Lua51)
);

Lua 5.4 with Attributes

var code = "local x <const> = 10";
var tree = LuaSyntaxTree.ParseText(
    code,
    new LuaParseOptions(LuaSyntaxOptions.Lua54)
);

Luau (Roblox)

var code = @"
local x: number = 10
local y = if x > 5 then 'big' else 'small'
";

var tree = LuaSyntaxTree.ParseText(
    code,
    new LuaParseOptions(LuaSyntaxOptions.Luau)
);

GLua (Garry’s Mod)

var code = @"
// C-style comment
local x = 10
if x > 5 && x < 15 then
    print('in range')
end
";

var tree = LuaSyntaxTree.ParseText(
    code,
    new LuaParseOptions(LuaSyntaxOptions.GMod)
);

Error Recovery

Loretta’s parser uses error recovery to produce a syntax tree even when the code has syntax errors. This is useful for tools like IDEs that need to analyze incomplete or invalid code.
// Code with syntax error (missing 'end')
var code = @"
local function test()
    print('hello')
-- missing 'end'
";

var tree = LuaSyntaxTree.ParseText(code);
var root = tree.GetRoot();

// The tree is still created
Console.WriteLine($"Got root: {root != null}");

// But diagnostics will show the error
var diagnostics = tree.GetDiagnostics();
foreach (var diagnostic in diagnostics)
{
    Console.WriteLine($"{diagnostic.Severity}: {diagnostic.GetMessage()}");
}
Even with syntax errors, you’ll get a complete syntax tree. The parser inserts “missing” tokens where necessary and reports diagnostics for the errors.

Incremental Parsing

When you modify a SourceText, Loretta can reuse parts of the existing syntax tree for better performance:
var originalText = SourceText.From("local x = 10\nprint(x)");
var tree = LuaSyntaxTree.ParseText(originalText);

// Make a small change to the text
var newText = originalText.WithChanges(new TextChange(
    new TextSpan(10, 2),  // Replace "10"
    "20"                   // With "20"
));

// Parse with the new text - this will reuse parts of the old tree
var newTree = tree.WithChangedText(newText);

Complete Parsing Example

Here’s a complete example showing various parsing scenarios:
using Loretta.CodeAnalysis.Lua;
using Loretta.CodeAnalysis.Lua.Syntax;
using Loretta.CodeAnalysis.Text;
using System.Text;

class ParsingExample
{
    static void Main()
    {
        // Example 1: Simple parsing
        SimpleParsingExample();
        
        // Example 2: Parsing with options
        ParsingWithOptionsExample();
        
        // Example 3: Parsing with SourceText
        ParsingWithSourceTextExample();
        
        // Example 4: Error recovery
        ErrorRecoveryExample();
    }
    
    static void SimpleParsingExample()
    {
        var code = "local x = 10";
        var tree = LuaSyntaxTree.ParseText(code);
        var root = tree.GetRoot();
        
        Console.WriteLine("Parsed successfully!");
        Console.WriteLine(root.ToFullString());
    }
    
    static void ParsingWithOptionsExample()
    {
        // Parse Luau code with type annotations
        var code = "local x: number = 10";
        var options = new LuaParseOptions(LuaSyntaxOptions.Luau);
        var tree = LuaSyntaxTree.ParseText(code, options);
        
        var diagnostics = tree.GetDiagnostics();
        if (!diagnostics.Any())
        {
            Console.WriteLine("Luau code parsed without errors");
        }
    }
    
    static void ParsingWithSourceTextExample()
    {
        var code = @"
local function greet(name)
    print('Hello, ' .. name)
end

greet('World')
";
        
        var sourceText = SourceText.From(code, Encoding.UTF8);
        var tree = LuaSyntaxTree.ParseText(
            sourceText,
            new LuaParseOptions(LuaSyntaxOptions.Lua54),
            "greet.lua"
        );
        
        Console.WriteLine($"File: {tree.FilePath}");
        Console.WriteLine($"Lines: {sourceText.Lines.Count}");
    }
    
    static void ErrorRecoveryExample()
    {
        // Intentionally broken code
        var code = @"
local function broken(
    -- missing closing parenthesis and end
    print('oops')
";
        
        var tree = LuaSyntaxTree.ParseText(code);
        var root = tree.GetRoot();
        
        Console.WriteLine("Tree created despite errors:");
        Console.WriteLine($"Root type: {root.GetType().Name}");
        
        Console.WriteLine("\nDiagnostics:");
        foreach (var diagnostic in tree.GetDiagnostics())
        {
            var span = diagnostic.Location.SourceSpan;
            Console.WriteLine($"  {diagnostic.Severity} at {span}: {diagnostic.GetMessage()}");
        }
    }
}

Best Practices

Use SourceText for File I/O

When reading from files, use SourceText.From with a stream to preserve encoding:
using var stream = File.OpenRead("script.lua");
var sourceText = SourceText.From(stream, Encoding.UTF8);
var tree = LuaSyntaxTree.ParseText(sourceText, path: "script.lua");

Specify the Correct Lua Version

Always specify parse options that match your target Lua version to get accurate diagnostics:
// For Lua 5.1 code
var tree = LuaSyntaxTree.ParseText(
    code,
    new LuaParseOptions(LuaSyntaxOptions.Lua51)
);

// For Luau/Roblox code
var tree = LuaSyntaxTree.ParseText(
    code,
    new LuaParseOptions(LuaSyntaxOptions.Luau)
);

Check Diagnostics After Parsing

Always check for diagnostics after parsing to handle syntax errors:
var tree = LuaSyntaxTree.ParseText(code);
var diagnostics = tree.GetDiagnostics().ToList();

if (diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error))
{
    Console.WriteLine("Parse errors found:");
    foreach (var diagnostic in diagnostics)
    {
        Console.WriteLine($"  {diagnostic.GetMessage()}");
    }
}

See Also

Build docs developers (and LLMs) love