Eccentricities
This section explains where AXSL deviates from standard GLSL or C-style compilers. These design choices prioritize simplicity and per-stage performance over conventional language features.
Flat AST with Integer IDs
Most AST implementations use heap-allocated nodes connected by pointers. AXSLC uses a single std::vector<ASTNode> and uint32_t indices.
Benefits
- Trivial include merging - append vectors and rebase indices
- No pointer invalidation - vector may reallocate, but indices remain stable
- High memory locality - sequential traversal benefits from cache coherency
- Simple serialization - no complex graph serialization needed
Trade-offs
- No compile-time type safety - accessing
m_nodes[id]andstd::get-ing the wrong variant type is a runtime crash - No parent pointers - traversal is always top-down from a known root ID
Why It Matters
This design makes the visit_offset() pattern possible, which is central to how includes work. See AST Design for details.
do-while → WhileStmt
do { body } while (cond) is parsed and represented internally as a WhileStmt{cond, body} - the same node type as a plain while loop.
The AST carries no information distinguishing the two.
Current Behavior
The emitter emits both as while (cond) { body }, losing the do-while semantics (the body executing at least once).
Impact
If you need a true do-while in emitted GLSL, this is a known limitation. Workaround:
// Instead of:
do {
body();
} while (cond);
// Write:
body(); // first iteration
while (cond) {
body();
}Per-Stage Uniform Dead-Code Elimination
Global uniforms are declared once at the top of the .axsl file but are only emitted into a stage's GLSL output if the linker determines that stage's code actually references the uniform name.
Why
- Prevents spurious unused-uniform warnings from the GLSL compiler
- Reduces shader binary size
- Makes it clear which stages use which resources
Example
uniform float u_vertex_time;
uniform float u_fragment_time;
@stage vertex {
void main() {
float t = u_vertex_time; // uses u_vertex_time
}
}
@stage fragment {
void main() {
float t = u_fragment_time; // uses u_fragment_time
}
}Vertex GLSL:
uniform float u_vertex_time; // present
// u_fragment_time is absentFragment GLSL:
uniform float u_fragment_time; // present
// u_vertex_time is absentUniform Shadowing
A stage-local uniform declaration with the same name as a global uniform completely replaces the global in that stage.
Why
Allows stages to declare more specific or differently-annotated versions of a shared concept without renaming.
Example
@binding(0) uniform float u_time; // global default
@stage compute {
@binding(5) uniform float u_time; // override binding for compute stage
void main() {
// uses binding 5 here
}
}Compute GLSL:
layout(binding = 5) uniform float u_time; // stage-local versionOther stages get the global version at binding 0.
Interface System
The interface keyword defines a named group of fields at global scope. An interface declaration is not itself emitted as GLSL. It serves purely as a template.
Why
Avoids repeating the field list in every stage that passes data between vertex and fragment shaders. DRY principle for interface blocks.
How It Works
When a stage declares in IfaceName inst, the emitter expands the template inline.
See Interface System for full details.
Unbounded Array Syntax
An array field with no size (mat4 models[]) is stored in the FieldDecl as array_size = std::optional<uint32_t>(0).
That is:
- The
optionalis set (meaning "is an array") - Its value is zero (a sentinel meaning "unbounded")
Emitter Behavior
The emitter checks for this and emits [] rather than [0].
Why
Unbounded arrays are a GLSL feature for SSBOs (shader storage buffer objects). The size is determined at runtime, not compile time.
@std430 @binding(0) buffer Data {
float values[]; // unbounded
} data;Emits:
layout(std430, binding = 0) buffer Data {
float values[];
} data;Trailing Dot Float Literals
AXSL accepts float literals with a trailing dot and no fractional digits: 0., 1., 2..
Tokenizer Logic
After consuming digits, checks if:
current() == '.'- The character after the dot is neither an alpha character nor
_
If both are true, the dot is consumed as part of the float.
Why
Matches common GLSL usage patterns where authors abbreviate 0.0 to 0. for brevity.
Examples
float a = 0.; // valid, same as 0.0
float b = 1.; // valid, same as 1.0But:
int x = 0.something; // parse error: 0. is a float, .something is invalidNo Type Checking
AXSLC performs no type inference, type checking, or type annotation beyond storing the declared type of variable and function declarations.
The ASTNode::type field is populated as void for all nodes by push_node. No phase attempts to infer or validate expression types.
Why
The downstream GLSL compiler (the OpenGL driver) is the authoritative type checker. AXSLC doesn't try to replicate that work.
What AXSLC Does Check
- Identifier resolution (is
xdeclared?) - Function existence (is
fooa known function or built-in?) - Include file loading (does
common/defs.axslexist?)
What AXSLC Doesn't Check
- Type correctness (is
x + yvalid for types ofxandy?) - Argument count/types (does
foo(a, b)matchfoo's signature?) - Return type matching (does
return x;match the function's return type?)
All of these are caught by the GLSL compiler.
70+ GLSL Built-in Pre-declarations
The parser pre-populates its m_declared set with the names of all commonly used GLSL built-in functions.
Why
Prevents these names from being flagged as UnresolvedRef when used as callees.
List
Covers:
- Trig:
sin,cos,tan,asin,acos,atan, ... - Math:
pow,exp,log,sqrt,abs,sign, ... - Geometric:
length,distance,dot,cross,normalize, ... - Texture:
texture,texture2D,textureLod, ... - Atomic:
atomicAdd,atomicMin, ... - Compute:
barrier,memoryBarrier, ...
See Pratt Parser - Declared Set for the complete list.
expect() Error Location
When a required token is missing, expect() reports the error at the end of the last successfully consumed token rather than at the start of the unexpected lookahead token.
The column is computed as:
last_token.col + token_src_length(last_token)Why
This produces error messages that point to the exact position where the missing token was expected, not to the token that happens to be next in the stream.
Example
void main() {
x = 1.0 // missing semicolon
y = 2.0;
}Error:
shader.axsl:2:12: expected ';' after expression
1 | void main() {
2 | x = 1.0
| ^
3 | y = 2.0;The error is reported at the end of line 2 (after 1.0), not at the start of line 3 (before y).
This makes it immediately clear that the semicolon is missing after 1.0.
Summary
These design choices prioritize:
- Simplicity - flat AST, no type checking
- Correctness - delegate to GLSL compiler where appropriate
- Efficiency - dead-code elimination, cache locality
- Convenience - interface system, uniform shadowing, trailing dots
Trade-offs include:
- Do-while semantics lost
- No compile-time type safety on AST node access
- Some features (like
@define) not yet implemented