No Aliasing in Nested Calls
In complex shaders, functions often delegate work to other helper functions, passing their pointer parameters deeper down the call stack. To guarantee zero-aliasing optimization safety across the entire program, WGSL's compiler does not just look at the immediate function you call—it recursively evaluates every nested function call triggered by that invocation.
This transitive, multi-layered tracking is known as Deep Call-Stack Alias Analysis.
How Transitive Aliasing Validation Works
When a shader is compiled, the compiler constructs a Call Graph of the program and performs a bottom-up analysis of memory access views:
- Leaf Analysis: The compiler starts at the bottom of the call stack (leaf functions that call no other user functions). It identifies all reads and writes performed via pointer parameters and references.
- Access Propagation: When a parent function calls a helper, the compiler maps the helper's pointer accesses back to the arguments passed by the parent.
- Root Trace Back: Finally, at the entry-point call site, the compiler traces all argument paths back to their original root identifiers (the base variables declared via
varat local or module scope). - Overlap Verification: If two distinct argument paths map back to the same root identifier, and any downstream nested function in the call tree contains a potential write along one of those paths, the entire compilation fails.
Visualizing Call Stack Access Conflicts
The diagram below illustrates how a conflict in a deeply nested helper propagates back up to fail the caller.
Nested Call Stack Aliasing Error
Declares a local variable `var a: i32` and calls `f2(&a, &a)`. Both pointer arguments share the same physical root block in the thread's stack.
Receives two pointer parameters and forwards them directly to the leaf function: `f1(p1, p2)`. It performs no accesses itself, but passes the aliased views.
Executes code: *p1 = *p2;
The compiler detects a read from `p2` and a **write** to `p1`. Since both trace back to the same root variable `a`, an illegal write-alias conflict is registered.
Under the Hood: Resolving to Backend Shaders
Why is this level of scrutiny necessary? When the GPU driver compiles your WGSL shader into native backend instructions (such as HLSL for Direct3D, MSL for Metal, or GLSL/SPIR-V for Vulkan), it often expands WGSL pointer parameters into the backend's native parameter representation.
In Vulkan (SPIR-V) or Metal (MSL), pointers are compiled as physical memory spaces or restricted register references. If they aliased with writes, hardware execution would cause memory ordering undefined behaviors, or trigger severe pipeline stalls while waiting for cache flushes.
In DX12 (HLSL), user-declared functions with inout pointer parameters are often compiled as a Copy-In / Copy-Out register operations. If you alias the same variable across two parameters, the value written back to the root variable when the function returns would be non-deterministic, as each parameter copies its local register back to the variable in an arbitrary sequence.
WGSL prevents all of these hardware-specific compilation and concurrency disasters by stopping compilation at the high-level stage!
Valid Disjoint Accesses
If you declare two separate variables var a: i32; and var b: i32;, you can freely call f2(&a, &b). Because a and b represent completely independent allocations with disjoint roots, there is no overlap hazard. The compiler can promote both to registers, run instructions in parallel, and execute the helper functions at maximum hardware speed.
/*
* Copyright ©2026 Michael R. Bernstein. All new modifications licensed under Apache 2.0.
* Upstream lineage ©2023 governed by original BSD 3-Clause. See README.md.
*/
// f1 writes via p1, reads via p2
fn f1(p1 : ptr<function, i32>, p2 : ptr<function, i32>) {
*p1 = *p2; // Writes via p1, reads via p2
}
// f2 delegates to f1.
fn f2(p1 : ptr<function, i32>, p2 : ptr<function, i32>) {
f1(p1, p2);
}
fn f3() {
var a: i32;
// f1(&a,&a); // Error: Cannot pass two pointer parameters
// with the same root identifier.
// The conflicting accesses are in the directly called
// function, f1.
// f2(&a,&a); // Error: This time the conflict is in f1, called by f2.
// The analysis looks through the whole call stack.
var b: i32;
f2(&a, &b); // This is ok, since a and b don't alias.
}