AST Bindings

The AST identifies each local binding with a stable LocalSymbol. A symbol is the identity of one declaration, independent of its source spelling, lexical owner, or eventual register slot. Declarations and references point to the same symbol through the canonical symbol or symbols field. Transforms should preserve those identities when moving nodes and create fresh symbols when they introduce new bindings.

Constructing local bindings

Create one symbol for each local declaration and reuse it for every read and write of that binding:

local Ast = require("prometheus.ast")
local value = Ast.LocalSymbol("value")

local declaration = Ast.LocalVariableDeclaration({value}, {
    Ast.NumberExpression(10),
})
local read = Ast.VariableExpression(value)
local write = Ast.AssignmentVariable(value)

Names are preferences: the unparser normalizes invalid identifier characters and leading digits, and resolves collisions and reserved words while preserving symbol identity. Valid names are retained when they are unambiguous.

The AST constructors are symbol-only. Ast.LocalFunctionDeclaration(symbol, args, body) declares a local function. Ast.FunctionDeclaration(target, indices, args, body) accepts a global reference or a local reference as its target. Ast.ForStatement(symbol, initial, final, increment, body) and Ast.ForInStatement(symbols, expressions, body) define loop bindings. Ast.LocalVariableDeclaration(symbols, expressions) takes symbols in its first argument. Ast.Block(statements) and Ast.TopNode(body) take no scope argument. Function arguments are local reference nodes (or symbols, which the function constructors convert to reference nodes); use Ast.VarargExpression() for a vararg argument.

Old scope-plus-numeric-ID construction signatures have been removed. Do not pass scopes or IDs to AST constructors.

Globals

Globals are identified by their names and have no local symbol. Use Ast.GlobalVariableExpression(name) for a read and Ast.AssignmentGlobalVariable(name) for a write. Their canonical identity is the node's name field.

Parsing, analysis, and naming

The parser uses temporary Bindings environments while it reads source. Those environments track declaration visibility, disabled names, and lexical lookup during parsing. They are distinct from the fresh plain scope records later derived by binding analysis, and are not AST binding identities.

require("prometheus.bindinganalysis")(ast) rebuilds derived lexical metadata from declarations, references, and tree structure. It returns an analysis record with declarations[symbol], references[symbol], reads[symbol], writes[symbol], captured[symbol], and owners[scope]. It also attaches fresh plain scope and slot metadata for consumers that need lexical ownership or lowering locations. That metadata is derived on each analysis pass; it is not the identity of a binding. The canonical AST identity remains node.symbol/node.symbols for locals and node.name for globals.

Parser:parse analyzes its result before returning. The pipeline refreshes analysis after transformations, and Unparser:unparse analyzes before output. Transforms that need derived ownership information at another point can call the analyzer after changing the tree.

prometheus.names allocates local names from analyzed metadata. It updates symbol names while preserving symbol identity and avoids names that would capture references from nested functions. Name allocation requires a fresh binding analysis first.

Visibility and captures

Analysis follows Lua declaration order. A local declaration's initializer is analyzed before its symbol becomes active, so the local is not visible in its own initializer. Local functions are active while their bodies are analyzed, which allows recursion. Function arguments and loop variables are active in their function or loop body. A reference is valid only while its symbol is active on the enclosing lexical path. References from nested functions are recorded as captures. Compound assignments record their local target as both a read and a write, while retaining one reference occurrence. Updating an indexed target reads its table and index bindings; it does not write those bindings.

Analysis does not widen a lexical region to make a reference valid. If a transform moves a reference outside its symbol's declaration region, it must also restructure the declarations and references. Otherwise analysis reports an inaccessible local symbol.

Moving and cloning nodes

Moving a complete subtree is safe when it remains within the lexical region of every symbol it uses and the destination preserves declaration order. Re-run binding analysis after structural changes that affect declarations, references, or lexical ownership.

Do not place one declaration symbol in two declaration nodes or share the same block object at multiple tree positions. Analysis rejects duplicate symbol declarations and repeated blocks. When cloning code that introduces a new local binding, create a fresh Ast.LocalSymbol for the clone and point its cloned references to that symbol. Reusing the original symbol would make two declarations claim one identity. Global nodes can be cloned by name.

Native Cff wrappers and standalone interpreter flattening are built directly as AST nodes. They preserve local symbols and use fresh symbols for bridge parameters; printing and reparsing fragments must not be used to recover binding identities.

Lowering backends may attach private slot or scope metadata for their own implementation. Such fields are derived and backend-specific; AST transforms must use symbols and must not rely on those fields for binding identity.

results matching ""

    No results matching ""