wesl 0.7.26 → 0.7.28

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 (67) hide show
  1. package/README.md +1 -1
  2. package/dist/index.d.ts +283 -147
  3. package/dist/index.js +1765 -1143
  4. package/package.json +2 -2
  5. package/src/AbstractElems.ts +264 -82
  6. package/src/ClickableError.ts +8 -1
  7. package/src/Linker.ts +63 -67
  8. package/src/LinkerUtil.ts +141 -7
  9. package/src/LowerAndEmit.ts +660 -304
  10. package/src/Mangler.ts +1 -1
  11. package/src/ModuleResolver.ts +15 -4
  12. package/src/ParseWESL.ts +21 -33
  13. package/src/SrcMap.ts +7 -19
  14. package/src/StandardTypes.ts +1 -1
  15. package/src/WeslDevice.ts +1 -0
  16. package/src/debug/ASTtoString.ts +92 -76
  17. package/src/index.ts +1 -1
  18. package/src/parse/AttachComments.ts +289 -0
  19. package/src/parse/ExpressionUtil.ts +3 -3
  20. package/src/parse/Keywords.ts +1 -1
  21. package/src/parse/ParseAttribute.ts +49 -71
  22. package/src/parse/ParseCall.ts +3 -4
  23. package/src/parse/ParseControlFlow.ts +100 -56
  24. package/src/parse/ParseDirective.ts +9 -8
  25. package/src/parse/ParseDoBlock.ts +63 -0
  26. package/src/parse/ParseExpression.ts +11 -10
  27. package/src/parse/ParseFn.ts +11 -33
  28. package/src/parse/ParseGlobalVar.ts +36 -27
  29. package/src/parse/ParseIdent.ts +4 -5
  30. package/src/parse/ParseLocalVar.ts +16 -12
  31. package/src/parse/ParseLoop.ts +76 -39
  32. package/src/parse/ParseModule.ts +65 -19
  33. package/src/parse/ParseSimpleStatement.ts +112 -66
  34. package/src/parse/ParseStatement.ts +40 -67
  35. package/src/parse/ParseStruct.ts +8 -22
  36. package/src/parse/ParseType.ts +10 -13
  37. package/src/parse/ParseUtil.ts +26 -12
  38. package/src/parse/ParseValueDeclaration.ts +18 -31
  39. package/src/parse/ParseWesl.ts +11 -14
  40. package/src/parse/ParsingContext.ts +11 -9
  41. package/src/parse/WeslStream.ts +138 -121
  42. package/src/parse/stream/RegexMatchers.ts +45 -0
  43. package/src/parse/stream/WeslLexer.ts +249 -0
  44. package/src/test/BevyLink.test.ts +18 -11
  45. package/src/test/ConditionalElif.test.ts +4 -2
  46. package/src/test/DeclCommentEmit.test.ts +122 -0
  47. package/src/test/DoBlock.test.ts +164 -0
  48. package/src/test/FilterValidElements.test.ts +4 -6
  49. package/src/test/Linker.test.ts +23 -7
  50. package/src/test/Mangling.test.ts +8 -4
  51. package/src/test/ParseComments.test.ts +234 -6
  52. package/src/test/ParseConditionsV2.test.ts +47 -175
  53. package/src/test/ParseElifV2.test.ts +12 -33
  54. package/src/test/ParseErrorV2.test.ts +0 -0
  55. package/src/test/ParseWeslV2.test.ts +247 -626
  56. package/src/test/StatementEmit.test.ts +143 -0
  57. package/src/test/StripWesl.ts +24 -3
  58. package/src/test/TestLink.ts +19 -3
  59. package/src/test/TestUtil.ts +18 -8
  60. package/src/test/Tokenizer.test.ts +96 -0
  61. package/src/test/__snapshots__/ParseWeslV2.test.ts.snap +10 -42
  62. package/src/RawEmit.ts +0 -103
  63. package/src/Reflection.ts +0 -336
  64. package/src/TransformBindingStructs.ts +0 -320
  65. package/src/parse/ContentsHelpers.ts +0 -70
  66. package/src/parse/stream/CachingStream.ts +0 -48
  67. package/src/parse/stream/MatchersStream.ts +0 -85
@@ -1,16 +1,28 @@
1
1
  import type {
2
2
  AbstractElem,
3
+ AbstractElemBase,
4
+ AssignElem,
3
5
  AttributeElem,
4
- ContainerElem,
6
+ BlockElem,
7
+ CommentElem,
5
8
  DeclIdentElem,
6
9
  DirectiveElem,
7
10
  ExpressionElem,
8
11
  FnElem,
12
+ ForElem,
13
+ IfElem,
14
+ ModuleElem,
9
15
  NameElem,
10
16
  RefIdentElem,
17
+ Statement,
11
18
  StructElem,
19
+ StructMemberElem,
20
+ SwitchClauseElem,
21
+ SwitchElem,
12
22
  SyntheticElem,
13
- TextElem,
23
+ TypedDeclElem,
24
+ TypeRefElem,
25
+ WhileElem,
14
26
  } from "./AbstractElems.ts";
15
27
  import { assertThatDebug, assertUnreachable } from "./Assertions.ts";
16
28
  import { failIdentElem } from "./ClickableError.ts";
@@ -35,28 +47,115 @@ interface EmitContext {
35
47
  srcBuilder: SrcMapBuilder;
36
48
  conditions: Conditions;
37
49
  extracting: boolean;
50
+ /** Current block nesting depth, for statement indentation. */
51
+ indent: number;
38
52
  }
39
53
 
54
+ /** Declarations emitted structurally (var/let/const/override/gvar/alias/assert),
55
+ * in either local-statement or root-declaration position. */
56
+ type ValueDeclElem = Extract<
57
+ AbstractElem,
58
+ { kind: "var" | "gvar" | "let" | "const" | "override" | "alias" | "assert" }
59
+ >;
60
+
61
+ /** Statement kinds that emitStatement must not follow with a ';': compound
62
+ * statements take none, locals already carry their own, empty needs none. */
63
+ const noSemicolon = new Set<Statement["kind"]>([
64
+ "block",
65
+ "if",
66
+ "for",
67
+ "while",
68
+ "loop",
69
+ "continuing",
70
+ "switch",
71
+ "empty",
72
+ "var",
73
+ "let",
74
+ "const",
75
+ "assert",
76
+ ]);
77
+
40
78
  /** Traverse the AST, starting from root elements, emitting WGSL for each. */
41
79
  export function lowerAndEmit(params: EmitParams): void {
42
80
  const { srcBuilder, rootElems, conditions } = params;
43
81
  const { extracting = true, skipConditionalFiltering = false } = params;
44
82
 
45
- const emitContext: EmitContext = { conditions, srcBuilder, extracting };
83
+ const emitContext: EmitContext = {
84
+ conditions,
85
+ srcBuilder,
86
+ extracting,
87
+ indent: 0,
88
+ };
46
89
  const validElements = skipConditionalFiltering
47
90
  ? rootElems
48
91
  : filterValidElements(rootElems, conditions);
49
92
  for (const e of validElements) lowerAndEmitElem(e, emitContext);
50
93
  }
51
94
 
95
+ /** Format a diagnostic control as "(severity, rule)" for @diagnostic text. */
96
+ export function diagnosticControlToString(
97
+ severity: NameElem,
98
+ rule: [NameElem, NameElem | null],
99
+ ): string {
100
+ const ruleStr = rule[0].name + (rule[1] !== null ? "." + rule[1].name : "");
101
+ return `(${severity.name}, ${ruleStr})`;
102
+ }
103
+
104
+ /** Render an expression back to WGSL source text (template args elided as <...>). */
105
+ export function expressionToString(elem: ExpressionElem): string {
106
+ const { kind } = elem;
107
+ switch (kind) {
108
+ case "binary-expression": {
109
+ const left = expressionToString(elem.left);
110
+ const right = expressionToString(elem.right);
111
+ return `${left} ${elem.operator.value} ${right}`;
112
+ }
113
+ case "unary-expression":
114
+ return `${elem.operator.value}${expressionToString(elem.expression)}`;
115
+ case "ref":
116
+ return elem.ident.originalName;
117
+ case "literal":
118
+ return elem.value;
119
+ case "parenthesized-expression":
120
+ return `(${expressionToString(elem.expression)})`;
121
+ case "component-expression":
122
+ return `${expressionToString(elem.base)}[${elem.access}]`;
123
+ case "component-member-expression":
124
+ return `${expressionToString(elem.base)}.${elem.access}`;
125
+ case "call-expression": {
126
+ const fn = elem.function;
127
+ const name =
128
+ fn.kind === "ref" ? fn.ident.originalName : fn.name.originalName;
129
+ const targs = elem.templateArgs ? `<...>` : "";
130
+ const args = elem.arguments.map(expressionToString).join(", ");
131
+ return `${name}${targs}(${args})`;
132
+ }
133
+ case "type":
134
+ return elem.name.originalName;
135
+ default:
136
+ assertUnreachable(kind);
137
+ }
138
+ }
139
+
140
+ /** Trace through refersTo links until we find the declaration. */
141
+ export function findDecl(ident: Ident): DeclIdent {
142
+ let i: Ident | undefined = ident;
143
+ do {
144
+ if (i.kind === "decl") return i;
145
+ i = i.refersTo;
146
+ } while (i);
147
+
148
+ // TODO show source position if this can happen in a non buggy linker.
149
+ throw new Error(`unresolved identifer: ${ident.originalName}`);
150
+ }
151
+
52
152
  function lowerAndEmitElem(e: AbstractElem, ctx: EmitContext): void {
53
153
  switch (e.kind) {
54
154
  case "import":
55
155
  return; // import statements are dropped from emitted text
156
+ case "do":
157
+ return; // do blocks are CPU-only, dropped from emitted text
56
158
 
57
- case "text":
58
- emitText(e, ctx);
59
- return;
60
159
  case "name":
61
160
  emitName(e, ctx);
62
161
  return;
@@ -82,19 +181,28 @@ function lowerAndEmitElem(e: AbstractElem, ctx: EmitContext): void {
82
181
  return;
83
182
 
84
183
  case "param":
184
+ emitAttributes(e.attributes, ctx);
185
+ emitTypedDecl(e.name, ctx);
186
+ return;
85
187
  case "typeDecl":
188
+ emitTypedDecl(e, ctx);
189
+ return;
86
190
  case "member":
87
- case "memberRef":
191
+ emitMember(e, ctx);
192
+ return;
193
+
88
194
  case "expression":
89
- case "type":
195
+ emitExpression(e.expression, ctx);
196
+ return;
197
+
198
+ // Switch clauses are normally emitted structurally by emitSwitch; handle the
199
+ // standalone case (e.g. a clause reached via a container walk) the same way.
90
200
  case "switch-clause":
91
- emitContents(e, ctx);
201
+ emitSwitchClause(e, ctx);
92
202
  return;
93
203
 
94
- // "stuff" elements (compound statements) need trimming for proper formatting
95
- // LATER get rid of "stuff" elements
96
- case "stuff":
97
- emitStuff(e, ctx);
204
+ case "type":
205
+ emitTypeRef(e, ctx);
98
206
  return;
99
207
 
100
208
  case "module":
@@ -103,8 +211,22 @@ function lowerAndEmitElem(e: AbstractElem, ctx: EmitContext): void {
103
211
 
104
212
  case "var":
105
213
  case "let":
106
- case "statement":
214
+ case "block":
215
+ case "if":
216
+ case "for":
217
+ case "while":
218
+ case "loop":
107
219
  case "continuing":
220
+ case "switch":
221
+ case "return":
222
+ case "break":
223
+ case "continue":
224
+ case "discard":
225
+ case "assign":
226
+ case "increment":
227
+ case "decrement":
228
+ case "call":
229
+ case "empty":
108
230
  emitStatement(e, ctx);
109
231
  return;
110
232
 
@@ -118,12 +240,16 @@ function lowerAndEmitElem(e: AbstractElem, ctx: EmitContext): void {
118
240
 
119
241
  case "fn":
120
242
  emitRootElemNl(ctx);
243
+ emitRootLeading(e, ctx);
121
244
  emitFn(e, ctx);
245
+ emitTrailingComments(e, ctx);
122
246
  return;
123
247
 
124
248
  case "struct":
125
249
  emitRootElemNl(ctx);
250
+ emitRootLeading(e, ctx);
126
251
  emitStruct(e, ctx);
252
+ emitTrailingComments(e, ctx);
127
253
  return;
128
254
 
129
255
  case "attribute":
@@ -131,7 +257,11 @@ function lowerAndEmitElem(e: AbstractElem, ctx: EmitContext): void {
131
257
  return;
132
258
 
133
259
  case "directive":
260
+ // each top-level directive on its own line (no source TextElems separate them)
261
+ ctx.srcBuilder.addNl();
262
+ emitRootLeading(e, ctx);
134
263
  emitDirective(e, ctx);
264
+ emitTrailingComments(e, ctx);
135
265
  return;
136
266
 
137
267
  default:
@@ -139,52 +269,123 @@ function lowerAndEmitElem(e: AbstractElem, ctx: EmitContext): void {
139
269
  }
140
270
  }
141
271
 
142
- function emitStuff(e: ContainerElem, ctx: EmitContext): void {
143
- emitContentsWithTrimming(e, ctx);
272
+ function emitName(e: NameElem, ctx: EmitContext): void {
273
+ ctx.srcBuilder.add(e.name, e.start, e.end);
144
274
  }
145
275
 
146
- function emitModule(e: ContainerElem, ctx: EmitContext): void {
147
- // Skip whitespace-only text elements at module level
148
- const validElements = filterValidElements(e.contents, ctx.conditions);
149
- for (const child of validElements) {
150
- if (child.kind === "text") {
151
- const text = child.srcModule.src.slice(child.start, child.end);
152
- if (text.trim() === "") continue;
153
- }
154
- lowerAndEmitElem(child, ctx);
276
+ function emitSynthetic(e: SyntheticElem, ctx: EmitContext): void {
277
+ const { text } = e;
278
+ ctx.srcBuilder.addSynthetic(text, text, 0, text.length);
279
+ }
280
+
281
+ function emitRefIdent(e: RefIdentElem, ctx: EmitContext): void {
282
+ if (e.ident.std) {
283
+ ctx.srcBuilder.add(e.ident.originalName, e.start, e.end);
284
+ } else {
285
+ ctx.srcBuilder.add(displayName(findDecl(e.ident)), e.start, e.end);
155
286
  }
156
287
  }
157
288
 
158
- function emitStatement(
159
- e: Extract<
160
- ContainerElem,
161
- { kind: "var" | "let" | "statement" | "continuing" }
162
- >,
289
+ function emitDeclIdent(e: DeclIdentElem, ctx: EmitContext): void {
290
+ ctx.srcBuilder.add(displayName(e.ident), e.start, e.end);
291
+ }
292
+
293
+ /** Emit an expression with any comments attached to it. Comments inside an
294
+ * expression (e.g. `foo(1, /* x *\/ 2)`) ride on the relevant sub-node and are
295
+ * emitted inline around it here. */
296
+ function emitExpression(e: ExpressionElem, ctx: EmitContext): void {
297
+ emitInlineLeading(e, ctx);
298
+ emitExpressionCore(e, ctx);
299
+ emitInlineTrailing(e, ctx);
300
+ }
301
+
302
+ function emitAttributes(
303
+ attributes: AttributeElem[] | undefined,
163
304
  ctx: EmitContext,
164
305
  ): void {
165
- const attrsInContents =
166
- e.contents.length > 0 && e.contents[0].kind === "attribute";
167
- if (!attrsInContents) {
168
- emitAttributes(e.attributes, ctx);
306
+ attributes?.forEach(a => {
307
+ emitInlineLeading(a, ctx);
308
+ if (emitAttribute(a, ctx)) {
309
+ emitInlineTrailing(a, ctx);
310
+ ctx.srcBuilder.add(" ", a.start, a.end);
311
+ }
312
+ });
313
+ }
314
+
315
+ /** Emit a declared identifier with its optional `: type` annotation. */
316
+ function emitTypedDecl(name: TypedDeclElem, ctx: EmitContext): void {
317
+ emitInlineLeading(name, ctx);
318
+ emitDeclIdent(name.decl, ctx);
319
+ if (name.typeRef) {
320
+ ctx.srcBuilder.appendNext(": ");
321
+ emitTypeRef(name.typeRef, ctx);
322
+ }
323
+ emitInlineTrailing(name, ctx);
324
+ }
325
+
326
+ /** Emit a struct member from its typed fields: `[attrs] name: type`. */
327
+ function emitMember(member: StructMemberElem, ctx: EmitContext): void {
328
+ emitAttributes(member.attributes, ctx);
329
+ emitName(member.name, ctx);
330
+ ctx.srcBuilder.appendNext(": ");
331
+ emitTypeRef(member.typeRef, ctx);
332
+ }
333
+
334
+ /** A `case sel, ...:` or `default:` clause with its `{ ... }` body. The selector
335
+ * colon is optional in WGSL but kept here as the canonical form. */
336
+ function emitSwitchClause(e: SwitchClauseElem, ctx: EmitContext): void {
337
+ const builder = ctx.srcBuilder;
338
+ emitLeadingComments(e, ctx);
339
+ newLine(ctx);
340
+ emitAttributes(e.attributes, ctx);
341
+ const defaultOnly = e.selectors.length === 1 && e.selectors[0] === "default";
342
+ if (defaultOnly) {
343
+ builder.appendNext("default");
344
+ } else {
345
+ builder.appendNext("case ");
346
+ e.selectors.forEach((sel, i) => {
347
+ if (i > 0) builder.appendNext(", ");
348
+ if (sel === "default") builder.appendNext("default");
349
+ else emitExpression(sel, ctx);
350
+ });
169
351
  }
170
- emitContents(e, ctx);
352
+ builder.appendNext(": ");
353
+ emitBlock(e.body, ctx);
354
+ emitTrailingComments(e, ctx);
355
+ }
356
+
357
+ /** Emit a type reference structurally: name plus an optional <...> arg list. */
358
+ function emitTypeRef(e: TypeRefElem, ctx: EmitContext): void {
359
+ emitRefIdent(e.name.refIdentElem, ctx);
360
+ if (e.templateParams) emitTemplateArgs(e.templateParams, ctx);
361
+ }
362
+
363
+ function emitModule(e: ModuleElem, ctx: EmitContext): void {
364
+ // The module's typed children emit structurally, each handling its own
365
+ // leading blank lines (emitRootElemNl / emitRootDecl); no TextElems remain.
366
+ const validElements = filterValidElements(e.decls, ctx.conditions);
367
+ for (const child of validElements) lowerAndEmitElem(child, ctx);
368
+ }
369
+
370
+ /** Emit one statement on its own line, with attached leading/trailing comments. */
371
+ function emitStatement(stmt: Statement, ctx: EmitContext): void {
372
+ emitLeadingComments(stmt, ctx);
373
+ newLine(ctx);
374
+ emitCoreSemi(stmt, ctx);
375
+ emitTrailingComments(stmt, ctx);
171
376
  }
172
377
 
173
378
  function emitRootDecl(
174
379
  e: Extract<
175
- ContainerElem,
380
+ ValueDeclElem,
176
381
  { kind: "override" | "const" | "assert" | "alias" | "gvar" }
177
382
  >,
178
383
  ctx: EmitContext,
179
384
  ): void {
180
385
  emitRootElemNl(ctx);
181
- const attrsInContents =
182
- e.contents.length > 0 && e.contents[0].kind === "attribute";
183
- if (!attrsInContents) {
184
- emitAttributes(e.attributes, ctx);
185
- }
186
-
187
- emitContentsWithTrimming(e, ctx);
386
+ emitRootLeading(e, ctx);
387
+ emitValueDecl(e, ctx);
388
+ emitTrailingComments(e, ctx);
188
389
  }
189
390
 
190
391
  /** Emit newlines between root elements. */
@@ -193,12 +394,14 @@ function emitRootElemNl(ctx: EmitContext): void {
193
394
  ctx.srcBuilder.addNl();
194
395
  }
195
396
 
196
- function emitText(e: TextElem, ctx: EmitContext): void {
197
- ctx.srcBuilder.addCopy(e.start, e.end);
198
- }
199
-
200
- function emitName(e: NameElem, ctx: EmitContext): void {
201
- ctx.srcBuilder.add(e.name, e.start, e.end);
397
+ /** Leading comments for a root declaration, each on its own line. The inter-decl
398
+ * spacing already left us at a fresh line, so a newline follows each comment
399
+ * (rather than preceding it as in emitLeadingComments). */
400
+ function emitRootLeading(e: AbstractElemBase, ctx: EmitContext): void {
401
+ for (const c of e.commentsBefore ?? []) {
402
+ emitComment(c, ctx);
403
+ newLine(ctx);
404
+ }
202
405
  }
203
406
 
204
407
  /** Emit function explicitly to control commas between conditional parameters. */
@@ -209,19 +412,17 @@ function emitFn(e: FnElem, ctx: EmitContext): void {
209
412
  emitAttributes(attributes, ctx);
210
413
 
211
414
  builder.add("fn ", name.start - 3, name.start);
415
+ emitInlineLeading(name, ctx);
212
416
  emitDeclIdent(name, ctx);
417
+ emitInlineTrailing(name, ctx);
213
418
 
214
419
  builder.appendNext("(");
215
420
  const validParams = filterValidElements(params, conditions);
216
421
  validParams.forEach((p, i) => {
217
- // Emit attributes separately only if not already in contents
218
- // LATER stop including attributes in contents when we emit from ast
219
- const attrsInContents =
220
- p.contents.length > 0 && p.contents[0].kind === "attribute";
221
- if (!attrsInContents) {
222
- emitAttributes(p.attributes, ctx);
223
- }
224
- emitContentsNoWs(p as ContainerElem, ctx);
422
+ emitInlineLeading(p, ctx);
423
+ emitAttributes(p.attributes, ctx);
424
+ emitTypedDecl(p.name, ctx);
425
+ emitInlineTrailing(p, ctx);
225
426
  if (i < validParams.length - 1) {
226
427
  builder.appendNext(", ");
227
428
  }
@@ -231,23 +432,21 @@ function emitFn(e: FnElem, ctx: EmitContext): void {
231
432
  if (returnType) {
232
433
  builder.appendNext("-> ");
233
434
  emitAttributes(returnAttributes, ctx);
234
- emitContentsNoWs(returnType, ctx);
435
+ emitInlineLeading(returnType, ctx);
436
+ emitTypeRef(returnType, ctx);
235
437
  builder.appendNext(" ");
236
438
  }
237
439
 
238
- emitContents(body, ctx);
440
+ emitBlock(body, ctx);
239
441
  }
240
442
 
241
- function emitAttributes(
242
- attributes: AttributeElem[] | undefined,
243
- ctx: EmitContext,
244
- ): void {
245
- attributes?.forEach(a => {
246
- const emitted = emitAttribute(a, ctx);
247
- if (emitted) {
248
- ctx.srcBuilder.add(" ", a.start, a.end);
249
- }
250
- });
443
+ /** Trailing comments: kept on the element's line, after its text. */
444
+ function emitTrailingComments(e: AbstractElemBase, ctx: EmitContext): void {
445
+ if (!e.commentsAfter) return;
446
+ for (const c of e.commentsAfter) {
447
+ ctx.srcBuilder.appendNext(" ");
448
+ emitComment(c, ctx);
449
+ }
251
450
  }
252
451
 
253
452
  /** Emit structs explicitly to control commas between conditional members. */
@@ -265,174 +464,26 @@ function emitStruct(e: StructElem, ctx: EmitContext): void {
265
464
 
266
465
  emitAttributes(attributes, ctx);
267
466
  srcBuilder.add("struct ", start, name.start);
467
+ emitInlineLeading(name, ctx);
268
468
  emitDeclIdent(name, ctx);
469
+ emitInlineTrailing(name, ctx);
269
470
 
270
- if (validLength === 1) {
471
+ // a member with attached comments forces the multi-line form (the one-line
472
+ // `{ x: i32 }` form has nowhere to put an own-line comment)
473
+ if (validLength === 1 && !hasComments(validMembers[0])) {
271
474
  srcBuilder.appendNext(" { ");
272
- emitContentsWithTrimming(validMembers[0] as ContainerElem, ctx);
475
+ emitMember(validMembers[0], ctx);
273
476
  srcBuilder.appendNext(" }");
274
477
  srcBuilder.addNl();
275
478
  } else {
276
479
  srcBuilder.appendNext(" {");
277
480
  srcBuilder.addNl();
278
-
279
- validMembers.forEach(m => {
280
- srcBuilder.appendNext(" ");
281
- emitContentsNoWs(m as ContainerElem, ctx);
282
- srcBuilder.appendNext(",");
283
- srcBuilder.addNl();
284
- });
285
-
481
+ for (const m of validMembers) emitMemberLine(m, ctx);
286
482
  srcBuilder.appendNext("}");
287
483
  srcBuilder.addNl();
288
484
  }
289
485
  }
290
486
 
291
- function warnEmptyStruct(e: StructElem): void {
292
- const { name, members } = e;
293
- const condStr = members.length ? "(with current conditions)" : "";
294
- const message = `struct '${name.ident.originalName}' has no members ${condStr}`;
295
- failIdentElem(name, message);
296
- }
297
-
298
- function emitSynthetic(e: SyntheticElem, ctx: EmitContext): void {
299
- const { text } = e;
300
- ctx.srcBuilder.addSynthetic(text, text, 0, text.length);
301
- }
302
-
303
- function emitContents(elem: ContainerElem, ctx: EmitContext): void {
304
- const validElements = filterValidElements(elem.contents, ctx.conditions);
305
- for (const e of validElements) lowerAndEmitElem(e, ctx);
306
- }
307
-
308
- /** Emit contents with leading/trailing whitespace trimming (V2 parser). */
309
- function emitContentsWithTrimming(elem: ContainerElem, ctx: EmitContext): void {
310
- const validElements = filterValidElements(elem.contents, ctx.conditions);
311
-
312
- // Find first/last non-conditional-attribute indices for trimming
313
- const firstEmit = validElements.findIndex(e => !isConditionalAttr(e));
314
- const lastEmit = validElements.findLastIndex(e => !isConditionalAttr(e));
315
-
316
- validElements.forEach((elem, i) => {
317
- if (elem.kind === "text") {
318
- let text = elem.srcModule.src.slice(elem.start, elem.end);
319
- if (i === firstEmit) text = text.trimStart();
320
- if (i === lastEmit) text = text.trimEnd();
321
- if (text) ctx.srcBuilder.add(text, elem.start, elem.end);
322
- } else {
323
- lowerAndEmitElem(elem, ctx);
324
- }
325
- });
326
- }
327
-
328
- function isConditionalAttr(e: AbstractElem): boolean {
329
- if (e.kind !== "attribute") return false;
330
- const { kind } = e.attribute;
331
- return kind === "@if" || kind === "@elif" || kind === "@else";
332
- }
333
-
334
- /** Emit contents without whitespace. */
335
- function emitContentsNoWs(elem: ContainerElem, ctx: EmitContext): void {
336
- const validElements = filterValidElements(elem.contents, ctx.conditions);
337
- validElements.forEach(e => {
338
- if (e.kind === "text") {
339
- const { srcModule, start, end } = e;
340
- const text = srcModule.src.slice(start, end);
341
- if (text.trim() === "") {
342
- return;
343
- }
344
- }
345
- lowerAndEmitElem(e, ctx);
346
- });
347
- }
348
-
349
- function emitRefIdent(e: RefIdentElem, ctx: EmitContext): void {
350
- if (e.ident.std) {
351
- ctx.srcBuilder.add(e.ident.originalName, e.start, e.end);
352
- } else {
353
- const declIdent = findDecl(e.ident);
354
- const mangledName = displayName(declIdent);
355
- ctx.srcBuilder.add(mangledName!, e.start, e.end);
356
- }
357
- }
358
-
359
- function emitDeclIdent(e: DeclIdentElem, ctx: EmitContext): void {
360
- const mangledName = displayName(e.ident);
361
- ctx.srcBuilder.add(mangledName!, e.start, e.end);
362
- }
363
-
364
- function emitExpression(e: ExpressionElem, ctx: EmitContext): void {
365
- const { kind } = e;
366
-
367
- if (kind === "literal") {
368
- ctx.srcBuilder.add(e.value, e.start, e.end);
369
- return;
370
- }
371
-
372
- if (kind === "ref") {
373
- emitRefIdent(e, ctx);
374
- return;
375
- }
376
-
377
- if (kind === "type") {
378
- emitContents(e, ctx);
379
- return;
380
- }
381
-
382
- if (kind === "binary-expression") {
383
- emitExpression(e.left, ctx);
384
- ctx.srcBuilder.add(
385
- ` ${e.operator.value} `,
386
- e.operator.span[0],
387
- e.operator.span[1],
388
- );
389
- emitExpression(e.right, ctx);
390
- return;
391
- }
392
-
393
- if (kind === "unary-expression") {
394
- ctx.srcBuilder.add(
395
- e.operator.value,
396
- e.operator.span[0],
397
- e.operator.span[1],
398
- );
399
- emitExpression(e.expression, ctx);
400
- return;
401
- }
402
-
403
- if (kind === "parenthesized-expression") {
404
- emitExpression(e.expression, ctx);
405
- return;
406
- }
407
-
408
- if (kind === "call-expression") {
409
- emitExpression(e.function, ctx);
410
- if (e.templateArgs) {
411
- for (const targ of e.templateArgs) lowerAndEmitElem(targ, ctx);
412
- }
413
- for (const arg of e.arguments) {
414
- emitExpression(arg, ctx);
415
- }
416
- return;
417
- }
418
-
419
- if (kind === "component-expression") {
420
- emitExpression(e.base, ctx);
421
- emitExpression(e.access, ctx);
422
- return;
423
- }
424
-
425
- if (kind === "component-member-expression") {
426
- emitExpression(e.base, ctx);
427
- if (e.access.kind === "name") {
428
- ctx.srcBuilder.add(e.access.name, e.access.start, e.access.end);
429
- }
430
- return;
431
- }
432
-
433
- assertUnreachable(kind);
434
- }
435
-
436
487
  function emitAttribute(e: AttributeElem, ctx: EmitContext): boolean {
437
488
  const { kind } = e.attribute;
438
489
 
@@ -449,18 +500,14 @@ function emitAttribute(e: AttributeElem, ctx: EmitContext): boolean {
449
500
  }
450
501
 
451
502
  if (kind === "@builtin") {
452
- ctx.srcBuilder.add(
453
- "@builtin(" + e.attribute.param.name + ")",
454
- e.start,
455
- e.end,
456
- );
503
+ const builtinStr = `@builtin(${e.attribute.param.name})`;
504
+ ctx.srcBuilder.add(builtinStr, e.start, e.end);
457
505
  return true;
458
506
  }
459
507
 
460
508
  if (kind === "@diagnostic") {
461
- const diagStr =
462
- "@diagnostic" +
463
- diagnosticControlToString(e.attribute.severity, e.attribute.rule);
509
+ const { severity, rule } = e.attribute;
510
+ const diagStr = `@diagnostic${diagnosticControlToString(severity, rule)}`;
464
511
  ctx.srcBuilder.add(diagStr, e.start, e.end);
465
512
  return true;
466
513
  }
@@ -474,68 +521,6 @@ function emitAttribute(e: AttributeElem, ctx: EmitContext): boolean {
474
521
  assertUnreachable(kind);
475
522
  }
476
523
 
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
- emitContents(params[i], ctx);
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);
494
- }
495
-
496
- export function diagnosticControlToString(
497
- severity: NameElem,
498
- rule: [NameElem, NameElem | null],
499
- ): string {
500
- const ruleStr = rule[0].name + (rule[1] !== null ? "." + rule[1].name : "");
501
- return `(${severity.name}, ${ruleStr})`;
502
- }
503
-
504
- export function expressionToString(elem: ExpressionElem): string {
505
- const { kind } = elem;
506
- switch (kind) {
507
- case "binary-expression": {
508
- const left = expressionToString(elem.left);
509
- const right = expressionToString(elem.right);
510
- return `${left} ${elem.operator.value} ${right}`;
511
- }
512
- case "unary-expression":
513
- return `${elem.operator.value}${expressionToString(elem.expression)}`;
514
- case "ref":
515
- return elem.ident.originalName;
516
- case "literal":
517
- return elem.value;
518
- case "parenthesized-expression":
519
- return `(${expressionToString(elem.expression)})`;
520
- case "component-expression":
521
- return `${expressionToString(elem.base)}[${elem.access}]`;
522
- case "component-member-expression":
523
- return `${expressionToString(elem.base)}.${elem.access}`;
524
- case "call-expression": {
525
- const fn = elem.function;
526
- const name =
527
- fn.kind === "ref" ? fn.ident.originalName : fn.name.originalName;
528
- const targs = elem.templateArgs ? `<...>` : "";
529
- const args = elem.arguments.map(expressionToString).join(", ");
530
- return `${name}${targs}(${args})`;
531
- }
532
- case "type":
533
- return elem.name.originalName;
534
- default:
535
- assertUnreachable(kind);
536
- }
537
- }
538
-
539
524
  function emitDirective(e: DirectiveElem, ctx: EmitContext): void {
540
525
  const { directive } = e;
541
526
  const { kind } = directive;
@@ -566,16 +551,387 @@ function displayName(declIdent: DeclIdent): string {
566
551
  return declIdent.mangledName || declIdent.originalName;
567
552
  }
568
553
 
569
- /** Trace through refersTo links until we find the declaration. */
570
- export function findDecl(ident: Ident): DeclIdent {
571
- let i: Ident | undefined = ident;
572
- do {
573
- if (i.kind === "decl") {
574
- return i;
554
+ /** Leading comments on an inline node: block comments get a trailing space, line
555
+ * comments force a newline so the following code is not swallowed. */
556
+ function emitInlineLeading(e: AbstractElemBase, ctx: EmitContext): void {
557
+ for (const c of e.commentsBefore ?? []) {
558
+ emitComment(c, ctx);
559
+ if (c.style === "line") newLine(ctx);
560
+ else ctx.srcBuilder.appendNext(" ");
561
+ }
562
+ }
563
+
564
+ function emitExpressionCore(e: ExpressionElem, ctx: EmitContext): void {
565
+ const builder = ctx.srcBuilder;
566
+ switch (e.kind) {
567
+ case "literal":
568
+ builder.add(e.value, e.start, e.end);
569
+ return;
570
+ case "ref":
571
+ emitRefIdent(e, ctx);
572
+ return;
573
+ case "type":
574
+ emitTypeRef(e, ctx);
575
+ return;
576
+ case "binary-expression": {
577
+ const [start, end] = e.operator.span;
578
+ emitExpression(e.left, ctx);
579
+ builder.add(` ${e.operator.value} `, start, end);
580
+ emitExpression(e.right, ctx);
581
+ return;
575
582
  }
576
- i = i.refersTo;
577
- } while (i);
583
+ case "unary-expression": {
584
+ const { value, start, end } = e.operator;
585
+ builder.add(value, start, end);
586
+ emitExpression(e.expression, ctx);
587
+ return;
588
+ }
589
+ case "parenthesized-expression":
590
+ builder.appendNext("(");
591
+ emitExpression(e.expression, ctx);
592
+ builder.appendNext(")");
593
+ return;
594
+ case "call-expression":
595
+ emitExpression(e.function, ctx);
596
+ if (e.templateArgs) emitTemplateArgs(e.templateArgs, ctx);
597
+ builder.appendNext("(");
598
+ e.arguments.forEach((arg, i) => {
599
+ if (i > 0) builder.appendNext(", ");
600
+ emitExpression(arg, ctx);
601
+ });
602
+ builder.appendNext(")");
603
+ return;
604
+ case "component-expression":
605
+ emitExpression(e.base, ctx);
606
+ builder.appendNext("[");
607
+ emitExpression(e.access, ctx);
608
+ builder.appendNext("]");
609
+ return;
610
+ case "component-member-expression":
611
+ emitExpression(e.base, ctx);
612
+ builder.add("." + e.access.name, e.access.start, e.access.end);
613
+ return;
614
+ default:
615
+ assertUnreachable(e);
616
+ }
617
+ }
578
618
 
579
- // TODO show source position if this can happen in a non buggy linker.
580
- throw new Error(`unresolved identifer: ${ident.originalName}`);
619
+ /** Trailing comments on an inline node: a leading space, plus a newline after a
620
+ * line comment so following code stays off its line. */
621
+ function emitInlineTrailing(e: AbstractElemBase, ctx: EmitContext): void {
622
+ for (const c of e.commentsAfter ?? []) {
623
+ ctx.srcBuilder.appendNext(" ");
624
+ emitComment(c, ctx);
625
+ if (c.style === "line") newLine(ctx);
626
+ }
627
+ }
628
+
629
+ /** Leading comments: each on its own indented line above the element. */
630
+ function emitLeadingComments(e: AbstractElemBase, ctx: EmitContext): void {
631
+ if (!e.commentsBefore) return;
632
+ for (const c of e.commentsBefore) {
633
+ if (c.blankBefore) ctx.srcBuilder.addNl();
634
+ newLine(ctx);
635
+ emitComment(c, ctx);
636
+ }
637
+ }
638
+
639
+ /** Start a fresh line at the current indent. */
640
+ function newLine(ctx: EmitContext): void {
641
+ ctx.srcBuilder.addNl();
642
+ if (ctx.indent > 0) ctx.srcBuilder.appendNext(" ".repeat(ctx.indent));
643
+ }
644
+
645
+ /** Emit a `{ ... }` block, one statement per indented line. A block with no
646
+ * statements collapses to `{ }` unless it holds dangling inner comments. */
647
+ function emitBlock(e: BlockElem, ctx: EmitContext): void {
648
+ emitAttributes(e.attributes, ctx);
649
+ const stmts = filterValidElements(e.body, ctx.conditions);
650
+ if (stmts.length === 0 && !e.innerComments?.length) {
651
+ ctx.srcBuilder.appendNext("{ }");
652
+ return;
653
+ }
654
+ ctx.srcBuilder.appendNext("{");
655
+ const inner = childIndent(ctx);
656
+ for (const comment of e.innerComments ?? []) {
657
+ newLine(inner);
658
+ emitComment(comment, inner);
659
+ }
660
+ for (const stmt of stmts) emitStatement(stmt, inner);
661
+ newLine(ctx);
662
+ ctx.srcBuilder.appendNext("}");
663
+ }
664
+
665
+ /** Emit a comma-separated template argument list: <a, b, c>. */
666
+ function emitTemplateArgs(args: ExpressionElem[], ctx: EmitContext): void {
667
+ ctx.srcBuilder.appendNext("<");
668
+ args.forEach((a, i) => {
669
+ if (i > 0) ctx.srcBuilder.appendNext(", ");
670
+ emitExpression(a, ctx);
671
+ });
672
+ ctx.srcBuilder.appendNext(">");
673
+ }
674
+
675
+ /** Emit a statement's syntax followed by ';' unless its kind takes none. */
676
+ function emitCoreSemi(stmt: Statement, ctx: EmitContext): void {
677
+ emitStatementCore(stmt, ctx);
678
+ if (!noSemicolon.has(stmt.kind)) ctx.srcBuilder.appendNext(";");
679
+ }
680
+
681
+ /** Emit a declaration from its typed fields, including its trailing ';':
682
+ * `[attrs] var<...> name: T = init;`, `const name = init;`, `override n;`,
683
+ * `alias name = T;`, `const_assert expr;`. */
684
+ function emitValueDecl(e: ValueDeclElem, ctx: EmitContext): void {
685
+ emitAttributes(e.attributes, ctx);
686
+ const builder = ctx.srcBuilder;
687
+ switch (e.kind) {
688
+ case "var":
689
+ case "gvar":
690
+ builder.appendNext("var");
691
+ if (e.template) emitVarTemplate(e.template, ctx);
692
+ builder.appendNext(" ");
693
+ emitTypedDecl(e.name, ctx);
694
+ emitInit(e.init, ctx);
695
+ break;
696
+ case "let":
697
+ case "const":
698
+ case "override":
699
+ builder.appendNext(`${e.kind} `);
700
+ emitTypedDecl(e.name, ctx);
701
+ emitInit(e.init, ctx);
702
+ break;
703
+ case "alias":
704
+ builder.appendNext("alias ");
705
+ emitInlineLeading(e.name, ctx);
706
+ emitDeclIdent(e.name, ctx);
707
+ emitInlineTrailing(e.name, ctx);
708
+ builder.appendNext(" = ");
709
+ emitTypeRef(e.typeRef, ctx);
710
+ break;
711
+ case "assert":
712
+ builder.appendNext("const_assert ");
713
+ emitExpression(e.expression, ctx);
714
+ break;
715
+ default:
716
+ assertUnreachable(e);
717
+ }
718
+ builder.appendNext(";");
719
+ }
720
+
721
+ function emitComment(c: CommentElem, ctx: EmitContext): void {
722
+ ctx.srcBuilder.add(c.srcModule.src.slice(c.start, c.end), c.start, c.end);
723
+ }
724
+
725
+ function warnEmptyStruct(e: StructElem): void {
726
+ const { name, members } = e;
727
+ const condStr = members.length ? "(with current conditions)" : "";
728
+ const message = `struct '${name.ident.originalName}' has no members ${condStr}`;
729
+ failIdentElem(name, message);
730
+ }
731
+
732
+ /** Whether an element carries any attached comments. */
733
+ function hasComments(e: AbstractElemBase): boolean {
734
+ return !!(e.commentsBefore?.length || e.commentsAfter?.length);
735
+ }
736
+
737
+ /** Emit one struct member on its own line: leading comments above, `name: type,`,
738
+ * then trailing comments inline. */
739
+ function emitMemberLine(m: StructMemberElem, ctx: EmitContext): void {
740
+ const builder = ctx.srcBuilder;
741
+ for (const c of m.commentsBefore ?? []) {
742
+ builder.appendNext(" ");
743
+ emitComment(c, ctx);
744
+ builder.addNl();
745
+ }
746
+ builder.appendNext(" ");
747
+ emitMember(m, ctx);
748
+ builder.appendNext(",");
749
+ for (const c of m.commentsAfter ?? []) {
750
+ builder.appendNext(" ");
751
+ emitComment(c, ctx);
752
+ }
753
+ builder.addNl();
754
+ }
755
+
756
+ function emitStandardAttribute(e: AttributeElem, ctx: EmitContext): void {
757
+ if (e.attribute.kind !== "@attribute") return;
758
+
759
+ const { params } = e.attribute;
760
+ if (!params || params.length === 0) {
761
+ ctx.srcBuilder.add("@" + e.attribute.name, e.start, e.end);
762
+ return;
763
+ }
764
+
765
+ ctx.srcBuilder.add("@" + e.attribute.name + "(", e.start, params[0].start);
766
+ params.forEach((param, i) => {
767
+ if (i > 0) ctx.srcBuilder.appendNext(", ");
768
+ emitExpression(param.expression, ctx);
769
+ });
770
+ ctx.srcBuilder.add(")", params[params.length - 1].end, e.end);
771
+ }
772
+
773
+ /** A child context indented one level deeper. */
774
+ function childIndent(ctx: EmitContext): EmitContext {
775
+ return { ...ctx, indent: ctx.indent + 1 };
776
+ }
777
+
778
+ /** Emit a statement's syntax, without surrounding line breaks, ';', or comments. */
779
+ function emitStatementCore(stmt: Statement, ctx: EmitContext): void {
780
+ if (
781
+ stmt.kind === "var" ||
782
+ stmt.kind === "let" ||
783
+ stmt.kind === "const" ||
784
+ stmt.kind === "assert"
785
+ ) {
786
+ emitValueDecl(stmt, ctx);
787
+ return;
788
+ }
789
+ // a block prints its own attributes (before its '{'); everything else prints
790
+ // them before its keyword.
791
+ if (stmt.kind !== "block") emitAttributes(stmt.attributes, ctx);
792
+ const builder = ctx.srcBuilder;
793
+ switch (stmt.kind) {
794
+ case "block":
795
+ emitBlock(stmt, ctx);
796
+ return;
797
+ case "if":
798
+ emitIf(stmt, ctx);
799
+ return;
800
+ case "for":
801
+ emitFor(stmt, ctx);
802
+ return;
803
+ case "while":
804
+ emitWhile(stmt, ctx);
805
+ return;
806
+ case "loop":
807
+ builder.appendNext("loop ");
808
+ emitBlock(stmt.body, ctx);
809
+ return;
810
+ case "continuing":
811
+ builder.appendNext("continuing ");
812
+ emitBlock(stmt.body, ctx);
813
+ return;
814
+ case "switch":
815
+ emitSwitch(stmt, ctx);
816
+ return;
817
+ case "return":
818
+ builder.appendNext("return");
819
+ if (stmt.value) {
820
+ builder.appendNext(" ");
821
+ emitExpression(stmt.value, ctx);
822
+ }
823
+ return;
824
+ case "break":
825
+ builder.appendNext("break");
826
+ if (stmt.condition) {
827
+ builder.appendNext(" if ");
828
+ emitExpression(stmt.condition, ctx);
829
+ }
830
+ return;
831
+ case "continue":
832
+ builder.appendNext("continue");
833
+ return;
834
+ case "discard":
835
+ builder.appendNext("discard");
836
+ return;
837
+ case "assign":
838
+ emitAssign(stmt, ctx);
839
+ return;
840
+ case "increment":
841
+ emitExpression(stmt.target, ctx);
842
+ builder.appendNext("++");
843
+ return;
844
+ case "decrement":
845
+ emitExpression(stmt.target, ctx);
846
+ builder.appendNext("--");
847
+ return;
848
+ case "call":
849
+ emitExpression(stmt.call, ctx);
850
+ return;
851
+ case "empty":
852
+ return;
853
+ default:
854
+ assertUnreachable(stmt);
855
+ }
856
+ }
857
+
858
+ /** Emit a var's `<address_space[, access_mode]>` enumerant template. */
859
+ function emitVarTemplate(template: NameElem[], ctx: EmitContext): void {
860
+ const builder = ctx.srcBuilder;
861
+ builder.appendNext("<");
862
+ template.forEach((name, i) => {
863
+ if (i > 0) builder.appendNext(", ");
864
+ emitName(name, ctx);
865
+ });
866
+ builder.appendNext(">");
867
+ }
868
+
869
+ /** Emit a ` = init` clause, if present. */
870
+ function emitInit(init: ExpressionElem | undefined, ctx: EmitContext): void {
871
+ if (!init) return;
872
+ ctx.srcBuilder.appendNext(" = ");
873
+ emitExpression(init, ctx);
874
+ }
875
+
876
+ /** if / else-if / else: a nested IfElem prints as `else if`, a BlockElem as `else`. */
877
+ function emitIf(e: IfElem, ctx: EmitContext): void {
878
+ const builder = ctx.srcBuilder;
879
+ builder.appendNext("if ");
880
+ emitExpression(e.condition, ctx);
881
+ builder.appendNext(" ");
882
+ emitBlock(e.body, ctx);
883
+ if (e.else) {
884
+ builder.appendNext(" else ");
885
+ if (e.else.kind === "if") emitIf(e.else, ctx);
886
+ else emitBlock(e.else, ctx);
887
+ }
888
+ }
889
+
890
+ /** for (init; condition; update) { ... }. The init takes a ';' like any
891
+ * statement; the update is the last clause before ')', so it gets none. */
892
+ function emitFor(e: ForElem, ctx: EmitContext): void {
893
+ const builder = ctx.srcBuilder;
894
+ builder.appendNext("for (");
895
+ if (e.init) emitCoreSemi(e.init, ctx);
896
+ else builder.appendNext(";");
897
+ builder.appendNext(" ");
898
+ if (e.condition) emitExpression(e.condition, ctx);
899
+ builder.appendNext("; ");
900
+ if (e.update) emitStatementCore(e.update, ctx);
901
+ builder.appendNext(") ");
902
+ emitBlock(e.body, ctx);
903
+ }
904
+
905
+ function emitWhile(e: WhileElem, ctx: EmitContext): void {
906
+ ctx.srcBuilder.appendNext("while ");
907
+ emitExpression(e.condition, ctx);
908
+ ctx.srcBuilder.appendNext(" ");
909
+ emitBlock(e.body, ctx);
910
+ }
911
+
912
+ function emitSwitch(e: SwitchElem, ctx: EmitContext): void {
913
+ const builder = ctx.srcBuilder;
914
+ builder.appendNext("switch ");
915
+ emitExpression(e.selector, ctx);
916
+ builder.appendNext(" ");
917
+ emitAttributes(e.bodyAttributes, ctx);
918
+ builder.appendNext("{");
919
+ const inner = childIndent(ctx);
920
+ for (const clause of filterValidElements(e.clauses, ctx.conditions)) {
921
+ emitSwitchClause(clause, inner);
922
+ }
923
+ newLine(ctx);
924
+ builder.appendNext("}");
925
+ }
926
+
927
+ /** lhs op rhs, with the phony target printed as `_`. */
928
+ function emitAssign(e: AssignElem, ctx: EmitContext): void {
929
+ if (e.lhs.kind === "phony") {
930
+ ctx.srcBuilder.add("_", e.lhs.span[0], e.lhs.span[1]);
931
+ } else {
932
+ emitExpression(e.lhs, ctx);
933
+ }
934
+ const [start, end] = e.op.span;
935
+ ctx.srcBuilder.add(` ${e.op.value} `, start, end);
936
+ emitExpression(e.rhs, ctx);
581
937
  }