Building a Shader Compiler in Pure Go: naga Reaches v0.4.0
Source: Dev.to

TL;DR
naga compiles WGSL (WebGPU Shading Language) to SPIR‑V bytecode.
- 17 000 lines of pure Go.
- No CGO. No external dependencies. Just
go build.
v0.4.0 now supports compute shaders with atomics and barriers
- Full type inference system
- 203 tests, ~61 % coverage
- Production‑ready for graphics and compute workloads
Why Build a Shader Compiler?
If you’ve worked with GPU programming in Go, you’ve hit this wall: shader compilation always requires external tools (Rust’s naga, Google’s glslc, NVIDIA’s toolchain). That means:
- Extra build dependencies
- Platform‑specific binaries
- CGO or subprocess calls
- Complex deployment
The GoGPU ecosystem aims to eliminate this friction. A pure‑Go shader compiler means:
go build ./...
# That's it. No cmake, no Rust toolchain, no DLLs.
The Journey: v0.1.0 to v0.4.0
v0.1.0 — Foundation (~10 K LOC)
- WGSL lexer recognizing 140+ tokens
- Recursive‑descent parser
- Intermediate representation (33 expression types, 16 statement types)
- SPIR‑V binary writer with 100+ opcodes
Result: vertex and fragment shaders compiled successfully.
v0.2.0 — Type System (~2 K LOC)
The hard part: type inference. SPIR‑V requires explicit types for everything, but WGSL allows inference:
let x = 1.0; // f32
let v = vec3(1.0); // vec3
let n = normalize(v); // vec3, inferred from function return
Built a complete type‑resolution engine that tracks types through every expression.
v0.3.0 — Textures (~3 K LOC)
Added texture operations — the bread and butter of graphics:
@fragment
fn main(@location(0) uv: vec2) -> @location(0) vec4 {
return textureSample(myTexture, mySampler, uv);
}
Implemented SPIR‑V image operations such as OpSampledImage, OpImageSampleImplicitLod, and friends.
v0.4.0 — Compute Shaders (~2 K LOC)
The latest release brings GPU compute capabilities:
@group(0) @binding(0)
var counter: atomic;
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id: vec3) {
atomicAdd(&counter, 1u);
workgroupBarrier();
}
Key additions
- Storage buffer access modes:
read,read_write - Workgroup shared memory:
var - 9 atomic operations: add, sub, min, max, and, or, xor, exchange, compare‑exchange
- 3 barrier types: workgroup, storage, texture
- Address‑of operator:
&for atomic pointers
Architecture
┌─────────────────────────────────────────────────────┐
│ WGSL Source │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Lexer (140+ tokens) │
│ wgsl/lexer.go — ~400 LOC │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Parser (recursive descent) │
│ wgsl/parser.go — ~1400 LOC │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ AST → IR Lowering │
│ wgsl/lower.go — ~1100 LOC │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Intermediate Representation │
│ 33 expression types, 16 statement types │
│ Type inference + deduplication │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ SPIR‑V Backend │
│ spirv/backend.go — ~1800 LOC │
│ 100+ opcodes, GLSL.std.450 │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ SPIR‑V Binary │
│ Vulkan‑compatible bytecode │
└─────────────────────────────────────────────────────┘
Usage
As a Library
import "github.com/gogpu/naga"
func main() {
source := `
@vertex
fn main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4 {
return vec4(0.0, 0.0, 0.0, 1.0);
}
`
spirv, err := naga.Compile(source)
if err != nil {
log.Fatal(err)
}
// spirv is ready for Vulkan
}
CLI Tool
go install github.com/gogpu/naga/cmd/nagac@latest
nagac shader.wgsl -o shader.spv
nagac -debug shader.wgsl -o shader.spv # with debug names
With Warnings
v0.4.0 introduces unused‑variable detection:
result, err := naga.LowerWithWarnings(ast)
for _, w := range result.Warnings {
fmt.Printf("Warning: %s at line %d\n", w.Message, w.Span.Line)
}
Variables prefixed with _ are intentionally ignored (Go‑style).
Supported Features
| Category | Features |
|---|---|
| Types | f32, f64, i32, u32, bool, vec2‑4, mat2x2‑4x4, arrays, structs, atomics |
| Textures | texture_2d, texture_3d, texture_cube, sampler |
| Shaders | @vertex, @fragment, @compute |
| Bindings | @location, @group/@binding, @builtin |
| Storage | uniform, storage (read/read_write), workgroup |
| Functions | 50+ built‑ins (math, geometric, interpolation, atomic, barrier) |
| Control | if/else, for, while, loop, break, continue |
What’s Next
v0.5.0
- GLSL backend – output to GLSL for OpenGL compatibility
- Source maps – debug info mapping SPIR‑V back to WGSL
- Optimization passes – constant folding, dead‑code elimination
v1.0.0
- Full WGSL specification compliance
- HLSL/MSL backends for DirectX/Metal
- Production hardening
Performance
Compilation is fast. A typical shader compiles in under 5 ms. The entire test suite (203 tests) runs in ~2 seconds.
No benchmarks yet against Rust’s naga, but the goal is not raw speed—it’s to provide a pure Go solution that integrates seamlessly into Go projects.