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,14 +1,11 @@
1
1
  import type {
2
2
  AttributeElem,
3
- BlockStatement,
4
- ContinuingElem,
5
- ElifAttribute,
6
- ElseAttribute,
7
- IfAttribute,
8
- StatementElem,
3
+ BlockElem,
4
+ ElemKindMap,
5
+ HasAttributes,
6
+ Statement,
9
7
  } from "../AbstractElems.ts";
10
8
  import { findMap } from "../Util.ts";
11
- import { beginElem, finishElem } from "./ContentsHelpers.ts";
12
9
  import { parseAttributeList } from "./ParseAttribute.ts";
13
10
  import { parseIfStatement, parseSwitchStatement } from "./ParseControlFlow.ts";
14
11
  import { parseConstAssert } from "./ParseGlobalVar.ts";
@@ -22,16 +19,15 @@ import {
22
19
  import { parseSimpleStatement } from "./ParseSimpleStatement.ts";
23
20
  import {
24
21
  attachAttributes,
22
+ attrsOrUndef,
23
+ conditionalAttribute,
25
24
  expect,
26
25
  hasConditionalAttribute,
27
- isConditionalAttribute,
28
26
  throwParseError,
29
27
  } from "./ParseUtil.ts";
30
28
  import { parseConstDecl } from "./ParseValueDeclaration.ts";
31
29
  import type { ParsingContext } from "./ParsingContext.ts";
32
30
 
33
- type CondAttr = IfAttribute | ElifAttribute | ElseAttribute;
34
-
35
31
  interface CompoundOptions {
36
32
  loopBody?: boolean;
37
33
  noScope?: boolean; // for function bodies (scope shared with params)
@@ -39,11 +35,11 @@ interface CompoundOptions {
39
35
 
40
36
  // Experimental: declarations in conditional blocks visible in outer scope.
41
37
  // e.g. @if(X) { let y = 1; } makes y visible outside the block.
42
- // see https://github.com/wgsl-tooling-wg/wesl-spec/issues/158
38
+ // see https://github.com/webgpu-tools/wesl-spec/issues/158
43
39
  const conditionalBlockFeature = true;
44
40
 
45
41
  /** Function bodies share scope with parameters (per WGSL spec). */
46
- export function parseFunctionBody(ctx: ParsingContext): StatementElem | null {
42
+ export function parseFunctionBody(ctx: ParsingContext): BlockElem | null {
47
43
  return parseCompoundStatement(ctx, undefined, { noScope: true });
48
44
  }
49
45
 
@@ -55,22 +51,20 @@ export function parseCompoundStatement(
55
51
  ctx: ParsingContext,
56
52
  attributes?: AttributeElem[],
57
53
  options?: CompoundOptions,
58
- ): StatementElem | null {
54
+ ): BlockElem | null {
59
55
  const brace = ctx.stream.matchText("{");
60
56
  if (!brace) return null;
61
57
 
62
58
  const startPos = getStartWithAttributes(attributes, brace.span[0]);
63
59
 
64
- beginElem(ctx, "statement", attributes);
65
-
66
60
  const skipScope =
67
61
  options?.noScope ||
68
62
  (conditionalBlockFeature && hasConditionalAttr(attributes));
69
63
  if (!skipScope) ctx.pushScope();
70
- parseBlockStatements(ctx, options?.loopBody);
64
+ const body = parseBlockStatements(ctx, options?.loopBody);
71
65
  if (!skipScope) ctx.popScope();
72
66
 
73
- return finishBlockStatement(startPos, ctx, attributes);
67
+ return finishStatement("block", startPos, ctx, { body }, attributes);
74
68
  }
75
69
 
76
70
  /** Grammar: attribute* compound_statement (for control flow bodies) */
@@ -78,11 +72,10 @@ export function expectCompound(
78
72
  ctx: ParsingContext,
79
73
  errorMsg: string,
80
74
  loopBody?: boolean,
81
- ): StatementElem {
75
+ ): BlockElem {
82
76
  const attrs = parseAttributeList(ctx);
83
- const attrsOrUndef = attrs.length > 0 ? attrs : undefined;
84
77
  const options = loopBody ? { loopBody } : undefined;
85
- const block = parseCompoundStatement(ctx, attrsOrUndef, options);
78
+ const block = parseCompoundStatement(ctx, attrsOrUndef(attrs), options);
86
79
  if (!block) throwParseError(ctx.stream, errorMsg);
87
80
  return block;
88
81
  }
@@ -95,40 +88,30 @@ export function getStartWithAttributes(
95
88
  return attributes?.[0]?.start ?? keywordPos;
96
89
  }
97
90
 
98
- /** Match keyword and begin statement element. Returns start position or null. */
91
+ /** Match keyword and return the statement's start position (or null if no match). */
99
92
  export function beginStatement(
100
93
  ctx: ParsingContext,
101
94
  keyword: string,
102
95
  attributes?: AttributeElem[],
103
- kind: "statement" | "continuing" = "statement",
104
96
  ): number | null {
105
- const keywordPos = ctx.stream.checkpoint();
106
- if (!ctx.stream.matchText(keyword)) return null;
107
- const startPos = getStartWithAttributes(attributes, keywordPos);
108
- beginElem(ctx, kind, attributes);
109
- return startPos;
97
+ const token = ctx.stream.matchText(keyword);
98
+ if (!token) return null;
99
+ // Start at the keyword token, not any leading comment, so a preceding comment
100
+ // falls in the gap before the statement and attaches as leading.
101
+ return getStartWithAttributes(attributes, token.span[0]);
110
102
  }
111
103
 
112
- /** Finish block statement element: close contents, attach attributes. */
113
- export function finishBlockStatement(
114
- start: number,
115
- ctx: ParsingContext,
116
- attributes?: AttributeElem[],
117
- ): StatementElem;
118
- export function finishBlockStatement(
119
- start: number,
120
- ctx: ParsingContext,
121
- attributes: AttributeElem[] | undefined,
122
- kind: "continuing",
123
- ): ContinuingElem;
124
- export function finishBlockStatement(
104
+ /** Build a statement element from its typed fields and attach its attributes. */
105
+ export function finishStatement<K extends keyof ElemKindMap>(
106
+ kind: K,
125
107
  start: number,
126
108
  ctx: ParsingContext,
109
+ params: Omit<ElemKindMap[K], "kind" | "start" | "end">,
127
110
  attributes?: AttributeElem[],
128
- kind: "statement" | "continuing" = "statement",
129
- ): BlockStatement {
130
- const elem = finishElem(kind, start, ctx, {});
131
- attachAttributes(elem, attributes);
111
+ ): ElemKindMap[K] {
112
+ const end = ctx.stream.checkpoint();
113
+ const elem = { kind, start, end, ...params } as ElemKindMap[K];
114
+ attachAttributes(elem as HasAttributes, attributes);
132
115
  return elem;
133
116
  }
134
117
 
@@ -137,18 +120,23 @@ function hasConditionalAttr(attributes?: AttributeElem[]): boolean {
137
120
  }
138
121
 
139
122
  /** Grammar: statement* '}' (after '{' consumed). Loop bodies may end with continuing. */
140
- function parseBlockStatements(ctx: ParsingContext, loopBody?: boolean): void {
123
+ function parseBlockStatements(
124
+ ctx: ParsingContext,
125
+ loopBody?: boolean,
126
+ ): Statement[] {
141
127
  const { stream } = ctx;
128
+ const body: Statement[] = [];
142
129
  while (true) {
143
130
  if (stream.matchText("}")) break;
144
131
  const stmt = parseStatement(ctx);
145
132
  if (!stmt) throwParseError(stream, "Expected statement or '}'");
146
- ctx.addElem(stmt);
133
+ body.push(stmt);
147
134
  if (loopBody && stmt.kind === "continuing") {
148
135
  expect(stream, "}", "continuing block");
149
136
  break;
150
137
  }
151
138
  }
139
+ return body;
152
140
  }
153
141
 
154
142
  /**
@@ -159,7 +147,7 @@ function parseBlockStatements(ctx: ParsingContext, loopBody?: boolean): void {
159
147
  * | 'discard' ';' | variable_updating_statement ';' | compound_statement
160
148
  * | const_assert_statement ';'
161
149
  */
162
- function parseStatement(ctx: ParsingContext): BlockStatement | null {
150
+ function parseStatement(ctx: ParsingContext): Statement | null {
163
151
  const { stream } = ctx;
164
152
  const startPos = stream.checkpoint();
165
153
  const attributes = parseAttributeList(ctx);
@@ -174,7 +162,6 @@ function parseStatement(ctx: ParsingContext): BlockStatement | null {
174
162
  attributes.length > 0 && hasConditionalAttribute(attributes);
175
163
  if (hasConditional) ctx.pushScope("partial");
176
164
 
177
- const attrsOrUndef = attributes.length > 0 ? attributes : undefined;
178
165
  const parsers = [
179
166
  parseLocalVarDecl,
180
167
  parseLetDecl,
@@ -189,27 +176,13 @@ function parseStatement(ctx: ParsingContext): BlockStatement | null {
189
176
  parseContinuingStatement,
190
177
  parseSimpleStatement,
191
178
  ];
192
- const stmt = findMap(parsers, p => p(ctx, attrsOrUndef));
193
- if (!stmt) return null;
194
-
195
- finalizeConditional(ctx, hasConditional, attributes);
196
- return stmt as BlockStatement;
197
- }
179
+ const stmt = findMap(parsers, p => p(ctx, attrsOrUndef(attributes)));
198
180
 
199
- function finalizeConditional(
200
- ctx: ParsingContext,
201
- hasConditional: boolean,
202
- attributes: AttributeElem[],
203
- ): void {
181
+ // Always pop the partial scope we pushed, even on the no-match path, so the
182
+ // scope stack stays balanced; only a matched statement gets the condition.
204
183
  if (hasConditional) {
205
184
  const partialScope = ctx.popScope();
206
- partialScope.condAttribute = getConditionalAttribute(attributes);
185
+ if (stmt) partialScope.condAttribute = conditionalAttribute(attributes);
207
186
  }
208
- }
209
-
210
- function getConditionalAttribute(
211
- attributes: AttributeElem[],
212
- ): CondAttr | undefined {
213
- const found = attributes.find(a => isConditionalAttribute(a.attribute));
214
- return found?.attribute as CondAttr | undefined;
187
+ return stmt ? (stmt as Statement) : null;
215
188
  }
@@ -3,12 +3,11 @@ import type {
3
3
  StructElem,
4
4
  StructMemberElem,
5
5
  } from "../AbstractElems.ts";
6
- import { beginElem, finishElem } from "./ContentsHelpers.ts";
7
6
  import { parseAttributeList } from "./ParseAttribute.ts";
8
- import { getStartWithAttributes } from "./ParseStatement.ts";
7
+ import { finishStatement, getStartWithAttributes } from "./ParseStatement.ts";
9
8
  import { parseSimpleTypeRef } from "./ParseType.ts";
10
9
  import {
11
- attachAttributes,
10
+ attrsOrUndef,
12
11
  createDeclIdentElem,
13
12
  expect,
14
13
  expectWord,
@@ -37,53 +36,40 @@ export function parseStructDecl(
37
36
  const identElem = createDeclIdentElem(ctx, nameToken, true);
38
37
  ctx.saveIdent(identElem.ident);
39
38
 
40
- beginElem(ctx, "struct", attributes);
41
- ctx.addElem(identElem);
42
39
  expect(stream, "{", "struct name");
43
40
 
44
41
  ctx.pushScope();
45
- const members = parseStructMembers(ctx);
42
+ const members = parseCommaList(ctx, parseStructMember);
46
43
  identElem.ident.dependentScope = ctx.currentScope();
47
44
  ctx.popScope();
48
45
 
49
46
  expect(stream, "}", "struct member");
50
47
 
51
- const elem = finishElem("struct", start, ctx, { name: identElem, members });
52
- attachAttributes(elem, attributes);
48
+ const params = { name: identElem, members };
49
+ const elem = finishStatement("struct", start, ctx, params, attributes);
53
50
  linkDeclIdentElem(identElem, elem);
54
51
  return elem;
55
52
  }
56
53
 
57
- /** Grammar: struct_body_decl : '{' struct_member (',' struct_member)* ','? '}' */
58
- function parseStructMembers(ctx: ParsingContext): StructMemberElem[] {
59
- const members = parseCommaList(ctx, parseStructMember);
60
- for (const member of members) ctx.addElem(member);
61
- return members;
62
- }
63
-
64
54
  /** Grammar: struct_member : attribute* member_ident ':' type_specifier */
65
55
  function parseStructMember(ctx: ParsingContext): StructMemberElem | null {
66
56
  const { stream } = ctx;
67
57
  const checkpoint = stream.checkpoint();
68
- const attributes = parseAttributeList(ctx);
58
+ const attrs = parseAttributeList(ctx);
69
59
 
70
60
  const nameToken = stream.matchKind("word");
71
61
  if (!nameToken) {
72
62
  stream.reset(checkpoint);
73
63
  return null;
74
64
  }
65
+ const attributes = attrsOrUndef(attrs);
75
66
 
76
67
  const start = getStartWithAttributes(attributes, nameToken.span[0]);
77
- beginElem(ctx, "member", attributes.length ? attributes : undefined);
78
68
  const name = makeNameElem(nameToken);
79
- ctx.addElem(name);
80
69
  expect(stream, ":", "struct member name");
81
70
 
82
71
  const typeRef = parseSimpleTypeRef(ctx);
83
72
  if (!typeRef) throwParseError(stream, "Expected type after ':'");
84
- ctx.addElem(typeRef);
85
73
 
86
- const elem = finishElem("member", start, ctx, { name, typeRef });
87
- attachAttributes(elem, attributes.length ? attributes : undefined);
88
- return elem;
74
+ return finishStatement("member", start, ctx, { name, typeRef }, attributes);
89
75
  }
@@ -1,5 +1,4 @@
1
1
  import type { TypeRefElem, TypeTemplateParameter } from "../AbstractElems.ts";
2
- import { beginElem, finishElem } from "./ContentsHelpers.ts";
3
2
  import { parseExpression } from "./ParseExpression.ts";
4
3
  import { parseModulePath } from "./ParseIdent.ts";
5
4
  import { makeRefIdentElem, throwParseError } from "./ParseUtil.ts";
@@ -18,17 +17,15 @@ export function parseSimpleTypeRef(ctx: ParsingContext): TypeRefElem | null {
18
17
  const { parts, start, end: nameEnd } = path;
19
18
  const refIdent = ctx.createRefIdent(parts.join("::"));
20
19
 
21
- beginElem(ctx, "type");
22
-
23
- const refIdentElem = makeRefIdentElem(ctx, refIdent, start, nameEnd);
20
+ makeRefIdentElem(ctx, refIdent, start, nameEnd);
24
21
  ctx.saveIdent(refIdent);
25
- ctx.addElem(refIdentElem);
26
22
 
27
23
  const templateParams = ctx.stream.nextTemplateStartToken()
28
24
  ? parseTemplateParams(ctx)
29
25
  : undefined;
30
26
 
31
- return finishElem("type", start, ctx, { name: refIdent, templateParams });
27
+ const end = ctx.stream.checkpoint();
28
+ return { kind: "type", name: refIdent, templateParams, start, end };
32
29
  }
33
30
 
34
31
  /** Parse comma-separated template parameters until closing '>'. */
@@ -37,16 +34,15 @@ export function parseTemplateParams(
37
34
  ): TypeTemplateParameter[] {
38
35
  const { stream } = ctx;
39
36
 
40
- // Handle empty template <>
41
- if (consumeTemplateEnd(stream)) return [];
37
+ if (consumeTemplateEnd(stream)) {
38
+ throwParseError(stream, "Empty template parameter list '<>'");
39
+ }
42
40
 
43
- // Parse comma-separated params
44
- const params: TypeTemplateParameter[] = [parseTemplateParam(ctx)];
41
+ const params = [parseTemplateParam(ctx)];
45
42
  while (stream.matchText(",")) {
46
43
  params.push(parseTemplateParam(ctx));
47
44
  }
48
45
 
49
- // Must end with >
50
46
  if (!consumeTemplateEnd(stream))
51
47
  throwParseError(stream, "Expected '>' or ',' after template parameter");
52
48
 
@@ -66,6 +62,7 @@ function parseTemplateParam(ctx: ParsingContext): TypeTemplateParameter {
66
62
  // parseExpression handles template_elaborated_ident via parsePrimaryExpr
67
63
  // inTemplate prevents '>' from being parsed as comparison operator
68
64
  const expr = parseExpression(ctx, { inTemplate: true });
69
- if (expr) return expr;
70
- throwParseError(ctx.stream, "Expected expression in template parameters");
65
+ if (!expr)
66
+ throwParseError(ctx.stream, "Expected expression in template parameters");
67
+ return expr;
71
68
  }
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  Attribute,
3
3
  AttributeElem,
4
+ ConditionalAttribute,
4
5
  DeclarationElem,
5
6
  DeclIdentElem,
6
7
  ExpressionElem,
@@ -11,12 +12,10 @@ import type {
11
12
  import { ParseError } from "../ParseError.ts";
12
13
  import type { RefIdent } from "../Scope.ts";
13
14
  import type { Stream, Token } from "../Stream.ts";
14
- import { parseExpression } from "./ParseExpression.ts";
15
+ import { type ExpressionOpts, parseExpression } from "./ParseExpression.ts";
15
16
  import type { ParsingContext } from "./ParsingContext.ts";
16
17
  import type { WeslStream, WeslToken } from "./WeslStream.ts";
17
18
 
18
- // --- Stream/token expectations ---
19
-
20
19
  /** Match text and throw ParseError if not found. */
21
20
  export function expect(
22
21
  stream: WeslStream,
@@ -47,17 +46,24 @@ export function expectWord(
47
46
  return token as WeslToken<"word">;
48
47
  }
49
48
 
50
- /** Parse expression and throw ParseError if not found. */
49
+ /** Parse a required expression, throwing if absent. */
51
50
  export function expectExpression(
52
51
  ctx: ParsingContext,
53
52
  errorMsg = "Expected expression",
54
53
  ): ExpressionElem {
55
54
  const expr = parseExpression(ctx);
56
55
  if (!expr) throwParseError(ctx.stream, errorMsg);
57
- if (ctx.options.preserveExpressions) ctx.addElem(expr);
58
56
  return expr;
59
57
  }
60
58
 
59
+ /** Parse an optional expression. */
60
+ export function parseContentExpression(
61
+ ctx: ParsingContext,
62
+ opts?: ExpressionOpts,
63
+ ): ExpressionElem | null {
64
+ return parseExpression(ctx, opts);
65
+ }
66
+
61
67
  /** Throw a ParseError at the current/next token position. */
62
68
  export function throwParseError(stream: Stream<Token>, message: string): never {
63
69
  const weslStream = stream as WeslStream;
@@ -68,8 +74,6 @@ export function throwParseError(stream: Stream<Token>, message: string): never {
68
74
  throw new ParseError(message, span);
69
75
  }
70
76
 
71
- // --- List parsing ---
72
-
73
77
  /** Parse comma-separated items. Caller handles delimiters. */
74
78
  export function parseCommaList<T>(
75
79
  ctx: ParsingContext,
@@ -93,8 +97,6 @@ export function* parseMany<T>(
93
97
  for (let elem = parse(ctx); elem; elem = parse(ctx)) yield elem;
94
98
  }
95
99
 
96
- // --- Element creation ---
97
-
98
100
  /** Create a NameElem from a word token. */
99
101
  export function makeNameElem(token: WeslToken<"word">): NameElem {
100
102
  const [start, end] = token.span;
@@ -136,8 +138,7 @@ export function makeRefIdentElem(
136
138
  return elem;
137
139
  }
138
140
 
139
- // --- Attribute utilities ---
140
-
141
+ /** @return true if the attribute is a conditional (@if, @elif, @else) */
141
142
  export function isConditionalAttribute(attr: Attribute): boolean {
142
143
  const { kind } = attr;
143
144
  return kind === "@if" || kind === "@elif" || kind === "@else";
@@ -148,6 +149,14 @@ export function hasConditionalAttribute(attributes: AttributeElem[]): boolean {
148
149
  return attributes.some(attr => isConditionalAttribute(attr.attribute));
149
150
  }
150
151
 
152
+ /** The first conditional (@if, @elif, @else) attribute in the list, if any. */
153
+ export function conditionalAttribute(
154
+ attributes: AttributeElem[],
155
+ ): ConditionalAttribute | undefined {
156
+ const found = attributes.find(a => isConditionalAttribute(a.attribute));
157
+ return found?.attribute as ConditionalAttribute | undefined;
158
+ }
159
+
151
160
  /** Attach non-empty attributes array to element. */
152
161
  export function attachAttributes<T extends { attributes?: AttributeElem[] }>(
153
162
  elem: T,
@@ -156,7 +165,12 @@ export function attachAttributes<T extends { attributes?: AttributeElem[] }>(
156
165
  if (attributes?.length) elem.attributes = attributes;
157
166
  }
158
167
 
159
- // --- Declaration linking ---
168
+ /** Normalize an empty attribute list to undefined (the elem's "no attributes"). */
169
+ export function attrsOrUndef(
170
+ attrs: AttributeElem[],
171
+ ): AttributeElem[] | undefined {
172
+ return attrs.length ? attrs : undefined;
173
+ }
160
174
 
161
175
  /** Link a DeclIdentElem's ident to its parent declaration. */
162
176
  export function linkDeclIdentElem(
@@ -2,16 +2,15 @@ import type {
2
2
  AttributeElem,
3
3
  ConstElem,
4
4
  ElemKindMap,
5
+ ExpressionElem,
5
6
  OverrideElem,
6
7
  TypedDeclElem,
7
8
  TypeRefElem,
8
9
  } from "../AbstractElems.ts";
9
10
  import type { Scope } from "../Scope.ts";
10
- import { beginElem, finishContents } from "./ContentsHelpers.ts";
11
- import { getStartWithAttributes } from "./ParseStatement.ts";
11
+ import { finishStatement, getStartWithAttributes } from "./ParseStatement.ts";
12
12
  import { parseSimpleTypeRef } from "./ParseType.ts";
13
13
  import {
14
- attachAttributes,
15
14
  createDeclIdentElem,
16
15
  expect,
17
16
  expectExpression,
@@ -27,7 +26,7 @@ export function parseConstDecl(
27
26
  ctx: ParsingContext,
28
27
  attributes?: AttributeElem[],
29
28
  ): ConstElem | null {
30
- return parseValueDecl(ctx, "const", true, isModuleScope(ctx), attributes);
29
+ return parseValueDecl(ctx, "const", true, ctx.isModuleScope(), attributes);
31
30
  }
32
31
 
33
32
  /** Grammar: 'override' optionally_typed_ident ( '=' expression )? */
@@ -47,16 +46,13 @@ export function parseTypedDecl(
47
46
  if (!nameToken) return null;
48
47
  const start = nameToken.span[0];
49
48
 
50
- beginElem(ctx, "typeDecl");
51
49
  const decl = createDeclIdentElem(ctx, nameToken, isGlobal);
52
- ctx.addElem(decl);
53
50
  ctx.saveIdent(decl.ident);
54
51
 
55
52
  const { typeRef, typeScope } = parseOptionalType(ctx);
56
53
 
57
54
  const end = ctx.stream.checkpoint();
58
- const contents = finishContents(ctx, start, end);
59
- return { kind: "typeDecl", decl, typeRef, typeScope, start, end, contents };
55
+ return { kind: "typeDecl", decl, typeRef, typeScope, start, end };
60
56
  }
61
57
 
62
58
  /** Shared parser for const/override declarations. */
@@ -73,44 +69,36 @@ function parseValueDecl<K extends ValueDeclKind>(
73
69
 
74
70
  const startPos = getStartWithAttributes(attributes, token.span[0]);
75
71
  ctx.pushScope("partial");
76
- beginElem(ctx, keyword, attributes);
77
72
 
78
73
  const typedDecl = parseTypedDecl(ctx, isGlobal);
79
74
  if (!typedDecl)
80
75
  throwParseError(stream, `Expected identifier after '${keyword}'`);
81
- ctx.addElem(typedDecl);
82
76
 
77
+ let init: ExpressionElem | undefined;
83
78
  if (requiresInit) {
84
79
  expect(stream, "=", `${keyword} identifier`);
85
- expectExpression(ctx);
80
+ init = expectExpression(ctx);
86
81
  } else if (stream.matchText("=")) {
87
- expectExpression(ctx);
82
+ init = expectExpression(ctx);
88
83
  }
89
84
 
90
85
  expect(stream, ";", `${keyword} declaration`);
91
86
 
92
- const endPos = stream.checkpoint();
93
- const contents = finishContents(ctx, startPos, endPos);
94
87
  typedDecl.decl.ident.dependentScope = ctx.currentScope();
95
88
  ctx.popScope();
96
89
 
97
- const elem: ConstElem | OverrideElem = {
98
- kind: keyword,
99
- name: typedDecl,
100
- start: startPos,
101
- end: endPos,
102
- contents,
103
- };
104
- attachAttributes(elem, attributes);
90
+ // const/override share these fields; cast keyword to the union so the params
91
+ // type-check against the concrete elems rather than the opaque generic K.
92
+ const fields = { name: typedDecl, init };
93
+ const elem = finishStatement(
94
+ keyword as ValueDeclKind,
95
+ startPos,
96
+ ctx,
97
+ fields,
98
+ attributes,
99
+ ) as ElemKindMap[K];
105
100
  linkDeclIdent(typedDecl, elem);
106
- return elem as ElemKindMap[K];
107
- }
108
-
109
- /** @return true if ctx is at module level (not inside fn/block) */
110
- function isModuleScope(ctx: ParsingContext): boolean {
111
- let scope = ctx.currentScope();
112
- while (scope.kind === "partial" && scope.parent) scope = scope.parent;
113
- return scope.parent === null;
101
+ return elem;
114
102
  }
115
103
 
116
104
  /** Parse optional ': type' annotation, managing scope for type references. */
@@ -123,7 +111,6 @@ function parseOptionalType(ctx: ParsingContext): {
123
111
  ctx.pushScope();
124
112
  const typeRef = parseSimpleTypeRef(ctx);
125
113
  if (!typeRef) throwParseError(ctx.stream, "Expected type after ':'");
126
- ctx.addElem(typeRef);
127
114
  const typeScope = ctx.currentScope();
128
115
  ctx.popScope();
129
116
  return { typeRef, typeScope };
@@ -4,8 +4,8 @@ import type { WeslAST, WeslParseState } from "../ParseWESL.ts";
4
4
  import { WeslParseError } from "../ParseWESL.ts";
5
5
  import type { SrcModule } from "../Scope.ts";
6
6
  import { emptyScope } from "../Scope.ts";
7
- import { beginElem, finishContents } from "./ContentsHelpers.ts";
8
- import { parseModule } from "./ParseModule.ts";
7
+ import { attachComments } from "./AttachComments.ts";
8
+ import { checkDoBlockNames, parseModule } from "./ParseModule.ts";
9
9
  import { type ParseOptions, ParsingContext } from "./ParsingContext.ts";
10
10
  import { WeslStream } from "./WeslStream.ts";
11
11
 
@@ -16,10 +16,10 @@ export function parseWesl(
16
16
  ): WeslAST {
17
17
  const { ctx, state } = createParseState(srcModule, options);
18
18
  try {
19
- beginElem(ctx, "module");
20
19
  parseModule(ctx);
21
- const moduleElem = state.stable.moduleElem;
22
- moduleElem.contents = finishContents(ctx, 0, moduleElem.end);
20
+ const { moduleElem } = state.stable;
21
+ attachComments(ctx, moduleElem);
22
+ checkDoBlockNames(moduleElem);
23
23
  return state.stable;
24
24
  } catch (e) {
25
25
  if (e instanceof ParseError) {
@@ -35,23 +35,20 @@ export function parseWesl(
35
35
  /** Initialize parse state: token stream, root scope, and module element. */
36
36
  function createParseState(
37
37
  srcModule: SrcModule,
38
- options?: ParseOptions,
39
- ): {
40
- ctx: ParsingContext;
41
- state: WeslParseState;
42
- } {
38
+ parseOptions?: ParseOptions,
39
+ ): { ctx: ParsingContext; state: WeslParseState } {
43
40
  const stream = new WeslStream(srcModule.src);
44
41
  const rootScope = emptyScope(null);
45
42
  const moduleElem: ModuleElem = {
46
43
  kind: "module",
47
- contents: [],
44
+ decls: [],
48
45
  start: 0,
49
46
  end: srcModule.src.length,
50
47
  };
51
48
  const state: WeslParseState = {
52
- context: { scope: rootScope, openElems: [] },
53
- stable: { srcModule, moduleElem, rootScope, imports: [] },
49
+ context: { scope: rootScope },
50
+ stable: { srcModule, moduleElem, rootScope, imports: [], parseOptions },
54
51
  };
55
- const ctx = new ParsingContext(stream, state, options);
52
+ const ctx = new ParsingContext(stream, state, parseOptions);
56
53
  return { ctx, state };
57
54
  }
@@ -11,10 +11,15 @@ import {
11
11
  } from "../Scope.ts";
12
12
  import type { WeslStream } from "./WeslStream.ts";
13
13
 
14
+ /** Opt-in toggles for not-yet-spec'd WESL/WGSL features (for prototyping). */
15
+ export interface WeslExtensions {
16
+ /** Parse `do name(...) { ... }` blocks. */
17
+ doBlocks?: boolean;
18
+ }
19
+
14
20
  export interface ParseOptions {
15
- /** Store expression AST nodes in statement contents (for tooling/validation). */
16
- // LATER we'll always store expressions in the AST, (but this partial support is for wgsl-edit validation)
17
- preserveExpressions?: boolean;
21
+ /** Enable parsing of experimental, not-yet-spec'd syntax extensions. */
22
+ weslExtensions?: WeslExtensions;
18
23
  }
19
24
 
20
25
  /** Context for parsers to build AST and manage scopes. */
@@ -45,12 +50,9 @@ export class ParsingContext {
45
50
  return this.state.context.scope;
46
51
  }
47
52
 
48
- addElem(elem: AbstractElem): void {
49
- const { openElems } = this.state.context;
50
- if (openElems.length > 0) {
51
- const open = openElems[openElems.length - 1];
52
- open.contents.push(elem);
53
- }
53
+ /** Append a top-level declaration to the module, in source order. */
54
+ addModuleDecl(elem: AbstractElem): void {
55
+ this.state.stable.moduleElem.decls.push(elem);
54
56
  }
55
57
 
56
58
  pushScope(kind: Scope["kind"] = "scope"): void {