@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.
- package/package.json +1 -1
- package/src/engine/ecs/storage/populateEngineSerializationRegistry.d.ts.map +1 -1
- package/src/engine/ecs/storage/populateEngineSerializationRegistry.js +294 -290
- package/src/engine/graphics3/ParticipatingMedia.d.ts +111 -0
- package/src/engine/graphics3/ParticipatingMedia.d.ts.map +1 -0
- package/src/engine/graphics3/ParticipatingMedia.js +203 -0
- package/src/engine/graphics3/ParticipatingMediaSerializationAdapter.d.ts +34 -0
- package/src/engine/graphics3/ParticipatingMediaSerializationAdapter.d.ts.map +1 -0
- package/src/engine/graphics3/ParticipatingMediaSerializationAdapter.js +73 -0
- package/src/engine/graphics3/ParticipatingMediaSystem3.d.ts +87 -0
- package/src/engine/graphics3/ParticipatingMediaSystem3.d.ts.map +1 -0
- package/src/engine/graphics3/ParticipatingMediaSystem3.js +184 -0
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- package/src/shade/playground/volumetrics_froxel/README.md +104 -1
- package/src/shade/playground/volumetrics_froxel/index.html +20 -1
- package/src/shade/playground/volumetrics_froxel/main.d.ts.map +1 -1
- package/src/shade/playground/volumetrics_froxel/main.js +298 -0
- package/src/shade/playground/volumetrics_froxel/measure.d.ts +119 -0
- package/src/shade/playground/volumetrics_froxel/measure.d.ts.map +1 -0
- package/src/shade/playground/volumetrics_froxel/measure.js +559 -0
- package/src/shade/renderer/Renderer.d.ts.map +1 -1
- package/src/shade/renderer/Renderer.js +22 -18
- package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
- package/src/shade/renderer/shadow/map/shader/chunk_shadowmap_sample_point2.d.ts +14 -13
- package/src/shade/renderer/shadow/map/shader/chunk_shadowmap_sample_point2.d.ts.map +1 -1
- package/src/shade/renderer/shadow/map/shader/chunk_shadowmap_sample_point2.js +133 -129
- package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/GPUViewVolumetrics.js +480 -315
- package/src/shade/renderer/volumetrics/NOTES.md +649 -8
- package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.d.ts +5 -0
- package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.js +21 -2
- package/src/shade/renderer/volumetrics/SceneVolumetrics.d.ts +26 -0
- package/src/shade/renderer/volumetrics/SceneVolumetrics.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/SceneVolumetrics.js +64 -21
- package/src/shade/renderer/volumetrics/VOLUMETRICS_METADATA_STRUCT.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/VOLUMETRICS_METADATA_STRUCT.js +84 -28
- package/src/shade/renderer/volumetrics/VOLUMETRICS_SUN_SHADOW_RESOLUTION.d.ts +98 -0
- package/src/shade/renderer/volumetrics/VOLUMETRICS_SUN_SHADOW_RESOLUTION.d.ts.map +1 -0
- package/src/shade/renderer/volumetrics/VOLUMETRICS_SUN_SHADOW_RESOLUTION.js +111 -0
- package/src/shade/renderer/volumetrics/build_volumetrics_sun_shadow_transform.d.ts +84 -0
- package/src/shade/renderer/volumetrics/build_volumetrics_sun_shadow_transform.d.ts.map +1 -0
- package/src/shade/renderer/volumetrics/build_volumetrics_sun_shadow_transform.js +252 -0
- package/src/shade/renderer/volumetrics/chunk_integrate_optical_depth.js +75 -75
- package/src/shade/renderer/volumetrics/chunk_volumetrics_local_light_shadow.d.ts +43 -0
- package/src/shade/renderer/volumetrics/chunk_volumetrics_local_light_shadow.d.ts.map +1 -0
- package/src/shade/renderer/volumetrics/chunk_volumetrics_local_light_shadow.js +145 -0
- package/src/shade/renderer/volumetrics/chunk_volumetrics_sample_froxel_extinction.d.ts +20 -0
- package/src/shade/renderer/volumetrics/chunk_volumetrics_sample_froxel_extinction.d.ts.map +1 -0
- package/src/shade/renderer/volumetrics/chunk_volumetrics_sample_froxel_extinction.js +48 -0
- package/src/shade/renderer/volumetrics/chunk_volumetrics_sun_optical_depth.d.ts +38 -0
- package/src/shade/renderer/volumetrics/chunk_volumetrics_sun_optical_depth.d.ts.map +1 -0
- package/src/shade/renderer/volumetrics/chunk_volumetrics_sun_optical_depth.js +107 -0
- package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts +4 -1
- package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/graph_build_volumetrics.js +577 -471
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.js +64 -13
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_sun_shadow_volume.d.ts +8 -0
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_sun_shadow_volume.d.ts.map +1 -0
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_sun_shadow_volume.js +174 -0
- package/src/shade/wgsl/emulator/CPUBitmapData.d.ts +25 -0
- package/src/shade/wgsl/emulator/CPUBitmapData.d.ts.map +1 -1
- package/src/shade/wgsl/emulator/CPUBitmapData.js +33 -0
- package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +2221 -2215
- package/src/shade/wgsl/emulator/WGSLLanguageCore.d.ts.map +1 -1
- package/src/shade/wgsl/emulator/WGSLLanguageCore.js +1297 -1117
- package/src/shade/wgsl/emulator/cpu_texture_gather.d.ts +69 -0
- package/src/shade/wgsl/emulator/cpu_texture_gather.d.ts.map +1 -0
- package/src/shade/wgsl/emulator/cpu_texture_gather.js +148 -0
- package/src/shade/wgsl/emulator/cpu_texture_sample.d.ts +16 -0
- package/src/shade/wgsl/emulator/cpu_texture_sample.d.ts.map +1 -1
- 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
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
if (name === '
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
}
|
|
2138
|
-
if (name === '
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
}
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
//
|
|
2165
|
-
//
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
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
|
+
}
|