Skip to main content
Loretta’s parser includes robust error recovery and diagnostic reporting. This guide shows you how to check for errors, filter diagnostics, and report them to users.

Understanding Diagnostics

In Loretta (like Roslyn), errors, warnings, and informational messages are called diagnostics. A diagnostic represents an issue found during parsing or analysis.

Diagnostic Properties

Each diagnostic contains:
using Loretta.CodeAnalysis;

var diagnostic = diagnostics.First();

// The diagnostic ID (e.g., "LUA0001")
var id = diagnostic.Id;

// The severity level
var severity = diagnostic.Severity;
// DiagnosticSeverity.Error
// DiagnosticSeverity.Warning
// DiagnosticSeverity.Info
// DiagnosticSeverity.Hidden

// The location in source
var location = diagnostic.Location;
var span = location.SourceSpan;
var lineSpan = location.GetLineSpan();

// The message
var message = diagnostic.GetMessage();

// The descriptor (the "definition" of this diagnostic)
var descriptor = diagnostic.Descriptor;

Checking for Parse Errors

After parsing, check if the syntax tree contains errors:
using Loretta.CodeAnalysis;
using Loretta.CodeAnalysis.Lua;
using Loretta.CodeAnalysis.Text;

var code = "local x = " // Missing value
var syntaxTree = LuaSyntaxTree.ParseText(code);

// Get all diagnostics
var diagnostics = syntaxTree.GetDiagnostics();

// Check for errors
var hasErrors = diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error);

if (hasErrors)
{
    Console.WriteLine("Code contains errors!");
}

Quick Error Check

// Simple boolean check
if (syntaxTree.GetDiagnostics().Any(
    d => d.Severity == DiagnosticSeverity.Error))
{
    return;
}

// Or count errors
var errorCount = syntaxTree.GetDiagnostics()
    .Count(d => d.Severity == DiagnosticSeverity.Error);

Console.WriteLine($"Found {errorCount} error(s)");

Filtering Diagnostics by Severity

Filter diagnostics to show only specific severities:
var allDiagnostics = syntaxTree.GetDiagnostics();

// Get only errors
var errors = allDiagnostics
    .Where(d => d.Severity == DiagnosticSeverity.Error);

// Get warnings
var warnings = allDiagnostics
    .Where(d => d.Severity == DiagnosticSeverity.Warning);

// Get errors and warnings
var errorsAndWarnings = allDiagnostics
    .Where(d => d.Severity == DiagnosticSeverity.Error ||
                d.Severity == DiagnosticSeverity.Warning);

// Order by severity (errors first)
var ordered = allDiagnostics
    .OrderByDescending(d => d.Severity);

Getting Diagnostic Location

Diagnostics include precise location information for error reporting:

Source Span

var location = diagnostic.Location;

// Get the text span (character positions)
var span = location.SourceSpan;
var start = span.Start;
var end = span.End;
var length = span.Length;

Console.WriteLine($"Error at positions {start} to {end}");

Line and Column Position

// Get line/column information
var lineSpan = location.GetLineSpan();
var startPos = lineSpan.StartLinePosition;
var endPos = lineSpan.EndLinePosition;

Console.WriteLine(
    $"Error at line {startPos.Line + 1}, " +
    $"column {startPos.Character + 1}"
);

// Full position string
Console.WriteLine(
    $"Error from {startPos.Line + 1}:{startPos.Character + 1} " +
    $"to {endPos.Line + 1}:{endPos.Character + 1}"
);
Line and character positions are 0-based. Add 1 when displaying to users for 1-based line numbers.

Getting Source Text

Retrieve the text that caused the error:
var sourceText = syntaxTree.GetText();
var errorText = sourceText.GetSubText(diagnostic.Location.SourceSpan);

Console.WriteLine($"Problematic code: {errorText}");

Displaying Diagnostics

The simplest way to display diagnostics:
foreach (var diagnostic in syntaxTree.GetDiagnostics())
{
    // ToString() provides formatted output
    Console.WriteLine(diagnostic.ToString());
}
Output:
script.lua(1,11): error LUA0001: Expected expression

Custom Error Reporting

Build custom error messages for better user experience:
public static void ReportDiagnostics(
    SyntaxTree syntaxTree,
    IEnumerable<Diagnostic> diagnostics)
{
    var sourceText = syntaxTree.GetText();

    foreach (var diagnostic in diagnostics)
    {
        // Get location info
        var lineSpan = diagnostic.Location.GetLineSpan();
        var startPos = lineSpan.StartLinePosition;
        var line = sourceText.Lines[startPos.Line];

        // Format severity
        var severityText = diagnostic.Severity switch
        {
            DiagnosticSeverity.Error => "error",
            DiagnosticSeverity.Warning => "warning",
            DiagnosticSeverity.Info => "info",
            _ => "message"
        };

        // Print diagnostic
        Console.ForegroundColor = diagnostic.Severity switch
        {
            DiagnosticSeverity.Error => ConsoleColor.Red,
            DiagnosticSeverity.Warning => ConsoleColor.Yellow,
            _ => ConsoleColor.White
        };

        Console.WriteLine(
            $"{syntaxTree.FilePath}({startPos.Line + 1},{startPos.Character + 1}): " +
            $"{severityText} {diagnostic.Id}: {diagnostic.GetMessage()}"
        );

        // Print source line
        Console.ResetColor();
        Console.WriteLine(line.ToString());

        // Print caret (^) under the error
        var prefix = new string(' ', startPos.Character);
        var span = diagnostic.Location.SourceSpan;
        var squiggles = new string('^', Math.Max(1, span.Length));
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine(prefix + squiggles);
        Console.ResetColor();
        Console.WriteLine();
    }
}
Usage:
var diagnostics = syntaxTree.GetDiagnostics();
ReportDiagnostics(syntaxTree, diagnostics);
Output:
script.lua(2,11): error LUA0001: Expected expression
local x = 
          ^

Complete Example: Diagnostic Collection and Display

Here’s a complete example showing diagnostic collection and reporting:
using System;
using System.Linq;
using Loretta.CodeAnalysis;
using Loretta.CodeAnalysis.Lua;
using Loretta.CodeAnalysis.Text;

public class DiagnosticReporter
{
    public static bool ParseAndReport(string filePath)
    {
        // Read the file
        if (!File.Exists(filePath))
        {
            Console.WriteLine($"File not found: {filePath}");
            return false;
        }

        SourceText sourceText;
        using (var stream = File.OpenRead(filePath))
        {
            sourceText = SourceText.From(stream);
        }

        // Parse
        var parseOptions = new LuaParseOptions(LuaSyntaxOptions.Lua51);
        var syntaxTree = LuaSyntaxTree.ParseText(
            sourceText,
            parseOptions,
            path: filePath
        );

        // Get diagnostics
        var diagnostics = syntaxTree.GetDiagnostics().ToList();

        if (diagnostics.Count == 0)
        {
            Console.WriteLine("No issues found.");
            return true;
        }

        // Group by severity
        var errors = diagnostics
            .Where(d => d.Severity == DiagnosticSeverity.Error)
            .ToList();
        var warnings = diagnostics
            .Where(d => d.Severity == DiagnosticSeverity.Warning)
            .ToList();

        // Report summary
        Console.WriteLine(
            $"Found {errors.Count} error(s), {warnings.Count} warning(s)\n"
        );

        // Report each diagnostic
        foreach (var diagnostic in diagnostics
            .OrderByDescending(d => d.Severity)
            .ThenBy(d => d.Location.SourceSpan.Start))
        {
            ReportDiagnostic(syntaxTree, diagnostic);
        }

        return errors.Count == 0;
    }

    private static void ReportDiagnostic(
        SyntaxTree syntaxTree,
        Diagnostic diagnostic)
    {
        var sourceText = syntaxTree.GetText();
        var lineSpan = diagnostic.Location.GetLineSpan();
        var startPos = lineSpan.StartLinePosition;
        var line = sourceText.Lines[startPos.Line];

        // Severity prefix
        var prefix = diagnostic.Severity switch
        {
            DiagnosticSeverity.Error => "error",
            DiagnosticSeverity.Warning => "warning",
            DiagnosticSeverity.Info => "info",
            _ => "message"
        };

        // Set color
        Console.ForegroundColor = diagnostic.Severity switch
        {
            DiagnosticSeverity.Error => ConsoleColor.Red,
            DiagnosticSeverity.Warning => ConsoleColor.Yellow,
            _ => ConsoleColor.Cyan
        };

        // Print location and message
        Console.WriteLine(
            $"{syntaxTree.FilePath}({startPos.Line + 1},{startPos.Character + 1}): " +
            $"{prefix} {diagnostic.Id}: {diagnostic.GetMessage()}"
        );

        // Print source line
        Console.ResetColor();
        var lineText = line.ToString().TrimEnd();
        if (!string.IsNullOrWhiteSpace(lineText))
        {
            Console.WriteLine(lineText);

            // Print squigglies under the error
            var span = diagnostic.Location.SourceSpan;
            var lineStart = line.Start;
            var errorStart = Math.Max(0, span.Start - lineStart);
            var errorLength = Math.Min(line.Length - errorStart, span.Length);

            if (errorLength > 0)
            {
                var spaces = new string(' ', errorStart);
                var squiggles = new string('~', errorLength);
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine(spaces + squiggles);
                Console.ResetColor();
            }
        }

        Console.WriteLine();
    }
}

// Usage
var success = DiagnosticReporter.ParseAndReport("script.lua");
if (!success)
{
    Environment.Exit(1);
}

Error Recovery

Loretta’s parser includes error recovery - it continues parsing even after errors:
var code = @"
local x = 
local y = 20
print(y)
";

var syntaxTree = LuaSyntaxTree.ParseText(code);

// Despite the error on line 1, the parser continues
var root = syntaxTree.GetRoot();
var statements = root.DescendantNodes()
    .OfType<LocalVariableDeclarationStatementSyntax>()
    .ToList();

Console.WriteLine($"Found {statements.Count} local declarations");
// Output: Found 2 local declarations

// But there are errors
var errors = syntaxTree.GetDiagnostics()
    .Where(d => d.Severity == DiagnosticSeverity.Error);

foreach (var error in errors)
{
    Console.WriteLine(error.GetMessage());
}
// Output: Expected expression

Common Error IDs

Some common Loretta diagnostic IDs:
  • LUA0001 - Invalid string escape
  • LUA0002 - Unexpected character
  • LUA0003 - Expected token
  • LUA0004 - Syntax error
Error IDs and messages are defined in the ErrorCode and ErrorFacts classes in the Loretta source code.

Filtering by Diagnostic ID

// Suppress specific diagnostics
var diagnostics = syntaxTree.GetDiagnostics()
    .Where(d => d.Id != "LUA0001"); // Ignore string escape errors

// Only show specific diagnostics
var stringErrors = syntaxTree.GetDiagnostics()
    .Where(d => d.Id == "LUA0001");

Next Steps

Build docs developers (and LLMs) love