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,9 +1,12 @@
1
- import type { AttributeElem, LetElem, VarElem } from "../AbstractElems.ts";
2
- import { beginElem, finishElem } from "./ContentsHelpers.ts";
3
- import { skipTemplateList } from "./ParseGlobalVar.ts";
4
- import { getStartWithAttributes } from "./ParseStatement.ts";
1
+ import type {
2
+ AttributeElem,
3
+ ExpressionElem,
4
+ LetElem,
5
+ VarElem,
6
+ } from "../AbstractElems.ts";
7
+ import { parseTemplateList } from "./ParseGlobalVar.ts";
8
+ import { finishStatement, getStartWithAttributes } from "./ParseStatement.ts";
5
9
  import {
6
- attachAttributes,
7
10
  expect,
8
11
  expectExpression,
9
12
  linkDeclIdent,
@@ -44,26 +47,27 @@ function parseVarOrLet(
44
47
  if (!token) return null;
45
48
 
46
49
  const startPos = getStartWithAttributes(attributes, token.span[0]);
47
- beginElem(ctx, keyword, attributes);
48
- if (hasTemplate) skipTemplateList(ctx);
50
+ const template = hasTemplate ? parseTemplateList(ctx) : undefined;
49
51
 
50
52
  const typedDecl = parseTypedDecl(ctx, false);
51
53
  if (!typedDecl)
52
54
  throwParseError(stream, `Expected identifier after '${keyword}'`);
53
- ctx.addElem(typedDecl);
54
55
 
56
+ let init: ExpressionElem | undefined;
55
57
  if (requiresInit) {
56
58
  const msg = `${keyword} identifier (${keyword} requires initialization)`;
57
59
  expect(stream, "=", msg);
58
- expectExpression(ctx);
60
+ init = expectExpression(ctx);
59
61
  } else if (stream.matchText("=")) {
60
- expectExpression(ctx);
62
+ init = expectExpression(ctx);
61
63
  }
62
64
 
63
65
  expect(stream, ";", `${keyword} declaration`);
64
66
 
65
- const elem = finishElem(keyword, startPos, ctx, { name: typedDecl });
66
- attachAttributes(elem, attributes);
67
+ const params = { name: typedDecl, init };
68
+ const elem = finishStatement(keyword, startPos, ctx, params, attributes);
69
+ // template lives outside the shared fields: only VarElem (not LetElem) has it.
70
+ if (template && elem.kind === "var") elem.template = template;
67
71
  linkDeclIdent(typedDecl, elem);
68
72
  return elem;
69
73
  }
@@ -1,20 +1,26 @@
1
1
  import type {
2
2
  AttributeElem,
3
3
  ContinuingElem,
4
- StatementElem,
4
+ ExpressionElem,
5
+ ForElem,
6
+ ForInit,
7
+ ForUpdate,
8
+ LoopElem,
9
+ WhileElem,
5
10
  } from "../AbstractElems.ts";
6
- import { parseExpression } from "./ParseExpression.ts";
7
11
  import { parseLocalVarDecl } from "./ParseLocalVar.ts";
8
- import {
9
- parseAssignmentRhs,
10
- parseIncDecOperator,
11
- } from "./ParseSimpleStatement.ts";
12
+ import { parseAssignmentRhs, parseIncDecOp } from "./ParseSimpleStatement.ts";
12
13
  import {
13
14
  beginStatement,
14
15
  expectCompound,
15
- finishBlockStatement,
16
+ finishStatement,
16
17
  } from "./ParseStatement.ts";
17
- import { expect, expectExpression } from "./ParseUtil.ts";
18
+ import {
19
+ expect,
20
+ expectExpression,
21
+ parseContentExpression,
22
+ throwParseError,
23
+ } from "./ParseUtil.ts";
18
24
  import type { ParsingContext } from "./ParsingContext.ts";
19
25
 
20
26
  /**
@@ -24,7 +30,7 @@ import type { ParsingContext } from "./ParsingContext.ts";
24
30
  export function parseForStatement(
25
31
  ctx: ParsingContext,
26
32
  attributes?: AttributeElem[],
27
- ): StatementElem | null {
33
+ ): ForElem | null {
28
34
  const { stream } = ctx;
29
35
  const startPos = beginStatement(ctx, "for", attributes);
30
36
  if (startPos === null) return null;
@@ -32,48 +38,49 @@ export function parseForStatement(
32
38
  ctx.pushScope();
33
39
  expect(stream, "(", "'for'");
34
40
 
35
- parseForInit(ctx);
36
- const cond = parseExpression(ctx); // returns null if empty condition
37
- if (cond && ctx.options.preserveExpressions) ctx.addElem(cond);
41
+ const init = parseForInit(ctx);
42
+ const condition = parseContentExpression(ctx) ?? undefined;
38
43
  expect(stream, ";", "for loop condition");
39
- parseForUpdate(ctx);
44
+ const update = parseForUpdate(ctx);
40
45
  expect(stream, ")", "for loop header");
41
46
 
42
47
  const body = expectCompound(ctx, "Expected '{' after for loop header");
43
- ctx.addElem(body);
44
48
  ctx.popScope();
45
49
 
46
- return finishBlockStatement(startPos, ctx, attributes);
50
+ const params = { init, condition, update, body };
51
+ return finishStatement("for", startPos, ctx, params, attributes);
47
52
  }
48
53
 
49
54
  /** Grammar: while_statement : attribute* 'while' expression compound_statement */
50
55
  export function parseWhileStatement(
51
56
  ctx: ParsingContext,
52
57
  attributes?: AttributeElem[],
53
- ): StatementElem | null {
58
+ ): WhileElem | null {
54
59
  const startPos = beginStatement(ctx, "while", attributes);
55
60
  if (startPos === null) return null;
56
61
 
57
- expectExpression(ctx, "Expected condition expression after 'while'");
58
-
62
+ const condition = expectExpression(ctx, "Expected condition after 'while'");
59
63
  const body = expectCompound(ctx, "Expected '{' after while condition");
60
- ctx.addElem(body);
61
64
 
62
- return finishBlockStatement(startPos, ctx, attributes);
65
+ const params = { condition, body };
66
+ return finishStatement("while", startPos, ctx, params, attributes);
63
67
  }
64
68
 
65
69
  /** Grammar: loop_statement : attribute* 'loop' attribute* '{' statement* continuing_statement? '}' */
66
70
  export function parseLoopStatement(
67
71
  ctx: ParsingContext,
68
72
  attributes?: AttributeElem[],
69
- ): StatementElem | null {
73
+ ): LoopElem | null {
70
74
  const startPos = beginStatement(ctx, "loop", attributes);
71
75
  if (startPos === null) return null;
72
76
 
73
77
  const body = expectCompound(ctx, "Expected '{' after 'loop'", true);
74
- ctx.addElem(body);
78
+ const continuing = body.body.find(
79
+ (s): s is ContinuingElem => s.kind === "continuing",
80
+ );
75
81
 
76
- return finishBlockStatement(startPos, ctx, attributes);
82
+ const params = { body, continuing };
83
+ return finishStatement("loop", startPos, ctx, params, attributes);
77
84
  }
78
85
 
79
86
  /** Grammar: continuing_statement : 'continuing' continuing_compound_statement */
@@ -81,35 +88,65 @@ export function parseContinuingStatement(
81
88
  ctx: ParsingContext,
82
89
  attributes?: AttributeElem[],
83
90
  ): ContinuingElem | null {
84
- const startPos = beginStatement(ctx, "continuing", attributes, "continuing");
91
+ const startPos = beginStatement(ctx, "continuing", attributes);
85
92
  if (startPos === null) return null;
86
93
 
87
94
  const body = expectCompound(ctx, "Expected '{' after 'continuing'");
88
- ctx.addElem(body);
95
+ const breakIf = body.body.find(s => s.kind === "break")?.condition;
89
96
 
90
- return finishBlockStatement(startPos, ctx, attributes, "continuing");
97
+ const params = { body, breakIf };
98
+ return finishStatement("continuing", startPos, ctx, params, attributes);
91
99
  }
92
100
 
93
101
  /** Grammar: for_init? ';'
94
102
  * for_init : variable_or_value_statement | variable_updating_statement | func_call_statement
95
103
  */
96
- function parseForInit(ctx: ParsingContext): void {
104
+ function parseForInit(ctx: ParsingContext): ForInit | undefined {
97
105
  const { stream } = ctx;
98
106
  const varDecl = parseLocalVarDecl(ctx);
99
107
  if (varDecl) {
100
- ctx.addElem(varDecl);
101
- // parseLocalVarDecl already consumed the ';'
102
- } else {
103
- const expr = parseExpression(ctx); // returns null for empty case
104
- if (expr && ctx.options.preserveExpressions) ctx.addElem(expr);
105
- expect(stream, ";", "for loop init");
108
+ return varDecl; // parseLocalVarDecl already consumed the ';'
106
109
  }
110
+ const expr = parseContentExpression(ctx); // null for empty case
111
+ const update = expr ? finishForUpdate(ctx, expr) : undefined;
112
+ expect(stream, ";", "for loop init");
113
+ return update;
107
114
  }
108
115
 
109
- /** Grammar: for_update : variable_updating_statement | func_call_statement
110
- * variable_updating_statement : assignment_statement | increment_statement | decrement_statement */
111
- function parseForUpdate(ctx: ParsingContext): void {
112
- const expr = parseExpression(ctx);
113
- if (expr && ctx.options.preserveExpressions) ctx.addElem(expr);
114
- parseIncDecOperator(ctx.stream) || parseAssignmentRhs(ctx);
116
+ /** Grammar: for_update : variable_updating_statement | func_call_statement */
117
+ function parseForUpdate(ctx: ParsingContext): ForUpdate | undefined {
118
+ const expr = parseContentExpression(ctx);
119
+ if (!expr) return undefined;
120
+ return finishForUpdate(ctx, expr);
121
+ }
122
+
123
+ /**
124
+ * Build a typed for-init/for-update node from an already-parsed lhs expression,
125
+ * consuming its trailing operator (++/-- or an assignment + rhs).
126
+ */
127
+ function finishForUpdate(ctx: ParsingContext, expr: ExpressionElem): ForUpdate {
128
+ const { stream } = ctx;
129
+ const start = expr.start;
130
+ const incDec = parseIncDecOp(stream);
131
+ if (incDec) {
132
+ const end = stream.checkpoint();
133
+ if (incDec.op === "++")
134
+ return { kind: "increment", target: expr, start, end };
135
+ return { kind: "decrement", target: expr, start, end };
136
+ }
137
+ const assign = parseAssignmentRhs(ctx);
138
+ if (assign) {
139
+ const { op, rhs } = assign;
140
+ const end = stream.checkpoint();
141
+ return { kind: "assign", lhs: expr, op, rhs, start, end };
142
+ }
143
+ if (expr.kind === "call-expression") {
144
+ return { kind: "call", call: expr, start, end: stream.checkpoint() };
145
+ }
146
+ // A bare expression (no ++/--/assignment, not a call) is not a valid for
147
+ // init/update clause; reject it rather than silently dropping it.
148
+ throwParseError(
149
+ stream,
150
+ "Expected an assignment, increment, decrement, or call in for-clause",
151
+ );
115
152
  }
@@ -1,13 +1,16 @@
1
1
  import type {
2
- Attribute,
2
+ AbstractElem,
3
3
  AttributeElem,
4
- ConditionalAttribute,
5
4
  ConstAssertElem,
6
5
  GlobalDeclarationElem,
6
+ ModuleElem,
7
7
  } from "../AbstractElems.ts";
8
+ import { declsOfKind } from "../LinkerUtil.ts";
9
+ import { ParseError } from "../ParseError.ts";
8
10
  import { findMap } from "../Util.ts";
9
11
  import { parseAttributeList } from "./ParseAttribute.ts";
10
12
  import { parseDirective } from "./ParseDirective.ts";
13
+ import { parseDoBlock } from "./ParseDoBlock.ts";
11
14
  import { parseFnDecl } from "./ParseFn.ts";
12
15
  import {
13
16
  parseAliasDecl,
@@ -17,6 +20,8 @@ import {
17
20
  import { parseWeslImports } from "./ParseImport.ts";
18
21
  import { parseStructDecl } from "./ParseStruct.ts";
19
22
  import {
23
+ attrsOrUndef,
24
+ conditionalAttribute,
20
25
  hasConditionalAttribute,
21
26
  parseMany,
22
27
  throwParseError,
@@ -31,6 +36,7 @@ const declParsers = [
31
36
  parseAliasDecl,
32
37
  parseStructDecl,
33
38
  parseFnDecl,
39
+ parseDoBlock,
34
40
  parseConstAssert,
35
41
  ];
36
42
 
@@ -39,13 +45,46 @@ export function parseModule(ctx: ParsingContext): void {
39
45
  parseImports(ctx);
40
46
  parseDirectives(ctx);
41
47
  while (parseNextDeclaration(ctx)) {}
48
+ // reject input the declaration loop couldn't consume (e.g. a directive after a
49
+ // declaration, or stray tokens); otherwise it would be silently dropped
50
+ if (ctx.stream.peek() !== null)
51
+ throwParseError(ctx.stream, "Expected a declaration or directive");
52
+ }
53
+
54
+ /**
55
+ * Reject a module that gives a `do` block a name that clashes with a fn/global
56
+ * or with another `do` block (`do` blocks share the module's declaration
57
+ * namespace, and runners key blocks by name so a duplicate would silently
58
+ * shadow the earlier one). This is a small module-local pass, deliberately not
59
+ * part of bindIdents.
60
+ */
61
+ export function checkDoBlockNames(moduleElem: ModuleElem): void {
62
+ const doBlocks = declsOfKind(moduleElem, "do");
63
+ if (doBlocks.length === 0) return;
64
+
65
+ const declNames = new Set(moduleElem.decls.map(globalDeclName));
66
+ const seen = new Set<string>();
67
+ for (const block of doBlocks) {
68
+ const { name, start, end } = block.name;
69
+ if (declNames.has(name))
70
+ throw new ParseError(`'${name}' declared as both fn and do`, [
71
+ start,
72
+ end,
73
+ ]);
74
+ if (seen.has(name))
75
+ throw new ParseError(`'${name}' declared as do more than once`, [
76
+ start,
77
+ end,
78
+ ]);
79
+ seen.add(name);
80
+ }
42
81
  }
43
82
 
44
83
  /** Parse WESL import statements at the start of the module. */
45
84
  function parseImports(ctx: ParsingContext): void {
46
85
  const importElems = parseWeslImports(ctx);
47
86
  for (const importElem of importElems) {
48
- ctx.addElem(importElem);
87
+ ctx.addModuleDecl(importElem);
49
88
  ctx.state.stable.imports.push(importElem.imports);
50
89
  }
51
90
  }
@@ -53,7 +92,7 @@ function parseImports(ctx: ParsingContext): void {
53
92
  /** Grammar: global_directive : diagnostic_directive | enable_directive | requires_directive */
54
93
  function parseDirectives(ctx: ParsingContext): void {
55
94
  const directives = parseMany(ctx, parseDirective);
56
- for (const elem of directives) ctx.addElem(elem);
95
+ for (const elem of directives) ctx.addModuleDecl(elem);
57
96
  }
58
97
 
59
98
  /** Parse one declaration, return true if more may exist. */
@@ -74,15 +113,28 @@ function parseNextDeclaration(ctx: ParsingContext): boolean {
74
113
  return false;
75
114
  }
76
115
 
116
+ /** @return the declared name of a module-level declaration, if it has one. */
117
+ function globalDeclName(elem: AbstractElem): string | undefined {
118
+ switch (elem.kind) {
119
+ case "fn":
120
+ case "struct":
121
+ case "alias":
122
+ return elem.name.ident.originalName;
123
+ case "gvar":
124
+ case "const":
125
+ case "override":
126
+ return elem.name.decl.ident.originalName;
127
+ default:
128
+ return undefined;
129
+ }
130
+ }
131
+
77
132
  /** Try each declaration parser until one succeeds. */
78
133
  function parseDecl(ctx: ParsingContext, attrs: AttributeElem[]): boolean {
79
- const attrsOrUndef = attrs.length ? attrs : undefined;
80
- const elem = findMap(declParsers, p => p(ctx, attrsOrUndef));
81
- if (elem) {
82
- recordDecl(ctx, elem, attrs);
83
- return true;
84
- }
85
- return false;
134
+ const elem = findMap(declParsers, p => p(ctx, attrsOrUndef(attrs)));
135
+ if (!elem) return false;
136
+ recordDecl(ctx, elem, attrs);
137
+ return true;
86
138
  }
87
139
 
88
140
  /** Pop conditional scope and attach the conditional attribute. */
@@ -91,9 +143,7 @@ function finalizeConditional(
91
143
  attrs: AttributeElem[],
92
144
  ): void {
93
145
  const partialScope = ctx.popScope();
94
- partialScope.condAttribute = findMap(attrs, ({ attribute }) =>
95
- isConditionalAttribute(attribute) ? attribute : undefined,
96
- );
146
+ partialScope.condAttribute = conditionalAttribute(attrs);
97
147
  }
98
148
 
99
149
  /** Record a parsed declaration, extending start to include attributes. */
@@ -103,14 +153,10 @@ function recordDecl(
103
153
  attrs: AttributeElem[],
104
154
  ): void {
105
155
  if (attrs.length && elem.start > attrs[0].start) elem.start = attrs[0].start;
106
- ctx.addElem(elem);
156
+ ctx.addModuleDecl(elem);
107
157
  if (elem.kind === "assert") {
108
158
  const { stable } = ctx.state;
109
159
  stable.moduleAsserts ??= [];
110
160
  stable.moduleAsserts.push(elem);
111
161
  }
112
162
  }
113
-
114
- function isConditionalAttribute(a: Attribute): a is ConditionalAttribute {
115
- return a.kind === "@if" || a.kind === "@elif" || a.kind === "@else";
116
- }
@@ -1,11 +1,28 @@
1
- import type { AttributeElem, StatementElem } from "../AbstractElems.ts";
2
- import { beginElem, finishContents } from "./ContentsHelpers.ts";
1
+ import type {
2
+ AssignElem,
3
+ AssignOp,
4
+ AttributeElem,
5
+ BreakElem,
6
+ CallElem,
7
+ ContinueElem,
8
+ DecrementElem,
9
+ DiscardElem,
10
+ EmptyElem,
11
+ ExpressionElem,
12
+ IncrementElem,
13
+ PhonyTarget,
14
+ ReturnElem,
15
+ Statement,
16
+ } from "../AbstractElems.ts";
17
+ import type { Span } from "../Span.ts";
3
18
  import { parseExpression } from "./ParseExpression.ts";
19
+ import { finishStatement, getStartWithAttributes } from "./ParseStatement.ts";
4
20
  import {
5
- finishBlockStatement,
6
- getStartWithAttributes,
7
- } from "./ParseStatement.ts";
8
- import { expect, expectExpression, throwParseError } from "./ParseUtil.ts";
21
+ expect,
22
+ expectExpression,
23
+ parseContentExpression,
24
+ throwParseError,
25
+ } from "./ParseUtil.ts";
9
26
  import type { ParsingContext } from "./ParsingContext.ts";
10
27
  import type { WeslStream } from "./WeslStream.ts";
11
28
 
@@ -33,34 +50,61 @@ const assignmentOps = new Set([
33
50
  export function parseSimpleStatement(
34
51
  ctx: ParsingContext,
35
52
  attributes?: AttributeElem[],
36
- ): StatementElem | null {
53
+ ): Statement | null {
37
54
  const { stream } = ctx;
38
55
  const startPos = getStartWithAttributes(attributes, stream.checkpoint());
39
56
 
40
57
  return (
41
58
  parseReturnStmt(ctx, startPos, attributes) ||
42
59
  parseBreakStmt(ctx, startPos, attributes) ||
43
- parseKeywordStmt(ctx, startPos, attributes, "continue") ||
44
- parseKeywordStmt(ctx, startPos, attributes, "discard") ||
60
+ parseContinueStmt(ctx, startPos, attributes) ||
61
+ parseDiscardStmt(ctx, startPos, attributes) ||
45
62
  parseEmptyStmt(stream, startPos) ||
46
63
  parsePhonyAssignment(ctx, startPos, attributes) ||
47
64
  parseExpressionStmt(ctx, startPos, attributes)
48
65
  );
49
66
  }
50
67
 
68
+ /** Match an assignment operator, capturing its value and span. */
69
+ export function parseAssignmentOp(stream: WeslStream): AssignOp | null {
70
+ const token = stream.nextIf(({ text }) => assignmentOps.has(text));
71
+ if (!token) return null;
72
+ return { value: token.text as AssignOp["value"], span: token.span };
73
+ }
74
+
75
+ /** Match '++' or '--', returning its text and span (or null if absent). */
76
+ export function parseIncDecOp(
77
+ stream: WeslStream,
78
+ ): { op: "++" | "--"; span: Span } | null {
79
+ const token = stream.nextIf(({ text }) => text === "++" || text === "--");
80
+ if (!token) return null;
81
+ return { op: token.text as "++" | "--", span: token.span };
82
+ }
83
+
84
+ /** Grammar: ( '=' | compound_assignment_operator ) expression. Returns op + rhs. */
85
+ export function parseAssignmentRhs(
86
+ ctx: ParsingContext,
87
+ ): { op: AssignOp; rhs: ExpressionElem } | null {
88
+ const op = parseAssignmentOp(ctx.stream);
89
+ if (!op) return null;
90
+ const rhs = expectExpression(
91
+ ctx,
92
+ "Expected expression after assignment operator",
93
+ );
94
+ return { op, rhs };
95
+ }
96
+
51
97
  /** Grammar: return_statement : 'return' expression? ';' */
52
98
  function parseReturnStmt(
53
99
  ctx: ParsingContext,
54
100
  startPos: number,
55
101
  attributes?: AttributeElem[],
56
- ): StatementElem | null {
102
+ ): ReturnElem | null {
57
103
  const { stream } = ctx;
58
104
  if (!stream.matchText("return")) return null;
59
- beginElem(ctx, "statement", attributes);
60
- const expr = parseExpression(ctx);
61
- if (expr && ctx.options.preserveExpressions) ctx.addElem(expr);
105
+ const value = parseContentExpression(ctx) ?? undefined;
62
106
  expect(stream, ";", "return statement");
63
- return finishBlockStatement(startPos, ctx, attributes);
107
+ return finishStatement("return", startPos, ctx, { value }, attributes);
64
108
  }
65
109
 
66
110
  /**
@@ -71,39 +115,46 @@ function parseBreakStmt(
71
115
  ctx: ParsingContext,
72
116
  startPos: number,
73
117
  attributes?: AttributeElem[],
74
- ): StatementElem | null {
118
+ ): BreakElem | null {
75
119
  const { stream } = ctx;
76
120
  if (!stream.matchText("break")) return null;
77
- beginElem(ctx, "statement", attributes);
121
+ let condition: ExpressionElem | undefined;
78
122
  if (stream.matchText("if")) {
79
- expectExpression(ctx, "Expected condition after 'break if'");
123
+ condition = expectExpression(ctx, "Expected condition after 'break if'");
80
124
  }
81
125
  expect(stream, ";", "break statement");
82
- return finishBlockStatement(startPos, ctx, attributes);
126
+ return finishStatement("break", startPos, ctx, { condition }, attributes);
83
127
  }
84
128
 
85
- /** Grammar: continue_statement : 'continue' ';' also handles 'discard' */
86
- function parseKeywordStmt(
129
+ /** Grammar: continue_statement : 'continue' ';' */
130
+ function parseContinueStmt(
87
131
  ctx: ParsingContext,
88
132
  startPos: number,
89
- attributes: AttributeElem[] | undefined,
90
- keyword: string,
91
- ): StatementElem | null {
133
+ attributes?: AttributeElem[],
134
+ ): ContinueElem | null {
92
135
  const { stream } = ctx;
93
- if (!stream.matchText(keyword)) return null;
94
- beginElem(ctx, "statement", attributes);
95
- expect(stream, ";", `${keyword} statement`);
96
- return finishBlockStatement(startPos, ctx, attributes);
136
+ if (!stream.matchText("continue")) return null;
137
+ expect(stream, ";", "continue statement");
138
+ return finishStatement("continue", startPos, ctx, {}, attributes);
97
139
  }
98
140
 
99
- /** Parse empty statement (just ';'). */
100
- function parseEmptyStmt(
101
- stream: WeslStream,
102
- start: number,
103
- ): StatementElem | null {
141
+ /** Grammar: 'discard' ';' */
142
+ function parseDiscardStmt(
143
+ ctx: ParsingContext,
144
+ startPos: number,
145
+ attributes?: AttributeElem[],
146
+ ): DiscardElem | null {
147
+ const { stream } = ctx;
148
+ if (!stream.matchText("discard")) return null;
149
+ expect(stream, ";", "discard statement");
150
+ return finishStatement("discard", startPos, ctx, {}, attributes);
151
+ }
152
+
153
+ /** Parse empty statement (just ';'). Spans the ';' so it emits no extra text. */
154
+ function parseEmptyStmt(stream: WeslStream, start: number): EmptyElem | null {
104
155
  if (!stream.matchText(";")) return null;
105
156
  const end = stream.checkpoint();
106
- return { kind: "statement", start, end, contents: [] };
157
+ return { kind: "empty", start, end };
107
158
  }
108
159
 
109
160
  /** Grammar: assignment_statement : '_' '=' expression ';' (phony assignment) */
@@ -111,54 +162,49 @@ function parsePhonyAssignment(
111
162
  ctx: ParsingContext,
112
163
  startPos: number,
113
164
  attributes?: AttributeElem[],
114
- ): StatementElem | null {
165
+ ): AssignElem | null {
115
166
  const { stream } = ctx;
116
- if (!stream.matchText("_")) return null;
117
- if (!parseAssignmentOperator(stream))
118
- throwParseError(stream, "Expected assignment operator after '_'");
119
- beginElem(ctx, "statement", attributes);
120
- expectExpression(ctx, "Expected expression after assignment operator");
167
+ const underscore = stream.matchText("_");
168
+ if (!underscore) return null;
169
+ const lhs: PhonyTarget = { kind: "phony", span: underscore.span };
170
+ // WGSL phony assignment uses only `=`, never a compound operator.
171
+ const eq = stream.matchText("=");
172
+ if (!eq) throwParseError(stream, "Expected '=' after '_'");
173
+ const op: AssignOp = { value: "=", span: eq.span };
174
+ const rhs = expectExpression(ctx, "Expected expression after '_ ='");
121
175
  expect(stream, ";", "assignment");
122
- return finishBlockStatement(startPos, ctx, attributes);
176
+ return finishStatement("assign", startPos, ctx, { lhs, op, rhs }, attributes);
123
177
  }
124
178
 
125
- /**
126
- * Parses expression statements: assignments, increments/decrements, or function calls.
127
- * Grammar: ( assignment_statement | increment_statement | decrement_statement | call_phrase ) ';'
128
- */
179
+ /** Grammar: ( assignment_statement | increment_statement | decrement_statement | call_phrase ) ';' */
129
180
  function parseExpressionStmt(
130
181
  ctx: ParsingContext,
131
182
  startPos: number,
132
183
  attributes?: AttributeElem[],
133
- ): StatementElem | null {
184
+ ): AssignElem | IncrementElem | DecrementElem | CallElem | null {
134
185
  const { stream } = ctx;
135
- beginElem(ctx, "statement", attributes);
136
186
  const expr = parseExpression(ctx);
137
187
  if (!expr) {
138
- finishContents(ctx, startPos, startPos);
139
188
  stream.reset(startPos);
140
189
  return null;
141
190
  }
142
- if (ctx.options.preserveExpressions) ctx.addElem(expr);
143
-
144
- if (!parseIncDecOperator(stream)) parseAssignmentRhs(ctx);
145
- expect(stream, ";", "expression");
146
- return finishBlockStatement(startPos, ctx, attributes);
147
- }
148
191
 
149
- /** Grammar: assignment_statement : lhs_expression ( '=' | compound_assignment_operator ) expression */
150
- export function parseAssignmentOperator(stream: WeslStream): boolean {
151
- return !!stream.nextIf(({ text }) => assignmentOps.has(text));
152
- }
192
+ const incDec = parseIncDecOp(stream);
193
+ if (incDec) {
194
+ expect(stream, ";", "expression");
195
+ const kind = incDec.op === "++" ? "increment" : "decrement";
196
+ return finishStatement(kind, startPos, ctx, { target: expr }, attributes);
197
+ }
153
198
 
154
- /** Grammar: ( '=' | compound_assignment_operator ) expression (rhs of assignment_statement) */
155
- export function parseAssignmentRhs(ctx: ParsingContext): boolean {
156
- if (!parseAssignmentOperator(ctx.stream)) return false;
157
- expectExpression(ctx, "Expected expression after assignment operator");
158
- return true;
159
- }
199
+ const assign = parseAssignmentRhs(ctx);
200
+ expect(stream, ";", "expression");
201
+ if (assign) {
202
+ const params = { lhs: expr, op: assign.op, rhs: assign.rhs };
203
+ return finishStatement("assign", startPos, ctx, params, attributes);
204
+ }
160
205
 
161
- /** Grammar: increment_statement : lhs_expression '++' ; decrement_statement : lhs_expression '--' */
162
- export function parseIncDecOperator(stream: WeslStream): boolean {
163
- return !!stream.nextIf(({ text }) => text === "++" || text === "--");
206
+ if (expr.kind !== "call-expression") {
207
+ throwParseError(stream, "Expected call, assignment, or increment");
208
+ }
209
+ return finishStatement("call", startPos, ctx, { call: expr }, attributes);
164
210
  }