What is Scope Analysis?
Scope analysis in Loretta tracks variables, their declarations, and their usage across Lua code. TheScript 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
TheScript 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
}
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; }
}
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
TheScript 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