Skip to main content

Overview

The IVariable interface represents a variable in Lua code. Variables can be local, global, parameters, or iteration variables. The interface provides information about where variables are declared, how they’re used, and which scopes can access them.

Variable Kinds

Loretta recognizes four kinds of variables through the VariableKind enum:
  • Local - A local variable declared with local
  • Global - A global variable (assigned without local)
  • Parameter - A function parameter (including ... and arg)
  • Iteration - A loop iteration variable (from for loops)

Properties

Name

Gets the variable’s name.
string Name { get; }
Example:
Console.WriteLine($"Variable name: {variable.Name}");

Kind

Gets the kind of this variable.
VariableKind Kind { get; }
Example:
if (variable.Kind == VariableKind.Local)
{
    Console.WriteLine("This is a local variable");
}
else if (variable.Kind == VariableKind.Global)
{
    Console.WriteLine("Warning: Global variable used");
}

Declaration

Gets the syntax node where this variable is declared. Returns null for global variables or implicit variables (like the file’s arg and ...).
SyntaxNode? Declaration { get; }
Example:
if (variable.Declaration != null)
{
    var location = variable.Declaration.GetLocation();
    var lineSpan = location.GetLineSpan();
    Console.WriteLine($"Declared at line {lineSpan.StartLinePosition.Line + 1}");
}
else
{
    Console.WriteLine("Implicit or global variable");
}

ContainingScope

Gets the scope that contains this variable’s declaration.
IScope ContainingScope { get; }
Example:
Console.WriteLine($"{variable.Name} is declared in a {variable.ContainingScope.Kind} scope");

ReadLocations

Gets all locations where this variable is read from.
IEnumerable<SyntaxNode> ReadLocations { get; }
Example:
Console.WriteLine($"{variable.Name} is read {variable.ReadLocations.Count()} times:");
foreach (var readLocation in variable.ReadLocations)
{
    var line = readLocation.GetLocation().GetLineSpan().StartLinePosition.Line + 1;
    Console.WriteLine($"  Line {line}");
}

WriteLocations

Gets all locations where this variable is written to (assigned).
IEnumerable<SyntaxNode> WriteLocations { get; }
Example:
Console.WriteLine($"{variable.Name} is written {variable.WriteLocations.Count()} times:");
foreach (var writeLocation in variable.WriteLocations)
{
    var line = writeLocation.GetLocation().GetLineSpan().StartLinePosition.Line + 1;
    Console.WriteLine($"  Line {line}");
}

ReferencingScopes

Gets all scopes that reference this variable.
IEnumerable<IScope> ReferencingScopes { get; }
Example:
var scopeCount = variable.ReferencingScopes.Count();
Console.WriteLine($"{variable.Name} is referenced by {scopeCount} scope(s)");

CapturingScopes

Gets all scopes that capture this variable as an upvalue (closures that reference this variable from an outer scope).
IEnumerable<IScope> CapturingScopes { get; }
Example:
if (variable.CapturingScopes.Any())
{
    Console.WriteLine($"{variable.Name} is captured by {variable.CapturingScopes.Count()} closure(s)");
    Console.WriteLine("This variable cannot be optimized away");
}

Methods

CanBeAccessedIn

Returns whether this variable can be accessed in the provided scope.
bool CanBeAccessedIn(IScope scope)
Parameters:
  • scope - The scope to check access in
Returns:
  • true if the variable can be accessed in the scope, false otherwise
Example:
var innerScope = script.FindScope(someNode, ScopeKind.Block);

if (variable.CanBeAccessedIn(innerScope))
{
    Console.WriteLine($"{variable.Name} is accessible here");
}
else
{
    Console.WriteLine($"{variable.Name} is out of scope");
}

Finding and Analyzing Variables

Here’s a complete example showing how to find and analyze variables:
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
    print(result)
    return result
end

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

var tree = LuaSyntaxTree.ParseText(code);
var script = new Script(ImmutableArray.Create(tree));
var root = tree.GetRoot();

// Find all identifiers and analyze their variables
var identifiers = root.DescendantNodes().OfType<IdentifierNameSyntax>();

foreach (var identifier in identifiers)
{
    var variable = script.GetVariable(identifier);
    
    if (variable != null)
    {
        Console.WriteLine($"\nVariable: {variable.Name}");
        Console.WriteLine($"  Kind: {variable.Kind}");
        Console.WriteLine($"  Scope: {variable.ContainingScope.Kind}");
        Console.WriteLine($"  Reads: {variable.ReadLocations.Count()}");
        Console.WriteLine($"  Writes: {variable.WriteLocations.Count()}");
        
        if (variable.CapturingScopes.Any())
        {
            Console.WriteLine($"  Captured by: {variable.CapturingScopes.Count()} closure(s)");
        }
    }
}

Detecting Unused Variables

You can use the variable analysis to detect unused variables:
var code = @"
local unused = 10
local used = 20
local written = 0

function test()
    print(used)
    written = 5
end
";

var tree = LuaSyntaxTree.ParseText(code);
var script = new Script(ImmutableArray.Create(tree));
var root = tree.GetRoot();
var fileScope = script.GetScope(root);

if (fileScope != null)
{
    Console.WriteLine("Variable usage analysis:");
    
    foreach (var variable in fileScope.DeclaredVariables)
    {
        if (variable.Kind == VariableKind.Local)
        {
            var readCount = variable.ReadLocations.Count();
            var writeCount = variable.WriteLocations.Count();
            
            // Subtract 1 from write count if there's a declaration
            // (declaration counts as a write)
            if (variable.Declaration != null)
            {
                writeCount--;
            }
            
            if (readCount == 0 && writeCount == 0)
            {
                Console.WriteLine($"  {variable.Name}: Unused (never read or written)");
            }
            else if (readCount == 0)
            {
                Console.WriteLine($"  {variable.Name}: Write-only (written {writeCount} times, never read)");
            }
            else if (writeCount == 0)
            {
                Console.WriteLine($"  {variable.Name}: Read-only (read {readCount} times, never written)");
            }
            else
            {
                Console.WriteLine($"  {variable.Name}: Used (read {readCount} times, written {writeCount} times)");
            }
        }
    }
}

Detecting Captured Variables

Detect which variables are captured by closures, which is important for optimization analysis:
var code = @"
function makeCounter()
    local count = 0
    local temp = 5  -- Not captured
    
    return function()
        count = count + 1  -- Captures 'count'
        return count
    end
end

function simple()
    local x = 10  -- Not captured
    print(x)
end
";

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

// Find all function scopes
var functionNodes = tree.GetRoot()
    .DescendantNodes()
    .Where(n => n is FunctionDeclarationStatementSyntax || n is AnonymousFunctionExpressionSyntax);

foreach (var funcNode in functionNodes)
{
    var funcScope = script.GetScope(funcNode);
    
    if (funcScope != null)
    {
        Console.WriteLine($"\nFunction at line {funcNode.GetLocation().GetLineSpan().StartLinePosition.Line + 1}:");
        
        // Check each declared variable
        foreach (var variable in funcScope.DeclaredVariables)
        {
            if (variable.Kind == VariableKind.Local)
            {
                if (variable.CapturingScopes.Any())
                {
                    Console.WriteLine($"  {variable.Name}: Captured by {variable.CapturingScopes.Count()} closure(s) - CANNOT be stack-allocated");
                }
                else
                {
                    Console.WriteLine($"  {variable.Name}: Not captured - CAN be stack-allocated");
                }
            }
        }
    }
}

Analyzing Variable Scope Access

Check which variables are accessible from different scopes:
var code = @"
local global = 1

function outer()
    local outerLocal = 2
    
    function inner()
        local innerLocal = 3
        -- All three variables are accessible here
    end
    
    -- Only global and outerLocal are accessible here
end

-- Only global is accessible here
";

var tree = LuaSyntaxTree.ParseText(code);
var script = new Script(ImmutableArray.Create(tree));
var root = tree.GetRoot();

// Get all scopes
var allScopes = new List<IScope>();
void CollectScopes(IScope scope)
{
    allScopes.Add(scope);
    foreach (var child in scope.ContainedScopes)
    {
        CollectScopes(child);
    }
}
CollectScopes(script.RootScope);

// Get all variables
var allVariables = allScopes
    .SelectMany(s => s.DeclaredVariables)
    .Where(v => v.Kind == VariableKind.Local)
    .Distinct()
    .ToList();

Console.WriteLine("Variable accessibility matrix:\n");
Console.Write("Scope\\Variable".PadRight(20));
foreach (var variable in allVariables)
{
    Console.Write(variable.Name.PadRight(15));
}
Console.WriteLine();

foreach (var scope in allScopes.Where(s => s.Kind != ScopeKind.Global))
{
    var scopeName = $"{scope.Kind}";
    Console.Write(scopeName.PadRight(20));
    
    foreach (var variable in allVariables)
    {
        var accessible = variable.CanBeAccessedIn(scope) ? "✓" : "✗";
        Console.Write(accessible.PadRight(15));
    }
    Console.WriteLine();
}

Common Use Cases

Finding All Global Variables

var globals = script.RootScope
    .DescendantScopes()
    .SelectMany(s => s.DeclaredVariables)
    .Where(v => v.Kind == VariableKind.Global)
    .Select(v => v.Name)
    .Distinct();

Console.WriteLine("Global variables: " + string.Join(", ", globals));

Detecting Variables That Are Never Read

var unreadVariables = script.RootScope
    .DescendantScopes()
    .SelectMany(s => s.DeclaredVariables)
    .Where(v => v.Kind == VariableKind.Local && !v.ReadLocations.Any());

foreach (var variable in unreadVariables)
{
    Console.WriteLine($"Warning: {variable.Name} is never read");
}

Detecting Single-Assignment Variables (Constants)

var constants = script.RootScope
    .DescendantScopes()
    .SelectMany(s => s.DeclaredVariables)
    .Where(v => v.Kind == VariableKind.Local)
    .Where(v => v.WriteLocations.Count() == 1 && v.Declaration != null);

foreach (var constant in constants)
{
    Console.WriteLine($"{constant.Name} is effectively constant (assigned once)");
}

Helper Extension Methods

You can create extension methods to make variable analysis easier:
public static class VariableExtensions
{
    public static bool IsUnused(this IVariable variable)
    {
        return !variable.ReadLocations.Any() && !variable.WriteLocations.Any();
    }
    
    public static bool IsReadOnly(this IVariable variable)
    {
        var writeCount = variable.WriteLocations.Count();
        // If there's a declaration, it counts as one write
        if (variable.Declaration != null)
            writeCount--;
        return writeCount == 0 && variable.ReadLocations.Any();
    }
    
    public static bool IsCaptured(this IVariable variable)
    {
        return variable.CapturingScopes.Any();
    }
    
    public static IEnumerable<IScope> DescendantScopes(this IScope scope)
    {
        foreach (var child in scope.ContainedScopes)
        {
            yield return child;
            foreach (var descendant in child.DescendantScopes())
            {
                yield return descendant;
            }
        }
    }
}

// Usage
if (variable.IsUnused())
{
    Console.WriteLine($"{variable.Name} is unused");
}

if (variable.IsCaptured())
{
    Console.WriteLine($"{variable.Name} is captured by a closure");
}

See Also

Build docs developers (and LLMs) love