wesl 0.6.48 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/dist/index.d.ts +295 -214
  2. package/dist/index.js +2947 -1550
  3. package/package.json +6 -8
  4. package/src/AbstractElems.ts +81 -81
  5. package/src/Assertions.ts +5 -5
  6. package/src/BindIdents.ts +193 -319
  7. package/src/ClickableError.ts +3 -2
  8. package/src/Conditions.ts +2 -2
  9. package/src/LinkedWesl.ts +1 -1
  10. package/src/Linker.ts +4 -3
  11. package/src/LinkerUtil.ts +1 -1
  12. package/src/Logging.ts +165 -0
  13. package/src/LowerAndEmit.ts +278 -110
  14. package/src/ModulePathUtil.ts +59 -0
  15. package/src/ModuleResolver.ts +26 -62
  16. package/src/ParseError.ts +9 -0
  17. package/src/ParseWESL.ts +30 -94
  18. package/src/RawEmit.ts +1 -4
  19. package/src/Reflection.ts +1 -1
  20. package/src/Scope.ts +3 -0
  21. package/src/Span.ts +2 -0
  22. package/src/SrcMap.ts +208 -0
  23. package/src/Stream.ts +30 -0
  24. package/src/TransformBindingStructs.ts +2 -2
  25. package/src/Util.ts +1 -1
  26. package/src/debug/ASTtoString.ts +84 -135
  27. package/src/discovery/FindUnboundIdents.ts +14 -5
  28. package/src/index.ts +5 -0
  29. package/src/parse/ContentsHelpers.ts +70 -0
  30. package/src/parse/ExpressionUtil.ts +121 -0
  31. package/src/parse/Keywords.ts +12 -12
  32. package/src/parse/OperatorBinding.ts +146 -0
  33. package/src/parse/ParseAttribute.ts +272 -0
  34. package/src/parse/ParseCall.ts +77 -0
  35. package/src/parse/ParseControlFlow.ts +129 -0
  36. package/src/parse/ParseDirective.ts +105 -0
  37. package/src/parse/ParseExpression.ts +288 -0
  38. package/src/parse/ParseFn.ts +151 -0
  39. package/src/parse/ParseGlobalVar.ts +131 -0
  40. package/src/parse/ParseIdent.ts +77 -0
  41. package/src/parse/ParseImport.ts +160 -0
  42. package/src/parse/ParseLocalVar.ts +69 -0
  43. package/src/parse/ParseLoop.ts +112 -0
  44. package/src/parse/ParseModule.ts +116 -0
  45. package/src/parse/ParseSimpleStatement.ts +162 -0
  46. package/src/parse/ParseStatement.ts +215 -0
  47. package/src/parse/ParseStruct.ts +89 -0
  48. package/src/parse/ParseType.ts +71 -0
  49. package/src/parse/ParseUtil.ts +174 -0
  50. package/src/parse/ParseValueDeclaration.ts +130 -0
  51. package/src/parse/ParseWesl.ts +51 -0
  52. package/src/parse/ParsingContext.ts +93 -0
  53. package/src/parse/WeslStream.ts +63 -20
  54. package/src/parse/stream/CachingStream.ts +48 -0
  55. package/src/parse/stream/MatchersStream.ts +85 -0
  56. package/src/parse/stream/RegexHelpers.ts +38 -0
  57. package/src/test/BevyLink.test.ts +100 -0
  58. package/src/test/BindStdTypes.test.ts +110 -0
  59. package/src/test/{BindWESL.test.ts → BindWESLV2.test.ts} +21 -22
  60. package/src/test/BulkTests.test.ts +11 -12
  61. package/src/test/ConditionLinking.test.ts +107 -0
  62. package/src/test/ConditionalElif.test.ts +1 -13
  63. package/src/test/ConditionalTranslationCases.test.ts +5 -0
  64. package/src/test/ErrorLogging.test.ts +2 -2
  65. package/src/test/ImportCasesV2.test.ts +63 -0
  66. package/src/test/LinkFails.test.ts +69 -0
  67. package/src/test/LinkPackage.test.ts +1 -1
  68. package/src/test/Linker.test.ts +75 -2
  69. package/src/test/LogCatcher.ts +53 -0
  70. package/src/test/Mangling.test.ts +1 -1
  71. package/src/test/ParseComments.test.ts +1 -2
  72. package/src/test/{ParseConditions.test.ts → ParseConditionsV2.test.ts} +57 -49
  73. package/src/test/ParseErrorV2.test.ts +73 -0
  74. package/src/test/{ParseWESL.test.ts → ParseWeslV2.test.ts} +288 -370
  75. package/src/test/{ScopeWESL.test.ts → ScopeWESLV2.test.ts} +205 -176
  76. package/src/test/TestLink.ts +51 -51
  77. package/src/test/TestSetup.ts +9 -3
  78. package/src/test/TestUtil.ts +47 -77
  79. package/src/test/TrimmedMatch.ts +40 -0
  80. package/src/test/VirtualModules.test.ts +33 -2
  81. package/src/test/WeslDevice.test.ts +9 -2
  82. package/src/test/__snapshots__/ParseWeslV2.test.ts.snap +67 -0
  83. package/src/test-util.ts +7 -0
  84. package/src/WESLCollect.ts +0 -656
  85. package/src/parse/AttributeGrammar.ts +0 -232
  86. package/src/parse/ImportGrammar.ts +0 -195
  87. package/src/parse/WeslBaseGrammar.ts +0 -11
  88. package/src/parse/WeslExpression.ts +0 -231
  89. package/src/parse/WeslGrammar.ts +0 -739
  90. package/src/test/Expression.test.ts +0 -22
  91. package/src/test/ImportSyntaxCases.test.ts +0 -24
  92. package/src/test/ParseError.test.ts +0 -45
  93. package/src/test/Reflection.test.ts +0 -176
  94. package/src/test/TransformBindingStructs.test.ts +0 -238
  95. /package/src/test/{ParseElif.test.ts → ParseElifV2.test.ts} +0 -0
@@ -1,4 +1,3 @@
1
- import type { SrcMapBuilder } from "mini-parse";
2
1
  import type {
3
2
  AbstractElem,
4
3
  AttributeElem,
@@ -14,11 +13,11 @@ import type {
14
13
  TextElem,
15
14
  } from "./AbstractElems.ts";
16
15
  import { assertThatDebug, assertUnreachable } from "./Assertions.ts";
17
- import { isGlobal } from "./BindIdents.ts";
18
16
  import { failIdentElem } from "./ClickableError.ts";
19
17
  import { filterValidElements } from "./Conditions.ts";
20
18
  import { identToString } from "./debug/ScopeToString.ts";
21
19
  import type { Conditions, DeclIdent, Ident } from "./Scope.ts";
20
+ import type { SrcMapBuilder } from "./SrcMap.ts";
22
21
 
23
22
  export interface EmitParams {
24
23
  srcBuilder: SrcMapBuilder;
@@ -30,14 +29,14 @@ export interface EmitParams {
30
29
  skipConditionalFiltering?: boolean;
31
30
  }
32
31
 
33
- /** passed to the emitters */
32
+ /** Passed to the emitters. */
34
33
  interface EmitContext {
35
34
  srcBuilder: SrcMapBuilder;
36
35
  conditions: Conditions;
37
36
  extracting: boolean;
38
37
  }
39
38
 
40
- /** traverse the AST, starting from root elements, emitting wgsl for each */
39
+ /** Traverse the AST, starting from root elements, emitting WGSL for each. */
41
40
  export function lowerAndEmit(params: EmitParams): void {
42
41
  const { srcBuilder, rootElems, conditions } = params;
43
42
  const { extracting = true, skipConditionalFiltering = false } = params;
@@ -51,13 +50,11 @@ export function lowerAndEmit(params: EmitParams): void {
51
50
  });
52
51
  }
53
52
 
54
- export function lowerAndEmitElem(e: AbstractElem, ctx: EmitContext): void {
53
+ function lowerAndEmitElem(e: AbstractElem, ctx: EmitContext): void {
55
54
  switch (e.kind) {
56
- // import statements are dropped from from emitted text
57
55
  case "import":
58
- return;
56
+ return; // import statements are dropped from emitted text
59
57
 
60
- // terminal elements copy strings to the output
61
58
  case "text":
62
59
  emitText(e, ctx);
63
60
  return;
@@ -68,7 +65,6 @@ export function lowerAndEmitElem(e: AbstractElem, ctx: EmitContext): void {
68
65
  emitSynthetic(e, ctx);
69
66
  return;
70
67
 
71
- // identifiers are copied to the output, but with potentially mangled names
72
68
  case "ref":
73
69
  emitRefIdent(e, ctx);
74
70
  return;
@@ -76,30 +72,49 @@ export function lowerAndEmitElem(e: AbstractElem, ctx: EmitContext): void {
76
72
  emitDeclIdent(e, ctx);
77
73
  return;
78
74
 
79
- // container elements just emit their child elements
75
+ case "literal":
76
+ case "binary-expression":
77
+ case "unary-expression":
78
+ case "call-expression":
79
+ case "parenthesized-expression":
80
+ case "component-expression":
81
+ case "component-member-expression":
82
+ emitExpression(e, ctx);
83
+ return;
84
+
80
85
  case "param":
81
- case "var":
82
86
  case "typeDecl":
83
- case "let":
84
- case "module":
85
87
  case "member":
86
88
  case "memberRef":
87
89
  case "expression":
88
90
  case "type":
89
- case "statement":
90
- case "stuff":
91
91
  case "switch-clause":
92
92
  emitContents(e, ctx);
93
93
  return;
94
94
 
95
- // root level container elements get some extra newlines to make the output prettier
95
+ // "stuff" elements (compound statements) need trimming for proper formatting
96
+ // LATER get rid of "stuff" elements
97
+ case "stuff":
98
+ emitStuff(e, ctx);
99
+ return;
100
+
101
+ case "module":
102
+ emitModule(e, ctx);
103
+ return;
104
+
105
+ case "var":
106
+ case "let":
107
+ case "statement":
108
+ case "continuing":
109
+ emitStatement(e, ctx);
110
+ return;
111
+
96
112
  case "override":
97
113
  case "const":
98
114
  case "assert":
99
115
  case "alias":
100
116
  case "gvar":
101
- emitRootElemNl(ctx);
102
- emitContents(e, ctx);
117
+ emitRootDecl(e, ctx);
103
118
  return;
104
119
 
105
120
  case "fn":
@@ -125,24 +140,70 @@ export function lowerAndEmitElem(e: AbstractElem, ctx: EmitContext): void {
125
140
  }
126
141
  }
127
142
 
128
- /** emit root elems with a blank line inbetween */
129
- function emitRootElemNl(ctx: EmitContext): void {
130
- if (ctx.extracting) {
131
- ctx.srcBuilder.addNl();
132
- ctx.srcBuilder.addNl();
143
+ function emitStuff(e: ContainerElem, ctx: EmitContext): void {
144
+ emitContentsWithTrimming(e, ctx);
145
+ }
146
+
147
+ function emitModule(e: ContainerElem, ctx: EmitContext): void {
148
+ // Skip whitespace-only text elements at module level
149
+ const validElements = filterValidElements(e.contents, ctx.conditions);
150
+ for (const child of validElements) {
151
+ if (child.kind === "text") {
152
+ const text = child.srcModule.src.slice(child.start, child.end);
153
+ if (text.trim() === "") continue;
154
+ }
155
+ lowerAndEmitElem(child, ctx);
133
156
  }
134
157
  }
135
158
 
136
- export function emitText(e: TextElem, ctx: EmitContext): void {
159
+ function emitStatement(
160
+ e: Extract<
161
+ ContainerElem,
162
+ { kind: "var" | "let" | "statement" | "continuing" }
163
+ >,
164
+ ctx: EmitContext,
165
+ ): void {
166
+ const attrsInContents =
167
+ e.contents.length > 0 && e.contents[0].kind === "attribute";
168
+ if (!attrsInContents) {
169
+ emitAttributes(e.attributes, ctx);
170
+ }
171
+ emitContents(e, ctx);
172
+ }
173
+
174
+ function emitRootDecl(
175
+ e: Extract<
176
+ ContainerElem,
177
+ { kind: "override" | "const" | "assert" | "alias" | "gvar" }
178
+ >,
179
+ ctx: EmitContext,
180
+ ): void {
181
+ emitRootElemNl(ctx);
182
+ const attrsInContents =
183
+ e.contents.length > 0 && e.contents[0].kind === "attribute";
184
+ if (!attrsInContents) {
185
+ emitAttributes(e.attributes, ctx);
186
+ }
187
+
188
+ emitContentsWithTrimming(e, ctx);
189
+ }
190
+
191
+ /** Emit newlines between root elements. */
192
+ function emitRootElemNl(ctx: EmitContext): void {
193
+ ctx.srcBuilder.addNl();
194
+ ctx.srcBuilder.addNl();
195
+ }
196
+
197
+ function emitText(e: TextElem, ctx: EmitContext): void {
137
198
  ctx.srcBuilder.addCopy(e.start, e.end);
138
199
  }
139
200
 
140
- export function emitName(e: NameElem, ctx: EmitContext): void {
201
+ function emitName(e: NameElem, ctx: EmitContext): void {
141
202
  ctx.srcBuilder.add(e.name, e.start, e.end);
142
203
  }
143
204
 
144
- /** emit function explicitly so we can control commas between conditional parameters */
145
- export function emitFn(e: FnElem, ctx: EmitContext): void {
205
+ /** Emit function explicitly to control commas between conditional parameters. */
206
+ function emitFn(e: FnElem, ctx: EmitContext): void {
146
207
  const { attributes, name, params, returnAttributes, returnType, body } = e;
147
208
  const { conditions, srcBuilder: builder } = ctx;
148
209
 
@@ -154,6 +215,13 @@ export function emitFn(e: FnElem, ctx: EmitContext): void {
154
215
  builder.appendNext("(");
155
216
  const validParams = filterValidElements(params, conditions);
156
217
  validParams.forEach((p, i) => {
218
+ // Emit attributes separately only if not already in contents
219
+ // LATER stop including attributes in contents when we emit from ast
220
+ const attrsInContents =
221
+ p.contents.length > 0 && p.contents[0].kind === "attribute";
222
+ if (!attrsInContents) {
223
+ emitAttributes(p.attributes, ctx);
224
+ }
157
225
  emitContentsNoWs(p as ContainerElem, ctx);
158
226
  if (i < validParams.length - 1) {
159
227
  builder.appendNext(", ");
@@ -164,7 +232,7 @@ export function emitFn(e: FnElem, ctx: EmitContext): void {
164
232
  if (returnType) {
165
233
  builder.appendNext("-> ");
166
234
  emitAttributes(returnAttributes, ctx);
167
- emitContents(returnType, ctx);
235
+ emitContentsNoWs(returnType, ctx);
168
236
  builder.appendNext(" ");
169
237
  }
170
238
 
@@ -176,14 +244,16 @@ function emitAttributes(
176
244
  ctx: EmitContext,
177
245
  ): void {
178
246
  attributes?.forEach(a => {
179
- emitAttribute(a, ctx);
180
- ctx.srcBuilder.add(" ", a.start, a.end);
247
+ const emitted = emitAttribute(a, ctx);
248
+ if (emitted) {
249
+ ctx.srcBuilder.add(" ", a.start, a.end);
250
+ }
181
251
  });
182
252
  }
183
253
 
184
- /** emit structs explicitly so we can control commas between conditional members */
185
- export function emitStruct(e: StructElem, ctx: EmitContext): void {
186
- const { name, members, start, end } = e;
254
+ /** Emit structs explicitly to control commas between conditional members. */
255
+ function emitStruct(e: StructElem, ctx: EmitContext): void {
256
+ const { attributes, name, members, start } = e;
187
257
  const { srcBuilder, conditions } = ctx;
188
258
 
189
259
  const validMembers = filterValidElements(members, conditions);
@@ -194,24 +264,28 @@ export function emitStruct(e: StructElem, ctx: EmitContext): void {
194
264
  return;
195
265
  }
196
266
 
267
+ emitAttributes(attributes, ctx);
197
268
  srcBuilder.add("struct ", start, name.start);
198
269
  emitDeclIdent(name, ctx);
199
270
 
200
271
  if (validLength === 1) {
201
- srcBuilder.add(" { ", name.end, members[0].start);
202
- emitContentsNoWs(validMembers[0] as ContainerElem, ctx);
203
- srcBuilder.add(" }\n", end - 1, end);
272
+ srcBuilder.appendNext(" { ");
273
+ emitContentsWithTrimming(validMembers[0] as ContainerElem, ctx);
274
+ srcBuilder.appendNext(" }");
275
+ srcBuilder.addNl();
204
276
  } else {
205
- srcBuilder.add(" {\n", name.end, members[0].start);
277
+ srcBuilder.appendNext(" {");
278
+ srcBuilder.addNl();
206
279
 
207
280
  validMembers.forEach(m => {
208
- srcBuilder.add(" ", m.start - 1, m.start);
281
+ srcBuilder.appendNext(" ");
209
282
  emitContentsNoWs(m as ContainerElem, ctx);
210
- srcBuilder.add(",", m.end, m.end + 1);
283
+ srcBuilder.appendNext(",");
211
284
  srcBuilder.addNl();
212
285
  });
213
286
 
214
- srcBuilder.add("}\n", end - 1, end);
287
+ srcBuilder.appendNext("}");
288
+ srcBuilder.addNl();
215
289
  }
216
290
  }
217
291
 
@@ -222,21 +296,45 @@ function warnEmptyStruct(e: StructElem): void {
222
296
  failIdentElem(name, message);
223
297
  }
224
298
 
225
- export function emitSynthetic(e: SyntheticElem, ctx: EmitContext): void {
299
+ function emitSynthetic(e: SyntheticElem, ctx: EmitContext): void {
226
300
  const { text } = e;
227
301
  ctx.srcBuilder.addSynthetic(text, text, 0, text.length);
228
302
  }
229
303
 
230
- export function emitContents(elem: ContainerElem, ctx: EmitContext): void {
231
- // elem.contents.forEach(e => console.log("orig", astToString(e)));
304
+ function emitContents(elem: ContainerElem, ctx: EmitContext): void {
232
305
  const validElements = filterValidElements(elem.contents, ctx.conditions);
233
- // validElements.forEach(e => console.log("valid", astToString(e)));
234
306
  validElements.forEach(e => {
235
307
  lowerAndEmitElem(e, ctx);
236
308
  });
237
309
  }
238
310
 
239
- /** emit contents w/o white space */
311
+ /** Emit contents with leading/trailing whitespace trimming (V2 parser). */
312
+ function emitContentsWithTrimming(elem: ContainerElem, ctx: EmitContext): void {
313
+ const validElements = filterValidElements(elem.contents, ctx.conditions);
314
+
315
+ // Find first/last non-conditional-attribute indices for trimming
316
+ const firstEmit = validElements.findIndex(e => !isConditionalAttr(e));
317
+ const lastEmit = validElements.findLastIndex(e => !isConditionalAttr(e));
318
+
319
+ validElements.forEach((elem, i) => {
320
+ if (elem.kind === "text") {
321
+ let text = elem.srcModule.src.slice(elem.start, elem.end);
322
+ if (i === firstEmit) text = text.trimStart();
323
+ if (i === lastEmit) text = text.trimEnd();
324
+ if (text) ctx.srcBuilder.add(text, elem.start, elem.end);
325
+ } else {
326
+ lowerAndEmitElem(elem, ctx);
327
+ }
328
+ });
329
+ }
330
+
331
+ function isConditionalAttr(e: AbstractElem): boolean {
332
+ if (e.kind !== "attribute") return false;
333
+ const { kind } = e.attribute;
334
+ return kind === "@if" || kind === "@elif" || kind === "@else";
335
+ }
336
+
337
+ /** Emit contents without whitespace. */
240
338
  function emitContentsNoWs(elem: ContainerElem, ctx: EmitContext): void {
241
339
  const validElements = filterValidElements(elem.contents, ctx.conditions);
242
340
  validElements.forEach(e => {
@@ -251,7 +349,7 @@ function emitContentsNoWs(elem: ContainerElem, ctx: EmitContext): void {
251
349
  });
252
350
  }
253
351
 
254
- export function emitRefIdent(e: RefIdentElem, ctx: EmitContext): void {
352
+ function emitRefIdent(e: RefIdentElem, ctx: EmitContext): void {
255
353
  if (e.ident.std) {
256
354
  ctx.srcBuilder.add(e.ident.originalName, e.start, e.end);
257
355
  } else {
@@ -261,61 +359,138 @@ export function emitRefIdent(e: RefIdentElem, ctx: EmitContext): void {
261
359
  }
262
360
  }
263
361
 
264
- export function emitDeclIdent(e: DeclIdentElem, ctx: EmitContext): void {
362
+ function emitDeclIdent(e: DeclIdentElem, ctx: EmitContext): void {
265
363
  const mangledName = displayName(e.ident);
266
364
  ctx.srcBuilder.add(mangledName!, e.start, e.end);
267
365
  }
268
366
 
269
- function emitAttribute(e: AttributeElem, ctx: EmitContext): void {
270
- const { kind } = e.attribute;
271
- // LATER emit more precise source map info by making use of all the spans
272
- // Like the first case does
273
- if (kind === "@attribute") {
274
- const { params } = e.attribute;
275
- if (!params || params.length === 0) {
276
- ctx.srcBuilder.add("@" + e.attribute.name, e.start, e.end);
277
- } else {
278
- ctx.srcBuilder.add(
279
- "@" + e.attribute.name + "(",
280
- e.start,
281
- params[0].start,
282
- );
283
- for (let i = 0; i < params.length; i++) {
284
- emitContents(params[i], ctx);
285
- if (i < params.length - 1) {
286
- ctx.srcBuilder.add(",", params[i].end, params[i + 1].start);
287
- }
288
- }
289
- ctx.srcBuilder.add(")", params[params.length - 1].end, e.end);
290
- }
291
- } else if (kind === "@builtin") {
367
+ function emitExpression(e: ExpressionElem, ctx: EmitContext): void {
368
+ const { kind } = e;
369
+
370
+ if (kind === "literal") {
371
+ ctx.srcBuilder.add(e.value, e.start, e.end);
372
+ return;
373
+ }
374
+
375
+ if (kind === "ref") {
376
+ emitRefIdent(e, ctx);
377
+ return;
378
+ }
379
+
380
+ if (kind === "type") {
381
+ emitContents(e, ctx);
382
+ return;
383
+ }
384
+
385
+ if (kind === "binary-expression") {
386
+ emitExpression(e.left, ctx);
292
387
  ctx.srcBuilder.add(
293
- "@builtin(" + e.attribute.param.name + ")",
294
- e.start,
295
- e.end,
388
+ ` ${e.operator.value} `,
389
+ e.operator.span[0],
390
+ e.operator.span[1],
296
391
  );
297
- } else if (kind === "@diagnostic") {
392
+ emitExpression(e.right, ctx);
393
+ return;
394
+ }
395
+
396
+ if (kind === "unary-expression") {
298
397
  ctx.srcBuilder.add(
299
- "@diagnostic" +
300
- diagnosticControlToString(e.attribute.severity, e.attribute.rule),
301
- e.start,
302
- e.end,
398
+ e.operator.value,
399
+ e.operator.span[0],
400
+ e.operator.span[1],
303
401
  );
304
- } else if (kind === "@if") {
305
- // (@if is wesl only, dropped from wgsl)
306
- } else if (kind === "@interpolate") {
402
+ emitExpression(e.expression, ctx);
403
+ return;
404
+ }
405
+
406
+ if (kind === "parenthesized-expression") {
407
+ emitExpression(e.expression, ctx);
408
+ return;
409
+ }
410
+
411
+ if (kind === "call-expression") {
412
+ emitExpression(e.function, ctx);
413
+ if (e.templateArgs) {
414
+ for (const targ of e.templateArgs) lowerAndEmitElem(targ, ctx);
415
+ }
416
+ for (const arg of e.arguments) {
417
+ emitExpression(arg, ctx);
418
+ }
419
+ return;
420
+ }
421
+
422
+ if (kind === "component-expression") {
423
+ emitExpression(e.base, ctx);
424
+ emitExpression(e.access, ctx);
425
+ return;
426
+ }
427
+
428
+ if (kind === "component-member-expression") {
429
+ emitExpression(e.base, ctx);
430
+ if (e.access.kind === "name") {
431
+ ctx.srcBuilder.add(e.access.name, e.access.start, e.access.end);
432
+ }
433
+ return;
434
+ }
435
+
436
+ assertUnreachable(kind);
437
+ }
438
+
439
+ function emitAttribute(e: AttributeElem, ctx: EmitContext): boolean {
440
+ const { kind } = e.attribute;
441
+
442
+ if (kind === "@if" || kind === "@elif" || kind === "@else") {
443
+ return false; // WESL-only, dropped from WGSL
444
+ }
445
+
446
+ if (kind === "@attribute") {
447
+ emitStandardAttribute(e, ctx);
448
+ return true;
449
+ }
450
+
451
+ if (kind === "@builtin") {
307
452
  ctx.srcBuilder.add(
308
- `@interpolate(${e.attribute.params.map(v => v.name).join(", ")})`,
453
+ "@builtin(" + e.attribute.param.name + ")",
309
454
  e.start,
310
455
  e.end,
311
456
  );
312
- } else if (kind === "@elif") {
313
- // @elif is wesl only, dropped from wgsl
314
- } else if (kind === "@else") {
315
- // @else is wesl only, dropped from wgsl
316
- } else {
317
- assertUnreachable(kind);
457
+ return true;
318
458
  }
459
+
460
+ if (kind === "@diagnostic") {
461
+ const diagStr =
462
+ "@diagnostic" +
463
+ diagnosticControlToString(e.attribute.severity, e.attribute.rule);
464
+ ctx.srcBuilder.add(diagStr, e.start, e.end);
465
+ return true;
466
+ }
467
+
468
+ if (kind === "@interpolate") {
469
+ const params = e.attribute.params.map(v => v.name).join(", ");
470
+ ctx.srcBuilder.add(`@interpolate(${params})`, e.start, e.end);
471
+ return true;
472
+ }
473
+
474
+ assertUnreachable(kind);
475
+ }
476
+
477
+ function emitStandardAttribute(e: AttributeElem, ctx: EmitContext): void {
478
+ if (e.attribute.kind !== "@attribute") return;
479
+
480
+ const { params } = e.attribute;
481
+ if (!params || params.length === 0) {
482
+ ctx.srcBuilder.add("@" + e.attribute.name, e.start, e.end);
483
+ return;
484
+ }
485
+
486
+ ctx.srcBuilder.add("@" + e.attribute.name + "(", e.start, params[0].start);
487
+ for (let i = 0; i < params.length; i++) {
488
+ ctx.srcBuilder.addCopy(params[i].start, params[i].end);
489
+ if (i < params.length - 1) {
490
+ ctx.srcBuilder.add(",", params[i].end, params[i + 1].start);
491
+ }
492
+ }
493
+ ctx.srcBuilder.add(")", params[params.length - 1].end, e.end);
319
494
  }
320
495
 
321
496
  export function diagnosticControlToString(
@@ -336,8 +511,6 @@ export function expressionToString(elem: ExpressionElem): string {
336
511
  return elem.ident.originalName;
337
512
  } else if (kind === "literal") {
338
513
  return elem.value;
339
- } else if (kind === "translate-time-feature") {
340
- return elem.name;
341
514
  } else if (kind === "parenthesized-expression") {
342
515
  return `(${expressionToString(elem.expression)})`;
343
516
  } else if (kind === "component-expression") {
@@ -345,7 +518,13 @@ export function expressionToString(elem: ExpressionElem): string {
345
518
  } else if (kind === "component-member-expression") {
346
519
  return `${expressionToString(elem.base)}.${elem.access}`;
347
520
  } else if (kind === "call-expression") {
348
- return `${elem.function.ident.originalName}(${elem.arguments.map(expressionToString).join(", ")})`;
521
+ const fn = elem.function;
522
+ const name =
523
+ fn.kind === "ref" ? fn.ident.originalName : fn.name.originalName;
524
+ const targs = elem.templateArgs ? `<...>` : "";
525
+ return `${name}${targs}(${elem.arguments.map(expressionToString).join(", ")})`;
526
+ } else if (kind === "type") {
527
+ return elem.name.originalName;
349
528
  } else {
350
529
  assertUnreachable(kind);
351
530
  }
@@ -355,30 +534,21 @@ function emitDirective(e: DirectiveElem, ctx: EmitContext): void {
355
534
  const { directive } = e;
356
535
  const { kind } = directive;
357
536
  if (kind === "diagnostic") {
358
- ctx.srcBuilder.add(
359
- `diagnostic${diagnosticControlToString(directive.severity, directive.rule)};`,
360
- e.start,
361
- e.end,
362
- );
537
+ const diagStr = `diagnostic${diagnosticControlToString(directive.severity, directive.rule)};`;
538
+ ctx.srcBuilder.add(diagStr, e.start, e.end);
363
539
  } else if (kind === "enable") {
364
- ctx.srcBuilder.add(
365
- `enable ${directive.extensions.map(v => v.name).join(", ")};`,
366
- e.start,
367
- e.end,
368
- );
540
+ const exts = directive.extensions.map(v => v.name).join(", ");
541
+ ctx.srcBuilder.add(`enable ${exts};`, e.start, e.end);
369
542
  } else if (kind === "requires") {
370
- ctx.srcBuilder.add(
371
- `requires ${directive.extensions.map(v => v.name).join(", ")};`,
372
- e.start,
373
- e.end,
374
- );
543
+ const exts = directive.extensions.map(v => v.name).join(", ");
544
+ ctx.srcBuilder.add(`requires ${exts};`, e.start, e.end);
375
545
  } else {
376
546
  assertUnreachable(kind);
377
547
  }
378
548
  }
379
549
 
380
550
  function displayName(declIdent: DeclIdent): string {
381
- if (isGlobal(declIdent)) {
551
+ if (declIdent.isGlobal) {
382
552
  assertThatDebug(
383
553
  declIdent.mangledName,
384
554
  `ERR: mangled name not found for decl ident ${identToString(declIdent)}`,
@@ -390,9 +560,7 @@ function displayName(declIdent: DeclIdent): string {
390
560
  return declIdent.mangledName || declIdent.originalName;
391
561
  }
392
562
 
393
- /** trace through refersTo links in reference Idents until we find the declaration
394
- * expects that bindIdents has filled in all refersTo: links
395
- */
563
+ /** Trace through refersTo links until we find the declaration. */
396
564
  export function findDecl(ident: Ident): DeclIdent {
397
565
  let i: Ident | undefined = ident;
398
566
  do {
@@ -0,0 +1,59 @@
1
+ /** WESL module path utilities for converting between :: and / separators. */
2
+
3
+ /**
4
+ * Convert module path segments to relative file path.
5
+ * Handles package/packageName prefixes and super:: resolution.
6
+ *
7
+ * @param parts - module path as array e.g., ["package", "utils", "helper"]
8
+ * @param packageName - the current package's name (required)
9
+ * @param srcModuleParts - source module path for super:: resolution (optional)
10
+ * @returns relative file path e.g., "utils/helper", or undefined if external
11
+ */
12
+ export function modulePartsToRelativePath(
13
+ parts: string[],
14
+ packageName: string,
15
+ srcModuleParts?: string[],
16
+ ): string | undefined {
17
+ const resolved = srcModuleParts ? resolveSuper(parts, srcModuleParts) : parts;
18
+
19
+ const rootSegment = resolved[0];
20
+ if (rootSegment === "package" || rootSegment === packageName) {
21
+ return resolved.slice(1).join("/");
22
+ }
23
+ return undefined;
24
+ }
25
+
26
+ /** String variant of modulePartsToRelativePath. */
27
+ export function moduleToRelativePath(
28
+ modulePath: string,
29
+ packageName: string,
30
+ srcModulePath?: string,
31
+ ): string | undefined {
32
+ const srcParts = srcModulePath?.split("::");
33
+ const parts = modulePath.split("::");
34
+ return modulePartsToRelativePath(parts, packageName, srcParts);
35
+ }
36
+
37
+ /**
38
+ * Resolve super:: elements to absolute module path.
39
+ *
40
+ * @param parts - module path with potential super:: elements
41
+ * @param srcModuleParts - source module path for context
42
+ * @returns absolute module path parts (no super:: elements)
43
+ */
44
+ export function resolveSuper(
45
+ parts: string[],
46
+ srcModuleParts: string[],
47
+ ): string[] {
48
+ const lastSuper = parts.lastIndexOf("super");
49
+ if (lastSuper === -1) return parts;
50
+ const base = srcModuleParts.slice(0, -(lastSuper + 1));
51
+ return [...base, ...parts.slice(lastSuper + 1)];
52
+ }
53
+
54
+ /** Normalize debug root to end with / or be empty. */
55
+ export function normalizeDebugRoot(root?: string): string {
56
+ if (root === undefined) return "./";
57
+ if (root === "") return "";
58
+ return root.endsWith("/") ? root : root + "/";
59
+ }