[WIP] A pure functional language that compiles to WGSL (WebGPU Shading Language).
https://github.com/ubugeeei/fwgsl.git
A pure functional language that compiles to WGSL (WebGPU Shading Language).
fwgsl brings ML/Haskell-style programming to the GPU โ static typing, Hindley-Milner inference, algebraic data types, pattern matching, and function composition โ all compiled down to valid WGSL that runs on WebGPU.
double : I32 -> I32
double x = x + x
main : I32 -> I32
main x =
let y = double x
in y + 1
data Color = Red | Green | Blue
show : Color -> I32
show c = match c
| Red -> 0
| Green -> 1
| Blue -> 2
apply : (I32 -> I32) -> I32 -> I32
apply f x = f x
inc : I32 -> I32
inc = \x -> x + 1
doubleInc : I32 -> I32
doubleInc = double . inc
@vertex
vs_main : VertexInput -> Vec4F
vs_main input = input.position
@fragment
fs_main : FragmentInput -> Vec4F
fs_main input = vec4 1.0 0.0 0.0 1.0
@compute @workgroup_size(64, 1, 1)
cs_main : ComputeInput -> ()
cs_main input =
let idx = input.global_invocation_id.x
in store output idx (load input_buf idx)
A more complete example showing ADTs, pattern matching, and compute shaders working together:
data ParticleState
= Active { position : Vec3F, velocity : Vec3F, life : F32 }
| Dead
stepParticle : F32 -> ParticleState -> ParticleState
stepParticle dt particle =
match particle
| Active { position, velocity, life } ->
if life - dt <= 0.0
then Dead
else Active
{ position = position + velocity * dt
, velocity = velocity + gravity * dt
, life = life - dt
}
| Dead -> Dead
@compute @workgroup_size(64, 1, 1)
main : ComputeInput -> ()
main input =
let idx = input.global_invocation_id.x
state = load particles idx
state' = stepParticle deltaTime state
in store particles idx state'
Compiles to:
struct ParticleState {
tag: u32,
position: vec3<f32>,
velocity: vec3<f32>,
life: f32,
}
fn stepParticle(dt: f32, particle: ParticleState) -> ParticleState {
if (particle.tag == 0u) {
let position = particle.position;
let velocity = particle.velocity;
let life = particle.life;
if (life - dt <= 0.0) {
return ParticleState(1u, vec3<f32>(0.0), vec3<f32>(0.0), 0.0);
} else {
return ParticleState(0u, position + velocity * dt,
velocity + vec3<f32>(0.0, -9.8, 0.0) * dt, life - dt);
}
} else {
return particle;
}
}
@compute @workgroup_size(64, 1, 1)
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
let idx = gid.x;
let state = particles[idx];
let state_prime = stepParticle(deltaTime, state);
particles[idx] = state_prime;
}
Vec 3 F32 compiles to vec3<f32>, Mat 4 4 F32 to mat4x4<f32>, with compile-time dimension validation<%%INLINECODE4%%gt;)add : I32 -> I32 -> I32 using : (not ::)data Color = Red | Green | Blue compiled to tagged WGSL structsmatch with pipe-delimited arms, compiled to decision trees via Maranget's algorithm\x y -> x + ylet x = 1 in x + 2 with block-style let ... in ...normalize . getVelocity with the dot operator(+), (*) can be passed as first-class values x clamp lo turns any function into an infix operator
- **If-then-else** โ if x == 0 then 1 else 2 as expressions
### Syntax
- **Indentation-sensitive layout** โ Haskell 2010-style layout rules with virtual braces and semicolons
- **Entry point attributes** โ @vertex, @fragment, @compute @workgroupsize(64, 1, 1) map directly to WGSL shader stages
- **Expression-oriented** โ everything is an expression, including if, let, and match
- **Pratt parsing** โ operator precedence: . > application > *// > +/- > comparison > && > || > backtick > $
### Compiler Transforms
- **Monomorphization** โ polymorphic functions specialized at each call site for WGSL compatibility
- **Defunctionalization** โ higher-order functions lowered to tagged structs with switch dispatch
- **Tail-call elimination** โ tail recursion converted to loops (WGSL forbids recursion)
- **ADT lowering** โ algebraic data types compiled to flat structs with tag fields
## Architecture
Written in Rust, inspired by [Oxc](https://github.com/oxc-project/oxc)'s arena-allocated design for fast compilation.
%%CODEBLOCK6%%
### Crates
| Crate | Purpose |
|-------|---------|
| fwgslallocator | Arena allocation (bumpalo wrapper) |
| fwgslspan | Source spans, atoms, source types |
| fwgsldiagnostics | Error/warning reporting with labels (miette-based) |
| fwgslsyntax | SyntaxKind enum for all tokens and node kinds |
| fwgslparser | Hand-written lexer, Haskell-style layout resolver, recursive descent + Pratt parser |
| fwgsltypechecker | Type representation, HM inference engine, union-find unification |
| fwgslsemantic | Name resolution, scope analysis, type checking with constructor registration |
| fwgslhir | High-level IR โ desugared, type-annotated, still functional |
| fwgslmir | Mid-level IR โ monomorphized, first-order, imperative, WGSL-shaped |
| fwgslwgslcodegen | MIR to WGSL text emission with struct ordering and name mangling |
| fwgsllanguageserver | LSP server (tower-lsp) โ diagnostics, hover, completion, goto-definition, semantic tokens |
| fwgslintegrationtests | End-to-end pipeline tests (256+ tests) |
| fwgslcli | Command-line interface |
| fwgslwasm | WASM target for web playground |
## Getting Started
Requires [Rust](https://rustup.rs/) and [mise](https://mise.jdx.dev/).
%%CODEBLOCK7%%
## Tooling
### Language Server (LSP)
The fwgsl-lsp binary provides IDE support via the Language Server Protocol:
- **Diagnostics** โ parse and type errors reported on open/change
- **Hover** โ shows inferred types for identifiers and keyword descriptions
- **Completion** โ keywords, built-in types (I32, F32, Vec3F`, ...), WGSL builtins, and document identifiersA browser-based playground with Monaco editor, WGSL output pane, and WebGPU live preview.
mise run wasm # build WASM module
mise run playground # serve playground
mise run dev # quick dev server (no WASM rebuild)
Planned: CST-based formatter (preserves comments, canonical indentation) and modular lint rules (unused variables, incomplete patterns, WGSL-incompatible recursion).
WGSL imposes severe restrictions that the compiler must bridge:
| Constraint | How fwgsl handles it |
|---|---|
| No recursion | Tail-call elimination to loops; general recursion detected and rejected |
| No dynamic allocation | All data is fixed-size; arena allocation is compile-time only |
| No first-class functions | Defunctionalization: closures become tagged structs + dispatch |
| No generics | Monomorphization: polymorphic code specialized per call site |
| No ADTs | Tagged structs with flat field layout |
MIT