Skip to main content

Overview

The Script class is the entry point for scope and variable analysis in Loretta. It holds one or more syntax trees and provides access to scopes, variables, and goto labels throughout the analyzed code.

Constructor

Script()

Initializes an empty script with no syntax trees.
var emptyScript = new Script();

Script(ImmutableArray<SyntaxTree>)

Initializes a script from one or more syntax trees.
var tree = LuaSyntaxTree.ParseText("local x = 10");
var script = new Script(ImmutableArray.Create(tree));
Parameters:
  • syntaxTrees - An immutable array of syntax trees to analyze
Exceptions:
  • ArgumentException - Thrown when the syntax trees array is a default (uninitialized) array

Properties

SyntaxTrees

Gets the syntax trees contained in this script.
public ImmutableArray<SyntaxTree> SyntaxTrees { get; }

RootScope

Gets the root (global) scope of the script. This is the top-level scope that contains all file scopes.
public IScope RootScope { get; }

Methods

GetScope

Returns the scope associated with a specific syntax node, or null if the node has no associated scope.
public IScope? GetScope(SyntaxNode node)
Parameters:
  • node - The syntax node to get the scope for
Returns:
  • The scope for the node, or null if not found
Example:
var tree = LuaSyntaxTree.ParseText("local x = 10");
var script = new Script(ImmutableArray.Create(tree));
var root = tree.GetRoot();
var fileScope = script.GetScope(root); // Returns IFileScope

FindScope

Finds the outermost scope of the specified kind (or a more generic one) for a given node.
public IScope? FindScope(SyntaxNode node, ScopeKind kind = ScopeKind.Block)
Parameters:
  • node - The node to search from
  • kind - The kind of scope to search for (defaults to ScopeKind.Block)
Returns:
  • The found scope, or null if not found
Scope Search Hierarchy:
  • ScopeKind.Block - Searches for: Block, Function, File, or Global
  • ScopeKind.Function - Searches for: Function, File, or Global
  • ScopeKind.File - Searches for: File or Global
  • ScopeKind.Global - Searches for: Global only
Example:
var code = @"
function myFunc()
    local x = 10
end
";
var tree = LuaSyntaxTree.ParseText(code);
var script = new Script(ImmutableArray.Create(tree));

// Find the local variable declaration
var localVar = tree.GetRoot()
    .DescendantNodes()
    .OfType<LocalVariableDeclarationStatementSyntax>()
    .First();

// Find the function scope
var funcScope = script.FindScope(localVar, ScopeKind.Function);
Console.WriteLine(funcScope?.Kind); // Output: Function

GetVariable

Returns the variable associated with a specific syntax node.
public IVariable? GetVariable(SyntaxNode node)
Parameters:
  • node - The syntax node (typically an identifier) to get the variable for
Returns:
  • The variable, or null if the node is not associated with a variable
Example:
var tree = LuaSyntaxTree.ParseText("local x = 10\nprint(x)");
var script = new Script(ImmutableArray.Create(tree));

// Find the identifier in the print statement
var identifier = tree.GetRoot()
    .DescendantNodes()
    .OfType<IdentifierNameSyntax>()
    .First(i => i.Name == "x");

var variable = script.GetVariable(identifier);
Console.WriteLine(variable?.Name); // Output: x
Console.WriteLine(variable?.Kind); // Output: Local

GetLabel

Returns the goto label associated with a specific syntax node.
public IGotoLabel? GetLabel(SyntaxNode node)
Parameters:
  • node - The syntax node to get the label for
Returns:
  • The goto label, or null if the node is not associated with a label
Example:
var code = @"
::start::
print('Hello')
goto start
";
var tree = LuaSyntaxTree.ParseText(code);
var script = new Script(ImmutableArray.Create(tree));

var gotoStatement = tree.GetRoot()
    .DescendantNodes()
    .OfType<GotoStatementSyntax>()
    .First();

var label = script.GetLabel(gotoStatement);
Console.WriteLine(label?.Name); // Output: start

RenameVariable

Attempts to rename a variable throughout the script, checking for conflicts.
public Result<Script, IEnumerable<RenameError>> RenameVariable(
    IVariable variable, 
    string newName)
Parameters:
  • variable - The variable to rename
  • newName - The new name for the variable
Returns:
  • Result.Ok(Script) - A new script with the renamed variable if successful
  • Result.Err(IEnumerable<RenameError>) - A collection of errors if the rename would cause conflicts
Exceptions:
  • ArgumentNullException - Thrown when variable or newName is null
Example:
var tree = LuaSyntaxTree.ParseText("local oldName = 10\nprint(oldName)");
var script = new Script(ImmutableArray.Create(tree));

// Find the variable
var identifier = tree.GetRoot()
    .DescendantNodes()
    .OfType<IdentifierNameSyntax>()
    .First(i => i.Name == "oldName");

var variable = script.GetVariable(identifier);

if (variable != null)
{
    var result = script.RenameVariable(variable, "newName");
    
    if (result.IsOk)
    {
        var newScript = result.Unwrap();
        // Use the new script with renamed variable
    }
    else
    {
        var errors = result.UnwrapErr();
        foreach (var error in errors)
        {
            Console.WriteLine($"Rename error: {error}");
        }
    }
}

Complete Example

Here’s a complete example that creates a script and queries its scope and variable information:
using Loretta.CodeAnalysis;
using Loretta.CodeAnalysis.Lua;
using Loretta.CodeAnalysis.Lua.Syntax;
using System.Collections.Immutable;

var code = @"
local x = 10
local y = 20

function calculate(a, b)
    local result = a + b + x
    return result
end

local z = calculate(y, 30)
print(z)
";

// Parse the code
var tree = LuaSyntaxTree.ParseText(code);
var script = new Script(ImmutableArray.Create(tree));

// Get the root scope
var rootScope = script.RootScope;
Console.WriteLine($"Root scope kind: {rootScope.Kind}"); // Global

// Get the file scope
var root = tree.GetRoot();
var fileScope = script.GetScope(root);
Console.WriteLine($"File scope kind: {fileScope?.Kind}"); // File

// Find all variables in the file scope
if (fileScope != null)
{
    Console.WriteLine("\nVariables declared in file scope:");
    foreach (var variable in fileScope.DeclaredVariables)
    {
        Console.WriteLine($"  - {variable.Name} ({variable.Kind})");
    }
}

// Find the function scope
var funcDecl = root.DescendantNodes()
    .OfType<FunctionDeclarationStatementSyntax>()
    .First();
var funcScope = script.GetScope(funcDecl);

if (funcScope != null)
{
    Console.WriteLine($"\nFunction scope kind: {funcScope.Kind}");
    Console.WriteLine("Parameters and variables:");
    
    foreach (var variable in funcScope.DeclaredVariables)
    {
        Console.WriteLine($"  - {variable.Name} ({variable.Kind})");
        Console.WriteLine($"    Read locations: {variable.ReadLocations.Count()}");
        Console.WriteLine($"    Write locations: {variable.WriteLocations.Count()}");
    }
}

// Find a specific variable and analyze its usage
var xIdentifier = root.DescendantNodes()
    .OfType<IdentifierNameSyntax>()
    .First(i => i.Name == "x");

var xVariable = script.GetVariable(xIdentifier);
if (xVariable != null)
{
    Console.WriteLine($"\nVariable 'x' analysis:");
    Console.WriteLine($"  Kind: {xVariable.Kind}");
    Console.WriteLine($"  Declaring scope: {xVariable.ContainingScope.Kind}");
    Console.WriteLine($"  Read {xVariable.ReadLocations.Count()} times");
    Console.WriteLine($"  Written {xVariable.WriteLocations.Count()} times");
    Console.WriteLine($"  Referenced by {xVariable.ReferencingScopes.Count()} scopes");
    Console.WriteLine($"  Captured by {xVariable.CapturingScopes.Count()} scopes");
}

See Also

Build docs developers (and LLMs) love