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.
- package/README.md +1 -1
- package/dist/index.d.ts +283 -147
- package/dist/index.js +1765 -1143
- package/package.json +2 -2
- package/src/AbstractElems.ts +264 -82
- package/src/ClickableError.ts +8 -1
- package/src/Linker.ts +63 -67
- package/src/LinkerUtil.ts +141 -7
- package/src/LowerAndEmit.ts +660 -304
- package/src/Mangler.ts +1 -1
- package/src/ModuleResolver.ts +15 -4
- package/src/ParseWESL.ts +21 -33
- package/src/SrcMap.ts +7 -19
- package/src/StandardTypes.ts +1 -1
- package/src/WeslDevice.ts +1 -0
- package/src/debug/ASTtoString.ts +92 -76
- package/src/index.ts +1 -1
- package/src/parse/AttachComments.ts +289 -0
- package/src/parse/ExpressionUtil.ts +3 -3
- package/src/parse/Keywords.ts +1 -1
- package/src/parse/ParseAttribute.ts +49 -71
- package/src/parse/ParseCall.ts +3 -4
- package/src/parse/ParseControlFlow.ts +100 -56
- package/src/parse/ParseDirective.ts +9 -8
- package/src/parse/ParseDoBlock.ts +63 -0
- package/src/parse/ParseExpression.ts +11 -10
- package/src/parse/ParseFn.ts +11 -33
- package/src/parse/ParseGlobalVar.ts +36 -27
- package/src/parse/ParseIdent.ts +4 -5
- package/src/parse/ParseLocalVar.ts +16 -12
- package/src/parse/ParseLoop.ts +76 -39
- package/src/parse/ParseModule.ts +65 -19
- package/src/parse/ParseSimpleStatement.ts +112 -66
- package/src/parse/ParseStatement.ts +40 -67
- package/src/parse/ParseStruct.ts +8 -22
- package/src/parse/ParseType.ts +10 -13
- package/src/parse/ParseUtil.ts +26 -12
- package/src/parse/ParseValueDeclaration.ts +18 -31
- package/src/parse/ParseWesl.ts +11 -14
- package/src/parse/ParsingContext.ts +11 -9
- package/src/parse/WeslStream.ts +138 -121
- package/src/parse/stream/RegexMatchers.ts +45 -0
- package/src/parse/stream/WeslLexer.ts +249 -0
- package/src/test/BevyLink.test.ts +18 -11
- package/src/test/ConditionalElif.test.ts +4 -2
- package/src/test/DeclCommentEmit.test.ts +122 -0
- package/src/test/DoBlock.test.ts +164 -0
- package/src/test/FilterValidElements.test.ts +4 -6
- package/src/test/Linker.test.ts +23 -7
- package/src/test/Mangling.test.ts +8 -4
- package/src/test/ParseComments.test.ts +234 -6
- package/src/test/ParseConditionsV2.test.ts +47 -175
- package/src/test/ParseElifV2.test.ts +12 -33
- package/src/test/ParseErrorV2.test.ts +0 -0
- package/src/test/ParseWeslV2.test.ts +247 -626
- package/src/test/StatementEmit.test.ts +143 -0
- package/src/test/StripWesl.ts +24 -3
- package/src/test/TestLink.ts +19 -3
- package/src/test/TestUtil.ts +18 -8
- package/src/test/Tokenizer.test.ts +96 -0
- package/src/test/__snapshots__/ParseWeslV2.test.ts.snap +10 -42
- package/src/RawEmit.ts +0 -103
- package/src/Reflection.ts +0 -336
- package/src/TransformBindingStructs.ts +0 -320
- package/src/parse/ContentsHelpers.ts +0 -70
- package/src/parse/stream/CachingStream.ts +0 -48
- package/src/parse/stream/MatchersStream.ts +0 -85
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AbstractElem,
|
|
3
|
+
AbstractElemBase,
|
|
4
|
+
CommentElem,
|
|
5
|
+
ModuleElem,
|
|
6
|
+
SyntheticElem,
|
|
7
|
+
} from "../AbstractElems.ts";
|
|
8
|
+
import { childElems } from "../LinkerUtil.ts";
|
|
9
|
+
import type { SrcModule } from "../Scope.ts";
|
|
10
|
+
import type { ParsingContext } from "./ParsingContext.ts";
|
|
11
|
+
import type { CommentTrivia } from "./WeslStream.ts";
|
|
12
|
+
|
|
13
|
+
/** An AST node with a source position (i.e. not a synthetic elem). Every such
|
|
14
|
+
* node can carry comments via the {@link AbstractElemBase} fields. */
|
|
15
|
+
type Positioned = Exclude<AbstractElem, SyntheticElem>;
|
|
16
|
+
|
|
17
|
+
/** Positioned children per node, computed once per attach pass. Attached
|
|
18
|
+
* comments are not structural fields, so entries never go stale. */
|
|
19
|
+
type ChildCache = Map<Positioned, Positioned[]>;
|
|
20
|
+
|
|
21
|
+
const tab = 0x09;
|
|
22
|
+
const lineFeed = 0x0a;
|
|
23
|
+
const verticalTab = 0x0b;
|
|
24
|
+
const formFeed = 0x0c;
|
|
25
|
+
const carriageReturn = 0x0d;
|
|
26
|
+
const space = 0x20;
|
|
27
|
+
const nextLine = 0x85;
|
|
28
|
+
const leftToRightMark = 0x200e;
|
|
29
|
+
const rightToLeftMark = 0x200f;
|
|
30
|
+
const lineSeparator = 0x2028;
|
|
31
|
+
const paragraphSeparator = 0x2029;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Attach every recorded comment to a node in the parsed tree.
|
|
35
|
+
*
|
|
36
|
+
* Each comment run (a contiguous group of comments between two real tokens) is
|
|
37
|
+
* anchored to the deepest node that contains it in a gap between children, then
|
|
38
|
+
* distributed to the surrounding children:
|
|
39
|
+
* - a comment that begins its own line leads the next child (`commentsBefore`);
|
|
40
|
+
* - an inline comment trails the previous child (`commentsAfter`), unless it
|
|
41
|
+
* hugs the next child -- only blank space and comments, no separator token,
|
|
42
|
+
* between them -- in which case it leads the next child instead;
|
|
43
|
+
* - comments before a closing token with no following child trail the last child,
|
|
44
|
+
* or land in an empty block's `innerComments`.
|
|
45
|
+
*
|
|
46
|
+
* Descending into expressions (via {@link childElems}) means interior comments
|
|
47
|
+
* like the one in `foo(1, /* x *\/ 2)` are preserved on the `2`, not dropped.
|
|
48
|
+
*/
|
|
49
|
+
export function attachComments(
|
|
50
|
+
ctx: ParsingContext,
|
|
51
|
+
moduleElem: ModuleElem,
|
|
52
|
+
): void {
|
|
53
|
+
const { srcModule } = ctx.state.stable;
|
|
54
|
+
const cache: ChildCache = new Map();
|
|
55
|
+
for (const run of ctx.stream.commentRuns()) {
|
|
56
|
+
const anchor = deepestContaining(
|
|
57
|
+
moduleElem,
|
|
58
|
+
run[0].start,
|
|
59
|
+
runEnd(run),
|
|
60
|
+
cache,
|
|
61
|
+
);
|
|
62
|
+
distribute(anchor, run, srcModule, cache);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The deepest node whose span contains the whole [start, end) range, found by
|
|
67
|
+
* descending into the child that brackets it. Comments live in gaps between
|
|
68
|
+
* tokens, so the result is the node holding the gap, with the run between two
|
|
69
|
+
* of its children. */
|
|
70
|
+
function deepestContaining(
|
|
71
|
+
root: Positioned,
|
|
72
|
+
start: number,
|
|
73
|
+
end: number,
|
|
74
|
+
cache: ChildCache,
|
|
75
|
+
): Positioned {
|
|
76
|
+
let node = root;
|
|
77
|
+
while (true) {
|
|
78
|
+
const child = positionedChildren(node, cache).find(
|
|
79
|
+
c => c.start <= start && end <= c.end,
|
|
80
|
+
);
|
|
81
|
+
if (!child) return node;
|
|
82
|
+
node = child;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function runEnd(run: CommentTrivia[]): number {
|
|
87
|
+
return run[run.length - 1].end;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Split a comment run between the previous child (trailing) and the next child
|
|
91
|
+
* (leading) of its anchor, or onto the last child / inner comments when it
|
|
92
|
+
* dangles before a closing token. */
|
|
93
|
+
function distribute(
|
|
94
|
+
anchor: Positioned,
|
|
95
|
+
run: CommentTrivia[],
|
|
96
|
+
srcModule: SrcModule,
|
|
97
|
+
cache: ChildCache,
|
|
98
|
+
): void {
|
|
99
|
+
const children = positionedChildren(anchor, cache);
|
|
100
|
+
const start = run[0].start;
|
|
101
|
+
const end = runEnd(run);
|
|
102
|
+
|
|
103
|
+
// the AbstractElem nodes bracketing the comment run.
|
|
104
|
+
// prev ends at/before the comments
|
|
105
|
+
// next starts at/after the comments
|
|
106
|
+
// prev, next may be missing:
|
|
107
|
+
// prev when the run leads the anchor's first child
|
|
108
|
+
// next when the run dangles after the last child
|
|
109
|
+
// both when the anchor has no children (e.g. an empty block)
|
|
110
|
+
const prev = children.findLast(c => c.end <= start);
|
|
111
|
+
const next = children.find(c => c.start >= end);
|
|
112
|
+
|
|
113
|
+
if (!next) {
|
|
114
|
+
// run dangles after the last child, before the anchor's closing token
|
|
115
|
+
if (prev) addComments(prev, "commentsAfter", run, srcModule);
|
|
116
|
+
// empty block: nothing to attach to, so the comments live inside it
|
|
117
|
+
else if (anchor.kind === "block")
|
|
118
|
+
anchor.innerComments = run.map(t => makeComment(t, srcModule));
|
|
119
|
+
// empty non-block container: keep the comments rather than drop them
|
|
120
|
+
else addComments(anchor, "commentsBefore", run, srcModule);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// one run in a gap can hold comments belonging to both sides -- some trailing
|
|
125
|
+
// prev (on its line), some leading next -- so split it at the boundary:
|
|
126
|
+
// [0, split) trail prev, [split, end) lead next.
|
|
127
|
+
const split = splitPoint(prev, next, run, srcModule.src);
|
|
128
|
+
if (prev && split > 0)
|
|
129
|
+
addComments(prev, "commentsAfter", run.slice(0, split), srcModule);
|
|
130
|
+
if (split < run.length)
|
|
131
|
+
addComments(next, "commentsBefore", run.slice(split), srcModule);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function positionedChildren(node: Positioned, cache: ChildCache): Positioned[] {
|
|
135
|
+
let children = cache.get(node);
|
|
136
|
+
if (children === undefined) {
|
|
137
|
+
children = positioned(childElems(node));
|
|
138
|
+
cache.set(node, children);
|
|
139
|
+
}
|
|
140
|
+
return children;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Append converted comments to an element's leading or trailing list. */
|
|
144
|
+
function addComments(
|
|
145
|
+
elem: AbstractElemBase,
|
|
146
|
+
field: "commentsBefore" | "commentsAfter",
|
|
147
|
+
trivia: CommentTrivia[],
|
|
148
|
+
srcModule: SrcModule,
|
|
149
|
+
): void {
|
|
150
|
+
const comments = trivia.map(t => makeComment(t, srcModule));
|
|
151
|
+
const existing = elem[field];
|
|
152
|
+
elem[field] = existing ? [...existing, ...comments] : comments;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function makeComment(trivia: CommentTrivia, srcModule: SrcModule): CommentElem {
|
|
156
|
+
const { style, start, end } = trivia;
|
|
157
|
+
const comment: CommentElem = {
|
|
158
|
+
kind: "comment",
|
|
159
|
+
style,
|
|
160
|
+
start,
|
|
161
|
+
end,
|
|
162
|
+
srcModule,
|
|
163
|
+
};
|
|
164
|
+
// a fully blank line above the comment is preserved in the output
|
|
165
|
+
if (lineBreaksBefore(srcModule.src, start) >= 2) comment.blankBefore = true;
|
|
166
|
+
return comment;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Index into `run` where it flips from trailing `prev` to leading `next`:
|
|
171
|
+
* caller attaches run[0, split) to `prev` and run[split, end) to `next`.
|
|
172
|
+
*
|
|
173
|
+
* The aim is to keep each comment with the code it describes, so it stays
|
|
174
|
+
* meaningful after the tree is reordered or reformatted. That follows how people
|
|
175
|
+
* write comments: a comment on its own line documents what comes after it, while
|
|
176
|
+
* a comment sharing a line with code documents that code. Hence:
|
|
177
|
+
* - with no previous child the whole run leads (split 0);
|
|
178
|
+
* - otherwise the split is the first comment that begins its own line;
|
|
179
|
+
* - failing that (an all-inline run), it is the start of the suffix that hugs
|
|
180
|
+
* `next` on the same line, so the comment in `foo(1, /* x *\/ 2)` documents,
|
|
181
|
+
* and lands on, the `2`.
|
|
182
|
+
*/
|
|
183
|
+
function splitPoint(
|
|
184
|
+
prev: Positioned | undefined,
|
|
185
|
+
next: Positioned,
|
|
186
|
+
run: CommentTrivia[],
|
|
187
|
+
src: string,
|
|
188
|
+
): number {
|
|
189
|
+
if (!prev) return 0;
|
|
190
|
+
const ownLine = firstOwnLine(run, src);
|
|
191
|
+
return ownLine >= 0 ? ownLine : hugsNextStart(run, next.start, src);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Source-positioned children, sorted in source order. Synthetic elems (no
|
|
195
|
+
* source position) cannot anchor comments and are dropped. Children usually
|
|
196
|
+
* arrive already in source order, so the sort is skipped when possible.
|
|
197
|
+
* Filtering and order-checking share one pass: this runs per node, so it's
|
|
198
|
+
* kept deliberately lean. */
|
|
199
|
+
function positioned(elems: readonly AbstractElem[]): Positioned[] {
|
|
200
|
+
const result: Positioned[] = [];
|
|
201
|
+
let sorted = true; // starts in source order until proven otherwise
|
|
202
|
+
let lastStart = -1;
|
|
203
|
+
for (const e of elems) {
|
|
204
|
+
if (e.kind === "synthetic") continue;
|
|
205
|
+
if (e.start < lastStart) sorted = false;
|
|
206
|
+
lastStart = e.start;
|
|
207
|
+
result.push(e);
|
|
208
|
+
}
|
|
209
|
+
if (!sorted) result.sort((a, b) => a.start - b.start);
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Blankspace classification per https://www.w3.org/TR/WGSL/#blankspace-and-line-breaks
|
|
214
|
+
// (the tokenizer matches the same set via the blankspaces/lineBreak regexes).
|
|
215
|
+
|
|
216
|
+
/** Count line breaks in the whitespace run immediately before `pos`, capped at 2:
|
|
217
|
+
* callers only need none / one line break / a blank line. Scans backward over
|
|
218
|
+
* blankspace, stopping at the first non-blankspace char, so cost is the gap
|
|
219
|
+
* length, not the source length. `\r\n` counts as one break. */
|
|
220
|
+
function lineBreaksBefore(src: string, pos: number): number {
|
|
221
|
+
let count = 0;
|
|
222
|
+
for (let i = pos - 1; i >= 0; i--) {
|
|
223
|
+
const c = src.charCodeAt(i);
|
|
224
|
+
if (isLineBreak(c)) {
|
|
225
|
+
if (c === lineFeed && src.charCodeAt(i - 1) === carriageReturn) i--; // \r\n
|
|
226
|
+
} else if (isInlineSpace(c)) {
|
|
227
|
+
continue; // still inside the run
|
|
228
|
+
} else {
|
|
229
|
+
break; // run ended at a non-blankspace char
|
|
230
|
+
}
|
|
231
|
+
if (++count >= 2) return 2;
|
|
232
|
+
}
|
|
233
|
+
return count;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Index of the first comment that begins its own line (after a line break),
|
|
237
|
+
* or -1 if none do. */
|
|
238
|
+
function firstOwnLine(run: CommentTrivia[], src: string): number {
|
|
239
|
+
return run.findIndex(t => lineBreaksBefore(src, t.start) >= 1);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Start index of the trailing suffix of `run` that is joined to `next` by
|
|
243
|
+
* same-line whitespace only (no line break, no separator token between them).
|
|
244
|
+
* Returns run.length when nothing hugs `next`. */
|
|
245
|
+
function hugsNextStart(
|
|
246
|
+
run: CommentTrivia[],
|
|
247
|
+
nextStart: number,
|
|
248
|
+
src: string,
|
|
249
|
+
): number {
|
|
250
|
+
let suffixStart = run.length; // nothing hugs `next` until proven otherwise
|
|
251
|
+
let rightStart = nextStart; // start of the neighbor just right of this comment
|
|
252
|
+
for (let i = run.length - 1; i >= 0; i--) {
|
|
253
|
+
const comment = run[i];
|
|
254
|
+
// stop once more than blank space sits between this comment and its neighbor
|
|
255
|
+
if (!sameLineGap(src, comment.end, rightStart)) break;
|
|
256
|
+
suffixStart = i;
|
|
257
|
+
rightStart = comment.start;
|
|
258
|
+
}
|
|
259
|
+
return suffixStart;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** A WGSL line break code point. `\r\n` is two of these; callers coalesce it. */
|
|
263
|
+
function isLineBreak(c: number): boolean {
|
|
264
|
+
return (
|
|
265
|
+
c === lineFeed ||
|
|
266
|
+
c === carriageReturn ||
|
|
267
|
+
c === verticalTab ||
|
|
268
|
+
c === formFeed ||
|
|
269
|
+
c === nextLine ||
|
|
270
|
+
c === lineSeparator ||
|
|
271
|
+
c === paragraphSeparator
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** WGSL blankspace that stays on the same line (not a line break). */
|
|
276
|
+
function isInlineSpace(c: number): boolean {
|
|
277
|
+
return (
|
|
278
|
+
c === space || c === tab || c === leftToRightMark || c === rightToLeftMark
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** True when [from, to) is inline blankspace only (no line break): the two ends
|
|
283
|
+
* sit on the same line with nothing but same-line spaces between. */
|
|
284
|
+
function sameLineGap(src: string, from: number, to: number): boolean {
|
|
285
|
+
for (let i = from; i < to; i++) {
|
|
286
|
+
if (!isInlineSpace(src.charCodeAt(i))) return false;
|
|
287
|
+
}
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
@@ -21,7 +21,8 @@ export function makeLiteral(token: WeslToken<"keyword" | "number">): Literal {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export function makeUnaryOperator(token: WeslToken<"symbol">): UnaryOperator {
|
|
24
|
-
|
|
24
|
+
const [start, end] = token.span;
|
|
25
|
+
return { value: token.text as UnaryOperator["value"], start, end };
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export function makeBinaryOperator(token: {
|
|
@@ -36,12 +37,11 @@ export function makeUnaryExpression(
|
|
|
36
37
|
operator: UnaryOperator,
|
|
37
38
|
expr: ExpressionElem,
|
|
38
39
|
): UnaryExpression {
|
|
39
|
-
const [start] = operator.span;
|
|
40
40
|
return {
|
|
41
41
|
kind: "unary-expression",
|
|
42
42
|
operator,
|
|
43
43
|
expression: expr,
|
|
44
|
-
start,
|
|
44
|
+
start: operator.start,
|
|
45
45
|
end: expr.end,
|
|
46
46
|
};
|
|
47
47
|
}
|
package/src/parse/Keywords.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Use https://github.com/
|
|
1
|
+
// Use https://github.com/webgpu-tools/wgsl-spec to check this list in the future
|
|
2
2
|
// I recommend checking whether a new list and the current list are equal
|
|
3
3
|
|
|
4
4
|
/** https://www.w3.org/TR/WGSL/#keyword-summary */
|
|
@@ -6,7 +6,6 @@ import type {
|
|
|
6
6
|
DiagnosticRule,
|
|
7
7
|
ElifAttribute,
|
|
8
8
|
ElseAttribute,
|
|
9
|
-
ExpressionElem,
|
|
10
9
|
IfAttribute,
|
|
11
10
|
InterpolateAttribute,
|
|
12
11
|
NameElem,
|
|
@@ -15,7 +14,6 @@ import type {
|
|
|
15
14
|
UnknownExpressionElem,
|
|
16
15
|
} from "../AbstractElems.ts";
|
|
17
16
|
import { ParseError } from "../ParseError.ts";
|
|
18
|
-
import { beginElem, finishContents } from "./ContentsHelpers.ts";
|
|
19
17
|
import { parseExpression } from "./ParseExpression.ts";
|
|
20
18
|
import {
|
|
21
19
|
expect,
|
|
@@ -23,6 +21,7 @@ import {
|
|
|
23
21
|
makeNameElem,
|
|
24
22
|
parseCommaList,
|
|
25
23
|
parseMany,
|
|
24
|
+
throwParseError,
|
|
26
25
|
} from "./ParseUtil.ts";
|
|
27
26
|
import type { ParsingContext } from "./ParsingContext.ts";
|
|
28
27
|
|
|
@@ -47,6 +46,27 @@ export function parseElifAttribute(ctx: ParsingContext): ElifAttribute | null {
|
|
|
47
46
|
return parseConditionalAttribute(ctx, "elif", makeElifAttribute);
|
|
48
47
|
}
|
|
49
48
|
|
|
49
|
+
/** Parse WESL conditional attributes (@if, @elif, @else) */
|
|
50
|
+
export function parseWeslConditional(
|
|
51
|
+
ctx: ParsingContext,
|
|
52
|
+
): AttributeElem | null {
|
|
53
|
+
const { stream } = ctx;
|
|
54
|
+
const peeked = stream.peek();
|
|
55
|
+
if (peeked?.text !== "@") return null;
|
|
56
|
+
const startPos = peeked.span[0]; // Use token position, not stream checkpoint
|
|
57
|
+
|
|
58
|
+
const ifAttr = parseIfAttribute(ctx);
|
|
59
|
+
if (ifAttr) return attributeElem(ifAttr, startPos, stream.checkpoint());
|
|
60
|
+
|
|
61
|
+
const elifAttr = parseElifAttribute(ctx);
|
|
62
|
+
if (elifAttr) return attributeElem(elifAttr, startPos, stream.checkpoint());
|
|
63
|
+
|
|
64
|
+
const elseAttr = parseElseAttribute(ctx);
|
|
65
|
+
if (elseAttr) return attributeElem(elseAttr, startPos, stream.checkpoint());
|
|
66
|
+
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
50
70
|
/**
|
|
51
71
|
* Grammar: attribute :
|
|
52
72
|
* '@' ident_pattern_token argument_expression_list ?
|
|
@@ -57,18 +77,9 @@ export function parseElifAttribute(ctx: ParsingContext): ElifAttribute | null {
|
|
|
57
77
|
* WESL extensions: @if, @elif, @else
|
|
58
78
|
*/
|
|
59
79
|
function parseAttribute(ctx: ParsingContext): AttributeElem | null {
|
|
60
|
-
|
|
61
|
-
const startPos = stream.checkpoint();
|
|
62
|
-
if (!stream.matchText("@")) return null;
|
|
63
|
-
stream.reset(startPos);
|
|
64
|
-
|
|
65
|
-
const weslAttr = parseWeslConditional(ctx);
|
|
66
|
-
if (weslAttr) return weslAttr;
|
|
80
|
+
if (ctx.stream.peek()?.text !== "@") return null;
|
|
67
81
|
|
|
68
|
-
|
|
69
|
-
if (stdAttr) return stdAttr;
|
|
70
|
-
|
|
71
|
-
return null;
|
|
82
|
+
return parseWeslConditional(ctx) ?? parseStandardAttribute(ctx);
|
|
72
83
|
}
|
|
73
84
|
|
|
74
85
|
/** Parse `@if(expr)` or `@elif(expr)` conditional attributes. */
|
|
@@ -82,16 +93,21 @@ function parseConditionalAttribute<T>(
|
|
|
82
93
|
if (!stream.matchSequence("@", keyword)) return null;
|
|
83
94
|
|
|
84
95
|
expect(stream, "(", `@${keyword}`);
|
|
96
|
+
// Past `@keyword(` we're committed: a missing/invalid condition is a hard
|
|
97
|
+
// parse error, not a backtrack (the stream is already advanced past `(`).
|
|
85
98
|
const expr = parseExpression(ctx, true);
|
|
86
|
-
if (!expr)
|
|
99
|
+
if (!expr) throwParseError(stream, `Expected expression after @${keyword}(`);
|
|
87
100
|
|
|
88
101
|
stream.matchText(",");
|
|
89
102
|
expect(stream, ")", `@${keyword} expression`);
|
|
90
103
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
104
|
+
// TODO remove translate-time once we drop v1
|
|
105
|
+
const translateTimeExpr: TranslateTimeExpressionElem = {
|
|
106
|
+
kind: "translate-time-expression",
|
|
107
|
+
expression: expr,
|
|
108
|
+
start: startPos,
|
|
109
|
+
end: stream.checkpoint(),
|
|
110
|
+
};
|
|
95
111
|
return makeAttr(translateTimeExpr);
|
|
96
112
|
}
|
|
97
113
|
|
|
@@ -107,25 +123,12 @@ function makeElifAttribute(param: TranslateTimeExpressionElem): ElifAttribute {
|
|
|
107
123
|
return { kind: "@elif", param } as const;
|
|
108
124
|
}
|
|
109
125
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (peeked?.text !== "@") return null;
|
|
117
|
-
const startPos = peeked.span[0]; // Use token position, not stream checkpoint
|
|
118
|
-
|
|
119
|
-
const ifAttr = parseIfAttribute(ctx);
|
|
120
|
-
if (ifAttr) return attributeElem(ifAttr, startPos, stream.checkpoint());
|
|
121
|
-
|
|
122
|
-
const elifAttr = parseElifAttribute(ctx);
|
|
123
|
-
if (elifAttr) return attributeElem(elifAttr, startPos, stream.checkpoint());
|
|
124
|
-
|
|
125
|
-
const elseAttr = parseElseAttribute(ctx);
|
|
126
|
-
if (elseAttr) return attributeElem(elseAttr, startPos, stream.checkpoint());
|
|
127
|
-
|
|
128
|
-
return null;
|
|
126
|
+
function attributeElem(
|
|
127
|
+
attribute: Attribute,
|
|
128
|
+
start: number,
|
|
129
|
+
end: number,
|
|
130
|
+
): AttributeElem {
|
|
131
|
+
return { kind: "attribute", attribute, start, end };
|
|
129
132
|
}
|
|
130
133
|
|
|
131
134
|
/** Parse a standard attribute (not @if/@elif/@else) */
|
|
@@ -137,10 +140,7 @@ function parseStandardAttribute(ctx: ParsingContext): AttributeElem | null {
|
|
|
137
140
|
const startPos = atToken.span[0]; // Use actual @ position, not before whitespace
|
|
138
141
|
|
|
139
142
|
const nameToken = stream.peek();
|
|
140
|
-
if (
|
|
141
|
-
!nameToken ||
|
|
142
|
-
(nameToken.kind !== "word" && nameToken.kind !== "keyword")
|
|
143
|
-
) {
|
|
143
|
+
if (nameToken?.kind !== "word" && nameToken?.kind !== "keyword") {
|
|
144
144
|
stream.reset(resetPos);
|
|
145
145
|
return null;
|
|
146
146
|
}
|
|
@@ -171,26 +171,6 @@ function parseStandardAttribute(ctx: ParsingContext): AttributeElem | null {
|
|
|
171
171
|
return attributeElem(stdAttr, startPos, stream.checkpoint());
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
// TODO remove translate-time once we drop v1
|
|
175
|
-
function makeTranslateTimeExpressionElem(args: {
|
|
176
|
-
value: ExpressionElem;
|
|
177
|
-
span: [number, number];
|
|
178
|
-
}): TranslateTimeExpressionElem {
|
|
179
|
-
return {
|
|
180
|
-
kind: "translate-time-expression",
|
|
181
|
-
expression: args.value,
|
|
182
|
-
span: args.span,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function attributeElem(
|
|
187
|
-
attribute: Attribute,
|
|
188
|
-
start: number,
|
|
189
|
-
end: number,
|
|
190
|
-
): AttributeElem {
|
|
191
|
-
return { kind: "attribute", attribute, start, end, contents: [] };
|
|
192
|
-
}
|
|
193
|
-
|
|
194
174
|
function parseBuiltinAttribute(
|
|
195
175
|
ctx: ParsingContext,
|
|
196
176
|
startPos: number,
|
|
@@ -224,11 +204,6 @@ function parseInterpolateAttribute(
|
|
|
224
204
|
return attributeElem(interpolateAttr, startPos, stream.checkpoint());
|
|
225
205
|
}
|
|
226
206
|
|
|
227
|
-
function parseNameElem(ctx: ParsingContext): NameElem {
|
|
228
|
-
const nameToken = expectWord(ctx.stream, "Expected identifier");
|
|
229
|
-
return makeNameElem(nameToken);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
207
|
/** @diagnostic(severity, rule) or @diagnostic(severity, namespace.rule) */
|
|
233
208
|
function parseDiagnosticAttribute(
|
|
234
209
|
ctx: ParsingContext,
|
|
@@ -263,12 +238,15 @@ function parseAttributeParams(ctx: ParsingContext): UnknownExpressionElem[] {
|
|
|
263
238
|
return parseCommaList(ctx, parseAttrParam);
|
|
264
239
|
}
|
|
265
240
|
|
|
241
|
+
function parseNameElem(ctx: ParsingContext): NameElem {
|
|
242
|
+
const nameToken = expectWord(ctx.stream, "Expected identifier");
|
|
243
|
+
return makeNameElem(nameToken);
|
|
244
|
+
}
|
|
245
|
+
|
|
266
246
|
function parseAttrParam(ctx: ParsingContext): UnknownExpressionElem {
|
|
267
247
|
const { stream } = ctx;
|
|
268
248
|
const start = stream.checkpoint();
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const contents = finishContents(ctx, start, end);
|
|
273
|
-
return { kind: "expression", start, end, contents };
|
|
249
|
+
const expression = parseExpression(ctx);
|
|
250
|
+
if (!expression) throwParseError(stream, "Expected attribute parameter");
|
|
251
|
+
return { kind: "expression", expression, start, end: stream.checkpoint() };
|
|
274
252
|
}
|
package/src/parse/ParseCall.ts
CHANGED
|
@@ -24,11 +24,10 @@ export function parseCallSuffix(
|
|
|
24
24
|
if (current.kind !== "ref" && current.kind !== "type") return null;
|
|
25
25
|
|
|
26
26
|
const { stream } = ctx;
|
|
27
|
-
//
|
|
27
|
+
// Constructor calls (e.g. vec4<f32>(...)) already have their template args
|
|
28
|
+
// on the TypeRefElem; only function calls need template args parsed here.
|
|
28
29
|
const templateArgs =
|
|
29
|
-
current.kind === "
|
|
30
|
-
? (current.templateParams ?? null)
|
|
31
|
-
: parseCallTemplateArgs(ctx);
|
|
30
|
+
current.kind === "ref" ? parseCallTemplateArgs(ctx) : null;
|
|
32
31
|
|
|
33
32
|
if (!stream.matchText("(")) return null;
|
|
34
33
|
|