Skip to main content

What is Scope Analysis?

Scope analysis in Loretta tracks variables, their declarations, and their usage across Lua code. The Script class provides a complete scope tree that shows:
  • Where variables are declared
  • Where variables are read and written
  • Which variables are captured by closures
  • Variable visibility and lifetime

The Script Class

The Script class analyzes one or more syntax trees and builds a complete scope hierarchy:
using Loretta.CodeAnalysis.Lua;
using Loretta.CodeAnalysis.Lua.Syntax;

var code = @"
local x = 10
local function test()
    local y = x + 5
    return y
end
";

var tree = LuaSyntaxTree.ParseText(code);
var script = new Script(ImmutableArray.Create<SyntaxTree>(tree));

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

Creating Scripts

// Empty script
var emptyScript = new Script();
// or
var emptyScript = Script.Empty;

// Single file
var tree = LuaSyntaxTree.ParseText(code);
var script = new Script(ImmutableArray.Create<SyntaxTree>(tree));

// Multiple files
var tree1 = LuaSyntaxTree.ParseText(code1);
var tree2 = LuaSyntaxTree.ParseText(code2);
var script = new Script(ImmutableArray.Create(tree1, tree2));

Scope Interfaces

IScope

IScope is the base interface for all scopes:
public interface IScope
{
    ScopeKind Kind { get; }
    SyntaxNode? Node { get; }
    IScope? ContainingScope { get; }
    IEnumerable<IVariable> DeclaredVariables { get; }
    IEnumerable<IVariable> ReferencedVariables { get; }
    IEnumerable<IGotoLabel> GotoLabels { get; }
    IEnumerable<IScope> ContainedScopes { get; }
    
    IVariable? FindVariable(string name, ScopeKind kind = ScopeKind.Global);
}

ScopeKind

Scopes have different kinds representing their level:
public enum ScopeKind
{
    Global,   // The global scope containing all files
    File,     // A single file's scope
    Function, // A function's scope
    Block     // A block scope (if, while, etc.)
}

IFileScope

IFileScope represents a single file and provides access to implicit file-level variables:
public interface IFileScope : IScope
{
    IVariable ArgVariable { get; }      // The implicit 'arg' variable
    IVariable VarArgParameter { get; }  // The implicit '...' vararg
}
Example:
var code = "print(...)";  // Uses the implicit vararg
var tree = LuaSyntaxTree.ParseText(code);
var script = new Script(ImmutableArray.Create<SyntaxTree>(tree));

var root = tree.GetCompilationUnitRoot();
var fileScope = script.GetScope(root) as IFileScope;

if (fileScope != null)
{
    Console.WriteLine($"VarArg: {fileScope.VarArgParameter.Name}");
    Console.WriteLine($"Arg variable: {fileScope.ArgVariable.Name}");
}

IFunctionScope

IFunctionScope represents a function and tracks parameters and captured variables:
public interface IFunctionScope : IScope
{
    IEnumerable<IVariable> Parameters { get; }
    IEnumerable<IVariable> CapturedVariables { get; }
}
Example:
var code = @"
local x = 10
local function test(a, b)
    return x + a + b  -- 'x' is captured, 'a' and 'b' are parameters
end
";

var tree = LuaSyntaxTree.ParseText(code);
var script = new Script(ImmutableArray.Create<SyntaxTree>(tree));

var root = tree.GetCompilationUnitRoot();
var funcDecl = root.DescendantNodes()
    .OfType<LocalFunctionDeclarationStatementSyntax>()
    .First();

var funcScope = script.GetScope(funcDecl) as IFunctionScope;
if (funcScope != null)
{
    Console.WriteLine("Parameters:");
    foreach (var param in funcScope.Parameters)
    {
        Console.WriteLine($"  {param.Name}");
    }
    
    Console.WriteLine("Captured variables:");
    foreach (var captured in funcScope.CapturedVariables)
    {
        Console.WriteLine($"  {captured.Name}");
    }
}

Variable Interfaces

IVariable

IVariable represents a variable in the code:
public interface IVariable
{
    VariableKind Kind { get; }
    IScope ContainingScope { get; }
    string Name { get; }
    SyntaxNode? Declaration { get; }
    
    IEnumerable<IScope> ReferencingScopes { get; }
    IEnumerable<IScope> CapturingScopes { get; }
    IEnumerable<SyntaxNode> ReadLocations { get; }
    IEnumerable<SyntaxNode> WriteLocations { get; }
    
    bool CanBeAccessedIn(IScope scope);
}

VariableKind

Variables have different kinds:
public enum VariableKind
{
    Global,    // Global variable
    Local,     // Local variable
    Parameter  // Function parameter
}

Accessing Scopes

Get Scope for a Node

var code = @"
local x = 10
if x > 5 then
    local y = 20
end
";

var tree = LuaSyntaxTree.ParseText(code);
var script = new Script(ImmutableArray.Create<SyntaxTree>(tree));
var root = tree.GetCompilationUnitRoot();

// Get the if statement
var ifStmt = root.DescendantNodes()
    .OfType<IfStatementSyntax>()
    .First();

// Get the scope for the if block
var ifScope = script.GetScope(ifStmt.Body);
Console.WriteLine($"If block scope kind: {ifScope?.Kind}");

// List variables declared in the if block
if (ifScope != null)
{
    foreach (var variable in ifScope.DeclaredVariables)
    {
        Console.WriteLine($"Variable: {variable.Name}");
    }
}

Find Enclosing Scope

var code = @"
local x = 10
local function outer()
    local y = 20
    local function inner()
        local z = 30
        print(x + y + z)
    end
end
";

var tree = LuaSyntaxTree.ParseText(code);
var script = new Script(ImmutableArray.Create<SyntaxTree>(tree));
var root = tree.GetCompilationUnitRoot();

// Find the 'print' call
var printCall = root.DescendantNodes()
    .OfType<FunctionCallExpressionSyntax>()
    .First();

// Find the nearest function scope
var functionScope = script.FindScope(printCall, ScopeKind.Function);
Console.WriteLine($"Nearest function: {functionScope?.Node?.Kind()}");

// Find the file scope
var fileScope = script.FindScope(printCall, ScopeKind.File);
Console.WriteLine($"File scope kind: {fileScope?.Kind}");

Variable Lookup and Resolution

Get Variable for a Node

var code = @"
local x = 10
print(x)
";

var tree = LuaSyntaxTree.ParseText(code);
var script = new Script(ImmutableArray.Create<SyntaxTree>(tree));
var root = tree.GetCompilationUnitRoot();

// Find the identifier 'x' in the print call
var xIdentifier = root.DescendantNodes()
    .OfType<IdentifierNameSyntax>()
    .First(id => id.Name == "x");

var variable = script.GetVariable(xIdentifier);
if (variable != null)
{
    Console.WriteLine($"Variable: {variable.Name}");
    Console.WriteLine($"Kind: {variable.Kind}");
    Console.WriteLine($"Declared at: {variable.Declaration?.GetLocation()}");
}

Find Variables by Name

var code = @"
local x = 10
local function test()
    local x = 20  -- shadows the outer 'x'
    print(x)
end
";

var tree = LuaSyntaxTree.ParseText(code);
var script = new Script(ImmutableArray.Create<SyntaxTree>(tree));
var root = tree.GetCompilationUnitRoot();

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

// Find 'x' in the function scope (finds the inner 'x')
var innerX = funcScope?.FindVariable("x");
Console.WriteLine($"Inner x declared at: {innerX?.Declaration?.GetLocation()}");

// Find 'x' in the file scope (finds the outer 'x')
var fileScope = script.RootScope.ContainedScopes.First();
var outerX = fileScope.FindVariable("x", ScopeKind.File);
Console.WriteLine($"Outer x declared at: {outerX?.Declaration?.GetLocation()}");

Tracking Variable Usage

Read and Write Locations

var code = @"
local x = 10     -- write
x = x + 5        -- read and write
print(x)         -- read
";

var tree = LuaSyntaxTree.ParseText(code);
var script = new Script(ImmutableArray.Create<SyntaxTree>(tree));
var root = tree.GetCompilationUnitRoot();

// Get the 'x' variable
var xDecl = root.DescendantNodes()
    .OfType<LocalVariableDeclarationStatementSyntax>()
    .First()
    .Names[0];

var variable = script.GetVariable(xDecl);
if (variable != null)
{
    Console.WriteLine($"Variable '{variable.Name}' usage:");
    
    Console.WriteLine($"  Declared at: {variable.Declaration?.GetLocation()}");
    
    Console.WriteLine($"  Read at:");
    foreach (var read in variable.ReadLocations)
    {
        Console.WriteLine($"    {read.GetLocation()}");
    }
    
    Console.WriteLine($"  Written at:");
    foreach (var write in variable.WriteLocations)
    {
        Console.WriteLine($"    {write.GetLocation()}");
    }
}

Captured Variables and Closures

Captured variables are local variables from outer scopes that are used in inner functions:
var code = @"
local function makeCounter()
    local count = 0  -- This will be captured
    return function()
        count = count + 1  -- Captures 'count'
        return count
    end
end
";

var tree = LuaSyntaxTree.ParseText(code);
var script = new Script(ImmutableArray.Create<SyntaxTree>(tree));
var root = tree.GetCompilationUnitRoot();

// Find the inner function
var functions = root.DescendantNodes()
    .OfType<AnonymousFunctionExpressionSyntax>()
    .ToList();

var innerFunction = functions.First();
var innerScope = script.GetScope(innerFunction) as IFunctionScope;

if (innerScope != null)
{
    Console.WriteLine("Captured variables in inner function:");
    foreach (var captured in innerScope.CapturedVariables)
    {
        Console.WriteLine($"  {captured.Name} (declared at {captured.Declaration?.GetLocation()})");
        
        Console.WriteLine("  Captured by scopes:");
        foreach (var capturingScope in captured.CapturingScopes)
        {
            Console.WriteLine($"    {capturingScope.Kind} scope");
        }
    }
}

Complete Scope Analysis Example

using Loretta.CodeAnalysis;
using Loretta.CodeAnalysis.Lua;
using Loretta.CodeAnalysis.Lua.Syntax;

class ScopeAnalyzer
{
    static void AnalyzeScope(IScope scope, int indent = 0)
    {
        var prefix = new string(' ', indent * 2);
        Console.WriteLine($"{prefix}Scope: {scope.Kind}");
        
        // Show declared variables
        if (scope.DeclaredVariables.Any())
        {
            Console.WriteLine($"{prefix}  Declared variables:");
            foreach (var variable in scope.DeclaredVariables)
            {
                Console.WriteLine($"{prefix}    {variable.Kind} {variable.Name}");
                Console.WriteLine($"{prefix}      Read: {variable.ReadLocations.Count()} times");
                Console.WriteLine($"{prefix}      Written: {variable.WriteLocations.Count()} times");
                
                if (variable.CapturingScopes.Any())
                {
                    Console.WriteLine($"{prefix}      Captured by {variable.CapturingScopes.Count()} scope(s)");
                }
            }
        }
        
        // Show captured variables for function scopes
        if (scope is IFunctionScope funcScope && funcScope.CapturedVariables.Any())
        {
            Console.WriteLine($"{prefix}  Captured variables:");
            foreach (var captured in funcScope.CapturedVariables)
            {
                Console.WriteLine($"{prefix}    {captured.Name}");
            }
        }
        
        // Recursively analyze child scopes
        foreach (var childScope in scope.ContainedScopes)
        {
            AnalyzeScope(childScope, indent + 1);
        }
    }
    
    static void Main()
    {
        var code = @"
local x = 10
local y = 20

local function outer(a)
    local z = 30
    
    local function inner(b)
        return x + y + z + a + b
    end
    
    return inner
end

local counter = 0
for i = 1, 10 do
    counter = counter + i
end
";
        
        var tree = LuaSyntaxTree.ParseText(code);
        var script = new Script(ImmutableArray.Create<SyntaxTree>(tree));
        
        Console.WriteLine("=== Scope Analysis ===");
        AnalyzeScope(script.RootScope);
    }
}

Variable Renaming

The Script class supports safe variable renaming:
var code = @"
local oldName = 10
print(oldName)
";

var tree = LuaSyntaxTree.ParseText(code);
var script = new Script(ImmutableArray.Create<SyntaxTree>(tree));
var root = tree.GetCompilationUnitRoot();

// Get the variable
var varDecl = root.DescendantNodes()
    .OfType<LocalVariableDeclarationStatementSyntax>()
    .First()
    .Names[0];
var variable = script.GetVariable(varDecl);

if (variable != null)
{
    // Rename the variable
    var result = script.RenameVariable(variable, "newName");
    
    if (result.IsOk)
    {
        var newScript = result.Unwrap();
        var newTree = newScript.SyntaxTrees[0];
        var newRoot = newTree.GetRoot();
        
        Console.WriteLine("Renamed successfully:");
        Console.WriteLine(newRoot.ToFullString());
    }
    else
    {
        var errors = result.UnwrapErr();
        Console.WriteLine("Rename failed:");
        foreach (var error in errors)
        {
            Console.WriteLine($"  {error}");
        }
    }
}

Best Practices

Check Variable Accessibility

var scope = script.GetScope(someNode);
var variable = someScope.FindVariable("x");

if (variable != null && variable.CanBeAccessedIn(scope))
{
    Console.WriteLine($"Variable '{variable.Name}' is accessible here");
}

Detect Unused Variables

var fileScope = script.RootScope.ContainedScopes.First();

foreach (var variable in fileScope.DeclaredVariables)
{
    if (!variable.ReadLocations.Any() && variable.WriteLocations.Count() == 1)
    {
        Console.WriteLine($"Warning: Variable '{variable.Name}' is declared but never read");
    }
}

Analyze Closure Usage

if (scope is IFunctionScope funcScope)
{
    var capturedCount = funcScope.CapturedVariables.Count();
    if (capturedCount > 0)
    {
        Console.WriteLine($"Function captures {capturedCount} variable(s) from outer scopes");
    }
}

See Also

  • Syntax Trees - Understanding the structure analyzed by Script
  • Parsing - Creating the trees that Script analyzes

Build docs developers (and LLMs) love