@woosh/meep-engine 3.11.1 → 3.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/package.json +1 -1
  2. package/src/engine/ecs/storage/populateEngineSerializationRegistry.d.ts.map +1 -1
  3. package/src/engine/ecs/storage/populateEngineSerializationRegistry.js +294 -290
  4. package/src/engine/graphics3/ParticipatingMedia.d.ts +111 -0
  5. package/src/engine/graphics3/ParticipatingMedia.d.ts.map +1 -0
  6. package/src/engine/graphics3/ParticipatingMedia.js +203 -0
  7. package/src/engine/graphics3/ParticipatingMediaSerializationAdapter.d.ts +34 -0
  8. package/src/engine/graphics3/ParticipatingMediaSerializationAdapter.d.ts.map +1 -0
  9. package/src/engine/graphics3/ParticipatingMediaSerializationAdapter.js +73 -0
  10. package/src/engine/graphics3/ParticipatingMediaSystem3.d.ts +87 -0
  11. package/src/engine/graphics3/ParticipatingMediaSystem3.d.ts.map +1 -0
  12. package/src/engine/graphics3/ParticipatingMediaSystem3.js +184 -0
  13. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  14. package/src/shade/playground/volumetrics_froxel/README.md +104 -1
  15. package/src/shade/playground/volumetrics_froxel/index.html +20 -1
  16. package/src/shade/playground/volumetrics_froxel/main.d.ts.map +1 -1
  17. package/src/shade/playground/volumetrics_froxel/main.js +298 -0
  18. package/src/shade/playground/volumetrics_froxel/measure.d.ts +119 -0
  19. package/src/shade/playground/volumetrics_froxel/measure.d.ts.map +1 -0
  20. package/src/shade/playground/volumetrics_froxel/measure.js +559 -0
  21. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  22. package/src/shade/renderer/Renderer.js +22 -18
  23. package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
  24. package/src/shade/renderer/shadow/map/shader/chunk_shadowmap_sample_point2.d.ts +14 -13
  25. package/src/shade/renderer/shadow/map/shader/chunk_shadowmap_sample_point2.d.ts.map +1 -1
  26. package/src/shade/renderer/shadow/map/shader/chunk_shadowmap_sample_point2.js +133 -129
  27. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts.map +1 -1
  28. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.js +480 -315
  29. package/src/shade/renderer/volumetrics/NOTES.md +649 -8
  30. package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.d.ts +5 -0
  31. package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.d.ts.map +1 -1
  32. package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.js +21 -2
  33. package/src/shade/renderer/volumetrics/SceneVolumetrics.d.ts +26 -0
  34. package/src/shade/renderer/volumetrics/SceneVolumetrics.d.ts.map +1 -1
  35. package/src/shade/renderer/volumetrics/SceneVolumetrics.js +64 -21
  36. package/src/shade/renderer/volumetrics/VOLUMETRICS_METADATA_STRUCT.d.ts.map +1 -1
  37. package/src/shade/renderer/volumetrics/VOLUMETRICS_METADATA_STRUCT.js +84 -28
  38. package/src/shade/renderer/volumetrics/VOLUMETRICS_SUN_SHADOW_RESOLUTION.d.ts +98 -0
  39. package/src/shade/renderer/volumetrics/VOLUMETRICS_SUN_SHADOW_RESOLUTION.d.ts.map +1 -0
  40. package/src/shade/renderer/volumetrics/VOLUMETRICS_SUN_SHADOW_RESOLUTION.js +111 -0
  41. package/src/shade/renderer/volumetrics/build_volumetrics_sun_shadow_transform.d.ts +84 -0
  42. package/src/shade/renderer/volumetrics/build_volumetrics_sun_shadow_transform.d.ts.map +1 -0
  43. package/src/shade/renderer/volumetrics/build_volumetrics_sun_shadow_transform.js +252 -0
  44. package/src/shade/renderer/volumetrics/chunk_integrate_optical_depth.js +75 -75
  45. package/src/shade/renderer/volumetrics/chunk_volumetrics_local_light_shadow.d.ts +43 -0
  46. package/src/shade/renderer/volumetrics/chunk_volumetrics_local_light_shadow.d.ts.map +1 -0
  47. package/src/shade/renderer/volumetrics/chunk_volumetrics_local_light_shadow.js +145 -0
  48. package/src/shade/renderer/volumetrics/chunk_volumetrics_sample_froxel_extinction.d.ts +20 -0
  49. package/src/shade/renderer/volumetrics/chunk_volumetrics_sample_froxel_extinction.d.ts.map +1 -0
  50. package/src/shade/renderer/volumetrics/chunk_volumetrics_sample_froxel_extinction.js +48 -0
  51. package/src/shade/renderer/volumetrics/chunk_volumetrics_sun_optical_depth.d.ts +38 -0
  52. package/src/shade/renderer/volumetrics/chunk_volumetrics_sun_optical_depth.d.ts.map +1 -0
  53. package/src/shade/renderer/volumetrics/chunk_volumetrics_sun_optical_depth.js +107 -0
  54. package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts +4 -1
  55. package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts.map +1 -1
  56. package/src/shade/renderer/volumetrics/graph_build_volumetrics.js +577 -471
  57. package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.d.ts.map +1 -1
  58. package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.js +64 -13
  59. package/src/shade/renderer/volumetrics/shader_volumetrics_build_sun_shadow_volume.d.ts +8 -0
  60. package/src/shade/renderer/volumetrics/shader_volumetrics_build_sun_shadow_volume.d.ts.map +1 -0
  61. package/src/shade/renderer/volumetrics/shader_volumetrics_build_sun_shadow_volume.js +174 -0
  62. package/src/shade/wgsl/emulator/CPUBitmapData.d.ts +25 -0
  63. package/src/shade/wgsl/emulator/CPUBitmapData.d.ts.map +1 -1
  64. package/src/shade/wgsl/emulator/CPUBitmapData.js +33 -0
  65. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +2221 -2215
  66. package/src/shade/wgsl/emulator/WGSLLanguageCore.d.ts.map +1 -1
  67. package/src/shade/wgsl/emulator/WGSLLanguageCore.js +1297 -1117
  68. package/src/shade/wgsl/emulator/cpu_texture_gather.d.ts +69 -0
  69. package/src/shade/wgsl/emulator/cpu_texture_gather.d.ts.map +1 -0
  70. package/src/shade/wgsl/emulator/cpu_texture_gather.js +148 -0
  71. package/src/shade/wgsl/emulator/cpu_texture_sample.d.ts +16 -0
  72. package/src/shade/wgsl/emulator/cpu_texture_sample.d.ts.map +1 -1
  73. package/src/shade/wgsl/emulator/cpu_texture_sample.js +176 -172
@@ -1,2215 +1,2221 @@
1
- import { assert } from "../../../core/assert.js";
2
- import LineBuilder from "../../../core/codegen/LineBuilder.js";
3
-
4
- // =========================================================================
5
- // Type representation
6
- // =========================================================================
7
-
8
- /**
9
- * @typedef {object} WgslType
10
- * @property {'primitive' | 'vec' | 'mat' | 'array' | 'struct' | 'texture_2d' | 'sampler' | 'atomic' | 'unknown'} kind
11
- * @property {string} [name]
12
- * @property {number} [size]
13
- * @property {number} [cols]
14
- * @property {number} [rows]
15
- * @property {string} [scalar]
16
- * @property {WgslType} [element]
17
- * @property {number} [count]
18
- */
19
-
20
- const T_UNKNOWN = Object.freeze({ kind: 'unknown' });
21
-
22
- const VECTOR_ALIASES = {
23
- vec2i: { size: 2, scalar: 'i32' },
24
- vec3i: { size: 3, scalar: 'i32' },
25
- vec4i: { size: 4, scalar: 'i32' },
26
- vec2u: { size: 2, scalar: 'u32' },
27
- vec3u: { size: 3, scalar: 'u32' },
28
- vec4u: { size: 4, scalar: 'u32' },
29
- vec2f: { size: 2, scalar: 'f32' },
30
- vec3f: { size: 3, scalar: 'f32' },
31
- vec4f: { size: 4, scalar: 'f32' },
32
- vec2h: { size: 2, scalar: 'f16' },
33
- vec3h: { size: 3, scalar: 'f16' },
34
- vec4h: { size: 4, scalar: 'f16' },
35
- };
36
-
37
- const PRIMITIVE_NAMES = new Set(['i32', 'u32', 'f32', 'f16', 'bool']);
38
-
39
- const SWIZZLE_LETTERS = new Set(['x', 'y', 'z', 'w', 'r', 'g', 'b', 'a']);
40
-
41
- const SWIZZLE_INDEX = { x: 0, y: 1, z: 2, w: 3, r: 0, g: 1, b: 2, a: 3 };
42
-
43
- /**
44
- * Build a vec type alias name (e.g., {size:3, scalar:'f32'} → 'vec3f').
45
- */
46
- function vecAliasName(size, scalar) {
47
- const suffix = { i32: 'i', u32: 'u', f32: 'f', f16: 'h' }[scalar] || 'f';
48
- return `vec${size}${suffix}`;
49
- }
50
-
51
- // =========================================================================
52
- // Scope (symbol table) — tracks variable types for accurate codegen
53
- // =========================================================================
54
-
55
- class Scope {
56
- constructor(parent = null) {
57
- this.parent = parent;
58
- /** @type {Map<string, WgslType>} */
59
- this.symbols = new Map();
60
- }
61
-
62
- declare(name, type) {
63
- this.symbols.set(name, type || T_UNKNOWN);
64
- }
65
-
66
- /** @returns {WgslType} */
67
- lookup(name) {
68
- if (this.symbols.has(name)) return this.symbols.get(name);
69
- if (this.parent) return this.parent.lookup(name);
70
- return T_UNKNOWN;
71
- }
72
-
73
- push() {
74
- return new Scope(this);
75
- }
76
- }
77
-
78
- // =========================================================================
79
- // Tree-sitter helpers
80
- // =========================================================================
81
-
82
- /**
83
- * Return only the named children of a node (filters out punctuation tokens).
84
- */
85
- function namedChildren(node) {
86
- return node.namedChildren;
87
- }
88
-
89
- /**
90
- * First named child by type, or null.
91
- */
92
- function namedChildByType(node, type) {
93
- for (const c of node.namedChildren) {
94
- if (c.type === type) return c;
95
- }
96
- return null;
97
- }
98
-
99
- /**
100
- * All named children with the given type.
101
- */
102
- function namedChildrenByType(node, type) {
103
- return node.namedChildren.filter(c => c.type === type);
104
- }
105
-
106
- /**
107
- * Extract the source-text slice of `parent` that lies between the end of `left` and the start
108
- * of `right`. tree-sitter-web's `node.children` does not include unnamed tokens, so this is the
109
- * reliable way to read operator characters that sit between two named children.
110
- *
111
- * @param {Node} parent
112
- * @param {Node} left
113
- * @param {Node} right
114
- * @returns {string}
115
- */
116
- function textBetween(parent, left, right) {
117
- const left_offset = (left.endIndex - parent.startIndex);
118
- const right_offset = (right.startIndex - parent.startIndex);
119
- return parent.text.slice(left_offset, right_offset);
120
- }
121
-
122
- /**
123
- * Extract any leading source text in `parent` that precedes its first named child. Used for
124
- * unary operators (e.g. `-x`, `!cond`) where the operator is an unnamed token.
125
- *
126
- * @param {Node} parent
127
- * @returns {string}
128
- */
129
- function textBeforeFirstNamed(parent) {
130
- if (parent.namedChildCount === 0) return '';
131
- const first = parent.namedChildren[0];
132
- const offset = first.startIndex - parent.startIndex;
133
- return parent.text.slice(0, offset);
134
- }
135
-
136
- /**
137
- * Strip wrapper nodes that have a single named child of an expression-passthrough type.
138
- * The WGSL grammar has a long precedence chain: expression > relational > shift > additive > ...
139
- * When an expression is "simple" each level just wraps the next.
140
- */
141
- function unwrapExpression(node) {
142
- const PASSTHROUGH = new Set([
143
- 'expression',
144
- 'relational_expression',
145
- 'shift_expression',
146
- 'additive_expression',
147
- 'multiplicative_expression',
148
- 'unary_expression',
149
- 'singular_expression',
150
- 'primary_expression',
151
- ]);
152
- while (PASSTHROUGH.has(node.type) && node.namedChildren.length === 1) {
153
- // `unary_expression` keeps its operator as an unnamed token before the operand. If there
154
- // is one, don't unwrap — the caller needs to see the unary node so it can apply the op.
155
- if (node.type === 'unary_expression' && textBeforeFirstNamed(node).trim() !== '') {
156
- break;
157
- }
158
- node = node.namedChildren[0];
159
- }
160
- return node;
161
- }
162
-
163
- // =========================================================================
164
- // Type parsing (from `type_specifier` nodes or raw text)
165
- // =========================================================================
166
-
167
- /**
168
- * Parse a `type_specifier` tree-sitter node.
169
- * @param {Node} node
170
- * @returns {WgslType}
171
- */
172
- function parseTypeSpecifier(node) {
173
- if (node === null || node === undefined) return T_UNKNOWN;
174
- if (node.type === 'type_specifier') {
175
- return parseTypeSpecifier(node.namedChildren[0]);
176
- }
177
- if (node.type === 'template_elaborated_ident') {
178
- const ident = namedChildByType(node, 'ident');
179
- const ident_text = ident ? ident.text : node.text;
180
- const template = namedChildByType(node, 'template_list');
181
- const args = template ? extractTemplateArgs(template) : [];
182
- return classifyType(ident_text, args, template);
183
- }
184
- return T_UNKNOWN;
185
- }
186
-
187
- /**
188
- * Pull the textual template arguments from a `template_list` node, e.g. `<f32, 256>` → ['f32', '256'].
189
- */
190
- function extractTemplateArgs(template_list_node) {
191
- const out = [];
192
- const comma_list = namedChildByType(template_list_node, 'template_arg_comma_list');
193
- if (comma_list === null) return out;
194
- for (const c of comma_list.namedChildren) {
195
- if (c.type === 'template_arg_expression') {
196
- out.push(c.text.trim());
197
- }
198
- }
199
- return out;
200
- }
201
-
202
- /**
203
- * @param {string} ident
204
- * @param {string[]} args — textual template args (may be types or numbers)
205
- * @param {Node | null} template_list_node — the original node, for parsing nested array element types
206
- * @returns {WgslType}
207
- */
208
- function classifyType(ident, args, template_list_node) {
209
- if (PRIMITIVE_NAMES.has(ident)) {
210
- return { kind: 'primitive', name: ident };
211
- }
212
- if (VECTOR_ALIASES.hasOwnProperty(ident)) {
213
- const a = VECTOR_ALIASES[ident];
214
- return { kind: 'vec', size: a.size, scalar: a.scalar };
215
- }
216
- if (ident === 'vec2' || ident === 'vec3' || ident === 'vec4') {
217
- const size = parseInt(ident.slice(3));
218
- const scalar = args.length > 0 ? args[0] : 'f32';
219
- return { kind: 'vec', size, scalar };
220
- }
221
- {
222
- const m = ident.match(/^mat(\d)x(\d)([ifh]?)$/);
223
- if (m !== null) {
224
- const cols = parseInt(m[1]);
225
- const rows = parseInt(m[2]);
226
- const suffix = m[3];
227
- let scalar;
228
- if (suffix === 'f') scalar = 'f32';
229
- else if (suffix === 'i') scalar = 'i32';
230
- else if (suffix === 'h') scalar = 'f16';
231
- else scalar = (args.length > 0 ? args[0] : 'f32');
232
- return { kind: 'mat', cols, rows, scalar };
233
- }
234
- }
235
- if (ident === 'array') {
236
- // template arg 0 is the element type (sub-template), arg 1 is the count
237
- let element = T_UNKNOWN;
238
- let count = -1;
239
- if (template_list_node !== null) {
240
- const comma_list = namedChildByType(template_list_node, 'template_arg_comma_list');
241
- if (comma_list !== null) {
242
- const arg_nodes = namedChildrenByType(comma_list, 'template_arg_expression');
243
- if (arg_nodes.length > 0) {
244
- // Walk down to find the inner type ident
245
- const inner_expr = unwrapExpression(arg_nodes[0].namedChildren[0]);
246
- element = parseTypeSpecifier(inner_expr);
247
- }
248
- if (arg_nodes.length > 1) {
249
- const c = parseInt(arg_nodes[1].text.trim());
250
- if (!isNaN(c)) count = c;
251
- }
252
- }
253
- }
254
- return { kind: 'array', element, count };
255
- }
256
- if (ident === 'atomic') {
257
- const inner = args.length > 0 ? args[0] : 'i32';
258
- return { kind: 'atomic', scalar: inner };
259
- }
260
- // Texture types - 1D / 2D / 3D, sampled and storage variants. We
261
- // collapse all of them to a single kind because the intrinsics in
262
- // WGSLLanguageCore dispatch on the bound CPUBitmapData's
263
- // `dimensionality`, not on the static WGSL type. (The 'texture_2d'
264
- // kind label is preserved for backward compatibility with any
265
- // downstream consumer that may grep for it.)
266
- if (
267
- ident === 'texture_2d' || ident === 'texture_storage_2d'
268
- || ident === 'texture_1d' || ident === 'texture_storage_1d'
269
- || ident === 'texture_3d' || ident === 'texture_storage_3d'
270
- || ident === 'texture_2d_array'
271
- ) {
272
- const scalar = args.length > 0 ? args[0] : 'f32';
273
- return { kind: 'texture_2d', scalar };
274
- }
275
- if (ident === 'sampler') {
276
- return { kind: 'sampler' };
277
- }
278
- if (ident === 'ptr') {
279
- // Pointer — peel off the address space and read the inner type.
280
- if (template_list_node !== null) {
281
- const comma_list = namedChildByType(template_list_node, 'template_arg_comma_list');
282
- if (comma_list !== null) {
283
- const arg_nodes = namedChildrenByType(comma_list, 'template_arg_expression');
284
- if (arg_nodes.length >= 2) {
285
- const inner_expr = unwrapExpression(arg_nodes[1].namedChildren[0]);
286
- return parseTypeSpecifier(inner_expr);
287
- }
288
- }
289
- }
290
- return T_UNKNOWN;
291
- }
292
- // Otherwise: assume it's a struct name.
293
- return { kind: 'struct', name: ident };
294
- }
295
-
296
- // =========================================================================
297
- // Literal handling — strip WGSL type suffixes for JS
298
- // =========================================================================
299
-
300
- /**
301
- * Convert a WGSL literal token (e.g. `7u`, `1.0f`, `true`) to its JS equivalent.
302
- *
303
- * Stripping the type suffix is shape-sensitive: integer literals only carry `u`/`i`, and float
304
- * literals only carry `f`/`h`. Crucially, a hex int like `0x0300f00f` ends in a hex digit `f`
305
- * that must NOT be treated as a suffix — only an `f` after a decimal-format float counts.
306
- */
307
- function literalToJs(text) {
308
- const t = text.trim();
309
- if (t === 'true' || t === 'false') return t;
310
-
311
- // Hex int literals: `0xNN[ui]?`. Only `u`/`i` are real suffixes here; trailing `f`/`h` are
312
- // hex digits.
313
- if (/^0[xX]/.test(t)) {
314
- return t.replace(/[ui]$/, '');
315
- }
316
-
317
- // Decimal int / float. Float forms have a `.`, `e`/`E`, or end in `f`/`h`. Int forms don't.
318
- if (/[.eE]/.test(t)) {
319
- return t.replace(/[fh]$/, '');
320
- }
321
-
322
- // Plain integer (no `.`, no exponent): strip `u`/`i`/`f`/`h` (the latter two appear only on
323
- // forms like `1f` which WGSL allows as a float written without a decimal point).
324
- return t.replace(/[uifh]$/, '');
325
- }
326
-
327
- /**
328
- * The tracked type of a literal token, as far as its own spelling settles it.
329
- *
330
- * Only an explicit suffix is honoured. A bare `2043` is WGSL's AbstractInt, which takes its
331
- * concrete type from context this compiler does not track, so it stays unknown rather than being
332
- * guessed at.
333
- *
334
- * The suffix matters because operator selection reads these types. `index / 2043u` is truncating
335
- * integer division in WGSL and plain division in JavaScript, and the two differ for every pair
336
- * where the divisor does not divide the dividend — which is what a paged table's
337
- * `index / elements_per_page` is. Left untyped, the generated code returned a fractional page
338
- * index, indexed the page-address table with it, and read `undefined` back as zero: every record
339
- * past the first page slot came out of the emulator zero-filled, with nothing to say it had.
340
- *
341
- * @param {string} text
342
- * @returns {WgslType}
343
- */
344
- function literalType(text) {
345
- const t = text.trim();
346
-
347
- if (t === 'true' || t === 'false') {
348
- return { kind: 'primitive', name: 'bool' };
349
- }
350
-
351
- if (/[ui]$/.test(t)) {
352
- return { kind: 'primitive', name: t.endsWith('u') ? 'u32' : 'i32' };
353
- }
354
-
355
- // `f`/`h` are only suffixes on a decimal-format literal; on a hex int they are digits.
356
- if (!/^0[xX]/.test(t) && /[fh]$/.test(t)) {
357
- return { kind: 'primitive', name: t.endsWith('f') ? 'f32' : 'f16' };
358
- }
359
-
360
- if (/[.eE]/.test(t) && !/^0[xX]/.test(t)) {
361
- return { kind: 'primitive', name: 'f32' };
362
- }
363
-
364
- return T_UNKNOWN;
365
- }
366
-
367
- // =========================================================================
368
- // Operator handling
369
- // =========================================================================
370
-
371
- /**
372
- * Map a WGSL binary operator to a `wgsl.*` runtime helper or to a raw JS operator.
373
- * Arithmetic must go through wgsl.* so vectors are handled element-wise.
374
- */
375
- const ARITHMETIC_OP = {
376
- '+': 'add',
377
- '-': 'sub',
378
- '*': 'mul',
379
- '/': 'div',
380
- '%': 'rem',
381
- };
382
-
383
- const RAW_BINARY_OPS = new Set(['<', '>', '<=', '>=', '==', '!=', '&&', '||', '&', '|', '^', '<<', '>>']);
384
-
385
- // =========================================================================
386
- // Compiler context — central state during compilation
387
- // =========================================================================
388
-
389
- class Compiler {
390
- constructor() {
391
- /** @type {LineBuilder} */
392
- this.out = new LineBuilder();
393
-
394
- /** @type {Scope} */
395
- this.scope = new Scope();
396
-
397
- /** @type {Map<string, { name: string, fields: Array<{ name: string, type: WgslType }> }>} */
398
- this.structs = new Map();
399
-
400
- /** @type {Map<string, { name: string, returnType: WgslType, params: Array<{ name: string, type: WgslType }> }>} */
401
- this.functions = new Map();
402
-
403
- /** @type {Array<{ name: string, type: WgslType, group: number, binding: number, addressSpace: string | null, accessMode: string | null }>} */
404
- this.globalBindings = [];
405
-
406
- /** @type {Array<{ name: string, type: WgslType }>} */
407
- this.workgroupVars = [];
408
-
409
- /**
410
- * Module-scope `var<private>` and `const` declarations, in declaration order.
411
- * Each entry is `{ name, kind: 'let' | 'const', code }` — `code` is the JS line emitted
412
- * verbatim; `name` and `kind` are also tracked so the returned module object can expose
413
- * them as getter/setter (or read-only) properties for testing.
414
- * @type {Array<{ name: string, kind: 'let' | 'const', code: string }>}
415
- */
416
- this.privateGlobals = [];
417
-
418
- /** @type {{ name: string, params: Array<{ name: string, type: WgslType, builtin: string | null }>, workgroup_size: number[] } | null} */
419
- this.entry = null;
420
-
421
- /**
422
- * Label of the enclosing `loop` body block when that loop has a `continuing`
423
- * block: WGSL `continue` must transfer control INTO the continuing block, so it
424
- * is emitted as `break <label>` out of the labeled body block. `null` when the
425
- * nearest enclosing loop has no continuing block (plain JS `continue` suffices).
426
- * Loop emitters save/clear/restore this around nested loop bodies.
427
- * @type {string | null}
428
- */
429
- this.continue_label = null;
430
-
431
- /**
432
- * Monotonic counter used to generate unique loop-body labels.
433
- * @type {number}
434
- */
435
- this.loop_label_counter = 0;
436
- }
437
-
438
- error(node, message) {
439
- throw new Error(`WGSL compiler: ${message} (at ${node ? node.type : '<unknown>'} : ${node ? JSON.stringify(node.text.slice(0, 60)) : ''})`);
440
- }
441
- }
442
-
443
- // =========================================================================
444
- // Top-level compile entry point
445
- // =========================================================================
446
-
447
- export class WGSLJavaScriptCompiler {
448
-
449
- /**
450
- * Compile a tree-sitter WGSL parse tree to a JS source string.
451
- *
452
- * The output is a JavaScript expression: a function `factory(wgsl)` that returns
453
- * a shader module object. The shape depends on what the WGSL declared:
454
- *
455
- * - **Always present:** every top-level user function (e.g. `module.random()`),
456
- * every struct class (e.g. `module.Foo`), and a getter/setter property for each
457
- * `var<private>` and getter for each module-scope `const`.
458
- * - **Compute-shader fields (only when a `@compute` entry is present):**
459
- * ```
460
- * {
461
- * entry_name: string,
462
- * workgroup_size: [x, y, z],
463
- * param_names: string[],
464
- * entry: (bindings, workgroup, ...args) => void,
465
- * create_workgroup_state: () => object,
466
- * }
467
- * ```
468
- *
469
- * Without a `@compute` entry the result is a "library" module — useful for testing
470
- * pure WGSL helper code chunks (hashes, packing, math) one function at a time.
471
- *
472
- * @param {Tree} ast — tree-sitter WGSL AST (returned by `WGSLParser.parse`)
473
- * @returns {string}
474
- */
475
- compile(ast) {
476
- const c = new Compiler();
477
- compileTranslationUnit(c, ast.rootNode);
478
- return c.out.build();
479
- }
480
- }
481
-
482
- // =========================================================================
483
- // Translation unit (root)
484
- // =========================================================================
485
-
486
- /**
487
- * @param {Compiler} c
488
- * @param {Node} root
489
- */
490
- function compileTranslationUnit(c, root) {
491
- assert.equal(root.type, 'translation_unit', 'root must be translation_unit');
492
-
493
- // First pass — collect top-level decls so forward references and global scope resolve.
494
- for (const node of root.namedChildren) {
495
- switch (node.type) {
496
- case 'struct_decl':
497
- collectStruct(c, node);
498
- break;
499
- case 'function_decl':
500
- collectFunction(c, node);
501
- break;
502
- case 'global_variable_decl':
503
- collectGlobalVariable(c, node);
504
- break;
505
- case 'global_value_decl':
506
- collectGlobalValue(c, node);
507
- break;
508
- }
509
- }
510
-
511
- // Emit the wrapper: `(wgsl) => { ... }`
512
- c.out.add('(wgsl) => {');
513
- c.out.indent();
514
-
515
- // 1) Struct classes — must be defined before any user fn that references them or before
516
- // the workgroup state factory (which may need to construct struct instances).
517
- for (const node of root.namedChildren) {
518
- if (node.type === 'struct_decl') emitStruct(c, node);
519
- }
520
-
521
- // 2) Module-scope `let` slots for bindings and workgroup vars. The entry wrapper assigns
522
- // these on each dispatch so that user functions see them via their enclosing closure
523
- // — WGSL functions can read globals freely.
524
- for (const b of c.globalBindings) {
525
- c.out.add(`let ${b.name} = undefined;`);
526
- }
527
- for (const wv of c.workgroupVars) {
528
- c.out.add(`let ${wv.name} = undefined;`);
529
- }
530
-
531
- // 3) Module-scope `var<private>` and `const` declarations.
532
- for (const g of c.privateGlobals) {
533
- c.out.add(g.code);
534
- }
535
-
536
- // 4) User-defined functions (including the @compute entry, if any).
537
- for (const node of root.namedChildren) {
538
- if (node.type === 'function_decl') emitFunction(c, node);
539
- }
540
-
541
- // 5) Entry-stage support code: the dispatch wrapper for any entry, plus a workgroup-state
542
- // factory for @compute entries (fragment shaders have no var<workgroup>). Skipped entirely
543
- // in library mode (no entry attribute) so the resulting module is just a bag of pure
544
- // functions and globals.
545
- if (c.entry !== null) {
546
- if (c.entry.stage === 'compute') {
547
- emitWorkgroupStateFactory(c);
548
- }
549
- emitEntryWrapper(c);
550
- }
551
-
552
- // 6) Build and return the module object.
553
- emitModuleReturn(c);
554
-
555
- c.out.dedent();
556
- c.out.add('}');
557
- }
558
-
559
- /**
560
- * Emit the `return { ... }` block of the factory. Every top-level user function and struct
561
- * class is exposed by name; module-scope `var<private>` slots become getter/setter properties
562
- * (so tests can inspect or seed RNG state etc.), and module-scope `const`s become read-only
563
- * getters. When the WGSL had a `@compute` entry, the dispatch trio (`entry_name`,
564
- * `workgroup_size`, `param_names`, `entry`, `create_workgroup_state`) is included.
565
- */
566
- function emitModuleReturn(c) {
567
- c.out.add('const __module__ = {};');
568
-
569
- for (const fn_name of c.functions.keys()) {
570
- c.out.add(`__module__.${fn_name} = ${fn_name};`);
571
- }
572
- for (const struct_name of c.structs.keys()) {
573
- c.out.add(`__module__.${struct_name} = ${struct_name};`);
574
- }
575
-
576
- for (const g of c.privateGlobals) {
577
- if (g.kind === 'let') {
578
- c.out.add(`Object.defineProperty(__module__, ${JSON.stringify(g.name)}, { get: () => ${g.name}, set: (v) => { ${g.name} = v; } });`);
579
- } else {
580
- c.out.add(`Object.defineProperty(__module__, ${JSON.stringify(g.name)}, { get: () => ${g.name} });`);
581
- }
582
- }
583
-
584
- if (c.entry !== null) {
585
- c.out.add(`__module__.entry_name = ${JSON.stringify(c.entry.name)};`);
586
- c.out.add(`__module__.stage = ${JSON.stringify(c.entry.stage)};`);
587
- c.out.add(`__module__.param_names = [${c.entry.params.map(p => JSON.stringify(p.name)).join(', ')}];`);
588
- c.out.add(`__module__.entry = __entry__;`);
589
- if (c.entry.stage === 'compute') {
590
- c.out.add(`__module__.workgroup_size = [${c.entry.workgroup_size.join(', ')}];`);
591
- c.out.add(`__module__.create_workgroup_state = __create_workgroup_state__;`);
592
- }
593
- }
594
-
595
- c.out.add('return __module__;');
596
- }
597
-
598
- // =========================================================================
599
- // Struct collection + emission
600
- // =========================================================================
601
-
602
- /**
603
- * @param {Compiler} c
604
- * @param {Node} node
605
- */
606
- function collectStruct(c, node) {
607
- const ident = namedChildByType(node, 'ident');
608
- const name = ident.text;
609
-
610
- const body = namedChildByType(node, 'struct_body_decl');
611
- const members = namedChildrenByType(body, 'struct_member');
612
-
613
- /** @type {Array<{ name: string, type: WgslType }>} */
614
- const fields = [];
615
-
616
- for (const m of members) {
617
- const member_ident = namedChildByType(m, 'member_ident');
618
- const type_spec = namedChildByType(m, 'type_specifier');
619
- fields.push({
620
- name: member_ident.text,
621
- type: parseTypeSpecifier(type_spec),
622
- });
623
- }
624
-
625
- c.structs.set(name, { name, fields });
626
- }
627
-
628
- /**
629
- * @param {Compiler} c
630
- * @param {Node} node
631
- */
632
- function emitStruct(c, node) {
633
- const ident = namedChildByType(node, 'ident');
634
- const name = ident.text;
635
- const info = c.structs.get(name);
636
-
637
- c.out.add(`class ${name} {`);
638
- c.out.indent();
639
- c.out.add('constructor() {');
640
- c.out.indent();
641
- for (const f of info.fields) {
642
- c.out.add(`this.${f.name} = ${defaultValueForType(f.type)};`);
643
- }
644
- c.out.dedent();
645
- c.out.add('}');
646
- c.out.dedent();
647
- c.out.add('}');
648
- }
649
-
650
- /**
651
- * Initialiser expression for a fresh value of the given type.
652
- * @param {WgslType} type
653
- * @returns {string}
654
- */
655
- function defaultValueForType(type) {
656
- if (type === undefined || type === null) return 'undefined';
657
- switch (type.kind) {
658
- case 'primitive':
659
- return type.name === 'bool' ? 'false' : '0';
660
- case 'vec':
661
- return `wgsl.${vecAliasName(type.size, type.scalar)}()`;
662
- case 'mat':
663
- return `new Float32Array(${type.cols * type.rows})`;
664
- case 'array': {
665
- if (type.count > 0) {
666
- if (type.element.kind === 'primitive') {
667
- const ctor = arrayCtorForScalar(type.element.name);
668
- return `new ${ctor}(${type.count})`;
669
- }
670
- // For non-primitive elements (vec / mat / struct / nested array) build the
671
- // array eagerly so `out[i]` is a fresh, mutable element rather than `undefined`.
672
- return `Array.from({ length: ${type.count} }, () => ${defaultValueForType(type.element)})`;
673
- }
674
- return '[]';
675
- }
676
- case 'struct':
677
- return `new ${type.name}()`;
678
- case 'atomic':
679
- return '{ value: 0 }';
680
- default:
681
- return 'undefined';
682
- }
683
- }
684
-
685
- function arrayCtorForScalar(name) {
686
- switch (name) {
687
- case 'f32':
688
- return 'Float32Array';
689
- case 'f16':
690
- return 'Float16Array';
691
- case 'i32':
692
- return 'Int32Array';
693
- case 'u32':
694
- return 'Uint32Array';
695
- default:
696
- return 'Array';
697
- }
698
- }
699
-
700
- // =========================================================================
701
- // Function collection + emission
702
- // =========================================================================
703
-
704
- /**
705
- * @param {Compiler} c
706
- * @param {Node} node
707
- */
708
- function collectFunction(c, node) {
709
- const header = namedChildByType(node, 'function_header');
710
- const ident = namedChildByType(header, 'ident');
711
- const name = ident.text;
712
-
713
- const param_list_node = namedChildByType(header, 'param_list');
714
- /** @type {Array<{ name: string, type: WgslType, builtin: string | null }>} */
715
- const params = [];
716
- if (param_list_node !== null) {
717
- const param_nodes = namedChildrenByType(param_list_node, 'param');
718
- for (const p of param_nodes) {
719
- const p_ident = namedChildByType(p, 'ident');
720
- const p_type = namedChildByType(p, 'type_specifier');
721
- const builtin = extractBuiltin(p);
722
- params.push({
723
- name: p_ident.text,
724
- type: parseTypeSpecifier(p_type),
725
- builtin,
726
- });
727
- }
728
- }
729
-
730
- // Optional return type — `template_elaborated_ident` directly under the header.
731
- let returnType = T_UNKNOWN;
732
- for (const child of header.namedChildren) {
733
- if (child.type === 'template_elaborated_ident') {
734
- returnType = parseTypeSpecifier(child);
735
- }
736
- }
737
-
738
- c.functions.set(name, { name, returnType, params: params.map(p => ({ name: p.name, type: p.type })) });
739
-
740
- // Detect the entry attribute (@compute, @fragment, or @vertex) and capture stage-specific
741
- // metadata. The emulator supports compute and fragment entries; vertex is rejected because
742
- // ImageShader testing in the emulator skips the vertex phase intentionally.
743
- const attrs = namedChildrenByType(node, 'attribute');
744
- let stage = null;
745
- let workgroup_size = [1, 1, 1];
746
- for (const a of attrs) {
747
- const inner = a.namedChildren[0];
748
- if (inner.type === 'compute_attr') {
749
- stage = 'compute';
750
- } else if (inner.type === 'fragment_attr') {
751
- stage = 'fragment';
752
- } else if (inner.type === 'vertex_attr') {
753
- throw new Error(`WGSL compiler: @vertex stage is not supported by the emulator (function '${name}'). For ImageShader testing the vertex phase is skipped — invoke the @fragment entry directly.`);
754
- } else if (inner.type === 'workgroup_size_attr') {
755
- const sizes = namedChildrenByType(inner, 'expression').map(e => parseInt(e.text.trim()) | 0);
756
- for (let i = 0; i < sizes.length && i < 3; i++) {
757
- workgroup_size[i] = sizes[i];
758
- }
759
- }
760
- }
761
-
762
- if (stage !== null) {
763
- if (c.entry !== null) {
764
- throw new Error(`WGSL compiler: multiple @${stage} entry functions found ('${c.entry.name}' and '${name}')`);
765
- }
766
- c.entry = { name, params, stage, workgroup_size };
767
- }
768
- }
769
-
770
- /**
771
- * Pull the `@builtin(name)` attribute name from a function param node, if present.
772
- */
773
- function extractBuiltin(param_node) {
774
- for (const a of namedChildrenByType(param_node, 'attribute')) {
775
- const inner = a.namedChildren[0];
776
- if (inner.type === 'builtin_attr') {
777
- const name_node = namedChildByType(inner, 'builtin_value_name');
778
- if (name_node !== null) return name_node.text;
779
- }
780
- }
781
- return null;
782
- }
783
-
784
- /**
785
- * @param {Compiler} c
786
- * @param {Node} node
787
- */
788
- function emitFunction(c, node) {
789
- const header = namedChildByType(node, 'function_header');
790
- const ident = namedChildByType(header, 'ident');
791
- const name = ident.text;
792
- const info = c.functions.get(name);
793
-
794
- const param_list_node = namedChildByType(header, 'param_list');
795
- const param_nodes = param_list_node !== null ? namedChildrenByType(param_list_node, 'param') : [];
796
-
797
- const param_names = param_nodes.map(p => namedChildByType(p, 'ident').text);
798
-
799
- c.out.add(`function ${name}(${param_names.join(', ')}) {`);
800
- c.out.indent();
801
-
802
- c.scope = c.scope.push();
803
- for (let i = 0; i < param_nodes.length; i++) {
804
- c.scope.declare(param_names[i], info.params[i].type);
805
- }
806
-
807
- const body = namedChildByType(node, 'compound_statement');
808
- emitCompoundStatementBody(c, body);
809
-
810
- c.scope = c.scope.parent;
811
-
812
- c.out.dedent();
813
- c.out.add('}');
814
- }
815
-
816
- // =========================================================================
817
- // Globals — bindings, workgroup vars, private/const
818
- // =========================================================================
819
-
820
- /**
821
- * Inspect a `global_variable_decl` and record its metadata.
822
- * @param {Compiler} c
823
- * @param {Node} node
824
- */
825
- function collectGlobalVariable(c, node) {
826
- const var_decl = namedChildByType(node, 'variable_decl');
827
- if (var_decl === null) {
828
- collectGlobalValue(c, node);
829
- return;
830
- }
831
-
832
- const template = namedChildByType(var_decl, 'template_list');
833
- const opt_ident = namedChildByType(var_decl, 'optionally_typed_ident');
834
- const var_ident = namedChildByType(opt_ident, 'ident');
835
- const type_spec = namedChildByType(opt_ident, 'type_specifier');
836
- const name = var_ident.text;
837
- const type = parseTypeSpecifier(type_spec);
838
-
839
- let address_space = null;
840
- let access_mode = null;
841
- if (template !== null) {
842
- const args = extractTemplateArgs(template);
843
- address_space = args[0] || null;
844
- access_mode = args[1] || null;
845
- }
846
-
847
- let group = null;
848
- let binding = null;
849
- for (const a of namedChildrenByType(node, 'attribute')) {
850
- const inner = a.namedChildren[0];
851
- if (inner.type === 'group_attr') {
852
- const expr = namedChildByType(inner, 'expression');
853
- group = parseInt(expr.text.trim());
854
- } else if (inner.type === 'binding_attr') {
855
- const expr = namedChildByType(inner, 'expression');
856
- binding = parseInt(expr.text.trim());
857
- }
858
- }
859
-
860
- c.scope.declare(name, type);
861
-
862
- if (group !== null && binding !== null) {
863
- c.globalBindings.push({
864
- name, type, group, binding, addressSpace: address_space, accessMode: access_mode,
865
- });
866
- return;
867
- }
868
-
869
- if (address_space === 'workgroup') {
870
- c.workgroupVars.push({ name, type });
871
- return;
872
- }
873
-
874
- // Private / module-scope `var<private>`. If the WGSL has an explicit initializer
875
- // (e.g. `var<private> rnd_state: u32 = 2891336453u`), use it; otherwise fall back
876
- // to the type's default value.
877
- const initializer_node = namedChildByType(node, 'expression');
878
- const init_code = initializer_node !== null
879
- ? compileExpression(c, initializer_node).code
880
- : defaultValueForType(type);
881
- c.privateGlobals.push({
882
- name,
883
- kind: 'let',
884
- code: `let ${name} = ${init_code};`,
885
- });
886
- }
887
-
888
- /**
889
- * Inspect a `global_value_decl` (`const x = …;` / `let x = …;` at module scope) and record it.
890
- * @param {Compiler} c
891
- * @param {Node} node
892
- */
893
- function collectGlobalValue(c, node) {
894
- let ident = null;
895
- let value_node = null;
896
- let type_node = null;
897
- for (const child of node.namedChildren) {
898
- if (child.type === 'optionally_typed_ident') {
899
- ident = namedChildByType(child, 'ident');
900
- type_node = namedChildByType(child, 'type_specifier');
901
- } else if (child.type === 'expression') {
902
- value_node = child;
903
- }
904
- }
905
- if (ident === null || value_node === null) return;
906
- const name = ident.text;
907
- let type = type_node !== null ? parseTypeSpecifier(type_node) : T_UNKNOWN;
908
-
909
- const expr = compileExpression(c, value_node);
910
-
911
- // Infer the type from the RHS when there's no explicit annotation (mirrors the local
912
- // `let`/`var` path). Without this a `const M = mat3x3<f32>(...)` is left T_UNKNOWN, so a
913
- // later `M * v` can't tell M is a matrix and emits a vector multiply → length mismatch.
914
- if (type === T_UNKNOWN || type.kind === 'unknown') {
915
- type = expr.type;
916
- }
917
-
918
- c.scope.declare(name, type);
919
- c.privateGlobals.push({
920
- name,
921
- kind: 'const',
922
- code: `const ${name} = ${expr.code};`,
923
- });
924
- }
925
-
926
- // =========================================================================
927
- // Workgroup state factory + entry wrapper
928
- // =========================================================================
929
-
930
- function emitWorkgroupStateFactory(c) {
931
- c.out.add('function __create_workgroup_state__() {');
932
- c.out.indent();
933
- c.out.add('return {');
934
- c.out.indent();
935
- for (const wv of c.workgroupVars) {
936
- c.out.add(`${wv.name}: ${defaultValueForType(wv.type)},`);
937
- }
938
- c.out.dedent();
939
- c.out.add('};');
940
- c.out.dedent();
941
- c.out.add('}');
942
- }
943
-
944
- function emitEntryWrapper(c) {
945
- const param_names = c.entry.params.map(p => p.name);
946
- const is_compute = c.entry.stage === 'compute';
947
- // Compute entries take a per-dispatch workgroup-state object; fragment entries don't have
948
- // var<workgroup> at all, so the wrapper signature drops it.
949
- const fixed_args = is_compute ? '__bindings__, __workgroup__' : '__bindings__';
950
- const all_args = param_names.length > 0 ? `${fixed_args}, ${param_names.join(', ')}` : fixed_args;
951
-
952
- c.out.add(`function __entry__(${all_args}) {`);
953
- c.out.indent();
954
-
955
- // Refresh each module-scope binding/workgroup slot before invoking the user's main.
956
- // User functions read these via their enclosing closure, so this assignment makes the
957
- // current dispatch's bindings visible to every helper they may call.
958
- for (const b of c.globalBindings) {
959
- c.out.add(`${b.name} = __bindings__.${b.name};`);
960
- }
961
- if (is_compute) {
962
- for (const wv of c.workgroupVars) {
963
- c.out.add(`${wv.name} = __workgroup__.${wv.name};`);
964
- }
965
- }
966
-
967
- c.out.add(`return ${c.entry.name}(${param_names.join(', ')});`);
968
-
969
- c.out.dedent();
970
- c.out.add('}');
971
- }
972
-
973
- // =========================================================================
974
- // Statements
975
- // =========================================================================
976
-
977
- /**
978
- * Emit only the inside of a compound_statement (no `{` / `}`).
979
- * @param {Compiler} c
980
- * @param {Node} node — `compound_statement`
981
- */
982
- function emitCompoundStatementBody(c, node) {
983
- for (const child of node.namedChildren) {
984
- if (child.type === 'statement') {
985
- emitStatement(c, child);
986
- }
987
- }
988
- }
989
-
990
- function emitCompoundStatement(c, node) {
991
- c.out.add('{');
992
- c.out.indent();
993
- c.scope = c.scope.push();
994
- emitCompoundStatementBody(c, node);
995
- c.scope = c.scope.parent;
996
- c.out.dedent();
997
- c.out.add('}');
998
- }
999
-
1000
- /**
1001
- * Emit a single `statement` node (its child is the actual statement kind).
1002
- * @param {Compiler} c
1003
- * @param {Node} node
1004
- */
1005
- function emitStatement(c, node) {
1006
- const inner = node.namedChildren[0];
1007
- switch (inner.type) {
1008
- case 'variable_or_value_statement':
1009
- emitVariableOrValueStatement(c, inner);
1010
- break;
1011
- case 'variable_updating_statement':
1012
- emitVariableUpdatingStatement(c, inner);
1013
- break;
1014
- case 'return_statement':
1015
- emitReturnStatement(c, inner);
1016
- break;
1017
- case 'if_statement':
1018
- emitIfStatement(c, inner);
1019
- break;
1020
- case 'for_statement':
1021
- emitForStatement(c, inner);
1022
- break;
1023
- case 'while_statement':
1024
- emitWhileStatement(c, inner);
1025
- break;
1026
- case 'loop_statement':
1027
- emitLoopStatement(c, inner);
1028
- break;
1029
- case 'switch_statement':
1030
- emitSwitchStatement(c, inner);
1031
- break;
1032
- case 'break_statement':
1033
- c.out.add('break;');
1034
- break;
1035
- case 'continue_statement':
1036
- // Inside a `loop` with a `continuing` block, `continue` must run the
1037
- // continuing block: it is emitted as a break out of the labeled body block
1038
- // (the continuing code follows that block inside the same `while`).
1039
- if (c.continue_label !== null) {
1040
- c.out.add(`break ${c.continue_label};`);
1041
- } else {
1042
- c.out.add('continue;');
1043
- }
1044
- break;
1045
- case 'discard_statement':
1046
- c.out.add('return;');
1047
- break;
1048
- case 'compound_statement':
1049
- emitCompoundStatement(c, inner);
1050
- break;
1051
- case 'func_call_statement':
1052
- emitFunctionCallStatement(c, inner);
1053
- break;
1054
- case 'const_assert_statement':
1055
- // Skipped at runtime.
1056
- break;
1057
- default:
1058
- c.error(inner, `unsupported statement: ${inner.type}`);
1059
- }
1060
- }
1061
-
1062
- /**
1063
- * `let x = expr;` or `var x: T = expr;` or `var x: T;`
1064
- */
1065
- function emitVariableOrValueStatement(c, node) {
1066
- const var_decl = namedChildByType(node, 'variable_decl');
1067
- const opt_ident_direct = namedChildByType(node, 'optionally_typed_ident');
1068
- const expr_node = namedChildByType(node, 'expression');
1069
-
1070
- /** @type {'let' | 'const'} */
1071
- let kind = 'let';
1072
- let opt_ident = null;
1073
- let template = null;
1074
-
1075
- if (var_decl !== null) {
1076
- // WGSL `var` is the mutable form → JS `let`.
1077
- kind = 'let';
1078
- opt_ident = namedChildByType(var_decl, 'optionally_typed_ident');
1079
- template = namedChildByType(var_decl, 'template_list');
1080
- } else {
1081
- // WGSL `let` and `const` are both immutable bindings → JS `const`.
1082
- // Tree-sitter exposes the keyword as an unnamed leading token; read it from the source.
1083
- kind = 'const';
1084
- opt_ident = opt_ident_direct;
1085
- }
1086
-
1087
- const ident = namedChildByType(opt_ident, 'ident');
1088
- const type_spec = namedChildByType(opt_ident, 'type_specifier');
1089
- const name = ident.text;
1090
- let type = type_spec !== null ? parseTypeSpecifier(type_spec) : T_UNKNOWN;
1091
-
1092
- if (expr_node !== null) {
1093
- const expr = compileExpression(c, expr_node);
1094
- // Infer the type from the RHS when no explicit annotation is present.
1095
- if (type === T_UNKNOWN || type.kind === 'unknown') {
1096
- type = expr.type;
1097
- }
1098
- c.scope.declare(name, type);
1099
- c.out.add(`${kind} ${name} = ${expr.code};`);
1100
- } else {
1101
- c.scope.declare(name, type);
1102
- c.out.add(`${kind} ${name} = ${defaultValueForType(type)};`);
1103
- }
1104
- }
1105
-
1106
- /**
1107
- * Assignment / compound assignment / increment / decrement.
1108
- */
1109
- function emitVariableUpdatingStatement(c, node) {
1110
- const inner = node.namedChildren[0];
1111
- if (inner.type === 'assignment_statement') {
1112
- const lhs = namedChildByType(inner, 'lhs_expression');
1113
- const expr = namedChildByType(inner, 'expression');
1114
-
1115
- // The compound assignment operator sits between the LHS and the RHS as an unnamed token.
1116
- const op_text = textBetween(inner, lhs, expr).trim();
1117
-
1118
- const lhs_compiled = compileLhs(c, lhs);
1119
- const rhs_compiled = compileExpression(c, expr);
1120
-
1121
- if (op_text === '=' || op_text === '') {
1122
- c.out.add(emitWriteStatement(lhs_compiled, rhs_compiled.code));
1123
- return;
1124
- }
1125
-
1126
- const arith = {
1127
- '+=': 'add', '-=': 'sub', '*=': 'mul', '/=': 'div', '%=': 'rem',
1128
- };
1129
- if (arith.hasOwnProperty(op_text)) {
1130
- c.out.add(emitWriteStatement(lhs_compiled, `wgsl.${arith[op_text]}(${lhs_compiled.code}, ${rhs_compiled.code})`));
1131
- return;
1132
- }
1133
- c.out.add(`${lhs_compiled.code} ${op_text} ${rhs_compiled.code};`);
1134
- return;
1135
- }
1136
- if (inner.type === 'increment_statement') {
1137
- const lhs = namedChildByType(inner, 'lhs_expression');
1138
- const lhs_compiled = compileLhs(c, lhs);
1139
- c.out.add(emitWriteStatement(lhs_compiled, `wgsl.add(${lhs_compiled.code}, 1)`));
1140
- return;
1141
- }
1142
- if (inner.type === 'decrement_statement') {
1143
- const lhs = namedChildByType(inner, 'lhs_expression');
1144
- const lhs_compiled = compileLhs(c, lhs);
1145
- c.out.add(emitWriteStatement(lhs_compiled, `wgsl.sub(${lhs_compiled.code}, 1)`));
1146
- return;
1147
- }
1148
- c.error(inner, `unsupported variable_updating_statement: ${inner.type}`);
1149
- }
1150
-
1151
- /**
1152
- * Emit a write to an LHS. Most LHS expressions are plain JS LValues (`x`, `arr[i]`, `obj.f`) and
1153
- * are written with `=`. Mat-column LHS — `m[i]` for a mat — is special: the compiler emits it as
1154
- * `wgsl.mat_col(m, i, rows)`, which is a typed-array view (a function call result, not assignable).
1155
- * To write through the view we use `Float32Array.prototype.set(rhs)` instead, which copies into
1156
- * the underlying matrix storage in place.
1157
- *
1158
- * @param {{ code: string }} lhs_compiled
1159
- * @param {string} rhs_code
1160
- * @returns {string}
1161
- */
1162
- function emitWriteStatement(lhs_compiled, rhs_code) {
1163
- // Only the bare `wgsl.mat_col(...)` form (no chained `[i]` / `.x`) needs `.set()` — that's a
1164
- // typed-array view, an unassignable function-call result. Anything chained off it (e.g.
1165
- // `wgsl.mat_col(...)[3]` or `wgsl.mat_col(...).x` becoming `[i]`) is a scalar slot and must
1166
- // use plain `=`.
1167
- if (lhs_compiled.code.startsWith('wgsl.mat_col(') && lhs_compiled.code.endsWith(')')) {
1168
- return `${lhs_compiled.code}.set(${rhs_code});`;
1169
- }
1170
- // `*p = v` has to reach the caller's object; assigning would only rebind the parameter.
1171
- if (lhs_compiled.through_pointer === true) {
1172
- return `${lhs_compiled.code} = wgsl.store_ptr(${lhs_compiled.code}, ${rhs_code});`;
1173
- }
1174
- return `${lhs_compiled.code} = ${rhs_code};`;
1175
- }
1176
-
1177
- function emitReturnStatement(c, node) {
1178
- const expr = namedChildByType(node, 'expression');
1179
- if (expr === null) {
1180
- c.out.add('return;');
1181
- } else {
1182
- const e = compileExpression(c, expr);
1183
- c.out.add(`return ${e.code};`);
1184
- }
1185
- }
1186
-
1187
- function emitIfStatement(c, node) {
1188
- const if_clause = namedChildByType(node, 'if_clause');
1189
- const cond = namedChildByType(if_clause, 'expression');
1190
- const if_body = namedChildByType(if_clause, 'compound_statement');
1191
-
1192
- const cond_compiled = compileExpression(c, cond);
1193
- c.out.add(`if (${cond_compiled.code}) {`);
1194
- c.out.indent();
1195
- c.scope = c.scope.push();
1196
- emitCompoundStatementBody(c, if_body);
1197
- c.scope = c.scope.parent;
1198
- c.out.dedent();
1199
- c.out.add('}');
1200
-
1201
- // else-if and else clauses.
1202
- for (const child of node.namedChildren) {
1203
- if (child.type === 'else_if_clause') {
1204
- const eif_cond = namedChildByType(child, 'expression');
1205
- const eif_body = namedChildByType(child, 'compound_statement');
1206
- const eif_compiled = compileExpression(c, eif_cond);
1207
- c.out.add(`else if (${eif_compiled.code}) {`);
1208
- c.out.indent();
1209
- c.scope = c.scope.push();
1210
- emitCompoundStatementBody(c, eif_body);
1211
- c.scope = c.scope.parent;
1212
- c.out.dedent();
1213
- c.out.add('}');
1214
- } else if (child.type === 'else_clause') {
1215
- const else_body = namedChildByType(child, 'compound_statement');
1216
- // else_body may instead be an if_statement (else-if shorthand).
1217
- const nested_if = namedChildByType(child, 'if_statement');
1218
- if (else_body !== null) {
1219
- c.out.add('else {');
1220
- c.out.indent();
1221
- c.scope = c.scope.push();
1222
- emitCompoundStatementBody(c, else_body);
1223
- c.scope = c.scope.parent;
1224
- c.out.dedent();
1225
- c.out.add('}');
1226
- } else if (nested_if !== null) {
1227
- c.out.add('else');
1228
- emitIfStatement(c, nested_if);
1229
- }
1230
- }
1231
- }
1232
- }
1233
-
1234
- function emitForStatement(c, node) {
1235
- const for_header = namedChildByType(node, 'for_header');
1236
- const for_init = namedChildByType(for_header, 'for_init');
1237
- const cond_expr = namedChildByType(for_header, 'expression');
1238
- const for_update = namedChildByType(for_header, 'for_update');
1239
- const body = namedChildByType(node, 'compound_statement');
1240
-
1241
- c.scope = c.scope.push();
1242
-
1243
- // Init / cond / update parts.
1244
- let init_str = '';
1245
- if (for_init !== null) {
1246
- // Tree-sitter wraps it in a variable_or_value_statement; capture the emission as text.
1247
- const sub = new Compiler();
1248
- sub.scope = c.scope;
1249
- sub.structs = c.structs;
1250
- sub.functions = c.functions;
1251
- const inner = for_init.namedChildren[0];
1252
- emitForInitInline(sub, inner);
1253
- init_str = sub.out.build().trimEnd();
1254
- if (init_str.endsWith(';')) init_str = init_str.slice(0, -1);
1255
- }
1256
-
1257
- const cond_str = cond_expr !== null ? compileExpression(c, cond_expr).code : '';
1258
-
1259
- let update_str = '';
1260
- if (for_update !== null) {
1261
- const inner = for_update.namedChildren[0];
1262
- update_str = emitForUpdateInline(c, inner);
1263
- }
1264
-
1265
- c.out.add(`for (${init_str}; ${cond_str}; ${update_str}) {`);
1266
- c.out.indent();
1267
- // `continue` inside this loop targets this loop, not an outer `loop`'s continuing block
1268
- const saved_continue_label = c.continue_label;
1269
- c.continue_label = null;
1270
- emitCompoundStatementBody(c, body);
1271
- c.continue_label = saved_continue_label;
1272
- c.out.dedent();
1273
- c.out.add('}');
1274
-
1275
- c.scope = c.scope.parent;
1276
- }
1277
-
1278
- /**
1279
- * Emit a `variable_or_value_statement` inline (as a single line with no trailing newline structure).
1280
- * Used by `for(init; ...)`.
1281
- */
1282
- function emitForInitInline(c, node) {
1283
- if (node.type === 'variable_or_value_statement') {
1284
- const var_decl = node.namedChildren[0] && node.namedChildren[0].type === 'variable_decl'
1285
- ? node.namedChildren[0]
1286
- : null;
1287
- const opt_ident = namedChildByType(var_decl !== null ? var_decl : node, 'optionally_typed_ident');
1288
- const expr_node = namedChildByType(node, 'expression');
1289
- const ident = namedChildByType(opt_ident, 'ident');
1290
- const type_spec = namedChildByType(opt_ident, 'type_specifier');
1291
- const name = ident.text;
1292
- const type = type_spec !== null ? parseTypeSpecifier(type_spec) : T_UNKNOWN;
1293
- c.scope.declare(name, type);
1294
- const value = expr_node !== null ? compileExpression(c, expr_node).code : defaultValueForType(type);
1295
- // `var i = …` (mutable, common in `for (var i = 0; ...; i = i + 1)`) → JS `let`.
1296
- // `let i = …` / `const i = …` (immutable) → JS `const`.
1297
- const kind = var_decl !== null ? 'let' : 'const';
1298
- c.out.add(`${kind} ${name} = ${value};`);
1299
- return;
1300
- }
1301
- if (node.type === 'variable_updating_statement') {
1302
- emitVariableUpdatingStatement(c, node);
1303
- return;
1304
- }
1305
- if (node.type === 'func_call_statement') {
1306
- emitFunctionCallStatement(c, node);
1307
- return;
1308
- }
1309
- }
1310
-
1311
- /**
1312
- * @returns {string} JS update expression (no semicolon).
1313
- */
1314
- function emitForUpdateInline(c, node) {
1315
- if (node.type === 'variable_updating_statement') {
1316
- const inner = node.namedChildren[0];
1317
- if (inner.type === 'assignment_statement') {
1318
- const lhs = namedChildByType(inner, 'lhs_expression');
1319
- const expr = namedChildByType(inner, 'expression');
1320
- const op_text = textBetween(inner, lhs, expr).trim();
1321
- const lhs_compiled = compileLhs(c, lhs);
1322
- const rhs_compiled = compileExpression(c, expr);
1323
- if (op_text === '=' || op_text === '') {
1324
- if (lhs_compiled.through_pointer === true) {
1325
- return `${lhs_compiled.code} = wgsl.store_ptr(${lhs_compiled.code}, ${rhs_compiled.code})`;
1326
- }
1327
- return `${lhs_compiled.code} = ${rhs_compiled.code}`;
1328
- }
1329
- const arith = { '+=': 'add', '-=': 'sub', '*=': 'mul', '/=': 'div', '%=': 'rem' };
1330
- if (arith.hasOwnProperty(op_text)) {
1331
- if (lhs_compiled.through_pointer === true) {
1332
- return `${lhs_compiled.code} = wgsl.store_ptr(${lhs_compiled.code}, wgsl.${arith[op_text]}(${lhs_compiled.code}, ${rhs_compiled.code}))`;
1333
- }
1334
- return `${lhs_compiled.code} = wgsl.${arith[op_text]}(${lhs_compiled.code}, ${rhs_compiled.code})`;
1335
- }
1336
- return `${lhs_compiled.code} ${op_text} ${rhs_compiled.code}`;
1337
- }
1338
- if (inner.type === 'increment_statement') {
1339
- const lhs = namedChildByType(inner, 'lhs_expression');
1340
- const lhs_compiled = compileLhs(c, lhs);
1341
- return `${lhs_compiled.code} = wgsl.add(${lhs_compiled.code}, 1)`;
1342
- }
1343
- if (inner.type === 'decrement_statement') {
1344
- const lhs = namedChildByType(inner, 'lhs_expression');
1345
- const lhs_compiled = compileLhs(c, lhs);
1346
- return `${lhs_compiled.code} = wgsl.sub(${lhs_compiled.code}, 1)`;
1347
- }
1348
- }
1349
- return '';
1350
- }
1351
-
1352
- function emitWhileStatement(c, node) {
1353
- const cond = namedChildByType(node, 'expression');
1354
- const body = namedChildByType(node, 'compound_statement');
1355
- const cc = compileExpression(c, cond);
1356
- c.out.add(`while (${cc.code}) {`);
1357
- c.out.indent();
1358
- c.scope = c.scope.push();
1359
- // `continue` inside this loop targets this loop, not an outer `loop`'s continuing block
1360
- const saved_continue_label = c.continue_label;
1361
- c.continue_label = null;
1362
- emitCompoundStatementBody(c, body);
1363
- c.continue_label = saved_continue_label;
1364
- c.scope = c.scope.parent;
1365
- c.out.dedent();
1366
- c.out.add('}');
1367
- }
1368
-
1369
- function emitLoopStatement(c, node) {
1370
- // NOTE: the grammar puts the loop's `statement` children and the optional
1371
- // `continuing_statement` directly on the `loop_statement` node (no
1372
- // `compound_statement` wrapper); emitCompoundStatementBody visits only the
1373
- // `statement` children, which is exactly the loop body.
1374
- const body = node;
1375
- const continuing = namedChildByType(node, 'continuing_statement');
1376
-
1377
- const saved_continue_label = c.continue_label;
1378
-
1379
- c.out.add('while (true) {');
1380
- c.out.indent();
1381
- c.scope = c.scope.push();
1382
-
1383
- if (continuing === null) {
1384
-
1385
- // no continuing block: JS `continue` already has the right semantics
1386
- c.continue_label = null;
1387
- emitCompoundStatementBody(c, body);
1388
-
1389
- } else {
1390
-
1391
- // WGSL `continue` transfers control INTO the `continuing` block. Emit the loop
1392
- // body inside a labeled block: `continue` becomes `break <label>`, which lands on
1393
- // the continuing code that follows the block (still inside the `while`).
1394
- const label = `loop_body_${c.loop_label_counter++}`;
1395
- c.continue_label = label;
1396
-
1397
- c.out.add(`${label}: {`);
1398
- c.out.indent();
1399
- emitCompoundStatementBody(c, body);
1400
- c.out.dedent();
1401
- c.out.add('}');
1402
-
1403
- c.continue_label = null;
1404
-
1405
- const cont_body = namedChildByType(continuing, 'continuing_compound_statement');
1406
- if (cont_body !== null) {
1407
- emitCompoundStatementBody(c, cont_body);
1408
-
1409
- // `break if expr;` — only legal as the trailing statement of a continuing block.
1410
- // It is a direct named child of the continuing compound (not wrapped in `statement`),
1411
- // so emitCompoundStatementBody does not visit it.
1412
- const break_if = namedChildByType(cont_body, 'break_if_statement');
1413
- if (break_if !== null) {
1414
- const break_expr = namedChildByType(break_if, 'expression');
1415
- const bc = compileExpression(c, break_expr);
1416
- c.out.add(`if (${bc.code}) { break; }`);
1417
- }
1418
- }
1419
-
1420
- }
1421
-
1422
- c.continue_label = saved_continue_label;
1423
- c.scope = c.scope.parent;
1424
- c.out.dedent();
1425
- c.out.add('}');
1426
- }
1427
-
1428
- /**
1429
- * Emit a WGSL `switch`.
1430
- *
1431
- * Grammar (tree-sitter-wgsl): `switch_statement { expression, switch_body { switch_clause* } }`,
1432
- * where each `switch_clause` wraps either a `case_clause { case_selectors { case_selector+ },
1433
- * compound_statement }` or a `default_alone_clause { compound_statement }`. A `case_selector` holds
1434
- * either an `expression` (a case value) or the `default` keyword.
1435
- *
1436
- * WGSL switch has no fall-through — each selected clause runs its body and stops — so we terminate
1437
- * every clause body with `break;`. Multiple selectors on one clause become stacked JS `case` labels
1438
- * over a shared body.
1439
- */
1440
- function emitSwitchStatement(c, node) {
1441
- const expr = namedChildByType(node, 'expression');
1442
- const ec = compileExpression(c, expr);
1443
- c.out.add(`switch (${ec.code}) {`);
1444
- c.out.indent();
1445
-
1446
- const body = namedChildByType(node, 'switch_body');
1447
- if (body) {
1448
- for (const clause of namedChildrenByType(body, 'switch_clause')) {
1449
- const case_clause = namedChildByType(clause, 'case_clause');
1450
- const default_clause = namedChildByType(clause, 'default_alone_clause');
1451
- const inner = case_clause ?? default_clause;
1452
- if (!inner) {
1453
- continue;
1454
- }
1455
-
1456
- if (case_clause) {
1457
- const case_selectors = namedChildByType(case_clause, 'case_selectors');
1458
- if (case_selectors) {
1459
- for (const sel of namedChildrenByType(case_selectors, 'case_selector')) {
1460
- const sel_expr = namedChildByType(sel, 'expression');
1461
- if (sel_expr) {
1462
- c.out.add(`case ${compileExpression(c, sel_expr).code}:`);
1463
- } else {
1464
- // a bare `default` used within a case_selectors list
1465
- c.out.add('default:');
1466
- }
1467
- }
1468
- }
1469
- } else {
1470
- c.out.add('default:');
1471
- }
1472
-
1473
- // Wrap each clause body in its own JS block. WGSL case clauses are independent scopes,
1474
- // but JS switch clauses share the switch's block scope — without the braces, `let`
1475
- // temporaries declared in two clauses collide ("Identifier already declared").
1476
- const case_body = namedChildByType(inner, 'compound_statement');
1477
- c.out.add('{');
1478
- c.out.indent();
1479
- c.scope = c.scope.push();
1480
- if (case_body) {
1481
- emitCompoundStatementBody(c, case_body);
1482
- }
1483
- c.out.add('break;');
1484
- c.scope = c.scope.parent;
1485
- c.out.dedent();
1486
- c.out.add('}');
1487
- }
1488
- }
1489
-
1490
- c.out.dedent();
1491
- c.out.add('}');
1492
- }
1493
-
1494
- function emitFunctionCallStatement(c, node) {
1495
- const call_phrase = namedChildByType(node, 'call_phrase');
1496
- const r = compileCallPhraseTyped(c, call_phrase);
1497
- c.out.add(`${r.code};`);
1498
- }
1499
-
1500
- // =========================================================================
1501
- // Expressions
1502
- // =========================================================================
1503
-
1504
- /**
1505
- * Compile an expression node (any wrapper or specific kind).
1506
- * @param {Compiler} c
1507
- * @param {Node} node
1508
- * @returns {{ code: string, type: WgslType }}
1509
- */
1510
- function compileExpression(c, node) {
1511
- const u = unwrapExpression(node);
1512
-
1513
- // Binary operations
1514
- switch (u.type) {
1515
- case 'expression':
1516
- // tree-sitter-wgsl can leave `&&` / `||` directly under an `expression` node — i.e.
1517
- // the operator wraps two operands without a more specific short_circuit_*_expression
1518
- // wrapper around it. With 2 named children the unnamed operator sits between them.
1519
- return compileBinaryExpression(c, u, '__shortcircuit_unused__', 'raw');
1520
- case 'relational_expression':
1521
- return compileBinaryExpression(c, u, 'relational_operator', 'raw');
1522
- case 'shift_expression':
1523
- return compileBinaryExpression(c, u, 'shift_operator', 'raw');
1524
- case 'additive_expression':
1525
- return compileBinaryExpression(c, u, 'additive_operator', 'arith');
1526
- case 'multiplicative_expression':
1527
- return compileBinaryExpression(c, u, 'multiplicative_operator', 'arith');
1528
- case 'short_circuit_and_expression':
1529
- case 'short_circuit_or_expression':
1530
- return compileBinaryExpression(c, u, '__shortcircuit_unused__', 'raw');
1531
- case 'bitwise_expression':
1532
- // Top-level WGSL `^` / `&` / `|` operator. The operator is an unnamed token between
1533
- // the two named operands; compileBinaryExpression's 2-named-children path picks it up.
1534
- return compileBinaryExpression(c, u, '__bitwise_unused__', 'raw');
1535
- case 'binary_and_expression':
1536
- case 'binary_or_expression':
1537
- case 'binary_xor_expression':
1538
- // Inner chain nodes from the WGSL grammar (e.g. `a ^ b ^ c` nests as
1539
- // `binary_xor_expression(binary_xor_expression(a, b), c)`). When 2 named children
1540
- // appear, the unnamed operator sits between them; the 1-child case is a passthrough.
1541
- return compileBinaryExpression(c, u, '__bitwise_unused__', 'raw');
1542
- }
1543
-
1544
- // Unary
1545
- if (u.type === 'unary_expression') {
1546
- return compileUnaryExpression(c, u);
1547
- }
1548
-
1549
- // Singular (primary + chained component access)
1550
- if (u.type === 'singular_expression') {
1551
- return compileSingularExpression(c, u);
1552
- }
1553
-
1554
- // Primary subkinds
1555
- if (u.type === 'literal' || u.type === 'int_literal' || u.type === 'float_literal' || u.type === 'bool_literal') {
1556
- return { code: literalToJs(u.text), type: literalType(u.text) };
1557
- }
1558
- if (u.type === 'paren_expression') {
1559
- const inner = namedChildByType(u, 'expression');
1560
- const e = compileExpression(c, inner);
1561
- return { code: `(${e.code})`, type: e.type };
1562
- }
1563
- if (u.type === 'call_expression') {
1564
- const call_phrase = namedChildByType(u, 'call_phrase');
1565
- return compileCallPhraseTyped(c, call_phrase);
1566
- }
1567
- if (u.type === 'template_elaborated_ident') {
1568
- const ident = namedChildByType(u, 'ident');
1569
- const name = ident !== null ? ident.text : u.text;
1570
- return { code: name, type: c.scope.lookup(name) };
1571
- }
1572
- if (u.type === 'ident') {
1573
- return { code: u.text, type: c.scope.lookup(u.text) };
1574
- }
1575
-
1576
- c.error(u, `unsupported expression: ${u.type}`);
1577
- }
1578
-
1579
- /**
1580
- * Compile a binary expression node. Two grammar shapes are supported:
1581
- * - 3 named children where the middle child has type `op_type` — used for additive / multiplicative
1582
- * where the operator is an explicit named node (`additive_operator`, `multiplicative_operator`).
1583
- * - 2 named children with an unnamed operator token between them — used for relational / shift,
1584
- * where the operator (e.g. `<`, `>=`, `<<`) is a raw token.
1585
- *
1586
- * Mode:
1587
- * - `'arith'` → use `wgsl.add/sub/mul/div/rem` (handles vectors)
1588
- * - `'raw'` → emit raw JS operator
1589
- */
1590
- function compileBinaryExpression(c, node, op_type, mode) {
1591
- const named = node.namedChildren;
1592
- if (named.length === 1) {
1593
- return compileExpression(c, named[0]);
1594
- }
1595
-
1596
- let op_text = null;
1597
- let left_node;
1598
- let right_node;
1599
-
1600
- if (named.length === 3 && named[1].type === op_type) {
1601
- op_text = named[1].text;
1602
- left_node = named[0];
1603
- right_node = named[2];
1604
- } else if (named.length === 2) {
1605
- left_node = named[0];
1606
- right_node = named[1];
1607
- // tree-sitter-web doesn't surface unnamed children in `node.children`. Extract the
1608
- // operator by slicing the original text using the operands' source positions.
1609
- op_text = textBetween(node, left_node, right_node).trim();
1610
- } else {
1611
- return { code: node.text, type: T_UNKNOWN };
1612
- }
1613
-
1614
- const left = compileExpression(c, left_node);
1615
- const right = compileExpression(c, right_node);
1616
-
1617
- if (mode === 'arith') {
1618
- const helper = ARITHMETIC_OP[op_text];
1619
- if (helper !== undefined) {
1620
- // `*` on matrix/vector operands has WGSL-specific algebra (dot-products and
1621
- // accumulation) that the element-wise `wgsl.mul` doesn't capture. Route to a
1622
- // dedicated `mNxR_multiply_*` helper when at least one side is a matrix.
1623
- if (op_text === '*') {
1624
- const dispatched = dispatchMatrixMul(left, right);
1625
- if (dispatched !== null) return dispatched;
1626
- }
1627
- // WGSL `/` on integer types is truncated integer division. Detect when both
1628
- // operands' tracked types are integer (i32 / u32 scalars or matching int vecs)
1629
- // and route through the integer-truncating helper; otherwise use float div.
1630
- let actual_helper = helper;
1631
- if (op_text === '/' && bothOperandsInteger(left.type, right.type)) {
1632
- actual_helper = 'idiv';
1633
- }
1634
- return {
1635
- code: `wgsl.${actual_helper}(${left.code}, ${right.code})`,
1636
- type: inferBinaryArithResultType(left.type, right.type),
1637
- };
1638
- }
1639
- }
1640
-
1641
- // Bitwise (`&` / `|` / `^`) and shift (`<<` / `>>`) operators. Routed through element-wise
1642
- // helpers for vec/mat operands (JS raw operators coerce typed arrays to NaN), AND for `>>`
1643
- // on u32 scalars — JS's signed `>>` sign-extends, but WGSL u32 shift is logical.
1644
- const bitwise_helper = BITWISE_HELPER[op_text];
1645
- if (bitwise_helper !== undefined) {
1646
- if (isContainerType(left.type) || isContainerType(right.type)) {
1647
- return {
1648
- code: `wgsl.${bitwise_helper}(${left.code}, ${right.code})`,
1649
- type: inferRawBinaryResultType(left.type, right.type, op_text),
1650
- };
1651
- }
1652
- // For `>>` specifically: any u32-typed scalar operand must use the unsigned shift helper.
1653
- if (op_text === '>>' && (isScalarUnsigned(left.type) || isScalarUnsigned(right.type))) {
1654
- return {
1655
- code: `wgsl.shr(${left.code}, ${right.code})`,
1656
- type: inferRawBinaryResultType(left.type, right.type, op_text),
1657
- };
1658
- }
1659
- }
1660
-
1661
- // Comparison operators. JS's raw `==` etc. compare TypedArrays by reference; for WGSL
1662
- // `vec<T>` operands we need element-wise comparison returning a `vec<bool>`.
1663
- const cmp_helper = COMPARISON_HELPER[op_text];
1664
- if (cmp_helper !== undefined && (isContainerType(left.type) || isContainerType(right.type))) {
1665
- return {
1666
- code: `wgsl.${cmp_helper}(${left.code}, ${right.code})`,
1667
- type: inferRawBinaryResultType(left.type, right.type, op_text),
1668
- };
1669
- }
1670
-
1671
- return {
1672
- code: `${left.code} ${op_text} ${right.code}`,
1673
- type: inferRawBinaryResultType(left.type, right.type, op_text),
1674
- };
1675
- }
1676
-
1677
- /** Map WGSL bitwise/shift operators to language-core element-wise helper names. */
1678
- const BITWISE_HELPER = {
1679
- '&': 'bit_and',
1680
- '|': 'bit_or',
1681
- '^': 'bit_xor',
1682
- '<<': 'shl',
1683
- '>>': 'shr',
1684
- };
1685
-
1686
- /** Map WGSL comparison operators to language-core element-wise helper names. */
1687
- const COMPARISON_HELPER = {
1688
- '==': 'eq',
1689
- '!=': 'ne',
1690
- '<': 'lt',
1691
- '>': 'gt',
1692
- '<=': 'le',
1693
- '>=': 'ge',
1694
- };
1695
-
1696
- /** True if `t` is a vector or matrix — operand types that need element-wise dispatch. */
1697
- function isContainerType(t) {
1698
- return t && (t.kind === 'vec' || t.kind === 'mat');
1699
- }
1700
-
1701
- /** True if `t` is a u32 scalar — needed to route `>>` through the unsigned-shift helper. */
1702
- function isScalarUnsigned(t) {
1703
- return t && t.kind === 'primitive' && t.name === 'u32';
1704
- }
1705
-
1706
- /** True if both operands are integer-typed (scalar or vec of i32/u32). */
1707
- function bothOperandsInteger(a, b) {
1708
- const isInt = (t) => {
1709
- if (!t) return false;
1710
- if (t.kind === 'primitive') return t.name === 'i32' || t.name === 'u32';
1711
- if (t.kind === 'vec' || t.kind === 'mat') return t.scalar === 'i32' || t.scalar === 'u32';
1712
- return false;
1713
- };
1714
- return isInt(a) && isInt(b);
1715
- }
1716
-
1717
- /**
1718
- * Dispatch a WGSL `*` expression to the appropriate matrix/vector helper in the language
1719
- * core when at least one operand is a matrix. Returns `null` to indicate "fall through to the
1720
- * element-wise multiply" — used for scalar×scalar, vec×scalar, vec×vec (Hadamard product),
1721
- * matrix×scalar, etc., which `wgsl.mul` already handles correctly.
1722
- *
1723
- * Naming includes the scalar type suffix (`f` / `i` / `u` / `h`), e.g.
1724
- * `m4x4f_multiply_v4f`, `v3f_multiply_m3x3f`, `m4x4f_multiply_m4x4f`. Unsupported
1725
- * shapes (e.g. mat3x4 × vec3) produce a runtime `TypeError: wgsl.<name> is not a function`
1726
- * — adding support is a single new function in WGSLLanguageCore.js.
1727
- *
1728
- * @param {{ code: string, type: WgslType }} left
1729
- * @param {{ code: string, type: WgslType }} right
1730
- * @returns {{ code: string, type: WgslType } | null}
1731
- */
1732
- function dispatchMatrixMul(left, right) {
1733
- const lt = left.type;
1734
- const rt = right.type;
1735
- const isVec = (t) => t && t.kind === 'vec';
1736
- const isMat = (t) => t && t.kind === 'mat';
1737
-
1738
- // mat × mat → mat(b.cols, a.rows). Multiplication is defined only when a.cols == b.rows.
1739
- if (isMat(lt) && isMat(rt)) {
1740
- const lh = `m${lt.cols}x${lt.rows}${scalarTypeSuffix(lt.scalar)}`;
1741
- const rh = `m${rt.cols}x${rt.rows}${scalarTypeSuffix(rt.scalar)}`;
1742
- return {
1743
- code: `wgsl.${lh}_multiply_${rh}(${left.code}, ${right.code})`,
1744
- type: { kind: 'mat', cols: rt.cols, rows: lt.rows, scalar: lt.scalar },
1745
- };
1746
- }
1747
-
1748
- // mat × vec → vec(rows). Vec must have length == cols.
1749
- if (isMat(lt) && isVec(rt)) {
1750
- const lh = `m${lt.cols}x${lt.rows}${scalarTypeSuffix(lt.scalar)}`;
1751
- const rh = `v${rt.size}${scalarTypeSuffix(rt.scalar)}`;
1752
- return {
1753
- code: `wgsl.${lh}_multiply_${rh}(${left.code}, ${right.code})`,
1754
- type: { kind: 'vec', size: lt.rows, scalar: lt.scalar },
1755
- };
1756
- }
1757
-
1758
- // vec × mat → vec(cols). Vec acts as a row vector; vec.length must equal rows.
1759
- if (isVec(lt) && isMat(rt)) {
1760
- const lh = `v${lt.size}${scalarTypeSuffix(lt.scalar)}`;
1761
- const rh = `m${rt.cols}x${rt.rows}${scalarTypeSuffix(rt.scalar)}`;
1762
- return {
1763
- code: `wgsl.${lh}_multiply_${rh}(${left.code}, ${right.code})`,
1764
- type: { kind: 'vec', size: rt.cols, scalar: rt.scalar },
1765
- };
1766
- }
1767
-
1768
- return null;
1769
- }
1770
-
1771
- /** Scalar type → suffix character used in matrix/vector helper names: f / i / u / h. */
1772
- function scalarTypeSuffix(scalar) {
1773
- return { f32: 'f', i32: 'i', u32: 'u', f16: 'h' }[scalar] || 'f';
1774
- }
1775
-
1776
- /**
1777
- * Pick the result type of an element-wise arithmetic op given the operand types.
1778
- * Vec wins over scalar; matrix wins over scalar; scalar otherwise.
1779
- */
1780
- function inferBinaryArithResultType(a, b) {
1781
- const isVec = (t) => t && t.kind === 'vec';
1782
- const isMat = (t) => t && t.kind === 'mat';
1783
- const isPrim = (t) => t && t.kind === 'primitive';
1784
-
1785
- if (isVec(a)) return a;
1786
- if (isVec(b)) return b;
1787
- if (isMat(a)) return a;
1788
- if (isMat(b)) return b;
1789
- if (isPrim(a)) return a;
1790
- if (isPrim(b)) return b;
1791
- return T_UNKNOWN;
1792
- }
1793
-
1794
- /**
1795
- * Pick the result type of a non-arithmetic binary op (comparison / logical / bitwise / shift).
1796
- * Comparisons return bool; bitwise and shift preserve the leftmost known integer operand type.
1797
- */
1798
- function inferRawBinaryResultType(a, b, op) {
1799
- const COMPARISON = new Set(['==', '!=', '<', '>', '<=', '>=']);
1800
- const LOGICAL = new Set(['&&', '||']);
1801
- if (COMPARISON.has(op) || LOGICAL.has(op)) {
1802
- return { kind: 'primitive', name: 'bool' };
1803
- }
1804
- // Bitwise / shift: propagate the first known integer-ish operand type. This lets `bitcast`
1805
- // and other downstream consumers see u32/i32 even when the literal on one side is untyped.
1806
- if (a && a.kind === 'primitive') return a;
1807
- if (b && b.kind === 'primitive') return b;
1808
- if (a && a.kind === 'vec') return a;
1809
- if (b && b.kind === 'vec') return b;
1810
- return T_UNKNOWN;
1811
- }
1812
-
1813
- function compileUnaryExpression(c, node) {
1814
- const named = node.namedChildren;
1815
- const prefix = textBeforeFirstNamed(node).trim();
1816
-
1817
- if (prefix === '') {
1818
- return compileExpression(c, named[0]);
1819
- }
1820
-
1821
- const operand = compileExpression(c, named[named.length - 1]);
1822
-
1823
- if (prefix === '-') return { code: `wgsl.neg(${operand.code})`, type: operand.type };
1824
- if (prefix === '!') return { code: `!${operand.code}`, type: operand.type };
1825
- if (prefix === '~') return { code: `wgsl.not(${operand.code})`, type: operand.type };
1826
- if (prefix === '*' || prefix === '&') return { code: operand.code, type: operand.type };
1827
-
1828
- return { code: `${prefix}${operand.code}`, type: operand.type };
1829
- }
1830
-
1831
- function compileSingularExpression(c, node) {
1832
- const named = node.namedChildren;
1833
- const head = named[0];
1834
- const head_result = head.type === 'primary_expression'
1835
- ? compileExpression(c, unwrapExpression(head))
1836
- : compileExpression(c, head);
1837
-
1838
- let code = head_result.code;
1839
- let type = head_result.type;
1840
-
1841
- for (let i = 1; i < named.length; i++) {
1842
- const spec = named[i];
1843
- if (spec.type !== 'component_or_swizzle_specifier') break;
1844
- const next = applyAccessSpecifier(c, spec, code, type);
1845
- code = next.code;
1846
- type = next.type;
1847
- }
1848
-
1849
- return { code, type };
1850
- }
1851
-
1852
- /**
1853
- * Apply a `.member`, `.swizzle`, or `[index]` access to an expression that has already been
1854
- * compiled to `lhs_code` of type `lhs_type`.
1855
- */
1856
- function applyAccessSpecifier(c, node, lhs_code, lhs_type) {
1857
- // tree-sitter-wgsl groups chained access like `[i].x` (or `.foo[i]`) as a SINGLE outer
1858
- // `component_or_swizzle_specifier` whose first named child is the immediate access (an
1859
- // `expression` for `[]` or a `member_ident` for `.field`) and whose second named child
1860
- // is a NESTED `component_or_swizzle_specifier` for the rest of the chain. We apply the
1861
- // immediate access first, then recurse into any nested specifier.
1862
- let code = lhs_code;
1863
- let type = lhs_type;
1864
-
1865
- const member_ident = namedChildByType(node, 'member_ident');
1866
- if (member_ident !== null) {
1867
- const r = applyMemberAccess(c, member_ident.text, code, type);
1868
- code = r.code;
1869
- type = r.type;
1870
- } else {
1871
- const expr = namedChildByType(node, 'expression');
1872
- if (expr !== null) {
1873
- const e = compileExpression(c, expr);
1874
- // Matrices are stored as flat column-major typed arrays, so the bare `m[i]`
1875
- // would index a scalar — route through `wgsl.mat_col` to get the column as a vec.
1876
- if (type && type.kind === 'mat') {
1877
- code = `wgsl.mat_col(${code}, ${e.code}, ${type.rows})`;
1878
- type = { kind: 'vec', size: type.rows, scalar: type.scalar };
1879
- } else {
1880
- code = `${code}[${e.code}]`;
1881
- if (type && type.kind === 'array') type = type.element;
1882
- else if (type && type.kind === 'vec') type = { kind: 'primitive', name: type.scalar };
1883
- else type = T_UNKNOWN;
1884
- }
1885
- }
1886
- }
1887
-
1888
- const nested = namedChildByType(node, 'component_or_swizzle_specifier');
1889
- if (nested !== null) {
1890
- const r = applyAccessSpecifier(c, nested, code, type);
1891
- code = r.code;
1892
- type = r.type;
1893
- }
1894
-
1895
- return { code, type };
1896
- }
1897
-
1898
- /** Apply a `.field` / `.swizzle` access to an already-compiled expression of `lhs_type`. */
1899
- function applyMemberAccess(c, name, lhs_code, lhs_type) {
1900
- if (lhs_type !== null && lhs_type !== undefined && lhs_type.kind === 'vec' && isSwizzlePattern(name)) {
1901
- if (name.length === 1) {
1902
- return {
1903
- code: `${lhs_code}[${SWIZZLE_INDEX[name]}]`,
1904
- type: { kind: 'primitive', name: lhs_type.scalar },
1905
- };
1906
- }
1907
- const new_size = name.length;
1908
- const ctor = vecAliasName(new_size, lhs_type.scalar);
1909
- const elements = Array.from(name).map(ch => `${lhs_code}[${SWIZZLE_INDEX[ch]}]`);
1910
- return {
1911
- code: `wgsl.${ctor}(${elements.join(', ')})`,
1912
- type: { kind: 'vec', size: new_size, scalar: lhs_type.scalar },
1913
- };
1914
- }
1915
- if (lhs_type !== null && lhs_type !== undefined && lhs_type.kind === 'struct') {
1916
- const struct_info = c.structs.get(lhs_type.name);
1917
- if (struct_info !== undefined) {
1918
- const field = struct_info.fields.find(f => f.name === name);
1919
- if (field !== undefined) {
1920
- return { code: `${lhs_code}.${name}`, type: field.type };
1921
- }
1922
- }
1923
- }
1924
- return { code: `${lhs_code}.${name}`, type: T_UNKNOWN };
1925
- }
1926
-
1927
- function isSwizzlePattern(name) {
1928
- if (name.length === 0 || name.length > 4) return false;
1929
- for (const ch of name) {
1930
- if (!SWIZZLE_LETTERS.has(ch)) return false;
1931
- }
1932
- return true;
1933
- }
1934
-
1935
- /**
1936
- * Compile a `call_phrase` and return both the JS code and the inferred result type.
1937
- *
1938
- * Special cases:
1939
- * - `vec3<f32>(a, b, c)` → `wgsl.vec3f(a, b, c)` (vec result)
1940
- * - `f32(x)` → `wgsl.f32(x)` (scalar result)
1941
- * - WGSL builtin (mix, clamp, dot, length, ...) → `wgsl.<name>(...)` (best-effort type)
1942
- * - User function → `name(...)` (uses declared return type)
1943
- *
1944
- * @returns {{ code: string, type: WgslType }}
1945
- */
1946
- function compileCallPhraseTyped(c, node) {
1947
- const ident_node = namedChildByType(node, 'template_elaborated_ident');
1948
- const arg_list = namedChildByType(node, 'argument_expression_list');
1949
-
1950
- const ident_inner = namedChildByType(ident_node, 'ident');
1951
- const callee_name = ident_inner.text;
1952
- const template = namedChildByType(ident_node, 'template_list');
1953
-
1954
- /** @type {Array<{ code: string, type: WgslType }>} */
1955
- const arg_results = [];
1956
- if (arg_list !== null) {
1957
- const comma_list = namedChildByType(arg_list, 'expression_comma_list');
1958
- if (comma_list !== null) {
1959
- for (const arg of namedChildrenByType(comma_list, 'expression')) {
1960
- arg_results.push(compileExpression(c, arg));
1961
- }
1962
- }
1963
- }
1964
- const args = arg_results.map(a => a.code);
1965
-
1966
- // 1) Vec aliases: vec3f, vec4i, vec2h, etc.
1967
- if (VECTOR_ALIASES.hasOwnProperty(callee_name)) {
1968
- const a = VECTOR_ALIASES[callee_name];
1969
- return {
1970
- code: `wgsl.${callee_name}(${args.join(', ')})`,
1971
- type: { kind: 'vec', size: a.size, scalar: a.scalar },
1972
- };
1973
- }
1974
- // 2) Templated vec: vec3<f32>(...).
1975
- if (callee_name === 'vec2' || callee_name === 'vec3' || callee_name === 'vec4') {
1976
- const size = parseInt(callee_name.slice(3));
1977
- let scalar;
1978
- if (template !== null) {
1979
- const targs = extractTemplateArgs(template);
1980
- if (targs.length > 0) scalar = targs[0];
1981
- }
1982
- if (scalar === undefined) {
1983
- // No explicit template — infer the component type from the arguments, mirroring
1984
- // WGSL's overload resolution. e.g. `vec4(some_u32)` is `vec4<u32>`, not `vec4<f32>`.
1985
- for (const a of arg_results) {
1986
- const t = a.type;
1987
- if (t && t.kind === 'primitive' && t.name !== 'bool') { scalar = t.name; break; }
1988
- if (t && (t.kind === 'vec' || t.kind === 'mat')) { scalar = t.scalar; break; }
1989
- }
1990
- if (scalar === undefined) scalar = 'f32';
1991
- }
1992
- return {
1993
- code: `wgsl.${vecAliasName(size, scalar)}(${args.join(', ')})`,
1994
- type: { kind: 'vec', size, scalar },
1995
- };
1996
- }
1997
- // 3) Matrix constructors.
1998
- {
1999
- const m = callee_name.match(/^mat(\d)x(\d)([ifh]?)$/);
2000
- if (m !== null) {
2001
- const cols = parseInt(m[1]);
2002
- const rows = parseInt(m[2]);
2003
- const suffix = m[3];
2004
- let scalar;
2005
- if (suffix === 'f') scalar = 'f32';
2006
- else if (suffix === 'i') scalar = 'i32';
2007
- else if (suffix === 'h') scalar = 'f16';
2008
- else if (template !== null) {
2009
- const targs = extractTemplateArgs(template);
2010
- scalar = targs[0] || 'f32';
2011
- } else {
2012
- scalar = 'f32';
2013
- }
2014
- const fn_suffix = suffix !== '' ? '' : ({ f32: 'f', i32: 'i', f16: 'h' }[scalar] || 'f');
2015
- return {
2016
- code: `wgsl.${callee_name}${fn_suffix}(${args.join(', ')})`,
2017
- type: { kind: 'mat', cols, rows, scalar },
2018
- };
2019
- }
2020
- }
2021
-
2022
- // 4) Scalar casts: f32(x), i32(x), u32(x), f16(x), bool(x).
2023
- if (PRIMITIVE_NAMES.has(callee_name)) {
2024
- return {
2025
- code: `wgsl.${callee_name}(${args.join(', ')})`,
2026
- type: { kind: 'primitive', name: callee_name },
2027
- };
2028
- }
2029
-
2030
- // 5) `bitcast<T>(x)` — JS can't tell a u32 from an f32 by value alone, so we emit the
2031
- // source type alongside the target. The compiler infers source from the argument's tracked
2032
- // type; if that's unknown the runtime falls back to a heuristic.
2033
- if (callee_name === 'bitcast') {
2034
- let target = 'f32';
2035
- if (template !== null) {
2036
- const targs = extractTemplateArgs(template);
2037
- if (targs.length > 0) target = targs[0];
2038
- }
2039
- const arg_type = arg_results[0] !== undefined ? arg_results[0].type : null;
2040
- let source = null;
2041
- if (arg_type) {
2042
- if (arg_type.kind === 'primitive') source = arg_type.name;
2043
- else if (arg_type.kind === 'vec') source = arg_type.scalar;
2044
- }
2045
- const source_arg = source !== null ? `, ${JSON.stringify(source)}` : '';
2046
- // Parse the target string: `vec3<u32>` etc. become a vec type so that downstream
2047
- // swizzle/component access compiles correctly. Bare `f32` / `u32` / `i32` stay primitive.
2048
- let resultType;
2049
- const vec_match = target.match(/^vec(\d)<([^>]+)>$/);
2050
- if (vec_match !== null) {
2051
- resultType = { kind: 'vec', size: parseInt(vec_match[1]), scalar: vec_match[2] };
2052
- } else {
2053
- resultType = { kind: 'primitive', name: target };
2054
- }
2055
- return {
2056
- code: `wgsl.bitcast(${args[0]}, ${JSON.stringify(target)}${source_arg})`,
2057
- type: resultType,
2058
- };
2059
- }
2060
-
2061
- // 6) array / array<T, N> constructor.
2062
- if (callee_name === 'array') {
2063
- return { code: `[${args.join(', ')}]`, type: T_UNKNOWN };
2064
- }
2065
-
2066
- // 7) Struct constructor.
2067
- if (c.structs.has(callee_name)) {
2068
- const info = c.structs.get(callee_name);
2069
- const parts = [`(() => { const __s__ = new ${callee_name}();`];
2070
- for (let i = 0; i < info.fields.length && i < args.length; i++) {
2071
- parts.push(`__s__.${info.fields[i].name} = ${args[i]};`);
2072
- }
2073
- parts.push(`return __s__; })()`);
2074
- return { code: parts.join(' '), type: { kind: 'struct', name: callee_name } };
2075
- }
2076
-
2077
- // 8) User-defined function.
2078
- if (c.functions.has(callee_name)) {
2079
- return {
2080
- code: `${callee_name}(${args.join(', ')})`,
2081
- type: c.functions.get(callee_name).returnType,
2082
- };
2083
- }
2084
-
2085
- // 9) WGSL builtins — best-effort type inference for common helpers.
2086
- return {
2087
- code: `wgsl.${callee_name}(${args.join(', ')})`,
2088
- type: inferBuiltinReturnType(callee_name, arg_results),
2089
- };
2090
- }
2091
-
2092
- /**
2093
- * Best-effort return type for WGSL builtins commonly used in compute shaders.
2094
- * Handles the cases needed for accurate swizzle / member-access codegen downstream.
2095
- */
2096
- function inferBuiltinReturnType(name, args) {
2097
- // Element-wise vector-preserving math: result follows the first vector argument.
2098
- const ELEMENT_WISE = new Set([
2099
- 'abs', 'sign', 'floor', 'ceil', 'round', 'trunc', 'fract', 'sqrt', 'inverseSqrt',
2100
- 'exp', 'exp2', 'log', 'log2', 'sin', 'cos', 'tan', 'asin', 'acos', 'atan',
2101
- 'pow', 'atan2', 'min', 'max', 'step', 'clamp', 'saturate', 'mix', 'smoothstep',
2102
- 'normalize', 'reflect', 'fma',
2103
- ]);
2104
- if (ELEMENT_WISE.has(name)) {
2105
- for (const a of args) {
2106
- if (a.type && (a.type.kind === 'vec' || a.type.kind === 'mat')) return a.type;
2107
- }
2108
- if (args[0] && args[0].type) return args[0].type;
2109
- return T_UNKNOWN;
2110
- }
2111
- if (name === 'length' || name === 'distance' || name === 'dot' || name === 'determinant') {
2112
- return { kind: 'primitive', name: 'f32' };
2113
- }
2114
- if (name === 'cross') {
2115
- return args[0] && args[0].type && args[0].type.kind === 'vec'
2116
- ? args[0].type
2117
- : { kind: 'vec', size: 3, scalar: 'f32' };
2118
- }
2119
- if (name === 'all' || name === 'any') return { kind: 'primitive', name: 'bool' };
2120
- if (name === 'select') {
2121
- if (args[0] && args[0].type) return args[0].type;
2122
- if (args[1] && args[1].type) return args[1].type;
2123
- return T_UNKNOWN;
2124
- }
2125
- if (name === 'textureLoad' || name === 'textureSampleLevel' || name === 'textureSample' || name === 'textureSampleGrad') {
2126
- return { kind: 'vec', size: 4, scalar: 'f32' };
2127
- }
2128
- if (name === 'textureDimensions') {
2129
- return { kind: 'vec', size: 2, scalar: 'u32' };
2130
- }
2131
- if (name === 'atomicLoad') return { kind: 'primitive', name: 'i32' };
2132
- if (name === 'unpack2x16unorm' || name === 'unpack2x16snorm' || name === 'unpack2x16float') {
2133
- return { kind: 'vec', size: 2, scalar: 'f32' };
2134
- }
2135
- if (name === 'unpack4x8unorm' || name === 'unpack4x8snorm') {
2136
- return { kind: 'vec', size: 4, scalar: 'f32' };
2137
- }
2138
- if (name === 'pack2x16unorm' || name === 'pack2x16snorm' || name === 'pack2x16float'
2139
- || name === 'pack4x8unorm' || name === 'pack4x8snorm') {
2140
- return { kind: 'primitive', name: 'u32' };
2141
- }
2142
- // extractBits/insertBits return the integer type (scalar or vector) of their first operand.
2143
- if (name === 'extractBits' || name === 'insertBits') {
2144
- return args[0] && args[0].type ? args[0].type : T_UNKNOWN;
2145
- }
2146
- return T_UNKNOWN;
2147
- }
2148
-
2149
- // =========================================================================
2150
- // LHS expressions (for assignments)
2151
- // =========================================================================
2152
-
2153
- function compileLhs(c, node) {
2154
- if (node.type !== 'lhs_expression') {
2155
- return compileExpression(c, node);
2156
- }
2157
-
2158
- // Pointer deref / address-of: `*lhs` / `&lhs`. The emulator models pointers as values, so
2159
- // both collapse to the operand's value. The grammar puts the `*` / `&` as an unnamed leading
2160
- // token with a nested `lhs_expression` named child.
2161
- //
2162
- // A whole-value store through a deref (`*p = v`, with nothing selected out of it) is flagged
2163
- // so {@link emitWriteStatement} writes through the reference rather than rebinding the local
2164
- // see `wgsl.store_ptr`. A store into part of the pointee (`(*p).x`, `(*p)[i]`) already
2165
- // lands on the shared object and needs no such treatment.
2166
- const prefix = textBeforeFirstNamed(node).trim();
2167
- if (prefix === '*' || prefix === '&') {
2168
- const nested_lhs = namedChildByType(node, 'lhs_expression');
2169
- if (nested_lhs !== null) {
2170
- const inner = compileLhs(c, nested_lhs);
2171
-
2172
- const selects_part = node.namedChildren.some(
2173
- child => child.type === 'component_or_swizzle_specifier'
2174
- );
2175
-
2176
- return {
2177
- ...inner,
2178
- through_pointer: prefix === '*' && !selects_part,
2179
- };
2180
- }
2181
- }
2182
-
2183
- const core = namedChildByType(node, 'core_lhs_expression');
2184
- let code;
2185
- let type;
2186
- if (core !== null) {
2187
- const ident = namedChildByType(core, 'ident');
2188
- if (ident !== null) {
2189
- code = ident.text;
2190
- type = c.scope.lookup(ident.text);
2191
- } else {
2192
- const nested = namedChildByType(core, 'lhs_expression');
2193
- if (nested !== null) {
2194
- const r = compileLhs(c, nested);
2195
- code = `(${r.code})`;
2196
- type = r.type;
2197
- } else {
2198
- code = core.text;
2199
- type = T_UNKNOWN;
2200
- }
2201
- }
2202
- } else {
2203
- code = node.text;
2204
- type = T_UNKNOWN;
2205
- }
2206
-
2207
- for (const child of node.namedChildren) {
2208
- if (child.type === 'component_or_swizzle_specifier') {
2209
- const next = applyAccessSpecifier(c, child, code, type);
2210
- code = next.code;
2211
- type = next.type;
2212
- }
2213
- }
2214
- return { code, type };
2215
- }
1
+ import { assert } from "../../../core/assert.js";
2
+ import LineBuilder from "../../../core/codegen/LineBuilder.js";
3
+
4
+ // =========================================================================
5
+ // Type representation
6
+ // =========================================================================
7
+
8
+ /**
9
+ * @typedef {object} WgslType
10
+ * @property {'primitive' | 'vec' | 'mat' | 'array' | 'struct' | 'texture_2d' | 'sampler' | 'atomic' | 'unknown'} kind
11
+ * @property {string} [name]
12
+ * @property {number} [size]
13
+ * @property {number} [cols]
14
+ * @property {number} [rows]
15
+ * @property {string} [scalar]
16
+ * @property {WgslType} [element]
17
+ * @property {number} [count]
18
+ */
19
+
20
+ const T_UNKNOWN = Object.freeze({ kind: 'unknown' });
21
+
22
+ const VECTOR_ALIASES = {
23
+ vec2i: { size: 2, scalar: 'i32' },
24
+ vec3i: { size: 3, scalar: 'i32' },
25
+ vec4i: { size: 4, scalar: 'i32' },
26
+ vec2u: { size: 2, scalar: 'u32' },
27
+ vec3u: { size: 3, scalar: 'u32' },
28
+ vec4u: { size: 4, scalar: 'u32' },
29
+ vec2f: { size: 2, scalar: 'f32' },
30
+ vec3f: { size: 3, scalar: 'f32' },
31
+ vec4f: { size: 4, scalar: 'f32' },
32
+ vec2h: { size: 2, scalar: 'f16' },
33
+ vec3h: { size: 3, scalar: 'f16' },
34
+ vec4h: { size: 4, scalar: 'f16' },
35
+ };
36
+
37
+ const PRIMITIVE_NAMES = new Set(['i32', 'u32', 'f32', 'f16', 'bool']);
38
+
39
+ const SWIZZLE_LETTERS = new Set(['x', 'y', 'z', 'w', 'r', 'g', 'b', 'a']);
40
+
41
+ const SWIZZLE_INDEX = { x: 0, y: 1, z: 2, w: 3, r: 0, g: 1, b: 2, a: 3 };
42
+
43
+ /**
44
+ * Build a vec type alias name (e.g., {size:3, scalar:'f32'} → 'vec3f').
45
+ */
46
+ function vecAliasName(size, scalar) {
47
+ const suffix = { i32: 'i', u32: 'u', f32: 'f', f16: 'h' }[scalar] || 'f';
48
+ return `vec${size}${suffix}`;
49
+ }
50
+
51
+ // =========================================================================
52
+ // Scope (symbol table) — tracks variable types for accurate codegen
53
+ // =========================================================================
54
+
55
+ class Scope {
56
+ constructor(parent = null) {
57
+ this.parent = parent;
58
+ /** @type {Map<string, WgslType>} */
59
+ this.symbols = new Map();
60
+ }
61
+
62
+ declare(name, type) {
63
+ this.symbols.set(name, type || T_UNKNOWN);
64
+ }
65
+
66
+ /** @returns {WgslType} */
67
+ lookup(name) {
68
+ if (this.symbols.has(name)) return this.symbols.get(name);
69
+ if (this.parent) return this.parent.lookup(name);
70
+ return T_UNKNOWN;
71
+ }
72
+
73
+ push() {
74
+ return new Scope(this);
75
+ }
76
+ }
77
+
78
+ // =========================================================================
79
+ // Tree-sitter helpers
80
+ // =========================================================================
81
+
82
+ /**
83
+ * Return only the named children of a node (filters out punctuation tokens).
84
+ */
85
+ function namedChildren(node) {
86
+ return node.namedChildren;
87
+ }
88
+
89
+ /**
90
+ * First named child by type, or null.
91
+ */
92
+ function namedChildByType(node, type) {
93
+ for (const c of node.namedChildren) {
94
+ if (c.type === type) return c;
95
+ }
96
+ return null;
97
+ }
98
+
99
+ /**
100
+ * All named children with the given type.
101
+ */
102
+ function namedChildrenByType(node, type) {
103
+ return node.namedChildren.filter(c => c.type === type);
104
+ }
105
+
106
+ /**
107
+ * Extract the source-text slice of `parent` that lies between the end of `left` and the start
108
+ * of `right`. tree-sitter-web's `node.children` does not include unnamed tokens, so this is the
109
+ * reliable way to read operator characters that sit between two named children.
110
+ *
111
+ * @param {Node} parent
112
+ * @param {Node} left
113
+ * @param {Node} right
114
+ * @returns {string}
115
+ */
116
+ function textBetween(parent, left, right) {
117
+ const left_offset = (left.endIndex - parent.startIndex);
118
+ const right_offset = (right.startIndex - parent.startIndex);
119
+ return parent.text.slice(left_offset, right_offset);
120
+ }
121
+
122
+ /**
123
+ * Extract any leading source text in `parent` that precedes its first named child. Used for
124
+ * unary operators (e.g. `-x`, `!cond`) where the operator is an unnamed token.
125
+ *
126
+ * @param {Node} parent
127
+ * @returns {string}
128
+ */
129
+ function textBeforeFirstNamed(parent) {
130
+ if (parent.namedChildCount === 0) return '';
131
+ const first = parent.namedChildren[0];
132
+ const offset = first.startIndex - parent.startIndex;
133
+ return parent.text.slice(0, offset);
134
+ }
135
+
136
+ /**
137
+ * Strip wrapper nodes that have a single named child of an expression-passthrough type.
138
+ * The WGSL grammar has a long precedence chain: expression > relational > shift > additive > ...
139
+ * When an expression is "simple" each level just wraps the next.
140
+ */
141
+ function unwrapExpression(node) {
142
+ const PASSTHROUGH = new Set([
143
+ 'expression',
144
+ 'relational_expression',
145
+ 'shift_expression',
146
+ 'additive_expression',
147
+ 'multiplicative_expression',
148
+ 'unary_expression',
149
+ 'singular_expression',
150
+ 'primary_expression',
151
+ ]);
152
+ while (PASSTHROUGH.has(node.type) && node.namedChildren.length === 1) {
153
+ // `unary_expression` keeps its operator as an unnamed token before the operand. If there
154
+ // is one, don't unwrap — the caller needs to see the unary node so it can apply the op.
155
+ if (node.type === 'unary_expression' && textBeforeFirstNamed(node).trim() !== '') {
156
+ break;
157
+ }
158
+ node = node.namedChildren[0];
159
+ }
160
+ return node;
161
+ }
162
+
163
+ // =========================================================================
164
+ // Type parsing (from `type_specifier` nodes or raw text)
165
+ // =========================================================================
166
+
167
+ /**
168
+ * Parse a `type_specifier` tree-sitter node.
169
+ * @param {Node} node
170
+ * @returns {WgslType}
171
+ */
172
+ function parseTypeSpecifier(node) {
173
+ if (node === null || node === undefined) return T_UNKNOWN;
174
+ if (node.type === 'type_specifier') {
175
+ return parseTypeSpecifier(node.namedChildren[0]);
176
+ }
177
+ if (node.type === 'template_elaborated_ident') {
178
+ const ident = namedChildByType(node, 'ident');
179
+ const ident_text = ident ? ident.text : node.text;
180
+ const template = namedChildByType(node, 'template_list');
181
+ const args = template ? extractTemplateArgs(template) : [];
182
+ return classifyType(ident_text, args, template);
183
+ }
184
+ return T_UNKNOWN;
185
+ }
186
+
187
+ /**
188
+ * Pull the textual template arguments from a `template_list` node, e.g. `<f32, 256>` → ['f32', '256'].
189
+ */
190
+ function extractTemplateArgs(template_list_node) {
191
+ const out = [];
192
+ const comma_list = namedChildByType(template_list_node, 'template_arg_comma_list');
193
+ if (comma_list === null) return out;
194
+ for (const c of comma_list.namedChildren) {
195
+ if (c.type === 'template_arg_expression') {
196
+ out.push(c.text.trim());
197
+ }
198
+ }
199
+ return out;
200
+ }
201
+
202
+ /**
203
+ * @param {string} ident
204
+ * @param {string[]} args — textual template args (may be types or numbers)
205
+ * @param {Node | null} template_list_node — the original node, for parsing nested array element types
206
+ * @returns {WgslType}
207
+ */
208
+ function classifyType(ident, args, template_list_node) {
209
+ if (PRIMITIVE_NAMES.has(ident)) {
210
+ return { kind: 'primitive', name: ident };
211
+ }
212
+ if (VECTOR_ALIASES.hasOwnProperty(ident)) {
213
+ const a = VECTOR_ALIASES[ident];
214
+ return { kind: 'vec', size: a.size, scalar: a.scalar };
215
+ }
216
+ if (ident === 'vec2' || ident === 'vec3' || ident === 'vec4') {
217
+ const size = parseInt(ident.slice(3));
218
+ const scalar = args.length > 0 ? args[0] : 'f32';
219
+ return { kind: 'vec', size, scalar };
220
+ }
221
+ {
222
+ const m = ident.match(/^mat(\d)x(\d)([ifh]?)$/);
223
+ if (m !== null) {
224
+ const cols = parseInt(m[1]);
225
+ const rows = parseInt(m[2]);
226
+ const suffix = m[3];
227
+ let scalar;
228
+ if (suffix === 'f') scalar = 'f32';
229
+ else if (suffix === 'i') scalar = 'i32';
230
+ else if (suffix === 'h') scalar = 'f16';
231
+ else scalar = (args.length > 0 ? args[0] : 'f32');
232
+ return { kind: 'mat', cols, rows, scalar };
233
+ }
234
+ }
235
+ if (ident === 'array') {
236
+ // template arg 0 is the element type (sub-template), arg 1 is the count
237
+ let element = T_UNKNOWN;
238
+ let count = -1;
239
+ if (template_list_node !== null) {
240
+ const comma_list = namedChildByType(template_list_node, 'template_arg_comma_list');
241
+ if (comma_list !== null) {
242
+ const arg_nodes = namedChildrenByType(comma_list, 'template_arg_expression');
243
+ if (arg_nodes.length > 0) {
244
+ // Walk down to find the inner type ident
245
+ const inner_expr = unwrapExpression(arg_nodes[0].namedChildren[0]);
246
+ element = parseTypeSpecifier(inner_expr);
247
+ }
248
+ if (arg_nodes.length > 1) {
249
+ const c = parseInt(arg_nodes[1].text.trim());
250
+ if (!isNaN(c)) count = c;
251
+ }
252
+ }
253
+ }
254
+ return { kind: 'array', element, count };
255
+ }
256
+ if (ident === 'atomic') {
257
+ const inner = args.length > 0 ? args[0] : 'i32';
258
+ return { kind: 'atomic', scalar: inner };
259
+ }
260
+ // Texture types - 1D / 2D / 3D, sampled and storage variants. We
261
+ // collapse all of them to a single kind because the intrinsics in
262
+ // WGSLLanguageCore dispatch on the bound CPUBitmapData's
263
+ // `dimensionality`, not on the static WGSL type. (The 'texture_2d'
264
+ // kind label is preserved for backward compatibility with any
265
+ // downstream consumer that may grep for it.)
266
+ if (
267
+ ident === 'texture_2d' || ident === 'texture_storage_2d'
268
+ || ident === 'texture_1d' || ident === 'texture_storage_1d'
269
+ || ident === 'texture_3d' || ident === 'texture_storage_3d'
270
+ || ident === 'texture_2d_array'
271
+ ) {
272
+ const scalar = args.length > 0 ? args[0] : 'f32';
273
+ return { kind: 'texture_2d', scalar };
274
+ }
275
+ if (ident === 'sampler') {
276
+ return { kind: 'sampler' };
277
+ }
278
+ if (ident === 'ptr') {
279
+ // Pointer — peel off the address space and read the inner type.
280
+ if (template_list_node !== null) {
281
+ const comma_list = namedChildByType(template_list_node, 'template_arg_comma_list');
282
+ if (comma_list !== null) {
283
+ const arg_nodes = namedChildrenByType(comma_list, 'template_arg_expression');
284
+ if (arg_nodes.length >= 2) {
285
+ const inner_expr = unwrapExpression(arg_nodes[1].namedChildren[0]);
286
+ return parseTypeSpecifier(inner_expr);
287
+ }
288
+ }
289
+ }
290
+ return T_UNKNOWN;
291
+ }
292
+ // Otherwise: assume it's a struct name.
293
+ return { kind: 'struct', name: ident };
294
+ }
295
+
296
+ // =========================================================================
297
+ // Literal handling — strip WGSL type suffixes for JS
298
+ // =========================================================================
299
+
300
+ /**
301
+ * Convert a WGSL literal token (e.g. `7u`, `1.0f`, `true`) to its JS equivalent.
302
+ *
303
+ * Stripping the type suffix is shape-sensitive: integer literals only carry `u`/`i`, and float
304
+ * literals only carry `f`/`h`. Crucially, a hex int like `0x0300f00f` ends in a hex digit `f`
305
+ * that must NOT be treated as a suffix — only an `f` after a decimal-format float counts.
306
+ */
307
+ function literalToJs(text) {
308
+ const t = text.trim();
309
+ if (t === 'true' || t === 'false') return t;
310
+
311
+ // Hex int literals: `0xNN[ui]?`. Only `u`/`i` are real suffixes here; trailing `f`/`h` are
312
+ // hex digits.
313
+ if (/^0[xX]/.test(t)) {
314
+ return t.replace(/[ui]$/, '');
315
+ }
316
+
317
+ // Decimal int / float. Float forms have a `.`, `e`/`E`, or end in `f`/`h`. Int forms don't.
318
+ if (/[.eE]/.test(t)) {
319
+ return t.replace(/[fh]$/, '');
320
+ }
321
+
322
+ // Plain integer (no `.`, no exponent): strip `u`/`i`/`f`/`h` (the latter two appear only on
323
+ // forms like `1f` which WGSL allows as a float written without a decimal point).
324
+ return t.replace(/[uifh]$/, '');
325
+ }
326
+
327
+ /**
328
+ * The tracked type of a literal token, as far as its own spelling settles it.
329
+ *
330
+ * Only an explicit suffix is honoured. A bare `2043` is WGSL's AbstractInt, which takes its
331
+ * concrete type from context this compiler does not track, so it stays unknown rather than being
332
+ * guessed at.
333
+ *
334
+ * The suffix matters because operator selection reads these types. `index / 2043u` is truncating
335
+ * integer division in WGSL and plain division in JavaScript, and the two differ for every pair
336
+ * where the divisor does not divide the dividend — which is what a paged table's
337
+ * `index / elements_per_page` is. Left untyped, the generated code returned a fractional page
338
+ * index, indexed the page-address table with it, and read `undefined` back as zero: every record
339
+ * past the first page slot came out of the emulator zero-filled, with nothing to say it had.
340
+ *
341
+ * @param {string} text
342
+ * @returns {WgslType}
343
+ */
344
+ function literalType(text) {
345
+ const t = text.trim();
346
+
347
+ if (t === 'true' || t === 'false') {
348
+ return { kind: 'primitive', name: 'bool' };
349
+ }
350
+
351
+ if (/[ui]$/.test(t)) {
352
+ return { kind: 'primitive', name: t.endsWith('u') ? 'u32' : 'i32' };
353
+ }
354
+
355
+ // `f`/`h` are only suffixes on a decimal-format literal; on a hex int they are digits.
356
+ if (!/^0[xX]/.test(t) && /[fh]$/.test(t)) {
357
+ return { kind: 'primitive', name: t.endsWith('f') ? 'f32' : 'f16' };
358
+ }
359
+
360
+ if (/[.eE]/.test(t) && !/^0[xX]/.test(t)) {
361
+ return { kind: 'primitive', name: 'f32' };
362
+ }
363
+
364
+ return T_UNKNOWN;
365
+ }
366
+
367
+ // =========================================================================
368
+ // Operator handling
369
+ // =========================================================================
370
+
371
+ /**
372
+ * Map a WGSL binary operator to a `wgsl.*` runtime helper or to a raw JS operator.
373
+ * Arithmetic must go through wgsl.* so vectors are handled element-wise.
374
+ */
375
+ const ARITHMETIC_OP = {
376
+ '+': 'add',
377
+ '-': 'sub',
378
+ '*': 'mul',
379
+ '/': 'div',
380
+ '%': 'rem',
381
+ };
382
+
383
+ const RAW_BINARY_OPS = new Set(['<', '>', '<=', '>=', '==', '!=', '&&', '||', '&', '|', '^', '<<', '>>']);
384
+
385
+ // =========================================================================
386
+ // Compiler context — central state during compilation
387
+ // =========================================================================
388
+
389
+ class Compiler {
390
+ constructor() {
391
+ /** @type {LineBuilder} */
392
+ this.out = new LineBuilder();
393
+
394
+ /** @type {Scope} */
395
+ this.scope = new Scope();
396
+
397
+ /** @type {Map<string, { name: string, fields: Array<{ name: string, type: WgslType }> }>} */
398
+ this.structs = new Map();
399
+
400
+ /** @type {Map<string, { name: string, returnType: WgslType, params: Array<{ name: string, type: WgslType }> }>} */
401
+ this.functions = new Map();
402
+
403
+ /** @type {Array<{ name: string, type: WgslType, group: number, binding: number, addressSpace: string | null, accessMode: string | null }>} */
404
+ this.globalBindings = [];
405
+
406
+ /** @type {Array<{ name: string, type: WgslType }>} */
407
+ this.workgroupVars = [];
408
+
409
+ /**
410
+ * Module-scope `var<private>` and `const` declarations, in declaration order.
411
+ * Each entry is `{ name, kind: 'let' | 'const', code }` — `code` is the JS line emitted
412
+ * verbatim; `name` and `kind` are also tracked so the returned module object can expose
413
+ * them as getter/setter (or read-only) properties for testing.
414
+ * @type {Array<{ name: string, kind: 'let' | 'const', code: string }>}
415
+ */
416
+ this.privateGlobals = [];
417
+
418
+ /** @type {{ name: string, params: Array<{ name: string, type: WgslType, builtin: string | null }>, workgroup_size: number[] } | null} */
419
+ this.entry = null;
420
+
421
+ /**
422
+ * Label of the enclosing `loop` body block when that loop has a `continuing`
423
+ * block: WGSL `continue` must transfer control INTO the continuing block, so it
424
+ * is emitted as `break <label>` out of the labeled body block. `null` when the
425
+ * nearest enclosing loop has no continuing block (plain JS `continue` suffices).
426
+ * Loop emitters save/clear/restore this around nested loop bodies.
427
+ * @type {string | null}
428
+ */
429
+ this.continue_label = null;
430
+
431
+ /**
432
+ * Monotonic counter used to generate unique loop-body labels.
433
+ * @type {number}
434
+ */
435
+ this.loop_label_counter = 0;
436
+ }
437
+
438
+ error(node, message) {
439
+ throw new Error(`WGSL compiler: ${message} (at ${node ? node.type : '<unknown>'} : ${node ? JSON.stringify(node.text.slice(0, 60)) : ''})`);
440
+ }
441
+ }
442
+
443
+ // =========================================================================
444
+ // Top-level compile entry point
445
+ // =========================================================================
446
+
447
+ export class WGSLJavaScriptCompiler {
448
+
449
+ /**
450
+ * Compile a tree-sitter WGSL parse tree to a JS source string.
451
+ *
452
+ * The output is a JavaScript expression: a function `factory(wgsl)` that returns
453
+ * a shader module object. The shape depends on what the WGSL declared:
454
+ *
455
+ * - **Always present:** every top-level user function (e.g. `module.random()`),
456
+ * every struct class (e.g. `module.Foo`), and a getter/setter property for each
457
+ * `var<private>` and getter for each module-scope `const`.
458
+ * - **Compute-shader fields (only when a `@compute` entry is present):**
459
+ * ```
460
+ * {
461
+ * entry_name: string,
462
+ * workgroup_size: [x, y, z],
463
+ * param_names: string[],
464
+ * entry: (bindings, workgroup, ...args) => void,
465
+ * create_workgroup_state: () => object,
466
+ * }
467
+ * ```
468
+ *
469
+ * Without a `@compute` entry the result is a "library" module — useful for testing
470
+ * pure WGSL helper code chunks (hashes, packing, math) one function at a time.
471
+ *
472
+ * @param {Tree} ast — tree-sitter WGSL AST (returned by `WGSLParser.parse`)
473
+ * @returns {string}
474
+ */
475
+ compile(ast) {
476
+ const c = new Compiler();
477
+ compileTranslationUnit(c, ast.rootNode);
478
+ return c.out.build();
479
+ }
480
+ }
481
+
482
+ // =========================================================================
483
+ // Translation unit (root)
484
+ // =========================================================================
485
+
486
+ /**
487
+ * @param {Compiler} c
488
+ * @param {Node} root
489
+ */
490
+ function compileTranslationUnit(c, root) {
491
+ assert.equal(root.type, 'translation_unit', 'root must be translation_unit');
492
+
493
+ // First pass — collect top-level decls so forward references and global scope resolve.
494
+ for (const node of root.namedChildren) {
495
+ switch (node.type) {
496
+ case 'struct_decl':
497
+ collectStruct(c, node);
498
+ break;
499
+ case 'function_decl':
500
+ collectFunction(c, node);
501
+ break;
502
+ case 'global_variable_decl':
503
+ collectGlobalVariable(c, node);
504
+ break;
505
+ case 'global_value_decl':
506
+ collectGlobalValue(c, node);
507
+ break;
508
+ }
509
+ }
510
+
511
+ // Emit the wrapper: `(wgsl) => { ... }`
512
+ c.out.add('(wgsl) => {');
513
+ c.out.indent();
514
+
515
+ // 1) Struct classes — must be defined before any user fn that references them or before
516
+ // the workgroup state factory (which may need to construct struct instances).
517
+ for (const node of root.namedChildren) {
518
+ if (node.type === 'struct_decl') emitStruct(c, node);
519
+ }
520
+
521
+ // 2) Module-scope `let` slots for bindings and workgroup vars. The entry wrapper assigns
522
+ // these on each dispatch so that user functions see them via their enclosing closure
523
+ // — WGSL functions can read globals freely.
524
+ for (const b of c.globalBindings) {
525
+ c.out.add(`let ${b.name} = undefined;`);
526
+ }
527
+ for (const wv of c.workgroupVars) {
528
+ c.out.add(`let ${wv.name} = undefined;`);
529
+ }
530
+
531
+ // 3) Module-scope `var<private>` and `const` declarations.
532
+ for (const g of c.privateGlobals) {
533
+ c.out.add(g.code);
534
+ }
535
+
536
+ // 4) User-defined functions (including the @compute entry, if any).
537
+ for (const node of root.namedChildren) {
538
+ if (node.type === 'function_decl') emitFunction(c, node);
539
+ }
540
+
541
+ // 5) Entry-stage support code: the dispatch wrapper for any entry, plus a workgroup-state
542
+ // factory for @compute entries (fragment shaders have no var<workgroup>). Skipped entirely
543
+ // in library mode (no entry attribute) so the resulting module is just a bag of pure
544
+ // functions and globals.
545
+ if (c.entry !== null) {
546
+ if (c.entry.stage === 'compute') {
547
+ emitWorkgroupStateFactory(c);
548
+ }
549
+ emitEntryWrapper(c);
550
+ }
551
+
552
+ // 6) Build and return the module object.
553
+ emitModuleReturn(c);
554
+
555
+ c.out.dedent();
556
+ c.out.add('}');
557
+ }
558
+
559
+ /**
560
+ * Emit the `return { ... }` block of the factory. Every top-level user function and struct
561
+ * class is exposed by name; module-scope `var<private>` slots become getter/setter properties
562
+ * (so tests can inspect or seed RNG state etc.), and module-scope `const`s become read-only
563
+ * getters. When the WGSL had a `@compute` entry, the dispatch trio (`entry_name`,
564
+ * `workgroup_size`, `param_names`, `entry`, `create_workgroup_state`) is included.
565
+ */
566
+ function emitModuleReturn(c) {
567
+ c.out.add('const __module__ = {};');
568
+
569
+ for (const fn_name of c.functions.keys()) {
570
+ c.out.add(`__module__.${fn_name} = ${fn_name};`);
571
+ }
572
+ for (const struct_name of c.structs.keys()) {
573
+ c.out.add(`__module__.${struct_name} = ${struct_name};`);
574
+ }
575
+
576
+ for (const g of c.privateGlobals) {
577
+ if (g.kind === 'let') {
578
+ c.out.add(`Object.defineProperty(__module__, ${JSON.stringify(g.name)}, { get: () => ${g.name}, set: (v) => { ${g.name} = v; } });`);
579
+ } else {
580
+ c.out.add(`Object.defineProperty(__module__, ${JSON.stringify(g.name)}, { get: () => ${g.name} });`);
581
+ }
582
+ }
583
+
584
+ if (c.entry !== null) {
585
+ c.out.add(`__module__.entry_name = ${JSON.stringify(c.entry.name)};`);
586
+ c.out.add(`__module__.stage = ${JSON.stringify(c.entry.stage)};`);
587
+ c.out.add(`__module__.param_names = [${c.entry.params.map(p => JSON.stringify(p.name)).join(', ')}];`);
588
+ c.out.add(`__module__.entry = __entry__;`);
589
+ if (c.entry.stage === 'compute') {
590
+ c.out.add(`__module__.workgroup_size = [${c.entry.workgroup_size.join(', ')}];`);
591
+ c.out.add(`__module__.create_workgroup_state = __create_workgroup_state__;`);
592
+ }
593
+ }
594
+
595
+ c.out.add('return __module__;');
596
+ }
597
+
598
+ // =========================================================================
599
+ // Struct collection + emission
600
+ // =========================================================================
601
+
602
+ /**
603
+ * @param {Compiler} c
604
+ * @param {Node} node
605
+ */
606
+ function collectStruct(c, node) {
607
+ const ident = namedChildByType(node, 'ident');
608
+ const name = ident.text;
609
+
610
+ const body = namedChildByType(node, 'struct_body_decl');
611
+ const members = namedChildrenByType(body, 'struct_member');
612
+
613
+ /** @type {Array<{ name: string, type: WgslType }>} */
614
+ const fields = [];
615
+
616
+ for (const m of members) {
617
+ const member_ident = namedChildByType(m, 'member_ident');
618
+ const type_spec = namedChildByType(m, 'type_specifier');
619
+ fields.push({
620
+ name: member_ident.text,
621
+ type: parseTypeSpecifier(type_spec),
622
+ });
623
+ }
624
+
625
+ c.structs.set(name, { name, fields });
626
+ }
627
+
628
+ /**
629
+ * @param {Compiler} c
630
+ * @param {Node} node
631
+ */
632
+ function emitStruct(c, node) {
633
+ const ident = namedChildByType(node, 'ident');
634
+ const name = ident.text;
635
+ const info = c.structs.get(name);
636
+
637
+ c.out.add(`class ${name} {`);
638
+ c.out.indent();
639
+ c.out.add('constructor() {');
640
+ c.out.indent();
641
+ for (const f of info.fields) {
642
+ c.out.add(`this.${f.name} = ${defaultValueForType(f.type)};`);
643
+ }
644
+ c.out.dedent();
645
+ c.out.add('}');
646
+ c.out.dedent();
647
+ c.out.add('}');
648
+ }
649
+
650
+ /**
651
+ * Initialiser expression for a fresh value of the given type.
652
+ * @param {WgslType} type
653
+ * @returns {string}
654
+ */
655
+ function defaultValueForType(type) {
656
+ if (type === undefined || type === null) return 'undefined';
657
+ switch (type.kind) {
658
+ case 'primitive':
659
+ return type.name === 'bool' ? 'false' : '0';
660
+ case 'vec':
661
+ return `wgsl.${vecAliasName(type.size, type.scalar)}()`;
662
+ case 'mat':
663
+ return `new Float32Array(${type.cols * type.rows})`;
664
+ case 'array': {
665
+ if (type.count > 0) {
666
+ if (type.element.kind === 'primitive') {
667
+ const ctor = arrayCtorForScalar(type.element.name);
668
+ return `new ${ctor}(${type.count})`;
669
+ }
670
+ // For non-primitive elements (vec / mat / struct / nested array) build the
671
+ // array eagerly so `out[i]` is a fresh, mutable element rather than `undefined`.
672
+ return `Array.from({ length: ${type.count} }, () => ${defaultValueForType(type.element)})`;
673
+ }
674
+ return '[]';
675
+ }
676
+ case 'struct':
677
+ return `new ${type.name}()`;
678
+ case 'atomic':
679
+ return '{ value: 0 }';
680
+ default:
681
+ return 'undefined';
682
+ }
683
+ }
684
+
685
+ function arrayCtorForScalar(name) {
686
+ switch (name) {
687
+ case 'f32':
688
+ return 'Float32Array';
689
+ case 'f16':
690
+ return 'Float16Array';
691
+ case 'i32':
692
+ return 'Int32Array';
693
+ case 'u32':
694
+ return 'Uint32Array';
695
+ default:
696
+ return 'Array';
697
+ }
698
+ }
699
+
700
+ // =========================================================================
701
+ // Function collection + emission
702
+ // =========================================================================
703
+
704
+ /**
705
+ * @param {Compiler} c
706
+ * @param {Node} node
707
+ */
708
+ function collectFunction(c, node) {
709
+ const header = namedChildByType(node, 'function_header');
710
+ const ident = namedChildByType(header, 'ident');
711
+ const name = ident.text;
712
+
713
+ const param_list_node = namedChildByType(header, 'param_list');
714
+ /** @type {Array<{ name: string, type: WgslType, builtin: string | null }>} */
715
+ const params = [];
716
+ if (param_list_node !== null) {
717
+ const param_nodes = namedChildrenByType(param_list_node, 'param');
718
+ for (const p of param_nodes) {
719
+ const p_ident = namedChildByType(p, 'ident');
720
+ const p_type = namedChildByType(p, 'type_specifier');
721
+ const builtin = extractBuiltin(p);
722
+ params.push({
723
+ name: p_ident.text,
724
+ type: parseTypeSpecifier(p_type),
725
+ builtin,
726
+ });
727
+ }
728
+ }
729
+
730
+ // Optional return type — `template_elaborated_ident` directly under the header.
731
+ let returnType = T_UNKNOWN;
732
+ for (const child of header.namedChildren) {
733
+ if (child.type === 'template_elaborated_ident') {
734
+ returnType = parseTypeSpecifier(child);
735
+ }
736
+ }
737
+
738
+ c.functions.set(name, { name, returnType, params: params.map(p => ({ name: p.name, type: p.type })) });
739
+
740
+ // Detect the entry attribute (@compute, @fragment, or @vertex) and capture stage-specific
741
+ // metadata. The emulator supports compute and fragment entries; vertex is rejected because
742
+ // ImageShader testing in the emulator skips the vertex phase intentionally.
743
+ const attrs = namedChildrenByType(node, 'attribute');
744
+ let stage = null;
745
+ let workgroup_size = [1, 1, 1];
746
+ for (const a of attrs) {
747
+ const inner = a.namedChildren[0];
748
+ if (inner.type === 'compute_attr') {
749
+ stage = 'compute';
750
+ } else if (inner.type === 'fragment_attr') {
751
+ stage = 'fragment';
752
+ } else if (inner.type === 'vertex_attr') {
753
+ throw new Error(`WGSL compiler: @vertex stage is not supported by the emulator (function '${name}'). For ImageShader testing the vertex phase is skipped — invoke the @fragment entry directly.`);
754
+ } else if (inner.type === 'workgroup_size_attr') {
755
+ const sizes = namedChildrenByType(inner, 'expression').map(e => parseInt(e.text.trim()) | 0);
756
+ for (let i = 0; i < sizes.length && i < 3; i++) {
757
+ workgroup_size[i] = sizes[i];
758
+ }
759
+ }
760
+ }
761
+
762
+ if (stage !== null) {
763
+ if (c.entry !== null) {
764
+ throw new Error(`WGSL compiler: multiple @${stage} entry functions found ('${c.entry.name}' and '${name}')`);
765
+ }
766
+ c.entry = { name, params, stage, workgroup_size };
767
+ }
768
+ }
769
+
770
+ /**
771
+ * Pull the `@builtin(name)` attribute name from a function param node, if present.
772
+ */
773
+ function extractBuiltin(param_node) {
774
+ for (const a of namedChildrenByType(param_node, 'attribute')) {
775
+ const inner = a.namedChildren[0];
776
+ if (inner.type === 'builtin_attr') {
777
+ const name_node = namedChildByType(inner, 'builtin_value_name');
778
+ if (name_node !== null) return name_node.text;
779
+ }
780
+ }
781
+ return null;
782
+ }
783
+
784
+ /**
785
+ * @param {Compiler} c
786
+ * @param {Node} node
787
+ */
788
+ function emitFunction(c, node) {
789
+ const header = namedChildByType(node, 'function_header');
790
+ const ident = namedChildByType(header, 'ident');
791
+ const name = ident.text;
792
+ const info = c.functions.get(name);
793
+
794
+ const param_list_node = namedChildByType(header, 'param_list');
795
+ const param_nodes = param_list_node !== null ? namedChildrenByType(param_list_node, 'param') : [];
796
+
797
+ const param_names = param_nodes.map(p => namedChildByType(p, 'ident').text);
798
+
799
+ c.out.add(`function ${name}(${param_names.join(', ')}) {`);
800
+ c.out.indent();
801
+
802
+ c.scope = c.scope.push();
803
+ for (let i = 0; i < param_nodes.length; i++) {
804
+ c.scope.declare(param_names[i], info.params[i].type);
805
+ }
806
+
807
+ const body = namedChildByType(node, 'compound_statement');
808
+ emitCompoundStatementBody(c, body);
809
+
810
+ c.scope = c.scope.parent;
811
+
812
+ c.out.dedent();
813
+ c.out.add('}');
814
+ }
815
+
816
+ // =========================================================================
817
+ // Globals — bindings, workgroup vars, private/const
818
+ // =========================================================================
819
+
820
+ /**
821
+ * Inspect a `global_variable_decl` and record its metadata.
822
+ * @param {Compiler} c
823
+ * @param {Node} node
824
+ */
825
+ function collectGlobalVariable(c, node) {
826
+ const var_decl = namedChildByType(node, 'variable_decl');
827
+ if (var_decl === null) {
828
+ collectGlobalValue(c, node);
829
+ return;
830
+ }
831
+
832
+ const template = namedChildByType(var_decl, 'template_list');
833
+ const opt_ident = namedChildByType(var_decl, 'optionally_typed_ident');
834
+ const var_ident = namedChildByType(opt_ident, 'ident');
835
+ const type_spec = namedChildByType(opt_ident, 'type_specifier');
836
+ const name = var_ident.text;
837
+ const type = parseTypeSpecifier(type_spec);
838
+
839
+ let address_space = null;
840
+ let access_mode = null;
841
+ if (template !== null) {
842
+ const args = extractTemplateArgs(template);
843
+ address_space = args[0] || null;
844
+ access_mode = args[1] || null;
845
+ }
846
+
847
+ let group = null;
848
+ let binding = null;
849
+ for (const a of namedChildrenByType(node, 'attribute')) {
850
+ const inner = a.namedChildren[0];
851
+ if (inner.type === 'group_attr') {
852
+ const expr = namedChildByType(inner, 'expression');
853
+ group = parseInt(expr.text.trim());
854
+ } else if (inner.type === 'binding_attr') {
855
+ const expr = namedChildByType(inner, 'expression');
856
+ binding = parseInt(expr.text.trim());
857
+ }
858
+ }
859
+
860
+ c.scope.declare(name, type);
861
+
862
+ if (group !== null && binding !== null) {
863
+ c.globalBindings.push({
864
+ name, type, group, binding, addressSpace: address_space, accessMode: access_mode,
865
+ });
866
+ return;
867
+ }
868
+
869
+ if (address_space === 'workgroup') {
870
+ c.workgroupVars.push({ name, type });
871
+ return;
872
+ }
873
+
874
+ // Private / module-scope `var<private>`. If the WGSL has an explicit initializer
875
+ // (e.g. `var<private> rnd_state: u32 = 2891336453u`), use it; otherwise fall back
876
+ // to the type's default value.
877
+ const initializer_node = namedChildByType(node, 'expression');
878
+ const init_code = initializer_node !== null
879
+ ? compileExpression(c, initializer_node).code
880
+ : defaultValueForType(type);
881
+ c.privateGlobals.push({
882
+ name,
883
+ kind: 'let',
884
+ code: `let ${name} = ${init_code};`,
885
+ });
886
+ }
887
+
888
+ /**
889
+ * Inspect a `global_value_decl` (`const x = …;` / `let x = …;` at module scope) and record it.
890
+ * @param {Compiler} c
891
+ * @param {Node} node
892
+ */
893
+ function collectGlobalValue(c, node) {
894
+ let ident = null;
895
+ let value_node = null;
896
+ let type_node = null;
897
+ for (const child of node.namedChildren) {
898
+ if (child.type === 'optionally_typed_ident') {
899
+ ident = namedChildByType(child, 'ident');
900
+ type_node = namedChildByType(child, 'type_specifier');
901
+ } else if (child.type === 'expression') {
902
+ value_node = child;
903
+ }
904
+ }
905
+ if (ident === null || value_node === null) return;
906
+ const name = ident.text;
907
+ let type = type_node !== null ? parseTypeSpecifier(type_node) : T_UNKNOWN;
908
+
909
+ const expr = compileExpression(c, value_node);
910
+
911
+ // Infer the type from the RHS when there's no explicit annotation (mirrors the local
912
+ // `let`/`var` path). Without this a `const M = mat3x3<f32>(...)` is left T_UNKNOWN, so a
913
+ // later `M * v` can't tell M is a matrix and emits a vector multiply → length mismatch.
914
+ if (type === T_UNKNOWN || type.kind === 'unknown') {
915
+ type = expr.type;
916
+ }
917
+
918
+ c.scope.declare(name, type);
919
+ c.privateGlobals.push({
920
+ name,
921
+ kind: 'const',
922
+ code: `const ${name} = ${expr.code};`,
923
+ });
924
+ }
925
+
926
+ // =========================================================================
927
+ // Workgroup state factory + entry wrapper
928
+ // =========================================================================
929
+
930
+ function emitWorkgroupStateFactory(c) {
931
+ c.out.add('function __create_workgroup_state__() {');
932
+ c.out.indent();
933
+ c.out.add('return {');
934
+ c.out.indent();
935
+ for (const wv of c.workgroupVars) {
936
+ c.out.add(`${wv.name}: ${defaultValueForType(wv.type)},`);
937
+ }
938
+ c.out.dedent();
939
+ c.out.add('};');
940
+ c.out.dedent();
941
+ c.out.add('}');
942
+ }
943
+
944
+ function emitEntryWrapper(c) {
945
+ const param_names = c.entry.params.map(p => p.name);
946
+ const is_compute = c.entry.stage === 'compute';
947
+ // Compute entries take a per-dispatch workgroup-state object; fragment entries don't have
948
+ // var<workgroup> at all, so the wrapper signature drops it.
949
+ const fixed_args = is_compute ? '__bindings__, __workgroup__' : '__bindings__';
950
+ const all_args = param_names.length > 0 ? `${fixed_args}, ${param_names.join(', ')}` : fixed_args;
951
+
952
+ c.out.add(`function __entry__(${all_args}) {`);
953
+ c.out.indent();
954
+
955
+ // Refresh each module-scope binding/workgroup slot before invoking the user's main.
956
+ // User functions read these via their enclosing closure, so this assignment makes the
957
+ // current dispatch's bindings visible to every helper they may call.
958
+ for (const b of c.globalBindings) {
959
+ c.out.add(`${b.name} = __bindings__.${b.name};`);
960
+ }
961
+ if (is_compute) {
962
+ for (const wv of c.workgroupVars) {
963
+ c.out.add(`${wv.name} = __workgroup__.${wv.name};`);
964
+ }
965
+ }
966
+
967
+ c.out.add(`return ${c.entry.name}(${param_names.join(', ')});`);
968
+
969
+ c.out.dedent();
970
+ c.out.add('}');
971
+ }
972
+
973
+ // =========================================================================
974
+ // Statements
975
+ // =========================================================================
976
+
977
+ /**
978
+ * Emit only the inside of a compound_statement (no `{` / `}`).
979
+ * @param {Compiler} c
980
+ * @param {Node} node — `compound_statement`
981
+ */
982
+ function emitCompoundStatementBody(c, node) {
983
+ for (const child of node.namedChildren) {
984
+ if (child.type === 'statement') {
985
+ emitStatement(c, child);
986
+ }
987
+ }
988
+ }
989
+
990
+ function emitCompoundStatement(c, node) {
991
+ c.out.add('{');
992
+ c.out.indent();
993
+ c.scope = c.scope.push();
994
+ emitCompoundStatementBody(c, node);
995
+ c.scope = c.scope.parent;
996
+ c.out.dedent();
997
+ c.out.add('}');
998
+ }
999
+
1000
+ /**
1001
+ * Emit a single `statement` node (its child is the actual statement kind).
1002
+ * @param {Compiler} c
1003
+ * @param {Node} node
1004
+ */
1005
+ function emitStatement(c, node) {
1006
+ const inner = node.namedChildren[0];
1007
+ switch (inner.type) {
1008
+ case 'variable_or_value_statement':
1009
+ emitVariableOrValueStatement(c, inner);
1010
+ break;
1011
+ case 'variable_updating_statement':
1012
+ emitVariableUpdatingStatement(c, inner);
1013
+ break;
1014
+ case 'return_statement':
1015
+ emitReturnStatement(c, inner);
1016
+ break;
1017
+ case 'if_statement':
1018
+ emitIfStatement(c, inner);
1019
+ break;
1020
+ case 'for_statement':
1021
+ emitForStatement(c, inner);
1022
+ break;
1023
+ case 'while_statement':
1024
+ emitWhileStatement(c, inner);
1025
+ break;
1026
+ case 'loop_statement':
1027
+ emitLoopStatement(c, inner);
1028
+ break;
1029
+ case 'switch_statement':
1030
+ emitSwitchStatement(c, inner);
1031
+ break;
1032
+ case 'break_statement':
1033
+ c.out.add('break;');
1034
+ break;
1035
+ case 'continue_statement':
1036
+ // Inside a `loop` with a `continuing` block, `continue` must run the
1037
+ // continuing block: it is emitted as a break out of the labeled body block
1038
+ // (the continuing code follows that block inside the same `while`).
1039
+ if (c.continue_label !== null) {
1040
+ c.out.add(`break ${c.continue_label};`);
1041
+ } else {
1042
+ c.out.add('continue;');
1043
+ }
1044
+ break;
1045
+ case 'discard_statement':
1046
+ c.out.add('return;');
1047
+ break;
1048
+ case 'compound_statement':
1049
+ emitCompoundStatement(c, inner);
1050
+ break;
1051
+ case 'func_call_statement':
1052
+ emitFunctionCallStatement(c, inner);
1053
+ break;
1054
+ case 'const_assert_statement':
1055
+ // Skipped at runtime.
1056
+ break;
1057
+ default:
1058
+ c.error(inner, `unsupported statement: ${inner.type}`);
1059
+ }
1060
+ }
1061
+
1062
+ /**
1063
+ * `let x = expr;` or `var x: T = expr;` or `var x: T;`
1064
+ */
1065
+ function emitVariableOrValueStatement(c, node) {
1066
+ const var_decl = namedChildByType(node, 'variable_decl');
1067
+ const opt_ident_direct = namedChildByType(node, 'optionally_typed_ident');
1068
+ const expr_node = namedChildByType(node, 'expression');
1069
+
1070
+ /** @type {'let' | 'const'} */
1071
+ let kind = 'let';
1072
+ let opt_ident = null;
1073
+ let template = null;
1074
+
1075
+ if (var_decl !== null) {
1076
+ // WGSL `var` is the mutable form → JS `let`.
1077
+ kind = 'let';
1078
+ opt_ident = namedChildByType(var_decl, 'optionally_typed_ident');
1079
+ template = namedChildByType(var_decl, 'template_list');
1080
+ } else {
1081
+ // WGSL `let` and `const` are both immutable bindings → JS `const`.
1082
+ // Tree-sitter exposes the keyword as an unnamed leading token; read it from the source.
1083
+ kind = 'const';
1084
+ opt_ident = opt_ident_direct;
1085
+ }
1086
+
1087
+ const ident = namedChildByType(opt_ident, 'ident');
1088
+ const type_spec = namedChildByType(opt_ident, 'type_specifier');
1089
+ const name = ident.text;
1090
+ let type = type_spec !== null ? parseTypeSpecifier(type_spec) : T_UNKNOWN;
1091
+
1092
+ if (expr_node !== null) {
1093
+ const expr = compileExpression(c, expr_node);
1094
+ // Infer the type from the RHS when no explicit annotation is present.
1095
+ if (type === T_UNKNOWN || type.kind === 'unknown') {
1096
+ type = expr.type;
1097
+ }
1098
+ c.scope.declare(name, type);
1099
+ c.out.add(`${kind} ${name} = ${expr.code};`);
1100
+ } else {
1101
+ c.scope.declare(name, type);
1102
+ c.out.add(`${kind} ${name} = ${defaultValueForType(type)};`);
1103
+ }
1104
+ }
1105
+
1106
+ /**
1107
+ * Assignment / compound assignment / increment / decrement.
1108
+ */
1109
+ function emitVariableUpdatingStatement(c, node) {
1110
+ const inner = node.namedChildren[0];
1111
+ if (inner.type === 'assignment_statement') {
1112
+ const lhs = namedChildByType(inner, 'lhs_expression');
1113
+ const expr = namedChildByType(inner, 'expression');
1114
+
1115
+ // The compound assignment operator sits between the LHS and the RHS as an unnamed token.
1116
+ const op_text = textBetween(inner, lhs, expr).trim();
1117
+
1118
+ const lhs_compiled = compileLhs(c, lhs);
1119
+ const rhs_compiled = compileExpression(c, expr);
1120
+
1121
+ if (op_text === '=' || op_text === '') {
1122
+ c.out.add(emitWriteStatement(lhs_compiled, rhs_compiled.code));
1123
+ return;
1124
+ }
1125
+
1126
+ const arith = {
1127
+ '+=': 'add', '-=': 'sub', '*=': 'mul', '/=': 'div', '%=': 'rem',
1128
+ };
1129
+ if (arith.hasOwnProperty(op_text)) {
1130
+ c.out.add(emitWriteStatement(lhs_compiled, `wgsl.${arith[op_text]}(${lhs_compiled.code}, ${rhs_compiled.code})`));
1131
+ return;
1132
+ }
1133
+ c.out.add(`${lhs_compiled.code} ${op_text} ${rhs_compiled.code};`);
1134
+ return;
1135
+ }
1136
+ if (inner.type === 'increment_statement') {
1137
+ const lhs = namedChildByType(inner, 'lhs_expression');
1138
+ const lhs_compiled = compileLhs(c, lhs);
1139
+ c.out.add(emitWriteStatement(lhs_compiled, `wgsl.add(${lhs_compiled.code}, 1)`));
1140
+ return;
1141
+ }
1142
+ if (inner.type === 'decrement_statement') {
1143
+ const lhs = namedChildByType(inner, 'lhs_expression');
1144
+ const lhs_compiled = compileLhs(c, lhs);
1145
+ c.out.add(emitWriteStatement(lhs_compiled, `wgsl.sub(${lhs_compiled.code}, 1)`));
1146
+ return;
1147
+ }
1148
+ c.error(inner, `unsupported variable_updating_statement: ${inner.type}`);
1149
+ }
1150
+
1151
+ /**
1152
+ * Emit a write to an LHS. Most LHS expressions are plain JS LValues (`x`, `arr[i]`, `obj.f`) and
1153
+ * are written with `=`. Mat-column LHS — `m[i]` for a mat — is special: the compiler emits it as
1154
+ * `wgsl.mat_col(m, i, rows)`, which is a typed-array view (a function call result, not assignable).
1155
+ * To write through the view we use `Float32Array.prototype.set(rhs)` instead, which copies into
1156
+ * the underlying matrix storage in place.
1157
+ *
1158
+ * @param {{ code: string }} lhs_compiled
1159
+ * @param {string} rhs_code
1160
+ * @returns {string}
1161
+ */
1162
+ function emitWriteStatement(lhs_compiled, rhs_code) {
1163
+ // Only the bare `wgsl.mat_col(...)` form (no chained `[i]` / `.x`) needs `.set()` — that's a
1164
+ // typed-array view, an unassignable function-call result. Anything chained off it (e.g.
1165
+ // `wgsl.mat_col(...)[3]` or `wgsl.mat_col(...).x` becoming `[i]`) is a scalar slot and must
1166
+ // use plain `=`.
1167
+ if (lhs_compiled.code.startsWith('wgsl.mat_col(') && lhs_compiled.code.endsWith(')')) {
1168
+ return `${lhs_compiled.code}.set(${rhs_code});`;
1169
+ }
1170
+ // `*p = v` has to reach the caller's object; assigning would only rebind the parameter.
1171
+ if (lhs_compiled.through_pointer === true) {
1172
+ return `${lhs_compiled.code} = wgsl.store_ptr(${lhs_compiled.code}, ${rhs_code});`;
1173
+ }
1174
+ return `${lhs_compiled.code} = ${rhs_code};`;
1175
+ }
1176
+
1177
+ function emitReturnStatement(c, node) {
1178
+ const expr = namedChildByType(node, 'expression');
1179
+ if (expr === null) {
1180
+ c.out.add('return;');
1181
+ } else {
1182
+ const e = compileExpression(c, expr);
1183
+ c.out.add(`return ${e.code};`);
1184
+ }
1185
+ }
1186
+
1187
+ function emitIfStatement(c, node) {
1188
+ const if_clause = namedChildByType(node, 'if_clause');
1189
+ const cond = namedChildByType(if_clause, 'expression');
1190
+ const if_body = namedChildByType(if_clause, 'compound_statement');
1191
+
1192
+ const cond_compiled = compileExpression(c, cond);
1193
+ c.out.add(`if (${cond_compiled.code}) {`);
1194
+ c.out.indent();
1195
+ c.scope = c.scope.push();
1196
+ emitCompoundStatementBody(c, if_body);
1197
+ c.scope = c.scope.parent;
1198
+ c.out.dedent();
1199
+ c.out.add('}');
1200
+
1201
+ // else-if and else clauses.
1202
+ for (const child of node.namedChildren) {
1203
+ if (child.type === 'else_if_clause') {
1204
+ const eif_cond = namedChildByType(child, 'expression');
1205
+ const eif_body = namedChildByType(child, 'compound_statement');
1206
+ const eif_compiled = compileExpression(c, eif_cond);
1207
+ c.out.add(`else if (${eif_compiled.code}) {`);
1208
+ c.out.indent();
1209
+ c.scope = c.scope.push();
1210
+ emitCompoundStatementBody(c, eif_body);
1211
+ c.scope = c.scope.parent;
1212
+ c.out.dedent();
1213
+ c.out.add('}');
1214
+ } else if (child.type === 'else_clause') {
1215
+ const else_body = namedChildByType(child, 'compound_statement');
1216
+ // else_body may instead be an if_statement (else-if shorthand).
1217
+ const nested_if = namedChildByType(child, 'if_statement');
1218
+ if (else_body !== null) {
1219
+ c.out.add('else {');
1220
+ c.out.indent();
1221
+ c.scope = c.scope.push();
1222
+ emitCompoundStatementBody(c, else_body);
1223
+ c.scope = c.scope.parent;
1224
+ c.out.dedent();
1225
+ c.out.add('}');
1226
+ } else if (nested_if !== null) {
1227
+ c.out.add('else');
1228
+ emitIfStatement(c, nested_if);
1229
+ }
1230
+ }
1231
+ }
1232
+ }
1233
+
1234
+ function emitForStatement(c, node) {
1235
+ const for_header = namedChildByType(node, 'for_header');
1236
+ const for_init = namedChildByType(for_header, 'for_init');
1237
+ const cond_expr = namedChildByType(for_header, 'expression');
1238
+ const for_update = namedChildByType(for_header, 'for_update');
1239
+ const body = namedChildByType(node, 'compound_statement');
1240
+
1241
+ c.scope = c.scope.push();
1242
+
1243
+ // Init / cond / update parts.
1244
+ let init_str = '';
1245
+ if (for_init !== null) {
1246
+ // Tree-sitter wraps it in a variable_or_value_statement; capture the emission as text.
1247
+ const sub = new Compiler();
1248
+ sub.scope = c.scope;
1249
+ sub.structs = c.structs;
1250
+ sub.functions = c.functions;
1251
+ const inner = for_init.namedChildren[0];
1252
+ emitForInitInline(sub, inner);
1253
+ init_str = sub.out.build().trimEnd();
1254
+ if (init_str.endsWith(';')) init_str = init_str.slice(0, -1);
1255
+ }
1256
+
1257
+ const cond_str = cond_expr !== null ? compileExpression(c, cond_expr).code : '';
1258
+
1259
+ let update_str = '';
1260
+ if (for_update !== null) {
1261
+ const inner = for_update.namedChildren[0];
1262
+ update_str = emitForUpdateInline(c, inner);
1263
+ }
1264
+
1265
+ c.out.add(`for (${init_str}; ${cond_str}; ${update_str}) {`);
1266
+ c.out.indent();
1267
+ // `continue` inside this loop targets this loop, not an outer `loop`'s continuing block
1268
+ const saved_continue_label = c.continue_label;
1269
+ c.continue_label = null;
1270
+ emitCompoundStatementBody(c, body);
1271
+ c.continue_label = saved_continue_label;
1272
+ c.out.dedent();
1273
+ c.out.add('}');
1274
+
1275
+ c.scope = c.scope.parent;
1276
+ }
1277
+
1278
+ /**
1279
+ * Emit a `variable_or_value_statement` inline (as a single line with no trailing newline structure).
1280
+ * Used by `for(init; ...)`.
1281
+ */
1282
+ function emitForInitInline(c, node) {
1283
+ if (node.type === 'variable_or_value_statement') {
1284
+ const var_decl = node.namedChildren[0] && node.namedChildren[0].type === 'variable_decl'
1285
+ ? node.namedChildren[0]
1286
+ : null;
1287
+ const opt_ident = namedChildByType(var_decl !== null ? var_decl : node, 'optionally_typed_ident');
1288
+ const expr_node = namedChildByType(node, 'expression');
1289
+ const ident = namedChildByType(opt_ident, 'ident');
1290
+ const type_spec = namedChildByType(opt_ident, 'type_specifier');
1291
+ const name = ident.text;
1292
+ const type = type_spec !== null ? parseTypeSpecifier(type_spec) : T_UNKNOWN;
1293
+ c.scope.declare(name, type);
1294
+ const value = expr_node !== null ? compileExpression(c, expr_node).code : defaultValueForType(type);
1295
+ // `var i = …` (mutable, common in `for (var i = 0; ...; i = i + 1)`) → JS `let`.
1296
+ // `let i = …` / `const i = …` (immutable) → JS `const`.
1297
+ const kind = var_decl !== null ? 'let' : 'const';
1298
+ c.out.add(`${kind} ${name} = ${value};`);
1299
+ return;
1300
+ }
1301
+ if (node.type === 'variable_updating_statement') {
1302
+ emitVariableUpdatingStatement(c, node);
1303
+ return;
1304
+ }
1305
+ if (node.type === 'func_call_statement') {
1306
+ emitFunctionCallStatement(c, node);
1307
+ return;
1308
+ }
1309
+ }
1310
+
1311
+ /**
1312
+ * @returns {string} JS update expression (no semicolon).
1313
+ */
1314
+ function emitForUpdateInline(c, node) {
1315
+ if (node.type === 'variable_updating_statement') {
1316
+ const inner = node.namedChildren[0];
1317
+ if (inner.type === 'assignment_statement') {
1318
+ const lhs = namedChildByType(inner, 'lhs_expression');
1319
+ const expr = namedChildByType(inner, 'expression');
1320
+ const op_text = textBetween(inner, lhs, expr).trim();
1321
+ const lhs_compiled = compileLhs(c, lhs);
1322
+ const rhs_compiled = compileExpression(c, expr);
1323
+ if (op_text === '=' || op_text === '') {
1324
+ if (lhs_compiled.through_pointer === true) {
1325
+ return `${lhs_compiled.code} = wgsl.store_ptr(${lhs_compiled.code}, ${rhs_compiled.code})`;
1326
+ }
1327
+ return `${lhs_compiled.code} = ${rhs_compiled.code}`;
1328
+ }
1329
+ const arith = { '+=': 'add', '-=': 'sub', '*=': 'mul', '/=': 'div', '%=': 'rem' };
1330
+ if (arith.hasOwnProperty(op_text)) {
1331
+ if (lhs_compiled.through_pointer === true) {
1332
+ return `${lhs_compiled.code} = wgsl.store_ptr(${lhs_compiled.code}, wgsl.${arith[op_text]}(${lhs_compiled.code}, ${rhs_compiled.code}))`;
1333
+ }
1334
+ return `${lhs_compiled.code} = wgsl.${arith[op_text]}(${lhs_compiled.code}, ${rhs_compiled.code})`;
1335
+ }
1336
+ return `${lhs_compiled.code} ${op_text} ${rhs_compiled.code}`;
1337
+ }
1338
+ if (inner.type === 'increment_statement') {
1339
+ const lhs = namedChildByType(inner, 'lhs_expression');
1340
+ const lhs_compiled = compileLhs(c, lhs);
1341
+ return `${lhs_compiled.code} = wgsl.add(${lhs_compiled.code}, 1)`;
1342
+ }
1343
+ if (inner.type === 'decrement_statement') {
1344
+ const lhs = namedChildByType(inner, 'lhs_expression');
1345
+ const lhs_compiled = compileLhs(c, lhs);
1346
+ return `${lhs_compiled.code} = wgsl.sub(${lhs_compiled.code}, 1)`;
1347
+ }
1348
+ }
1349
+ return '';
1350
+ }
1351
+
1352
+ function emitWhileStatement(c, node) {
1353
+ const cond = namedChildByType(node, 'expression');
1354
+ const body = namedChildByType(node, 'compound_statement');
1355
+ const cc = compileExpression(c, cond);
1356
+ c.out.add(`while (${cc.code}) {`);
1357
+ c.out.indent();
1358
+ c.scope = c.scope.push();
1359
+ // `continue` inside this loop targets this loop, not an outer `loop`'s continuing block
1360
+ const saved_continue_label = c.continue_label;
1361
+ c.continue_label = null;
1362
+ emitCompoundStatementBody(c, body);
1363
+ c.continue_label = saved_continue_label;
1364
+ c.scope = c.scope.parent;
1365
+ c.out.dedent();
1366
+ c.out.add('}');
1367
+ }
1368
+
1369
+ function emitLoopStatement(c, node) {
1370
+ // NOTE: the grammar puts the loop's `statement` children and the optional
1371
+ // `continuing_statement` directly on the `loop_statement` node (no
1372
+ // `compound_statement` wrapper); emitCompoundStatementBody visits only the
1373
+ // `statement` children, which is exactly the loop body.
1374
+ const body = node;
1375
+ const continuing = namedChildByType(node, 'continuing_statement');
1376
+
1377
+ const saved_continue_label = c.continue_label;
1378
+
1379
+ c.out.add('while (true) {');
1380
+ c.out.indent();
1381
+ c.scope = c.scope.push();
1382
+
1383
+ if (continuing === null) {
1384
+
1385
+ // no continuing block: JS `continue` already has the right semantics
1386
+ c.continue_label = null;
1387
+ emitCompoundStatementBody(c, body);
1388
+
1389
+ } else {
1390
+
1391
+ // WGSL `continue` transfers control INTO the `continuing` block. Emit the loop
1392
+ // body inside a labeled block: `continue` becomes `break <label>`, which lands on
1393
+ // the continuing code that follows the block (still inside the `while`).
1394
+ const label = `loop_body_${c.loop_label_counter++}`;
1395
+ c.continue_label = label;
1396
+
1397
+ c.out.add(`${label}: {`);
1398
+ c.out.indent();
1399
+ emitCompoundStatementBody(c, body);
1400
+ c.out.dedent();
1401
+ c.out.add('}');
1402
+
1403
+ c.continue_label = null;
1404
+
1405
+ const cont_body = namedChildByType(continuing, 'continuing_compound_statement');
1406
+ if (cont_body !== null) {
1407
+ emitCompoundStatementBody(c, cont_body);
1408
+
1409
+ // `break if expr;` — only legal as the trailing statement of a continuing block.
1410
+ // It is a direct named child of the continuing compound (not wrapped in `statement`),
1411
+ // so emitCompoundStatementBody does not visit it.
1412
+ const break_if = namedChildByType(cont_body, 'break_if_statement');
1413
+ if (break_if !== null) {
1414
+ const break_expr = namedChildByType(break_if, 'expression');
1415
+ const bc = compileExpression(c, break_expr);
1416
+ c.out.add(`if (${bc.code}) { break; }`);
1417
+ }
1418
+ }
1419
+
1420
+ }
1421
+
1422
+ c.continue_label = saved_continue_label;
1423
+ c.scope = c.scope.parent;
1424
+ c.out.dedent();
1425
+ c.out.add('}');
1426
+ }
1427
+
1428
+ /**
1429
+ * Emit a WGSL `switch`.
1430
+ *
1431
+ * Grammar (tree-sitter-wgsl): `switch_statement { expression, switch_body { switch_clause* } }`,
1432
+ * where each `switch_clause` wraps either a `case_clause { case_selectors { case_selector+ },
1433
+ * compound_statement }` or a `default_alone_clause { compound_statement }`. A `case_selector` holds
1434
+ * either an `expression` (a case value) or the `default` keyword.
1435
+ *
1436
+ * WGSL switch has no fall-through — each selected clause runs its body and stops — so we terminate
1437
+ * every clause body with `break;`. Multiple selectors on one clause become stacked JS `case` labels
1438
+ * over a shared body.
1439
+ */
1440
+ function emitSwitchStatement(c, node) {
1441
+ const expr = namedChildByType(node, 'expression');
1442
+ const ec = compileExpression(c, expr);
1443
+ c.out.add(`switch (${ec.code}) {`);
1444
+ c.out.indent();
1445
+
1446
+ const body = namedChildByType(node, 'switch_body');
1447
+ if (body) {
1448
+ for (const clause of namedChildrenByType(body, 'switch_clause')) {
1449
+ const case_clause = namedChildByType(clause, 'case_clause');
1450
+ const default_clause = namedChildByType(clause, 'default_alone_clause');
1451
+ const inner = case_clause ?? default_clause;
1452
+ if (!inner) {
1453
+ continue;
1454
+ }
1455
+
1456
+ if (case_clause) {
1457
+ const case_selectors = namedChildByType(case_clause, 'case_selectors');
1458
+ if (case_selectors) {
1459
+ for (const sel of namedChildrenByType(case_selectors, 'case_selector')) {
1460
+ const sel_expr = namedChildByType(sel, 'expression');
1461
+ if (sel_expr) {
1462
+ c.out.add(`case ${compileExpression(c, sel_expr).code}:`);
1463
+ } else {
1464
+ // a bare `default` used within a case_selectors list
1465
+ c.out.add('default:');
1466
+ }
1467
+ }
1468
+ }
1469
+ } else {
1470
+ c.out.add('default:');
1471
+ }
1472
+
1473
+ // Wrap each clause body in its own JS block. WGSL case clauses are independent scopes,
1474
+ // but JS switch clauses share the switch's block scope — without the braces, `let`
1475
+ // temporaries declared in two clauses collide ("Identifier already declared").
1476
+ const case_body = namedChildByType(inner, 'compound_statement');
1477
+ c.out.add('{');
1478
+ c.out.indent();
1479
+ c.scope = c.scope.push();
1480
+ if (case_body) {
1481
+ emitCompoundStatementBody(c, case_body);
1482
+ }
1483
+ c.out.add('break;');
1484
+ c.scope = c.scope.parent;
1485
+ c.out.dedent();
1486
+ c.out.add('}');
1487
+ }
1488
+ }
1489
+
1490
+ c.out.dedent();
1491
+ c.out.add('}');
1492
+ }
1493
+
1494
+ function emitFunctionCallStatement(c, node) {
1495
+ const call_phrase = namedChildByType(node, 'call_phrase');
1496
+ const r = compileCallPhraseTyped(c, call_phrase);
1497
+ c.out.add(`${r.code};`);
1498
+ }
1499
+
1500
+ // =========================================================================
1501
+ // Expressions
1502
+ // =========================================================================
1503
+
1504
+ /**
1505
+ * Compile an expression node (any wrapper or specific kind).
1506
+ * @param {Compiler} c
1507
+ * @param {Node} node
1508
+ * @returns {{ code: string, type: WgslType }}
1509
+ */
1510
+ function compileExpression(c, node) {
1511
+ const u = unwrapExpression(node);
1512
+
1513
+ // Binary operations
1514
+ switch (u.type) {
1515
+ case 'expression':
1516
+ // tree-sitter-wgsl can leave `&&` / `||` directly under an `expression` node — i.e.
1517
+ // the operator wraps two operands without a more specific short_circuit_*_expression
1518
+ // wrapper around it. With 2 named children the unnamed operator sits between them.
1519
+ return compileBinaryExpression(c, u, '__shortcircuit_unused__', 'raw');
1520
+ case 'relational_expression':
1521
+ return compileBinaryExpression(c, u, 'relational_operator', 'raw');
1522
+ case 'shift_expression':
1523
+ return compileBinaryExpression(c, u, 'shift_operator', 'raw');
1524
+ case 'additive_expression':
1525
+ return compileBinaryExpression(c, u, 'additive_operator', 'arith');
1526
+ case 'multiplicative_expression':
1527
+ return compileBinaryExpression(c, u, 'multiplicative_operator', 'arith');
1528
+ case 'short_circuit_and_expression':
1529
+ case 'short_circuit_or_expression':
1530
+ return compileBinaryExpression(c, u, '__shortcircuit_unused__', 'raw');
1531
+ case 'bitwise_expression':
1532
+ // Top-level WGSL `^` / `&` / `|` operator. The operator is an unnamed token between
1533
+ // the two named operands; compileBinaryExpression's 2-named-children path picks it up.
1534
+ return compileBinaryExpression(c, u, '__bitwise_unused__', 'raw');
1535
+ case 'binary_and_expression':
1536
+ case 'binary_or_expression':
1537
+ case 'binary_xor_expression':
1538
+ // Inner chain nodes from the WGSL grammar (e.g. `a ^ b ^ c` nests as
1539
+ // `binary_xor_expression(binary_xor_expression(a, b), c)`). When 2 named children
1540
+ // appear, the unnamed operator sits between them; the 1-child case is a passthrough.
1541
+ return compileBinaryExpression(c, u, '__bitwise_unused__', 'raw');
1542
+ }
1543
+
1544
+ // Unary
1545
+ if (u.type === 'unary_expression') {
1546
+ return compileUnaryExpression(c, u);
1547
+ }
1548
+
1549
+ // Singular (primary + chained component access)
1550
+ if (u.type === 'singular_expression') {
1551
+ return compileSingularExpression(c, u);
1552
+ }
1553
+
1554
+ // Primary subkinds
1555
+ if (u.type === 'literal' || u.type === 'int_literal' || u.type === 'float_literal' || u.type === 'bool_literal') {
1556
+ return { code: literalToJs(u.text), type: literalType(u.text) };
1557
+ }
1558
+ if (u.type === 'paren_expression') {
1559
+ const inner = namedChildByType(u, 'expression');
1560
+ const e = compileExpression(c, inner);
1561
+ return { code: `(${e.code})`, type: e.type };
1562
+ }
1563
+ if (u.type === 'call_expression') {
1564
+ const call_phrase = namedChildByType(u, 'call_phrase');
1565
+ return compileCallPhraseTyped(c, call_phrase);
1566
+ }
1567
+ if (u.type === 'template_elaborated_ident') {
1568
+ const ident = namedChildByType(u, 'ident');
1569
+ const name = ident !== null ? ident.text : u.text;
1570
+ return { code: name, type: c.scope.lookup(name) };
1571
+ }
1572
+ if (u.type === 'ident') {
1573
+ return { code: u.text, type: c.scope.lookup(u.text) };
1574
+ }
1575
+
1576
+ c.error(u, `unsupported expression: ${u.type}`);
1577
+ }
1578
+
1579
+ /**
1580
+ * Compile a binary expression node. Two grammar shapes are supported:
1581
+ * - 3 named children where the middle child has type `op_type` — used for additive / multiplicative
1582
+ * where the operator is an explicit named node (`additive_operator`, `multiplicative_operator`).
1583
+ * - 2 named children with an unnamed operator token between them — used for relational / shift,
1584
+ * where the operator (e.g. `<`, `>=`, `<<`) is a raw token.
1585
+ *
1586
+ * Mode:
1587
+ * - `'arith'` → use `wgsl.add/sub/mul/div/rem` (handles vectors)
1588
+ * - `'raw'` → emit raw JS operator
1589
+ */
1590
+ function compileBinaryExpression(c, node, op_type, mode) {
1591
+ const named = node.namedChildren;
1592
+ if (named.length === 1) {
1593
+ return compileExpression(c, named[0]);
1594
+ }
1595
+
1596
+ let op_text = null;
1597
+ let left_node;
1598
+ let right_node;
1599
+
1600
+ if (named.length === 3 && named[1].type === op_type) {
1601
+ op_text = named[1].text;
1602
+ left_node = named[0];
1603
+ right_node = named[2];
1604
+ } else if (named.length === 2) {
1605
+ left_node = named[0];
1606
+ right_node = named[1];
1607
+ // tree-sitter-web doesn't surface unnamed children in `node.children`. Extract the
1608
+ // operator by slicing the original text using the operands' source positions.
1609
+ op_text = textBetween(node, left_node, right_node).trim();
1610
+ } else {
1611
+ return { code: node.text, type: T_UNKNOWN };
1612
+ }
1613
+
1614
+ const left = compileExpression(c, left_node);
1615
+ const right = compileExpression(c, right_node);
1616
+
1617
+ if (mode === 'arith') {
1618
+ const helper = ARITHMETIC_OP[op_text];
1619
+ if (helper !== undefined) {
1620
+ // `*` on matrix/vector operands has WGSL-specific algebra (dot-products and
1621
+ // accumulation) that the element-wise `wgsl.mul` doesn't capture. Route to a
1622
+ // dedicated `mNxR_multiply_*` helper when at least one side is a matrix.
1623
+ if (op_text === '*') {
1624
+ const dispatched = dispatchMatrixMul(left, right);
1625
+ if (dispatched !== null) return dispatched;
1626
+ }
1627
+ // WGSL `/` on integer types is truncated integer division. Detect when both
1628
+ // operands' tracked types are integer (i32 / u32 scalars or matching int vecs)
1629
+ // and route through the integer-truncating helper; otherwise use float div.
1630
+ let actual_helper = helper;
1631
+ if (op_text === '/' && bothOperandsInteger(left.type, right.type)) {
1632
+ actual_helper = 'idiv';
1633
+ }
1634
+ return {
1635
+ code: `wgsl.${actual_helper}(${left.code}, ${right.code})`,
1636
+ type: inferBinaryArithResultType(left.type, right.type),
1637
+ };
1638
+ }
1639
+ }
1640
+
1641
+ // Bitwise (`&` / `|` / `^`) and shift (`<<` / `>>`) operators. Routed through element-wise
1642
+ // helpers for vec/mat operands (JS raw operators coerce typed arrays to NaN), AND for `>>`
1643
+ // on u32 scalars — JS's signed `>>` sign-extends, but WGSL u32 shift is logical.
1644
+ const bitwise_helper = BITWISE_HELPER[op_text];
1645
+ if (bitwise_helper !== undefined) {
1646
+ if (isContainerType(left.type) || isContainerType(right.type)) {
1647
+ return {
1648
+ code: `wgsl.${bitwise_helper}(${left.code}, ${right.code})`,
1649
+ type: inferRawBinaryResultType(left.type, right.type, op_text),
1650
+ };
1651
+ }
1652
+ // For `>>` specifically: any u32-typed scalar operand must use the unsigned shift helper.
1653
+ if (op_text === '>>' && (isScalarUnsigned(left.type) || isScalarUnsigned(right.type))) {
1654
+ return {
1655
+ code: `wgsl.shr(${left.code}, ${right.code})`,
1656
+ type: inferRawBinaryResultType(left.type, right.type, op_text),
1657
+ };
1658
+ }
1659
+ }
1660
+
1661
+ // Comparison operators. JS's raw `==` etc. compare TypedArrays by reference; for WGSL
1662
+ // `vec<T>` operands we need element-wise comparison returning a `vec<bool>`.
1663
+ const cmp_helper = COMPARISON_HELPER[op_text];
1664
+ if (cmp_helper !== undefined && (isContainerType(left.type) || isContainerType(right.type))) {
1665
+ return {
1666
+ code: `wgsl.${cmp_helper}(${left.code}, ${right.code})`,
1667
+ type: inferRawBinaryResultType(left.type, right.type, op_text),
1668
+ };
1669
+ }
1670
+
1671
+ return {
1672
+ code: `${left.code} ${op_text} ${right.code}`,
1673
+ type: inferRawBinaryResultType(left.type, right.type, op_text),
1674
+ };
1675
+ }
1676
+
1677
+ /** Map WGSL bitwise/shift operators to language-core element-wise helper names. */
1678
+ const BITWISE_HELPER = {
1679
+ '&': 'bit_and',
1680
+ '|': 'bit_or',
1681
+ '^': 'bit_xor',
1682
+ '<<': 'shl',
1683
+ '>>': 'shr',
1684
+ };
1685
+
1686
+ /** Map WGSL comparison operators to language-core element-wise helper names. */
1687
+ const COMPARISON_HELPER = {
1688
+ '==': 'eq',
1689
+ '!=': 'ne',
1690
+ '<': 'lt',
1691
+ '>': 'gt',
1692
+ '<=': 'le',
1693
+ '>=': 'ge',
1694
+ };
1695
+
1696
+ /** True if `t` is a vector or matrix — operand types that need element-wise dispatch. */
1697
+ function isContainerType(t) {
1698
+ return t && (t.kind === 'vec' || t.kind === 'mat');
1699
+ }
1700
+
1701
+ /** True if `t` is a u32 scalar — needed to route `>>` through the unsigned-shift helper. */
1702
+ function isScalarUnsigned(t) {
1703
+ return t && t.kind === 'primitive' && t.name === 'u32';
1704
+ }
1705
+
1706
+ /** True if both operands are integer-typed (scalar or vec of i32/u32). */
1707
+ function bothOperandsInteger(a, b) {
1708
+ const isInt = (t) => {
1709
+ if (!t) return false;
1710
+ if (t.kind === 'primitive') return t.name === 'i32' || t.name === 'u32';
1711
+ if (t.kind === 'vec' || t.kind === 'mat') return t.scalar === 'i32' || t.scalar === 'u32';
1712
+ return false;
1713
+ };
1714
+ return isInt(a) && isInt(b);
1715
+ }
1716
+
1717
+ /**
1718
+ * Dispatch a WGSL `*` expression to the appropriate matrix/vector helper in the language
1719
+ * core when at least one operand is a matrix. Returns `null` to indicate "fall through to the
1720
+ * element-wise multiply" — used for scalar×scalar, vec×scalar, vec×vec (Hadamard product),
1721
+ * matrix×scalar, etc., which `wgsl.mul` already handles correctly.
1722
+ *
1723
+ * Naming includes the scalar type suffix (`f` / `i` / `u` / `h`), e.g.
1724
+ * `m4x4f_multiply_v4f`, `v3f_multiply_m3x3f`, `m4x4f_multiply_m4x4f`. Unsupported
1725
+ * shapes (e.g. mat3x4 × vec3) produce a runtime `TypeError: wgsl.<name> is not a function`
1726
+ * — adding support is a single new function in WGSLLanguageCore.js.
1727
+ *
1728
+ * @param {{ code: string, type: WgslType }} left
1729
+ * @param {{ code: string, type: WgslType }} right
1730
+ * @returns {{ code: string, type: WgslType } | null}
1731
+ */
1732
+ function dispatchMatrixMul(left, right) {
1733
+ const lt = left.type;
1734
+ const rt = right.type;
1735
+ const isVec = (t) => t && t.kind === 'vec';
1736
+ const isMat = (t) => t && t.kind === 'mat';
1737
+
1738
+ // mat × mat → mat(b.cols, a.rows). Multiplication is defined only when a.cols == b.rows.
1739
+ if (isMat(lt) && isMat(rt)) {
1740
+ const lh = `m${lt.cols}x${lt.rows}${scalarTypeSuffix(lt.scalar)}`;
1741
+ const rh = `m${rt.cols}x${rt.rows}${scalarTypeSuffix(rt.scalar)}`;
1742
+ return {
1743
+ code: `wgsl.${lh}_multiply_${rh}(${left.code}, ${right.code})`,
1744
+ type: { kind: 'mat', cols: rt.cols, rows: lt.rows, scalar: lt.scalar },
1745
+ };
1746
+ }
1747
+
1748
+ // mat × vec → vec(rows). Vec must have length == cols.
1749
+ if (isMat(lt) && isVec(rt)) {
1750
+ const lh = `m${lt.cols}x${lt.rows}${scalarTypeSuffix(lt.scalar)}`;
1751
+ const rh = `v${rt.size}${scalarTypeSuffix(rt.scalar)}`;
1752
+ return {
1753
+ code: `wgsl.${lh}_multiply_${rh}(${left.code}, ${right.code})`,
1754
+ type: { kind: 'vec', size: lt.rows, scalar: lt.scalar },
1755
+ };
1756
+ }
1757
+
1758
+ // vec × mat → vec(cols). Vec acts as a row vector; vec.length must equal rows.
1759
+ if (isVec(lt) && isMat(rt)) {
1760
+ const lh = `v${lt.size}${scalarTypeSuffix(lt.scalar)}`;
1761
+ const rh = `m${rt.cols}x${rt.rows}${scalarTypeSuffix(rt.scalar)}`;
1762
+ return {
1763
+ code: `wgsl.${lh}_multiply_${rh}(${left.code}, ${right.code})`,
1764
+ type: { kind: 'vec', size: rt.cols, scalar: rt.scalar },
1765
+ };
1766
+ }
1767
+
1768
+ return null;
1769
+ }
1770
+
1771
+ /** Scalar type → suffix character used in matrix/vector helper names: f / i / u / h. */
1772
+ function scalarTypeSuffix(scalar) {
1773
+ return { f32: 'f', i32: 'i', u32: 'u', f16: 'h' }[scalar] || 'f';
1774
+ }
1775
+
1776
+ /**
1777
+ * Pick the result type of an element-wise arithmetic op given the operand types.
1778
+ * Vec wins over scalar; matrix wins over scalar; scalar otherwise.
1779
+ */
1780
+ function inferBinaryArithResultType(a, b) {
1781
+ const isVec = (t) => t && t.kind === 'vec';
1782
+ const isMat = (t) => t && t.kind === 'mat';
1783
+ const isPrim = (t) => t && t.kind === 'primitive';
1784
+
1785
+ if (isVec(a)) return a;
1786
+ if (isVec(b)) return b;
1787
+ if (isMat(a)) return a;
1788
+ if (isMat(b)) return b;
1789
+ if (isPrim(a)) return a;
1790
+ if (isPrim(b)) return b;
1791
+ return T_UNKNOWN;
1792
+ }
1793
+
1794
+ /**
1795
+ * Pick the result type of a non-arithmetic binary op (comparison / logical / bitwise / shift).
1796
+ * Comparisons return bool; bitwise and shift preserve the leftmost known integer operand type.
1797
+ */
1798
+ function inferRawBinaryResultType(a, b, op) {
1799
+ const COMPARISON = new Set(['==', '!=', '<', '>', '<=', '>=']);
1800
+ const LOGICAL = new Set(['&&', '||']);
1801
+ if (COMPARISON.has(op) || LOGICAL.has(op)) {
1802
+ return { kind: 'primitive', name: 'bool' };
1803
+ }
1804
+ // Bitwise / shift: propagate the first known integer-ish operand type. This lets `bitcast`
1805
+ // and other downstream consumers see u32/i32 even when the literal on one side is untyped.
1806
+ if (a && a.kind === 'primitive') return a;
1807
+ if (b && b.kind === 'primitive') return b;
1808
+ if (a && a.kind === 'vec') return a;
1809
+ if (b && b.kind === 'vec') return b;
1810
+ return T_UNKNOWN;
1811
+ }
1812
+
1813
+ function compileUnaryExpression(c, node) {
1814
+ const named = node.namedChildren;
1815
+ const prefix = textBeforeFirstNamed(node).trim();
1816
+
1817
+ if (prefix === '') {
1818
+ return compileExpression(c, named[0]);
1819
+ }
1820
+
1821
+ const operand = compileExpression(c, named[named.length - 1]);
1822
+
1823
+ if (prefix === '-') return { code: `wgsl.neg(${operand.code})`, type: operand.type };
1824
+ if (prefix === '!') return { code: `!${operand.code}`, type: operand.type };
1825
+ if (prefix === '~') return { code: `wgsl.not(${operand.code})`, type: operand.type };
1826
+ if (prefix === '*' || prefix === '&') return { code: operand.code, type: operand.type };
1827
+
1828
+ return { code: `${prefix}${operand.code}`, type: operand.type };
1829
+ }
1830
+
1831
+ function compileSingularExpression(c, node) {
1832
+ const named = node.namedChildren;
1833
+ const head = named[0];
1834
+ const head_result = head.type === 'primary_expression'
1835
+ ? compileExpression(c, unwrapExpression(head))
1836
+ : compileExpression(c, head);
1837
+
1838
+ let code = head_result.code;
1839
+ let type = head_result.type;
1840
+
1841
+ for (let i = 1; i < named.length; i++) {
1842
+ const spec = named[i];
1843
+ if (spec.type !== 'component_or_swizzle_specifier') break;
1844
+ const next = applyAccessSpecifier(c, spec, code, type);
1845
+ code = next.code;
1846
+ type = next.type;
1847
+ }
1848
+
1849
+ return { code, type };
1850
+ }
1851
+
1852
+ /**
1853
+ * Apply a `.member`, `.swizzle`, or `[index]` access to an expression that has already been
1854
+ * compiled to `lhs_code` of type `lhs_type`.
1855
+ */
1856
+ function applyAccessSpecifier(c, node, lhs_code, lhs_type) {
1857
+ // tree-sitter-wgsl groups chained access like `[i].x` (or `.foo[i]`) as a SINGLE outer
1858
+ // `component_or_swizzle_specifier` whose first named child is the immediate access (an
1859
+ // `expression` for `[]` or a `member_ident` for `.field`) and whose second named child
1860
+ // is a NESTED `component_or_swizzle_specifier` for the rest of the chain. We apply the
1861
+ // immediate access first, then recurse into any nested specifier.
1862
+ let code = lhs_code;
1863
+ let type = lhs_type;
1864
+
1865
+ const member_ident = namedChildByType(node, 'member_ident');
1866
+ if (member_ident !== null) {
1867
+ const r = applyMemberAccess(c, member_ident.text, code, type);
1868
+ code = r.code;
1869
+ type = r.type;
1870
+ } else {
1871
+ const expr = namedChildByType(node, 'expression');
1872
+ if (expr !== null) {
1873
+ const e = compileExpression(c, expr);
1874
+ // Matrices are stored as flat column-major typed arrays, so the bare `m[i]`
1875
+ // would index a scalar — route through `wgsl.mat_col` to get the column as a vec.
1876
+ if (type && type.kind === 'mat') {
1877
+ code = `wgsl.mat_col(${code}, ${e.code}, ${type.rows})`;
1878
+ type = { kind: 'vec', size: type.rows, scalar: type.scalar };
1879
+ } else {
1880
+ code = `${code}[${e.code}]`;
1881
+ if (type && type.kind === 'array') type = type.element;
1882
+ else if (type && type.kind === 'vec') type = { kind: 'primitive', name: type.scalar };
1883
+ else type = T_UNKNOWN;
1884
+ }
1885
+ }
1886
+ }
1887
+
1888
+ const nested = namedChildByType(node, 'component_or_swizzle_specifier');
1889
+ if (nested !== null) {
1890
+ const r = applyAccessSpecifier(c, nested, code, type);
1891
+ code = r.code;
1892
+ type = r.type;
1893
+ }
1894
+
1895
+ return { code, type };
1896
+ }
1897
+
1898
+ /** Apply a `.field` / `.swizzle` access to an already-compiled expression of `lhs_type`. */
1899
+ function applyMemberAccess(c, name, lhs_code, lhs_type) {
1900
+ if (lhs_type !== null && lhs_type !== undefined && lhs_type.kind === 'vec' && isSwizzlePattern(name)) {
1901
+ if (name.length === 1) {
1902
+ return {
1903
+ code: `${lhs_code}[${SWIZZLE_INDEX[name]}]`,
1904
+ type: { kind: 'primitive', name: lhs_type.scalar },
1905
+ };
1906
+ }
1907
+ const new_size = name.length;
1908
+ const ctor = vecAliasName(new_size, lhs_type.scalar);
1909
+ const elements = Array.from(name).map(ch => `${lhs_code}[${SWIZZLE_INDEX[ch]}]`);
1910
+ return {
1911
+ code: `wgsl.${ctor}(${elements.join(', ')})`,
1912
+ type: { kind: 'vec', size: new_size, scalar: lhs_type.scalar },
1913
+ };
1914
+ }
1915
+ if (lhs_type !== null && lhs_type !== undefined && lhs_type.kind === 'struct') {
1916
+ const struct_info = c.structs.get(lhs_type.name);
1917
+ if (struct_info !== undefined) {
1918
+ const field = struct_info.fields.find(f => f.name === name);
1919
+ if (field !== undefined) {
1920
+ return { code: `${lhs_code}.${name}`, type: field.type };
1921
+ }
1922
+ }
1923
+ }
1924
+ return { code: `${lhs_code}.${name}`, type: T_UNKNOWN };
1925
+ }
1926
+
1927
+ function isSwizzlePattern(name) {
1928
+ if (name.length === 0 || name.length > 4) return false;
1929
+ for (const ch of name) {
1930
+ if (!SWIZZLE_LETTERS.has(ch)) return false;
1931
+ }
1932
+ return true;
1933
+ }
1934
+
1935
+ /**
1936
+ * Compile a `call_phrase` and return both the JS code and the inferred result type.
1937
+ *
1938
+ * Special cases:
1939
+ * - `vec3<f32>(a, b, c)` → `wgsl.vec3f(a, b, c)` (vec result)
1940
+ * - `f32(x)` → `wgsl.f32(x)` (scalar result)
1941
+ * - WGSL builtin (mix, clamp, dot, length, ...) → `wgsl.<name>(...)` (best-effort type)
1942
+ * - User function → `name(...)` (uses declared return type)
1943
+ *
1944
+ * @returns {{ code: string, type: WgslType }}
1945
+ */
1946
+ function compileCallPhraseTyped(c, node) {
1947
+ const ident_node = namedChildByType(node, 'template_elaborated_ident');
1948
+ const arg_list = namedChildByType(node, 'argument_expression_list');
1949
+
1950
+ const ident_inner = namedChildByType(ident_node, 'ident');
1951
+ const callee_name = ident_inner.text;
1952
+ const template = namedChildByType(ident_node, 'template_list');
1953
+
1954
+ /** @type {Array<{ code: string, type: WgslType }>} */
1955
+ const arg_results = [];
1956
+ if (arg_list !== null) {
1957
+ const comma_list = namedChildByType(arg_list, 'expression_comma_list');
1958
+ if (comma_list !== null) {
1959
+ for (const arg of namedChildrenByType(comma_list, 'expression')) {
1960
+ arg_results.push(compileExpression(c, arg));
1961
+ }
1962
+ }
1963
+ }
1964
+ const args = arg_results.map(a => a.code);
1965
+
1966
+ // 1) Vec aliases: vec3f, vec4i, vec2h, etc.
1967
+ if (VECTOR_ALIASES.hasOwnProperty(callee_name)) {
1968
+ const a = VECTOR_ALIASES[callee_name];
1969
+ return {
1970
+ code: `wgsl.${callee_name}(${args.join(', ')})`,
1971
+ type: { kind: 'vec', size: a.size, scalar: a.scalar },
1972
+ };
1973
+ }
1974
+ // 2) Templated vec: vec3<f32>(...).
1975
+ if (callee_name === 'vec2' || callee_name === 'vec3' || callee_name === 'vec4') {
1976
+ const size = parseInt(callee_name.slice(3));
1977
+ let scalar;
1978
+ if (template !== null) {
1979
+ const targs = extractTemplateArgs(template);
1980
+ if (targs.length > 0) scalar = targs[0];
1981
+ }
1982
+ if (scalar === undefined) {
1983
+ // No explicit template — infer the component type from the arguments, mirroring
1984
+ // WGSL's overload resolution. e.g. `vec4(some_u32)` is `vec4<u32>`, not `vec4<f32>`.
1985
+ for (const a of arg_results) {
1986
+ const t = a.type;
1987
+ if (t && t.kind === 'primitive' && t.name !== 'bool') { scalar = t.name; break; }
1988
+ if (t && (t.kind === 'vec' || t.kind === 'mat')) { scalar = t.scalar; break; }
1989
+ }
1990
+ if (scalar === undefined) scalar = 'f32';
1991
+ }
1992
+ return {
1993
+ code: `wgsl.${vecAliasName(size, scalar)}(${args.join(', ')})`,
1994
+ type: { kind: 'vec', size, scalar },
1995
+ };
1996
+ }
1997
+ // 3) Matrix constructors.
1998
+ {
1999
+ const m = callee_name.match(/^mat(\d)x(\d)([ifh]?)$/);
2000
+ if (m !== null) {
2001
+ const cols = parseInt(m[1]);
2002
+ const rows = parseInt(m[2]);
2003
+ const suffix = m[3];
2004
+ let scalar;
2005
+ if (suffix === 'f') scalar = 'f32';
2006
+ else if (suffix === 'i') scalar = 'i32';
2007
+ else if (suffix === 'h') scalar = 'f16';
2008
+ else if (template !== null) {
2009
+ const targs = extractTemplateArgs(template);
2010
+ scalar = targs[0] || 'f32';
2011
+ } else {
2012
+ scalar = 'f32';
2013
+ }
2014
+ const fn_suffix = suffix !== '' ? '' : ({ f32: 'f', i32: 'i', f16: 'h' }[scalar] || 'f');
2015
+ return {
2016
+ code: `wgsl.${callee_name}${fn_suffix}(${args.join(', ')})`,
2017
+ type: { kind: 'mat', cols, rows, scalar },
2018
+ };
2019
+ }
2020
+ }
2021
+
2022
+ // 4) Scalar casts: f32(x), i32(x), u32(x), f16(x), bool(x).
2023
+ if (PRIMITIVE_NAMES.has(callee_name)) {
2024
+ return {
2025
+ code: `wgsl.${callee_name}(${args.join(', ')})`,
2026
+ type: { kind: 'primitive', name: callee_name },
2027
+ };
2028
+ }
2029
+
2030
+ // 5) `bitcast<T>(x)` — JS can't tell a u32 from an f32 by value alone, so we emit the
2031
+ // source type alongside the target. The compiler infers source from the argument's tracked
2032
+ // type; if that's unknown the runtime falls back to a heuristic.
2033
+ if (callee_name === 'bitcast') {
2034
+ let target = 'f32';
2035
+ if (template !== null) {
2036
+ const targs = extractTemplateArgs(template);
2037
+ if (targs.length > 0) target = targs[0];
2038
+ }
2039
+ const arg_type = arg_results[0] !== undefined ? arg_results[0].type : null;
2040
+ let source = null;
2041
+ if (arg_type) {
2042
+ if (arg_type.kind === 'primitive') source = arg_type.name;
2043
+ else if (arg_type.kind === 'vec') source = arg_type.scalar;
2044
+ }
2045
+ const source_arg = source !== null ? `, ${JSON.stringify(source)}` : '';
2046
+ // Parse the target string: `vec3<u32>` etc. become a vec type so that downstream
2047
+ // swizzle/component access compiles correctly. Bare `f32` / `u32` / `i32` stay primitive.
2048
+ let resultType;
2049
+ const vec_match = target.match(/^vec(\d)<([^>]+)>$/);
2050
+ if (vec_match !== null) {
2051
+ resultType = { kind: 'vec', size: parseInt(vec_match[1]), scalar: vec_match[2] };
2052
+ } else {
2053
+ resultType = { kind: 'primitive', name: target };
2054
+ }
2055
+ return {
2056
+ code: `wgsl.bitcast(${args[0]}, ${JSON.stringify(target)}${source_arg})`,
2057
+ type: resultType,
2058
+ };
2059
+ }
2060
+
2061
+ // 6) array / array<T, N> constructor.
2062
+ if (callee_name === 'array') {
2063
+ return { code: `[${args.join(', ')}]`, type: T_UNKNOWN };
2064
+ }
2065
+
2066
+ // 7) Struct constructor.
2067
+ if (c.structs.has(callee_name)) {
2068
+ const info = c.structs.get(callee_name);
2069
+ const parts = [`(() => { const __s__ = new ${callee_name}();`];
2070
+ for (let i = 0; i < info.fields.length && i < args.length; i++) {
2071
+ parts.push(`__s__.${info.fields[i].name} = ${args[i]};`);
2072
+ }
2073
+ parts.push(`return __s__; })()`);
2074
+ return { code: parts.join(' '), type: { kind: 'struct', name: callee_name } };
2075
+ }
2076
+
2077
+ // 8) User-defined function.
2078
+ if (c.functions.has(callee_name)) {
2079
+ return {
2080
+ code: `${callee_name}(${args.join(', ')})`,
2081
+ type: c.functions.get(callee_name).returnType,
2082
+ };
2083
+ }
2084
+
2085
+ // 9) WGSL builtins — best-effort type inference for common helpers.
2086
+ return {
2087
+ code: `wgsl.${callee_name}(${args.join(', ')})`,
2088
+ type: inferBuiltinReturnType(callee_name, arg_results),
2089
+ };
2090
+ }
2091
+
2092
+ /**
2093
+ * Best-effort return type for WGSL builtins commonly used in compute shaders.
2094
+ * Handles the cases needed for accurate swizzle / member-access codegen downstream.
2095
+ */
2096
+ function inferBuiltinReturnType(name, args) {
2097
+ // Element-wise vector-preserving math: result follows the first vector argument.
2098
+ const ELEMENT_WISE = new Set([
2099
+ 'abs', 'sign', 'floor', 'ceil', 'round', 'trunc', 'fract', 'sqrt', 'inverseSqrt',
2100
+ 'exp', 'exp2', 'log', 'log2', 'sin', 'cos', 'tan', 'asin', 'acos', 'atan',
2101
+ 'pow', 'atan2', 'min', 'max', 'step', 'clamp', 'saturate', 'mix', 'smoothstep',
2102
+ 'normalize', 'reflect', 'fma',
2103
+ ]);
2104
+ if (ELEMENT_WISE.has(name)) {
2105
+ for (const a of args) {
2106
+ if (a.type && (a.type.kind === 'vec' || a.type.kind === 'mat')) return a.type;
2107
+ }
2108
+ if (args[0] && args[0].type) return args[0].type;
2109
+ return T_UNKNOWN;
2110
+ }
2111
+ if (name === 'length' || name === 'distance' || name === 'dot' || name === 'determinant') {
2112
+ return { kind: 'primitive', name: 'f32' };
2113
+ }
2114
+ if (name === 'cross') {
2115
+ return args[0] && args[0].type && args[0].type.kind === 'vec'
2116
+ ? args[0].type
2117
+ : { kind: 'vec', size: 3, scalar: 'f32' };
2118
+ }
2119
+ if (name === 'all' || name === 'any') return { kind: 'primitive', name: 'bool' };
2120
+ if (name === 'select') {
2121
+ if (args[0] && args[0].type) return args[0].type;
2122
+ if (args[1] && args[1].type) return args[1].type;
2123
+ return T_UNKNOWN;
2124
+ }
2125
+ if (name === 'textureLoad' || name === 'textureSampleLevel' || name === 'textureSample' || name === 'textureSampleGrad') {
2126
+ return { kind: 'vec', size: 4, scalar: 'f32' };
2127
+ }
2128
+ // Both gathers return the four texels of a 2x2 footprint. Saying so here matters more than
2129
+ // for most builtins: the shadow chunks immediately swizzle the result (`samples.wzxy`), and
2130
+ // an unknown type degrades swizzle codegen downstream.
2131
+ if (name === 'textureGather' || name === 'textureGatherCompare') {
2132
+ return { kind: 'vec', size: 4, scalar: 'f32' };
2133
+ }
2134
+ if (name === 'textureDimensions') {
2135
+ return { kind: 'vec', size: 2, scalar: 'u32' };
2136
+ }
2137
+ if (name === 'atomicLoad') return { kind: 'primitive', name: 'i32' };
2138
+ if (name === 'unpack2x16unorm' || name === 'unpack2x16snorm' || name === 'unpack2x16float') {
2139
+ return { kind: 'vec', size: 2, scalar: 'f32' };
2140
+ }
2141
+ if (name === 'unpack4x8unorm' || name === 'unpack4x8snorm') {
2142
+ return { kind: 'vec', size: 4, scalar: 'f32' };
2143
+ }
2144
+ if (name === 'pack2x16unorm' || name === 'pack2x16snorm' || name === 'pack2x16float'
2145
+ || name === 'pack4x8unorm' || name === 'pack4x8snorm') {
2146
+ return { kind: 'primitive', name: 'u32' };
2147
+ }
2148
+ // extractBits/insertBits return the integer type (scalar or vector) of their first operand.
2149
+ if (name === 'extractBits' || name === 'insertBits') {
2150
+ return args[0] && args[0].type ? args[0].type : T_UNKNOWN;
2151
+ }
2152
+ return T_UNKNOWN;
2153
+ }
2154
+
2155
+ // =========================================================================
2156
+ // LHS expressions (for assignments)
2157
+ // =========================================================================
2158
+
2159
+ function compileLhs(c, node) {
2160
+ if (node.type !== 'lhs_expression') {
2161
+ return compileExpression(c, node);
2162
+ }
2163
+
2164
+ // Pointer deref / address-of: `*lhs` / `&lhs`. The emulator models pointers as values, so
2165
+ // both collapse to the operand's value. The grammar puts the `*` / `&` as an unnamed leading
2166
+ // token with a nested `lhs_expression` named child.
2167
+ //
2168
+ // A whole-value store through a deref (`*p = v`, with nothing selected out of it) is flagged
2169
+ // so {@link emitWriteStatement} writes through the reference rather than rebinding the local
2170
+ // see `wgsl.store_ptr`. A store into part of the pointee (`(*p).x`, `(*p)[i]`) already
2171
+ // lands on the shared object and needs no such treatment.
2172
+ const prefix = textBeforeFirstNamed(node).trim();
2173
+ if (prefix === '*' || prefix === '&') {
2174
+ const nested_lhs = namedChildByType(node, 'lhs_expression');
2175
+ if (nested_lhs !== null) {
2176
+ const inner = compileLhs(c, nested_lhs);
2177
+
2178
+ const selects_part = node.namedChildren.some(
2179
+ child => child.type === 'component_or_swizzle_specifier'
2180
+ );
2181
+
2182
+ return {
2183
+ ...inner,
2184
+ through_pointer: prefix === '*' && !selects_part,
2185
+ };
2186
+ }
2187
+ }
2188
+
2189
+ const core = namedChildByType(node, 'core_lhs_expression');
2190
+ let code;
2191
+ let type;
2192
+ if (core !== null) {
2193
+ const ident = namedChildByType(core, 'ident');
2194
+ if (ident !== null) {
2195
+ code = ident.text;
2196
+ type = c.scope.lookup(ident.text);
2197
+ } else {
2198
+ const nested = namedChildByType(core, 'lhs_expression');
2199
+ if (nested !== null) {
2200
+ const r = compileLhs(c, nested);
2201
+ code = `(${r.code})`;
2202
+ type = r.type;
2203
+ } else {
2204
+ code = core.text;
2205
+ type = T_UNKNOWN;
2206
+ }
2207
+ }
2208
+ } else {
2209
+ code = node.text;
2210
+ type = T_UNKNOWN;
2211
+ }
2212
+
2213
+ for (const child of node.namedChildren) {
2214
+ if (child.type === 'component_or_swizzle_specifier') {
2215
+ const next = applyAccessSpecifier(c, child, code, type);
2216
+ code = next.code;
2217
+ type = next.type;
2218
+ }
2219
+ }
2220
+ return { code, type };
2221
+ }