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

Published: (December 11, 2025 at 07:58 PM EST)
3 min read
Source: Dev.to

Source: Dev.to

Cover image for Building a Shader Compiler in Pure Go: naga Reaches v0.4.0

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

CategoryFeatures
Typesf32, f64, i32, u32, bool, vec2‑4, mat2x2‑4x4, arrays, structs, atomics
Texturestexture_2d, texture_3d, texture_cube, sampler
Shaders@vertex, @fragment, @compute
Bindings@location, @group/@binding, @builtin
Storageuniform, storage (read/read_write), workgroup
Functions50+ built‑ins (math, geometric, interpolation, atomic, barrier)
Controlif/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.

Back to Blog

Related posts

Read more »