Skip to content

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:

  1. Include resolution - load and parse @include files, merge their AST nodes
  2. Symbol table construction - gather all declared names from globals and stages
  3. Uniform usage scanning - determine which global uniforms are referenced by each stage
  4. Identifier validation - report errors for unresolved symbols and undefined identifiers

Include Resolution

For each @include "path" directive in the program:

Process

  1. The file is read relative to base_path (or as-is if base_path is empty)
  2. The file is tokenized and parsed independently
  3. offset_node_ids() remaps all NodeIDs in the included AST by N = current_node_count
  4. The included nodes are appended to result.all_nodes
  5. 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 @include directives
  • Circular include detection - not currently implemented (may cause infinite loop)

Example

common/defs.axsl:

axsl
@version 450;

struct Material {
    vec3 albedo;
    float roughness;
};

const float PI = 3.14159;

main.axsl:

axsl
@version 450;
@include "common/defs.axsl";

@stage fragment {
    uniform Material u_mat;  // uses Material from include
    // PI is also available
}

The linker will:

  1. Parse common/defs.axsl
  2. Extract Material and PI nodes
  3. Offset their node IDs
  4. Append them to the main program's globals

Include Merging Visualization

Scope Info

cpp
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 UniformDecl instances
  • All BufferDecl instance 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 block
  • IfStmt → scan condition, then-branch, else-branch
  • ForStmt → scan init, condition, increment, body
  • WhileStmt → scan condition, body
  • ReturnStmt → scan return expression
  • ExprStmt → scan expression
  • DeclStmt → scan initializer

scan_expr_for_uniforms

Recursively visits expressions:

  • IdentifierExpr → if name is in global_uniforms or stage_uniforms[stage], record it
  • BinaryExpr → scan left and right
  • UnaryExpr → scan operand
  • CallExpr → scan callee and all arguments
  • FieldExpr → scan base expression
  • IndexExpr → 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:col

This 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 VarDecl and ParamDecl)

If not, report an error:

undefined identifier: 'name' at line:col

Local Variable Collection

The linker calls collect_locals() which traverses all function bodies and collects names from:

  • VarDecl nodes (local variables)
  • ParamDecl nodes (function parameters)

These are added to a per-function local variable set used during validation.

LinkResult

cpp
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

axsl
void main() {
    vec3 result = unknown_function(1.0);  // error
}

Error:

unresolved symbol: 'unknown_function' at 2:19

Undefined Variable

axsl
void main() {
    float x = undefined_var + 1.0;  // error
}

Error:

undefined identifier: 'undefined_var' at 2:15

Missing Include

axsl
@include "missing.axsl";  // error: file doesn't exist

Error:

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.