An experiment with creating a programming language that compiles to the SPIR-V GPU shading language
Excise hardcoded Vec4F
Making variable-length vectors isn't too hard maybe
Refactored function type a little.


browse log




A small experiment in making a toy programming language that compiles to SPIR-V shaders for Vulkan.

Basically, I want a compiler targeting SPIRV that is easier to embed than shaderc, and I want to have a mostly-pure-functional shading language (because shaders are, you know, basically pure functions already), so here we are. I'll probably never finish it.


So far it's only been tested on Linux. Probably will work fine on whatever, since there's no platform-specific dependencies.

Running unit tests requires the spirv-val command line program from the official Khronos SPIR-V tools; on Debian you just have to run apt install spirv-tools. glslang-tools may also be useful.

To actually see specifics of what's going on it may also be useful to test with cargo test -- --nocapture --test-threads=1.


This is an experiment on making something compile to SPIR-V more than anything else, so. It's going to be a strongly-typed, pure functional language that probably looks kinda like ML. But I'm starting with the IR and backend for now, 'cause writing parsers is frustrating and slow.

Actually, having no mutation really doesn't make anything harder, since SPIR-V is a SSA form anyway.

Though traditionally the way to handle loops in such language is via recursion, and shaders generally can't be recursive, so that's going to be tricky. We're probably going to either have to introduce loops with attendant mutation (fake or real), or only allow recursion that can be flattened into loops. Though the SPIR-V spec is oddly mostly silent on whether recursion is allowed...

I guess it's not even a real functional language yet either, since it doesn't yet implement functions as values, though it looks like SPIR-V can support it.


  • Compiles to SPIR-V
  • Executes vertex+fragment shaders on a real GPU
  • Simple to use
  • Simple/fast(?) to build
  • Runs as a library or as a standalone program


  • Optimized
  • Super complete
  • Super ergonomic
  • Other sorts of shaders
  • Type inference
  • Modules


Actually to do:

  • Replace unwrap with expect.
  • Ponder error handling better.
  • Replace strings with interned symbols
  • General cleanup
  • Ponder vector operations
  • Get entry points working better.
  • Names/labels/stuff for better debugging.
  • idk mang make a parser or something?

To think about:

  • Vector swizzling -- do we need it? Can we just do it all with pattern matching? Let's try.
  • Vector types -- can they just be structs? That would work well with pattern matching. But vec's are a special case in spir-v anyway.
  • Enums/option types
  • Structure layouts and binding
  • Loops (recursion?)
  • Math and type system
  • Play with fuzzing?
  • Make more shader built-in state accessible?
  • Better handling of locations, bindings, groups, etc.

To compare against code generated by a (hopefully) known-valid compiler:

# -G for GLSL semantics, -V for Vulkan semantics.
glslangValidator -V test.frag.glsl; spirv-dis frag.spv

To not do YET:

  • Entry point decl's
  • Scalars besides f32
  • Better struct and vector handling

#Syntax notes

ML-y syntax or Lispy syntax?

Let's go with ML-y to start, just to see what it looks like.

Really I'm not sure how to handle math operations; we have no generics or traits, so making an Add trait doesn't really work. However, we also have no type inference, so we don't necessarily need OCaml style + vs +. Seems like we can either do it C style and overload math operators as a special case, which I dislike but which is easy in practice, or we can do it ML style and overload nothing, which is simpler but annoying to write.

But we could make it so that adding integers is +, floats is +., vectors is +/ and matrices is ++. Which is almost too cute to resist. For now though, undefined.

-- Yes, Lua style comments
-- just to mess with people.
/- Because why not -/


-- Anyway, two types of decl's, functions and structs.

fn foo x: F32, y: F32 -> bool
    -- ...

-- Types must start with a capital letter
struct SomeStruct
    x: F32,
    y: F32,


-- Floats must have a decimal point
let foo: F32 = 10.0

-- Math
x + y
x - y
x * y
x / y
x % y
-- Comparison
x == y
x != y
x > y
x >= y
x < y
x <= y

-- Logic, on booleans.  No bitwise stuff yet.
x or y
x and y
x xor y
not x

-- Blocks

-- Function calls
foo(x, y, z)

-- Structure literals
SomeStruct { x: 1.0, y: 2.0 }

-- Pattern matches

match foo
    | 1.0 -> foo * 2.0
    | 2.0 -> foo / 2.0
    | _ -> -9999.0

-- Destructuring matches
-- variables always are lowercase.

let SomeStruct { x, _ } = foo
-- x is now bound