DEV Community

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

Posted on

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

A few weeks ago, I published GoGPU: A Pure Go Graphics Library for GPU Programming — introducing an ecosystem for GPU computing in Go without CGO. Today, I want to share progress on one of the most challenging components: naga, a shader compiler written entirely in Go.

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, or 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.
Enter fullscreen mode Exit fullscreen mode

The Journey: v0.1.0 to v0.4.0

v0.1.0 — Foundation (~10K LOC)

Started with the basics:

  • 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 (~2K LOC)

The hard part: type inference. SPIR-V requires explicit types for everything, but WGSL allows inference:

let x = 1.0;           // What type? f32
let v = vec3(1.0);     // vec3<f32>
let n = normalize(v);  // Also vec3<f32>, inferred from function return
Enter fullscreen mode Exit fullscreen mode

Built a complete type resolution engine that tracks types through every expression.

v0.3.0 — Textures (~3K LOC)

Added texture operations — the bread and butter of graphics:

@fragment
fn main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
    return textureSample(myTexture, mySampler, uv);
}
Enter fullscreen mode Exit fullscreen mode

This required implementing SPIR-V image operations: OpSampledImage, OpImageSampleImplicitLod, and friends.

v0.4.0 — Compute Shaders (~2K LOC)

The latest release brings GPU compute capabilities:

@group(0) @binding(0)
var<storage, read_write> counter: atomic<u32>;

@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
    atomicAdd(&counter, 1u);
    workgroupBarrier();
}
Enter fullscreen mode Exit fullscreen mode

This required:

  • Storage buffer access modes: read, read_write
  • Workgroup shared memory: var<workgroup>
  • 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                  │
└─────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Usage

As a Library

import "github.com/gogpu/naga"

func main() {
    source := `
@vertex
fn main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {
    return vec4<f32>(0.0, 0.0, 0.0, 1.0);
}
`
    spirv, err := naga.Compile(source)
    if err != nil {
        log.Fatal(err)
    }
    // spirv is ready for Vulkan
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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-in (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 5ms. The entire test suite (203 tests) runs in ~2 seconds.

No benchmarks yet against Rust's naga, but the goal isn't to be faster — it's to be pure Go and integrate seamlessly into Go toolchains.

Part of the GoGPU Ecosystem

naga is one piece of a larger vision:

Project Description Status
gogpu/gogpu Graphics framework v0.3.0
gogpu/wgpu Pure Go WebGPU v0.3.0
gogpu/naga Shader compiler v0.4.0
gogpu/gg 2D graphics Planned
gogpu/ui GUI toolkit Planned

Together, this gives Go a complete GPU computing stack — from low-level shaders to high-level graphics, all without CGO.

Try It

go get github.com/gogpu/naga@v0.4.0
Enter fullscreen mode Exit fullscreen mode

Or check out the repository: github.com/gogpu/naga

Contributing

Areas where contributions would help:

  • Test cases — Real shaders from production apps
  • GLSL backend — Help with the v0.5.0 target
  • Documentation — Examples, tutorials
  • Edge cases — WGSL features we haven't covered

Building a shader compiler from scratch has been one of the most educational projects I've worked on. Parsing, type systems, code generation — it's a crash course in compiler design.

If you're interested in GPU programming, graphics, or just want to see what 17K lines of Go can do, check out the GoGPU organization.

Star the repos if you find them useful. And if you hit any issues, open a GitHub issue — that's how we make this better.


We Need Testers with Real Projects

Here's the honest truth: naga has been tested against our own shaders and synthetic test cases. But compilers are only as good as the code they're tested against.

If you have real WGSL shaders from production projects, we want them.

What we're looking for:

  • Shaders from actual games or applications
  • Complex compute shaders with real algorithms
  • Edge cases that might break our parser
  • Anything that works with other compilers but fails with naga

How to help:

  1. Try compiling your shaders with nagac
  2. If something fails — open an issue with the shader code
  3. If it works — let us know! We'd love to add it to our test suite (with attribution)

Even anonymous shader submissions help. The more real-world code we test against, the more robust naga becomes.

# Quick test
go install github.com/gogpu/naga/cmd/nagac@v0.4.0
nagac your_shader.wgsl -o test.spv

# Did it work? Did it fail? Either way, we want to know.
Enter fullscreen mode Exit fullscreen mode

Open an issue: github.com/gogpu/naga/issues


Links:

Top comments (0)