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,21 @@
1
- import type { AttributeElem, StatementElem } from "../AbstractElems.ts";
2
- import { beginElem, finishElem } from "./ContentsHelpers.ts";
1
+ import type {
2
+ AttributeElem,
3
+ BlockElem,
4
+ ExpressionElem,
5
+ IfElem,
6
+ SwitchClauseElem,
7
+ SwitchElem,
8
+ } from "../AbstractElems.ts";
3
9
  import { parseAttributeList } from "./ParseAttribute.ts";
4
10
  import {
5
11
  beginStatement,
6
12
  expectCompound,
7
- finishBlockStatement,
13
+ finishStatement,
14
+ getStartWithAttributes,
8
15
  parseCompoundStatement,
9
16
  } from "./ParseStatement.ts";
10
17
  import {
11
- attachAttributes,
18
+ attrsOrUndef,
12
19
  expect,
13
20
  expectExpression,
14
21
  throwParseError,
@@ -22,52 +29,67 @@ import type { ParsingContext } from "./ParsingContext.ts";
22
29
  export function parseIfStatement(
23
30
  ctx: ParsingContext,
24
31
  attributes?: AttributeElem[],
25
- ): StatementElem | null {
32
+ ): IfElem | null {
26
33
  const startPos = beginStatement(ctx, "if", attributes);
27
34
  if (startPos === null) return null;
28
35
 
29
- expectExpression(ctx, "Expected condition expression after 'if'");
30
-
36
+ const condition = expectExpression(ctx, "Expected condition after 'if'");
31
37
  const body = expectCompound(ctx, "Expected '{' after if condition");
32
- ctx.addElem(body);
33
- parseElseChain(ctx);
38
+ const elseBranch = parseElseChain(ctx);
34
39
 
35
- return finishBlockStatement(startPos, ctx, attributes);
40
+ const params = { condition, body, else: elseBranch };
41
+ return finishStatement("if", startPos, ctx, params, attributes);
36
42
  }
37
43
 
38
44
  /** Grammar: switch_statement : attribute* 'switch' expression switch_body */
39
45
  export function parseSwitchStatement(
40
46
  ctx: ParsingContext,
41
47
  attributes?: AttributeElem[],
42
- ): StatementElem | null {
48
+ ): SwitchElem | null {
43
49
  const startPos = beginStatement(ctx, "switch", attributes);
44
50
  if (startPos === null) return null;
45
51
 
46
- expectExpression(ctx, "Expected expression after 'switch'");
47
- expectSwitchClauses(ctx);
52
+ const selector = expectExpression(ctx, "Expected expression after 'switch'");
53
+ const { bodyAttributes, clauses } = expectSwitchClauses(ctx);
48
54
 
49
- return finishBlockStatement(startPos, ctx, attributes);
55
+ const params = { selector, clauses, bodyAttributes };
56
+ return finishStatement("switch", startPos, ctx, params, attributes);
50
57
  }
51
58
 
52
59
  /**
53
60
  * Grammar: else_if_clause : 'else' 'if' expression compound_statement
54
61
  * Grammar: else_clause : 'else' compound_statement
62
+ *
63
+ * An else-if nests as an IfElem in the outer if's `else` field; a plain else is
64
+ * a BlockElem. Emit and the AST dump read these typed fields.
55
65
  */
56
- function parseElseChain(ctx: ParsingContext): void {
66
+ function parseElseChain(ctx: ParsingContext): IfElem | BlockElem | undefined {
57
67
  const { stream } = ctx;
58
- while (stream.matchText("else")) {
59
- const elseIf = stream.matchText("if");
60
- if (elseIf) {
61
- expectExpression(ctx, "Expected expression after 'else if'");
62
- const body = expectCompound(ctx, "Expected '{' after else if");
63
- ctx.addElem(body);
64
- continue;
65
- }
66
-
67
- const body = expectCompound(ctx, "Expected '{' after else");
68
- ctx.addElem(body);
69
- break;
68
+ const elseToken = stream.matchText("else");
69
+ if (!elseToken) return undefined;
70
+
71
+ if (stream.matchText("if")) {
72
+ const condition = expectExpression(
73
+ ctx,
74
+ "Expected expression after 'else if'",
75
+ );
76
+ const body = expectCompound(ctx, "Expected '{' after else if");
77
+ const elseBranch = parseElseChain(ctx);
78
+ const end = stream.checkpoint();
79
+ // Start at the 'else' keyword, not the pre-keyword position, so a comment
80
+ // before 'else if' falls in the gap and leads the branch (matches
81
+ // beginStatement); otherwise the nested if swallows it.
82
+ return {
83
+ kind: "if",
84
+ condition,
85
+ body,
86
+ else: elseBranch,
87
+ start: elseToken.span[0],
88
+ end,
89
+ };
70
90
  }
91
+
92
+ return expectCompound(ctx, "Expected '{' after else");
71
93
  }
72
94
 
73
95
  /**
@@ -76,54 +98,76 @@ function parseElseChain(ctx: ParsingContext): void {
76
98
  * Grammar: case_clause : 'case' case_selectors ':'? compound_statement
77
99
  * Grammar: default_alone_clause : 'default' ':'? compound_statement
78
100
  */
79
- function expectSwitchClauses(ctx: ParsingContext): void {
101
+ function expectSwitchClauses(ctx: ParsingContext): {
102
+ bodyAttributes?: AttributeElem[];
103
+ clauses: SwitchClauseElem[];
104
+ } {
80
105
  const { stream } = ctx;
81
- parseAttributeList(ctx);
106
+ const bodyAttrs = parseAttributeList(ctx);
82
107
  expect(stream, "{", "switch expression");
108
+ const clauses: SwitchClauseElem[] = [];
83
109
  while (!stream.matchText("}")) {
84
- const clauseStart = stream.checkpoint();
85
- const clauseAttrs = parseAttributeList(ctx);
86
- beginElem(
87
- ctx,
88
- "switch-clause",
89
- clauseAttrs.length ? clauseAttrs : undefined,
90
- );
110
+ clauses.push(parseSwitchClause(ctx));
111
+ }
112
+ return { bodyAttributes: attrsOrUndef(bodyAttrs), clauses };
113
+ }
114
+
115
+ /** Parse one 'case'/'default' clause (the keyword has not yet been consumed). */
116
+ function parseSwitchClause(ctx: ParsingContext): SwitchClauseElem {
117
+ const { stream } = ctx;
118
+ const attrs = attrsOrUndef(parseAttributeList(ctx));
91
119
 
92
- if (stream.matchText("case")) {
93
- parseCaseSelectors(ctx);
94
- parseCaseBody(ctx, "Expected '{' after case value");
95
- } else if (stream.matchText("default")) {
96
- parseCaseBody(ctx, "Expected '{' after 'default'");
97
- } else {
98
- throwParseError(stream, "Expected 'case', 'default', or '}' in switch");
99
- }
100
-
101
- const clauseElem = finishElem("switch-clause", clauseStart, ctx, {});
102
- attachAttributes(clauseElem, clauseAttrs.length ? clauseAttrs : undefined);
103
- ctx.addElem(clauseElem);
120
+ const caseTok = stream.matchText("case");
121
+ const keyword = caseTok ?? stream.matchText("default");
122
+ if (!keyword) {
123
+ throwParseError(stream, "Expected 'case', 'default', or '}' in switch");
104
124
  }
125
+ // The clause start is the keyword token, not any leading comment, so a
126
+ // preceding comment falls in the gap before it and attaches as leading.
127
+ const clauseStart = getStartWithAttributes(attrs, keyword.span[0]);
128
+
129
+ let selectors: (ExpressionElem | "default")[];
130
+ let body: BlockElem;
131
+ if (caseTok) {
132
+ selectors = parseCaseSelectors(ctx);
133
+ body = parseCaseBody(ctx, "Expected '{' after case value");
134
+ } else {
135
+ selectors = ["default"];
136
+ body = parseCaseBody(ctx, "Expected '{' after 'default'");
137
+ }
138
+
139
+ return finishStatement(
140
+ "switch-clause",
141
+ clauseStart,
142
+ ctx,
143
+ { selectors, body },
144
+ attrs,
145
+ );
105
146
  }
106
147
 
107
148
  /** Grammar: case_selectors : case_selector (',' case_selector)* ','? */
108
- function parseCaseSelectors(ctx: ParsingContext): void {
149
+ function parseCaseSelectors(
150
+ ctx: ParsingContext,
151
+ ): (ExpressionElem | "default")[] {
109
152
  const { stream } = ctx;
110
- expectExpression(ctx, "Expected expression after 'case'");
153
+ const selectors = [expectExpression(ctx, "Expected expression after 'case'")];
111
154
  while (stream.matchText(",")) {
112
- expectExpression(ctx, "Expected expression after ',' in case values");
155
+ selectors.push(
156
+ expectExpression(ctx, "Expected expression after ',' in case values"),
157
+ );
113
158
  }
159
+ return selectors;
114
160
  }
115
161
 
116
162
  /**
117
163
  * Grammar: case_clause : 'case' case_selectors ':'? compound_statement
118
164
  * Grammar: default_alone_clause : 'default' ':'? compound_statement
119
165
  */
120
- function parseCaseBody(ctx: ParsingContext, errorMsg: string): void {
166
+ function parseCaseBody(ctx: ParsingContext, errorMsg: string): BlockElem {
121
167
  ctx.stream.matchText(":");
122
168
 
123
- const bodyAttrs = parseAttributeList(ctx);
124
- const attrs = bodyAttrs.length > 0 ? bodyAttrs : undefined;
125
-
169
+ const attrs = attrsOrUndef(parseAttributeList(ctx));
126
170
  const body = parseCompoundStatement(ctx, attrs);
127
171
  if (!body) throwParseError(ctx.stream, errorMsg);
128
- ctx.addElem(body);
172
+ return body;
129
173
  }
@@ -7,8 +7,10 @@ import type {
7
7
  RequiresDirective,
8
8
  } from "../AbstractElems.ts";
9
9
  import { parseAttributeList } from "./ParseAttribute.ts";
10
+ import { getStartWithAttributes } from "./ParseStatement.ts";
10
11
  import {
11
12
  attachAttributes,
13
+ attrsOrUndef,
12
14
  expect,
13
15
  expectWord,
14
16
  makeNameElem,
@@ -21,8 +23,7 @@ import type { WeslStream, WeslToken } from "./WeslStream.ts";
21
23
  export function parseDirective(ctx: ParsingContext): DirectiveElem | null {
22
24
  const { stream } = ctx;
23
25
  const startPos = stream.checkpoint();
24
- const attributes = parseAttributeList(ctx);
25
- const attrs = attributes.length > 0 ? attributes : undefined;
26
+ const attrs = attrsOrUndef(parseAttributeList(ctx));
26
27
 
27
28
  const result =
28
29
  parseExtensionDirective(ctx, "enable", attrs) ||
@@ -49,11 +50,6 @@ function parseExtensionDirective(
49
50
  return makeDirectiveElem(directive, token, stream, attributes);
50
51
  }
51
52
 
52
- function parseDirectiveName(ctx: ParsingContext): NameElem {
53
- const nameToken = expectWord(ctx.stream, "Expected identifier in name list");
54
- return makeNameElem(nameToken);
55
- }
56
-
57
53
  /**
58
54
  * Grammar: diagnostic_directive : 'diagnostic' diagnostic_control ';'
59
55
  * Grammar: diagnostic_control : '(' severity_control_name ',' diagnostic_rule_name ',' ? ')'
@@ -91,13 +87,18 @@ function parseDiagnosticDirective(
91
87
  return makeDirectiveElem(directive, token, stream, attributes);
92
88
  }
93
89
 
90
+ function parseDirectiveName(ctx: ParsingContext): NameElem {
91
+ const nameToken = expectWord(ctx.stream, "Expected identifier in name list");
92
+ return makeNameElem(nameToken);
93
+ }
94
+
94
95
  function makeDirectiveElem(
95
96
  directive: EnableDirective | RequiresDirective | DiagnosticDirective,
96
97
  token: WeslToken,
97
98
  stream: WeslStream,
98
99
  attributes?: AttributeElem[],
99
100
  ): DirectiveElem {
100
- const start = attributes?.[0]?.start ?? token.span[0];
101
+ const start = getStartWithAttributes(attributes, token.span[0]);
101
102
  const end = stream.checkpoint();
102
103
  const elem: DirectiveElem = { kind: "directive", directive, start, end };
103
104
  attachAttributes(elem, attributes);
@@ -0,0 +1,63 @@
1
+ import type { AttributeElem, DoBlockElem } from "../AbstractElems.ts";
2
+ import type { Scope } from "../Scope.ts";
3
+ import { parseFnParams } from "./ParseFn.ts";
4
+ import { getStartWithAttributes, parseFunctionBody } from "./ParseStatement.ts";
5
+ import {
6
+ attachAttributes,
7
+ expectWord,
8
+ makeNameElem,
9
+ throwParseError,
10
+ } from "./ParseUtil.ts";
11
+ import type { ParsingContext } from "./ParsingContext.ts";
12
+
13
+ /**
14
+ * Grammar: do_block_decl : attribute* 'do' ident '(' param_list? ')' compound_statement
15
+ *
16
+ * `do` is a module-level declaration when the `doBlocks` extension is on;
17
+ * otherwise it stays a reserved word. The body is parsed structurally but
18
+ * inside a scope that is detached from the module scope graph, so its idents
19
+ * never reach bindIdents. do blocks are module-local, resolved later by AST
20
+ * name match.
21
+ */
22
+ export function parseDoBlock(
23
+ ctx: ParsingContext,
24
+ attributes?: AttributeElem[],
25
+ ): DoBlockElem | null {
26
+ if (!ctx.options.weslExtensions?.doBlocks) return null;
27
+
28
+ const { stream } = ctx;
29
+ const doToken = stream.matchText("do");
30
+ if (!doToken) return null;
31
+
32
+ const startPos = getStartWithAttributes(attributes, doToken.span[0]);
33
+ const nameToken = expectWord(stream, "Expected identifier after 'do'");
34
+ const name = makeNameElem(nameToken);
35
+
36
+ const parentScope = ctx.currentScope();
37
+ ctx.pushScope();
38
+ const doScope = ctx.currentScope();
39
+
40
+ const params = parseFnParams(ctx);
41
+ const body = parseFunctionBody(ctx);
42
+ if (!body) throwParseError(stream, "Expected do block body");
43
+
44
+ ctx.popScope();
45
+ detachScope(parentScope, doScope);
46
+
47
+ const doBlock: DoBlockElem = {
48
+ kind: "do",
49
+ name,
50
+ params,
51
+ body,
52
+ start: startPos,
53
+ end: stream.checkpoint(),
54
+ };
55
+ attachAttributes(doBlock, attributes);
56
+ return doBlock;
57
+ }
58
+
59
+ /** Remove a child scope from its parent so bindIdents never traverses it. */
60
+ function detachScope(parent: Scope, child: Scope): void {
61
+ const i = parent.contents.indexOf(child);
62
+ if (i >= 0) parent.contents.splice(i, 1);
63
+ }
@@ -26,7 +26,6 @@ import { parseIdent } from "./ParseIdent.ts";
26
26
  import { parseTemplateParams } from "./ParseType.ts";
27
27
  import {
28
28
  expect,
29
- expectExpression,
30
29
  expectWord,
31
30
  makeNameElem,
32
31
  throwParseError,
@@ -145,10 +144,14 @@ function parseBinaryExpr(
145
144
  if (isBinaryOpInContext(nextToken, inTemplate)) {
146
145
  const nextPrec = getPrecedence(nextToken);
147
146
  if (nextPrec > precedence) {
148
- const group = "unary" as const;
149
- const left = right;
150
- const args = { prec: nextPrec, left, group, condRef, inTemplate };
151
- right = parseBinaryExpr(ctx, args).expr;
147
+ const rightArgs: BinaryExprArgs = {
148
+ prec: nextPrec,
149
+ left: right,
150
+ group: "unary",
151
+ condRef,
152
+ inTemplate,
153
+ };
154
+ right = parseBinaryExpr(ctx, rightArgs).expr;
152
155
  }
153
156
  }
154
157
 
@@ -196,7 +199,6 @@ function parseParenExpr(
196
199
  if (!expression) throwParseError(stream, "Expected expression after '('");
197
200
 
198
201
  const close = expect(stream, ")", "expression");
199
-
200
202
  const start = open.span[0];
201
203
  const end = close.span[1];
202
204
  return { kind: "parenthesized-expression", expression, start, end };
@@ -224,15 +226,13 @@ function parseTemplateElaboratedIdent(
224
226
  if (!ctx.stream.nextTemplateStartToken()) return refIdent;
225
227
 
226
228
  const templateParams = parseTemplateParams(ctx);
227
- const typeRef: TypeRefElem = {
229
+ return {
228
230
  kind: "type",
229
231
  name: refIdent.ident,
230
232
  templateParams,
231
233
  start: refIdent.start,
232
234
  end: ctx.stream.checkpoint(),
233
- contents: [],
234
235
  };
235
- return typeRef;
236
236
  }
237
237
 
238
238
  /** Parse postfix operators: member access, indexing, function calls. */
@@ -282,7 +282,8 @@ function parseIndexAccess(
282
282
  ): ComponentExpression | null {
283
283
  const { stream } = ctx;
284
284
  if (!stream.matchText("[")) return null;
285
- const indexExpr = expectExpression(ctx, "Expected expression in array index");
285
+ const indexExpr = parseExpression(ctx);
286
+ if (!indexExpr) throwParseError(stream, "Expected expression in array index");
286
287
  const closeBracket = expect(stream, "]", "array index");
287
288
  return makeComponentExpression(base, indexExpr, closeBracket.span[1]);
288
289
  }
@@ -1,18 +1,19 @@
1
1
  import type {
2
2
  AttributeElem,
3
- DeclIdentElem,
4
3
  FnElem,
5
4
  FnParamElem,
6
- GrammarElem,
7
- StatementElem,
8
5
  TypeRefElem,
9
6
  } from "../AbstractElems.ts";
10
- import { beginElem, finishElem } from "./ContentsHelpers.ts";
11
7
  import { parseAttributeList } from "./ParseAttribute.ts";
12
- import { getStartWithAttributes, parseFunctionBody } from "./ParseStatement.ts";
8
+ import {
9
+ finishStatement,
10
+ getStartWithAttributes,
11
+ parseFunctionBody,
12
+ } from "./ParseStatement.ts";
13
13
  import { parseSimpleTypeRef } from "./ParseType.ts";
14
14
  import {
15
15
  attachAttributes,
16
+ attrsOrUndef,
16
17
  createDeclIdentElem,
17
18
  expect,
18
19
  expectWord,
@@ -69,13 +70,6 @@ export function parseFnDecl(
69
70
  returnAttributes,
70
71
  start: startPos,
71
72
  end: stream.checkpoint(),
72
- contents: buildFnContents(
73
- attributes,
74
- declIdentElem,
75
- params,
76
- returnType,
77
- body,
78
- ),
79
73
  };
80
74
  attachAttributes(fnElem, attributes);
81
75
  linkDeclIdentElem(declIdentElem, fnElem);
@@ -83,7 +77,7 @@ export function parseFnDecl(
83
77
  }
84
78
 
85
79
  /** Grammar: '(' param_list? ')' where param_list : param ( ',' param )* ','? */
86
- function parseFnParams(ctx: ParsingContext): FnParamElem[] {
80
+ export function parseFnParams(ctx: ParsingContext): FnParamElem[] {
87
81
  const { stream } = ctx;
88
82
  const params: FnParamElem[] = [];
89
83
 
@@ -115,37 +109,21 @@ function parseFnReturn(ctx: ParsingContext): {
115
109
  const returnType = parseSimpleTypeRef(ctx);
116
110
  if (!returnType) throwParseError(stream, "Expected type after '->'");
117
111
 
118
- return { returnType, returnAttributes: attrs.length > 0 ? attrs : undefined };
119
- }
120
-
121
- /** Build contents array for function element */
122
- function buildFnContents(
123
- attributes: AttributeElem[] | undefined,
124
- decl: DeclIdentElem,
125
- params: FnParamElem[],
126
- returnType: TypeRefElem | undefined,
127
- body: StatementElem,
128
- ): GrammarElem[] {
129
- const base = returnType
130
- ? [decl, ...params, returnType, body]
131
- : [decl, ...params, body];
132
- return attributes?.length ? [...attributes, ...base] : base;
112
+ return { returnType, returnAttributes: attrsOrUndef(attrs) };
133
113
  }
134
114
 
135
115
  /** Grammar: param : attribute* optionally_typed_ident */
136
116
  function parseFnParam(ctx: ParsingContext): FnParamElem | null {
137
- const attributes = parseAttributeList(ctx);
117
+ const attrs = parseAttributeList(ctx);
138
118
  if (ctx.stream.peek()?.kind !== "word") return null;
119
+ const attributes = attrsOrUndef(attrs);
139
120
 
140
- beginElem(ctx, "param", attributes.length ? attributes : undefined);
141
121
  const name = parseTypedDecl(ctx, false);
142
122
  if (!name)
143
123
  throw new Error("Unexpected: peek succeeded but parseTypedDecl failed");
144
- ctx.addElem(name);
145
124
 
146
125
  const startPos = getStartWithAttributes(attributes, name.start);
147
- const elem = finishElem("param", startPos, ctx, { name });
126
+ const elem = finishStatement("param", startPos, ctx, { name }, attributes);
148
127
  linkDeclIdent(name, elem);
149
- attachAttributes(elem, attributes.length > 0 ? attributes : undefined);
150
128
  return elem;
151
129
  }
@@ -2,19 +2,20 @@ import type {
2
2
  AliasElem,
3
3
  AttributeElem,
4
4
  ConstAssertElem,
5
+ ExpressionElem,
5
6
  GlobalVarElem,
7
+ NameElem,
6
8
  } from "../AbstractElems.ts";
7
- import { beginElem, finishElem } from "./ContentsHelpers.ts";
8
- import { getStartWithAttributes } from "./ParseStatement.ts";
9
+ import { finishStatement, getStartWithAttributes } from "./ParseStatement.ts";
9
10
  import { parseSimpleTypeRef } from "./ParseType.ts";
10
11
  import {
11
- attachAttributes,
12
12
  createDeclIdentElem,
13
13
  expect,
14
14
  expectExpression,
15
15
  expectWord,
16
16
  linkDeclIdent,
17
17
  linkDeclIdentElem,
18
+ makeNameElem,
18
19
  throwParseError,
19
20
  } from "./ParseUtil.ts";
20
21
  import { parseTypedDecl } from "./ParseValueDeclaration.ts";
@@ -34,24 +35,28 @@ export function parseGlobalVarDecl(
34
35
 
35
36
  const startPos = getStartWithAttributes(attributes, varToken.span[0]);
36
37
  ctx.pushScope("partial");
37
- beginElem(ctx, "gvar", attributes);
38
38
 
39
- skipTemplateList(ctx);
39
+ const template = parseTemplateList(ctx);
40
40
 
41
41
  const typedDecl = parseTypedDecl(ctx);
42
42
  if (!typedDecl) throwParseError(stream, "Expected identifier after 'var'");
43
- ctx.addElem(typedDecl);
44
43
 
44
+ let init: ExpressionElem | undefined;
45
45
  if (stream.matchText("=")) {
46
- expectExpression(ctx);
46
+ init = expectExpression(ctx);
47
47
  }
48
48
  expect(stream, ";", "var declaration");
49
49
 
50
50
  typedDecl.decl.ident.dependentScope = ctx.currentScope();
51
51
  ctx.popScope();
52
52
 
53
- const varElem = finishElem("gvar", startPos, ctx, { name: typedDecl });
54
- attachAttributes(varElem, attributes);
53
+ const varElem = finishStatement(
54
+ "gvar",
55
+ startPos,
56
+ ctx,
57
+ { name: typedDecl, template, init },
58
+ attributes,
59
+ );
55
60
  linkDeclIdent(typedDecl, varElem);
56
61
  return varElem;
57
62
  }
@@ -66,12 +71,10 @@ export function parseAliasDecl(
66
71
  if (!aliasToken) return null;
67
72
 
68
73
  const startPos = getStartWithAttributes(attributes, aliasToken.span[0]);
69
- beginElem(ctx, "alias", attributes);
70
74
 
71
75
  const nameToken = expectWord(stream, "Expected identifier after 'alias'");
72
76
 
73
77
  const declIdentElem = createDeclIdentElem(ctx, nameToken, true);
74
- ctx.addElem(declIdentElem);
75
78
  ctx.saveIdent(declIdentElem.ident);
76
79
 
77
80
  expect(stream, "=", "alias name");
@@ -80,18 +83,19 @@ export function parseAliasDecl(
80
83
  const typeRef = parseSimpleTypeRef(ctx);
81
84
  if (!typeRef)
82
85
  throwParseError(stream, "Expected type after '=' in alias declaration");
83
- ctx.addElem(typeRef);
84
86
 
85
87
  declIdentElem.ident.dependentScope = ctx.currentScope();
86
88
  ctx.popScope();
87
89
 
88
90
  expect(stream, ";", "alias declaration");
89
91
 
90
- const aliasElem = finishElem("alias", startPos, ctx, {
91
- name: declIdentElem,
92
- typeRef,
93
- });
94
- attachAttributes(aliasElem, attributes);
92
+ const aliasElem = finishStatement(
93
+ "alias",
94
+ startPos,
95
+ ctx,
96
+ { name: declIdentElem, typeRef },
97
+ attributes,
98
+ );
95
99
  linkDeclIdentElem(declIdentElem, aliasElem);
96
100
  return aliasElem;
97
101
  }
@@ -105,27 +109,32 @@ export function parseConstAssert(
105
109
  if (!assertToken) return null;
106
110
 
107
111
  const startPos = getStartWithAttributes(attributes, assertToken.span[0]);
108
- beginElem(ctx, "assert", attributes);
109
- expectExpression(ctx);
112
+ const expression = expectExpression(ctx);
110
113
  expect(ctx.stream, ";", "const_assert expression");
111
114
 
112
- const elem = finishElem("assert", startPos, ctx, {});
113
- attachAttributes(elem, attributes);
114
- return elem;
115
+ return finishStatement("assert", startPos, ctx, { expression }, attributes);
115
116
  }
116
117
 
117
- /** Skip optional template list (e.g., <storage, read_write>). */
118
- export function skipTemplateList(ctx: ParsingContext): void {
118
+ /**
119
+ * Parse an optional var template list `<storage, read_write>`. The entries are
120
+ * predeclared enumerants (address space / access mode), not user idents, so they
121
+ * are captured as unbound NameElems.
122
+ */
123
+ export function parseTemplateList(ctx: ParsingContext): NameElem[] | undefined {
119
124
  const { stream } = ctx;
120
- if (!stream.nextTemplateStartToken()) return;
125
+ if (!stream.nextTemplateStartToken()) return undefined;
121
126
 
127
+ const names: NameElem[] = [];
122
128
  while (true) {
123
129
  const next = stream.peek();
124
130
  if (!next) throwParseError(stream, "Unclosed template in var declaration");
125
131
  if (next.text.startsWith(">")) {
126
132
  stream.nextTemplateEndToken();
127
- return;
133
+ return names;
128
134
  }
129
- stream.nextToken();
135
+ const word = stream.matchKind("word");
136
+ if (word) names.push(makeNameElem(word));
137
+ else if (!stream.matchText(","))
138
+ throwParseError(stream, "Expected ',' or '>' in var template list");
130
139
  }
131
140
  }
@@ -56,11 +56,10 @@ export function parseIdent(
56
56
 
57
57
  const refIdentElem = makeRefIdentElem(ctx, ident, start, end);
58
58
 
59
- // Don't add conditionRef idents to scope - they're only in the expression tree
60
- if (!conditionRef) {
61
- ctx.saveIdent(ident);
62
- ctx.addElem(refIdentElem);
63
- }
59
+ // conditionRef idents (@if/@elif) stay out of the scope tree entirely.
60
+ // The RefIdentElem is never added to container contents - it lives only
61
+ // inside the expression tree built by parseExpression.
62
+ if (!conditionRef) ctx.saveIdent(ident);
64
63
 
65
64
  return refIdentElem;
66
65
  }