Linker
The linker is responsible for merging included files, building a complete symbol table, tracking uniform usage, and validating identifier references.
Responsibilities
The Linker::link method performs four key tasks:
- Include resolution - load and parse
@includefiles, merge their AST nodes - Symbol table construction - gather all declared names from globals and stages
- Uniform usage scanning - determine which global uniforms are referenced by each stage
- Identifier validation - report errors for unresolved symbols and undefined identifiers
Include Resolution
For each @include "path" directive in the program:
Process
- The file is read relative to
base_path(or as-is ifbase_pathis empty) - The file is tokenized and parsed independently
offset_node_ids()remaps all NodeIDs in the included AST byN = current_node_count- The included nodes are appended to
result.all_nodes - The included program's global NodeIDs (also offset by
N) are appended to the main program's globals list
Important Notes
- Only globals are merged - stage blocks from included files are not merged
- Recursive includes - included files can themselves have
@includedirectives - Circular include detection - not currently implemented (may cause infinite loop)
Example
common/defs.axsl:
@version 450;
struct Material {
vec3 albedo;
float roughness;
};
const float PI = 3.14159;main.axsl:
@version 450;
@include "common/defs.axsl";
@stage fragment {
uniform Material u_mat; // uses Material from include
// PI is also available
}The linker will:
- Parse
common/defs.axsl - Extract
MaterialandPInodes - Offset their node IDs
- Append them to the main program's globals
Include Merging Visualization
Scope Info
struct ScopeInfo {
std::unordered_set<std::string> global_uniforms;
std::unordered_map<StageKind, std::unordered_set<std::string>> stage_uniforms;
};global_uniforms
Contains the names of:
- All global-scope
UniformDeclinstances - All
BufferDeclinstance names (the identifier after the block)
stage_uniforms
Maps each stage to its own set of stage-local uniform and buffer instance names.
This is used for:
- Dead-code elimination (only emit uniforms that are used)
- Shadowing detection (stage-local uniforms override globals)
Uniform Usage Scanning
After building the scope info, the linker recursively walks every function body inside each stage block.
scan_stmt_for_uniforms
Recursively visits statements:
BlockStmt→ scan all statements in the blockIfStmt→ scan condition, then-branch, else-branchForStmt→ scan init, condition, increment, bodyWhileStmt→ scan condition, bodyReturnStmt→ scan return expressionExprStmt→ scan expressionDeclStmt→ scan initializer
scan_expr_for_uniforms
Recursively visits expressions:
IdentifierExpr→ if name is inglobal_uniformsorstage_uniforms[stage], record itBinaryExpr→ scan left and rightUnaryExpr→ scan operandCallExpr→ scan callee and all argumentsFieldExpr→ scan base expressionIndexExpr→ scan base and index- ... etc.
Result
For each stage, a set of uniform names that are actually referenced in that stage's code. This is stored in LinkResult::uniform_usage[stage].
Identifier Validation
Two checks are performed after symbol table construction:
1. UnresolvedRef
For any node that is still UnresolvedRef, if its name is not in the declared set, report an error:
unresolved symbol: 'name' at line:colThis catches:
- Calls to undeclared functions
- References to nonexistent variables
2. FieldExpr / IndexExpr
For field access obj.field and array subscript array[index]:
If the base expression is an IdentifierExpr, verify the name is:
- In the declared set (functions, structs, uniforms)
- In the GLSL built-ins set (
gl_Position, etc.) - In the local variable set (collected from
VarDeclandParamDecl)
If not, report an error:
undefined identifier: 'name' at line:colLocal Variable Collection
The linker calls collect_locals() which traverses all function bodies and collects names from:
VarDeclnodes (local variables)ParamDeclnodes (function parameters)
These are added to a per-function local variable set used during validation.
LinkResult
struct LinkResult {
std::vector<ASTNode> all_nodes;
std::unordered_map<StageKind, std::unordered_set<std::string>> uniform_usage;
std::vector<std::string> errors;
};all_nodes
The merged, flat AST containing:
- All nodes from the main program
- All nodes from included files (with offset NodeIDs)
uniform_usage
Maps each stage to the set of uniform names that stage's code actually references.
Used by the emitter for dead-code elimination.
errors
List of error messages from:
- Failed include file reads
- Unresolved symbols
- Undefined identifiers
If errors is non-empty, compilation fails and these errors are reported to the user.
Error Examples
Unresolved Function
void main() {
vec3 result = unknown_function(1.0); // error
}Error:
unresolved symbol: 'unknown_function' at 2:19Undefined Variable
void main() {
float x = undefined_var + 1.0; // error
}Error:
undefined identifier: 'undefined_var' at 2:15Missing Include
@include "missing.axsl"; // error: file doesn't existError:
cannot open include: 'missing.axsl'Implementation Notes
Built-in Variables Set
The linker pre-populates a set with all GLSL built-in variable names:
gl_Position,gl_PointSize,gl_VertexID, etc.
These are never reported as "undefined identifier" errors.
Struct Names vs Variables
The declared set contains:
- Function names (from
FuncDecl) - Struct names (from
StructDecl) - Uniform names (from
UniformDecl,BufferDecl) - Interface names (from
InterfaceDecl)
Local variables are tracked separately per function.
No Cross-Stage Validation
The linker does not validate that interface blocks match between stages (e.g., that out Varyings in vertex matches in Varyings in fragment). This is delegated to the GLSL compiler.