Skip to content

Commit 6c3ee57

Browse files
committed
feat: Implement parser support for GPU/WGSL syntax
Add full parser support for GPU declarations, completing the WGSL code generation pipeline. Users can now write GPU shaders directly in .gx files and generate WGSL code. Key changes: - Updated GPUDecorator to parse Directive tokens from lexer - Reordered File struct to parse GPU declarations before regular types - Added decorator name stripping in WGSL generator (@gpu -> gpu) - Created comprehensive GPU parser tests (structs, bindings, functions) - Added end-to-end tests for complete parse→generate workflow Parser now supports: - GPU struct declarations (@gpu type Uniforms struct {...}) - GPU binding declarations (@binding(0,0) @uniform var uniforms Uniforms) - GPU functions (@vertex, @Fragment, @compute) - Field decorators (@Builtin, @location) - Parameter decorators (@Builtin(vertex_index)) - Mixed GPU and regular code in same file Test coverage: - 6 GPU parser tests (all passing) - 2 end-to-end generation tests (all passing) - Verified WGSL output correctness - Verified Go struct generation Example now working: ``` @gpu type Uniforms struct { viewportSize vec2 color vec4 } @binding(0, 0) @uniform var uniforms Uniforms @vertex func vsMain(@Builtin(vertex_index) idx uint32) vec4 { return vec4(0.0, 0.0, 0.0, 1.0) } ``` Generates valid WGSL and matching Go structs automatically.
1 parent 66dfe20 commit 6c3ee57

6 files changed

Lines changed: 523 additions & 9 deletions

File tree

pkg/ast/ast.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ type File struct {
1111
Pos lexer.Position
1212
Package string `"package" @Ident`
1313
Imports []*Import `@@*`
14-
Types []*TypeDef `@@*`
1514
GPUStructs []*GPUStructDecl `@@*`
1615
GPUBindings []*GPUBindingDecl `@@*`
1716
GPUFunctions []*GPUFuncDecl `@@*`
17+
Types []*TypeDef `@@*`
1818
Components []*Component `@@*`
1919
Methods []*Method `@@*`
2020
}

pkg/ast/base_visitor.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@ func (v *BaseVisitor) VisitFile(node *File) interface{} {
1515
for _, imp := range node.Imports {
1616
imp.Accept(v)
1717
}
18-
for _, typeDef := range node.Types {
19-
typeDef.Accept(v)
20-
}
2118
for _, gpuStruct := range node.GPUStructs {
2219
gpuStruct.Accept(v)
2320
}
@@ -27,6 +24,9 @@ func (v *BaseVisitor) VisitFile(node *File) interface{} {
2724
for _, gpuFunc := range node.GPUFunctions {
2825
gpuFunc.Accept(v)
2926
}
27+
for _, typeDef := range node.Types {
28+
typeDef.Accept(v)
29+
}
3030
for _, comp := range node.Components {
3131
comp.Accept(v)
3232
}

pkg/ast/gpu_ast.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import "github.com/alecthomas/participle/v2/lexer"
66
// GPUDecorator represents a GPU-specific decorator like @gpu, @vertex, @binding, etc.
77
type GPUDecorator struct {
88
Pos lexer.Position
9-
Name string `"@" @("gpu" | "vertex" | "fragment" | "compute" | "uniform" | "storage" | "binding" | "location" | "builtin" | "workgroup")`
9+
Name string `@Directive`
1010
Args []*Expr `("(" (@@ ("," @@)*)? ")")?`
1111
}
1212

pkg/codegen/e2e_wgsl_test.go

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
package codegen
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/gaarutyunov/guix/pkg/parser"
8+
)
9+
10+
func TestE2EWGSLGeneration(t *testing.T) {
11+
input := `package shaders
12+
13+
@gpu type Uniforms struct {
14+
viewportSize vec2
15+
color vec4
16+
}
17+
18+
@binding(0, 0) @uniform var uniforms Uniforms
19+
20+
@vertex
21+
func vsMain(@builtin(vertex_index) idx uint32) vec4 {
22+
return vec4(0.0, 0.0, 0.0, 1.0)
23+
}
24+
25+
@fragment
26+
func fsMain(@location(0) color vec4) vec4 {
27+
return color
28+
}`
29+
30+
// Parse
31+
p, err := parser.New()
32+
if err != nil {
33+
t.Fatalf("Failed to create parser: %v", err)
34+
}
35+
36+
file, err := p.ParseString(input)
37+
if err != nil {
38+
t.Fatalf("Failed to parse: %v", err)
39+
}
40+
41+
// Verify parsing
42+
if len(file.GPUStructs) != 1 {
43+
t.Errorf("Expected 1 GPU struct, got %d", len(file.GPUStructs))
44+
}
45+
46+
if len(file.GPUBindings) != 1 {
47+
t.Errorf("Expected 1 GPU binding, got %d", len(file.GPUBindings))
48+
}
49+
50+
if len(file.GPUFunctions) != 2 {
51+
t.Errorf("Expected 2 GPU functions, got %d", len(file.GPUFunctions))
52+
}
53+
54+
// Generate WGSL
55+
wgslGen := NewWGSLGenerator()
56+
wgslOutput, err := wgslGen.Generate(file)
57+
if err != nil {
58+
t.Fatalf("Failed to generate WGSL: %v", err)
59+
}
60+
61+
wgslCode := string(wgslOutput)
62+
t.Logf("Generated WGSL:\n%s", wgslCode)
63+
64+
// Verify WGSL output contains expected elements
65+
expectedStrings := []string{
66+
"struct Uniforms {",
67+
"viewportSize: vec2<f32>,",
68+
"color: vec4<f32>,",
69+
"@binding(0, 0)",
70+
"@uniform",
71+
"var<uniform> uniforms: Uniforms;",
72+
"@vertex",
73+
"fn vsMain(",
74+
"@builtin(vertex_index) idx: u32",
75+
"-> vec4<f32>",
76+
"@fragment",
77+
"fn fsMain(",
78+
"@location(0) color: vec4<f32>",
79+
}
80+
81+
for _, expected := range expectedStrings {
82+
if !strings.Contains(wgslCode, expected) {
83+
t.Errorf("Expected WGSL to contain %q, but it didn't", expected)
84+
}
85+
}
86+
87+
// Generate GPU Go code
88+
gpuGoGen := NewGPUGoGenerator(file.Package)
89+
gpuGoOutput, err := gpuGoGen.Generate(file)
90+
if err != nil {
91+
t.Fatalf("Failed to generate GPU Go code: %v", err)
92+
}
93+
94+
goCode := string(gpuGoOutput)
95+
t.Logf("Generated Go code:\n%s", goCode)
96+
97+
// Verify Go output contains expected elements
98+
expectedGoStrings := []string{
99+
"type Uniforms struct {",
100+
"ViewportSize",
101+
"[2]float32",
102+
"Color",
103+
"[4]float32",
104+
"func (s *Uniforms) ToBytes() []byte",
105+
"//go:embed shaders.wgsl",
106+
"var ShaderSource string",
107+
}
108+
109+
for _, expected := range expectedGoStrings {
110+
if !strings.Contains(goCode, expected) {
111+
t.Errorf("Expected Go code to contain %q, but it didn't", expected)
112+
}
113+
}
114+
}
115+
116+
func TestE2ECompleteShader(t *testing.T) {
117+
input := `package candle
118+
119+
@gpu type ChartUniforms struct {
120+
viewportSize vec2
121+
dataRange vec4
122+
candleWidth float32
123+
upColor vec4
124+
downColor vec4
125+
}
126+
127+
@gpu type Candle struct {
128+
timestamp float32
129+
open float32
130+
high float32
131+
low float32
132+
close float32
133+
}
134+
135+
@binding(0, 0) @uniform var uniforms ChartUniforms
136+
@binding(0, 1) @storage var candles []Candle
137+
138+
@vertex
139+
func vsCandle(@builtin(vertex_index) vid uint32, @builtin(instance_index) iid uint32) vec4 {
140+
c := candles[iid]
141+
bullish := c.close >= c.open
142+
x := (c.timestamp - uniforms.dataRange.x) / (uniforms.dataRange.z - uniforms.dataRange.x)
143+
return vec4(x, 0.0, 0.0, 1.0)
144+
}
145+
146+
@fragment
147+
func fsCandle(@location(0) color vec4) vec4 {
148+
return color
149+
}`
150+
151+
// Parse
152+
p, err := parser.New()
153+
if err != nil {
154+
t.Fatalf("Failed to create parser: %v", err)
155+
}
156+
157+
file, err := p.ParseString(input)
158+
if err != nil {
159+
t.Fatalf("Failed to parse: %v", err)
160+
}
161+
162+
// Generate WGSL
163+
wgslGen := NewWGSLGenerator()
164+
wgslOutput, err := wgslGen.Generate(file)
165+
if err != nil {
166+
t.Fatalf("Failed to generate WGSL: %v", err)
167+
}
168+
169+
wgslCode := string(wgslOutput)
170+
t.Logf("Generated complete WGSL:\n%s", wgslCode)
171+
172+
// Verify complex features
173+
if !strings.Contains(wgslCode, "array<Candle>") {
174+
t.Error("Expected array type for candles slice")
175+
}
176+
177+
if !strings.Contains(wgslCode, "let c = candles[iid];") {
178+
t.Error("Expected array indexing in generated code")
179+
}
180+
181+
if !strings.Contains(wgslCode, "c.close >= c.open") {
182+
t.Error("Expected comparison expression")
183+
}
184+
185+
if !strings.Contains(wgslCode, "uniforms.dataRange.x") {
186+
t.Error("Expected field access")
187+
}
188+
189+
// Generate Go code
190+
gpuGoGen := NewGPUGoGenerator(file.Package)
191+
gpuGoOutput, err := gpuGoGen.Generate(file)
192+
if err != nil {
193+
t.Fatalf("Failed to generate GPU Go code: %v", err)
194+
}
195+
196+
goCode := string(gpuGoOutput)
197+
198+
// Verify Go struct generation
199+
if !strings.Contains(goCode, "type ChartUniforms struct") {
200+
t.Error("Expected ChartUniforms struct")
201+
}
202+
203+
if !strings.Contains(goCode, "type Candle struct") {
204+
t.Error("Expected Candle struct")
205+
}
206+
}

pkg/codegen/wgsl_generator.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,9 @@ func (g *WGSLGenerator) generateBinding(binding *guixast.GPUBindingDecl) {
135135
// Get address space and access mode from decorators
136136
addressSpace := ""
137137
for _, decorator := range binding.Decorators {
138-
switch decorator.Name {
138+
// Strip @ prefix for comparison
139+
name := strings.TrimPrefix(decorator.Name, "@")
140+
switch name {
139141
case "uniform":
140142
addressSpace = "<uniform>"
141143
case "storage":
@@ -171,7 +173,9 @@ func (g *WGSLGenerator) generateFunction(fn *guixast.GPUFuncDecl) {
171173
var otherDecorators []string
172174

173175
for _, decorator := range fn.Decorators {
174-
if decorator.Name == "vertex" || decorator.Name == "fragment" || decorator.Name == "compute" {
176+
// Strip @ prefix for comparison
177+
name := strings.TrimPrefix(decorator.Name, "@")
178+
if name == "vertex" || name == "fragment" || name == "compute" {
175179
entryDecorators = append(entryDecorators, g.formatDecorator(decorator))
176180
} else {
177181
otherDecorators = append(otherDecorators, g.formatDecorator(decorator))
@@ -251,8 +255,11 @@ func (g *WGSLGenerator) generateParameter(param *guixast.GPUParameter) string {
251255

252256
// Format a decorator for WGSL output
253257
func (g *WGSLGenerator) formatDecorator(decorator *guixast.GPUDecorator) string {
258+
// Strip @ prefix if present (decorator.Name comes from Directive token which includes @)
259+
name := strings.TrimPrefix(decorator.Name, "@")
260+
254261
if len(decorator.Args) == 0 {
255-
return fmt.Sprintf("@%s", decorator.Name)
262+
return fmt.Sprintf("@%s", name)
256263
}
257264

258265
// Format arguments
@@ -261,7 +268,7 @@ func (g *WGSLGenerator) formatDecorator(decorator *guixast.GPUDecorator) string
261268
args[i] = g.generateExpression(arg)
262269
}
263270

264-
return fmt.Sprintf("@%s(%s)", decorator.Name, strings.Join(args, ", "))
271+
return fmt.Sprintf("@%s(%s)", name, strings.Join(args, ", "))
265272
}
266273

267274
// Generate function body

0 commit comments

Comments
 (0)