1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
package shader
import "core:fmt"
// Desugar pass — runs after parsing, before semantic analysis.
// Transforms inline entry point syntax sugar into explicit struct form.
desugar_module :: proc(mod: ^Ast_Module) {
for fn in mod.functions {
if !is_entry_function(fn) do continue
desugar_inline_entry(mod, fn)
}
}
@(private = "file")
is_entry_function :: proc(fn: ^Ast_Function) -> bool {
for attr in fn.attributes {
if attr.name == "entry" do return true
}
return false
}
@(private = "file")
desugar_inline_entry :: proc(mod: ^Ast_Module, fn: ^Ast_Function) {
// Desugar return type -> anonymous output struct
// Handles both tuple return (name: Type, ...) and plain type shorthand (-> vec4)
out_struct_name: string
out_struct_fields: []Ast_Struct_Field
if fn.return_type != nil {
if tt, ok := fn.return_type^.(Type_Tuple); ok {
// Tuple return: -> (color: vec4, ...)
out_struct_name = fmt.aprintf("_%s_Output", fn.name)
out_struct_fields = tt.fields
out_struct := new(Ast_Struct)
out_struct^ = Ast_Struct{
name = out_struct_name,
fields = tt.fields,
span = tt.span,
}
append(&mod.structs, out_struct)
fn.return_type^ = Type_Named{name = out_struct_name, span = tt.span}
} else if tn, ok := fn.return_type^.(Type_Named); ok && is_primitive_type_name(tn.name) {
// Plain type shorthand: -> vec4 (desugars to single-field output struct)
out_struct_name = fmt.aprintf("_%s_Output", fn.name)
field := Ast_Struct_Field{name = "_value", type = tn, span = tn.span}
out_struct_fields = make([]Ast_Struct_Field, 1)
out_struct_fields[0] = field
out_struct := new(Ast_Struct)
out_struct^ = Ast_Struct{
name = out_struct_name,
fields = out_struct_fields,
span = tn.span,
}
append(&mod.structs, out_struct)
fn.return_type^ = Type_Named{name = out_struct_name, span = tn.span}
}
}
// Implicit single-field return: rewrite `return expr` -> `return Struct { field = expr }`
if len(out_struct_fields) == 1 {
rewrite_returns_single_field(fn.body[:], out_struct_name, out_struct_fields[0].name)
}
// Desugar inline params -> anonymous input struct
// An inline entry point has multiple params OR params with attributes
// (vs the normal single-struct-param convention)
if should_desugar_params(fn) {
struct_name := fmt.aprintf("_%s_Input", fn.name)
fields := make([dynamic]Ast_Struct_Field)
for p in fn.params {
append(&fields, Ast_Struct_Field{
name = p.name,
type = p.type,
attributes = p.attributes,
span = p.span,
})
}
in_struct := new(Ast_Struct)
in_struct^ = Ast_Struct{
name = struct_name,
fields = fields[:],
span = fn.span,
}
append(&mod.structs, in_struct)
// Collect original param names for body rewriting
param_names: map[string]bool
for p in fn.params {
param_names[p.name] = true
}
// Rewrite function params to a single struct param
new_params := make([]Ast_Param, 1)
new_params[0] = Ast_Param{
name = "input",
type = Type_Named{name = struct_name, span = fn.span},
span = fn.span,
}
fn.params = new_params
// Rewrite body references: bare `uv` -> `input.uv`
for &stmt in fn.body {
rewrite_node(stmt, ¶m_names)
}
}
}
// Rewrite bare identifier references to desugared param names into field accesses
@(private = "file")
rewrite_node :: proc(node: ^Ast_Node, param_names: ^map[string]bool) {
if node == nil do return
#partial switch d in node.derived {
case ^Ast_Let:
rewrite_expr_in_place(&d.value, param_names)
case ^Ast_Assign:
rewrite_expr_in_place(&d.target, param_names)
rewrite_expr_in_place(&d.value, param_names)
case ^Ast_Output_Assign:
rewrite_expr_in_place(&d.value, param_names)
case ^Ast_Return:
rewrite_expr_in_place(&d.value, param_names)
case ^Ast_If:
rewrite_expr_in_place(&d.condition, param_names)
for &s in d.then_body do rewrite_node(s, param_names)
for &clause in d.elseif_clauses {
rewrite_expr_in_place(&clause.condition, param_names)
for &s in clause.body do rewrite_node(s, param_names)
}
for &s in d.else_body do rewrite_node(s, param_names)
case ^Ast_For:
rewrite_expr_in_place(&d.start, param_names)
rewrite_expr_in_place(&d.stop, param_names)
if d.step != nil do rewrite_expr_in_place(&d.step, param_names)
for &s in d.body do rewrite_node(s, param_names)
case ^Ast_While:
rewrite_expr_in_place(&d.condition, param_names)
for &s in d.body do rewrite_node(s, param_names)
case ^Ast_Call:
for &arg in d.args do rewrite_expr_in_place(&arg, param_names)
case ^Ast_Field_Access:
rewrite_expr_in_place(&d.object, param_names)
case ^Ast_Index:
rewrite_expr_in_place(&d.object, param_names)
rewrite_expr_in_place(&d.index, param_names)
case ^Ast_Unary:
rewrite_expr_in_place(&d.operand, param_names)
case ^Ast_Binary:
rewrite_expr_in_place(&d.left, param_names)
rewrite_expr_in_place(&d.right, param_names)
case ^Ast_Swizzle:
rewrite_expr_in_place(&d.object, param_names)
case ^Ast_Struct_Literal:
for &f in d.fields {
rewrite_expr_in_place(&f.value, param_names)
}
}
}
@(private = "file")
rewrite_expr_in_place :: proc(node_ptr: ^^Ast_Node, param_names: ^map[string]bool) {
node := node_ptr^
if node == nil do return
// If this is an identifier that matches a desugared param, rewrite to input.field
if ident, ok := node.derived.(^Ast_Ident); ok {
if ident.name in param_names^ {
// Create input.field_name
input_ident := new(Ast_Ident)
input_ident^ = Ast_Ident{name = "input", span = node.span}
input_node := make_node(.Ident, input_ident, node.span)
fa := new(Ast_Field_Access)
fa^ = Ast_Field_Access{object = input_node, field = ident.name, span = node.span}
node_ptr^ = make_node(.Field_Access, fa, node.span)
return
}
}
// Recurse into sub-expressions
rewrite_node(node, param_names)
}
// Rewrite `return expr` -> `return StructName { field_name = expr }` for single-field output structs.
// Skips returns that are already struct literals for the output type.
@(private = "file")
rewrite_returns_single_field :: proc(stmts: []^Ast_Node, struct_name: string, field_name: string) {
for stmt in stmts {
if stmt == nil do continue
#partial switch d in stmt.derived {
case ^Ast_Return:
if d.value == nil do continue
// Skip if already a struct literal for our output type
if sl, ok := d.value.derived.(^Ast_Struct_Literal); ok {
if sl.type_name == struct_name do continue
}
// Wrap: return expr -> return StructName { field_name = expr }
lit_fields := make([]Ast_Struct_Literal_Field, 1)
lit_fields[0] = Ast_Struct_Literal_Field{
name = field_name,
value = d.value,
span = d.value.span,
}
sl := new(Ast_Struct_Literal)
sl^ = Ast_Struct_Literal{
type_name = struct_name,
fields = lit_fields,
span = d.value.span,
}
d.value = make_node(.Struct_Literal, sl, d.value.span)
case ^Ast_If:
rewrite_returns_single_field(d.then_body[:], struct_name, field_name)
for &clause in d.elseif_clauses {
rewrite_returns_single_field(clause.body[:], struct_name, field_name)
}
rewrite_returns_single_field(d.else_body[:], struct_name, field_name)
case ^Ast_For:
rewrite_returns_single_field(d.body[:], struct_name, field_name)
case ^Ast_While:
rewrite_returns_single_field(d.body[:], struct_name, field_name)
}
}
}
@(private = "file")
should_desugar_params :: proc(fn: ^Ast_Function) -> bool {
if len(fn.params) == 0 do return false
if has_attribute(fn.attributes, "luma_workflow") do return true
// Multiple params means inline entry point
if len(fn.params) > 1 do return true
// Single param with attributes means inline entry point
if len(fn.params) == 1 && len(fn.params[0].attributes) > 0 do return true
// Single param with a primitive type (not a struct) means inline entry point
// Check if the type is a known primitive/vector/matrix type
if len(fn.params) == 1 {
name := type_expr_name(fn.params[0].type)
if is_primitive_type_name(name) do return true
}
return false
}
@(private = "file")
is_primitive_type_name :: proc(name: string) -> bool {
switch name {
case "bool", "int", "uint", "float", "half",
"vec2", "vec3", "vec4",
"ivec2", "ivec3", "ivec4",
"uvec2", "uvec3", "uvec4",
"bvec2", "bvec3", "bvec4",
"mat2", "mat3", "mat4",
"mat2x3", "mat2x4", "mat3x2", "mat3x4", "mat4x2", "mat4x3":
return true
}
return false
}