shaderkit 0.6.4 → 0.8.0

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/src/parser.ts CHANGED
@@ -1,802 +1,878 @@
1
- import {
2
- ArraySpecifier,
3
- AssignmentOperator,
4
- BinaryOperator,
5
- BlockStatement,
6
- BreakStatement,
7
- ConstantQualifier,
8
- ContinueStatement,
9
- DiscardStatement,
10
- DoWhileStatement,
11
- Expression,
12
- ForStatement,
13
- FunctionDeclaration,
14
- FunctionParameter,
15
- Identifier,
16
- IfStatement,
17
- InterpolationQualifier,
18
- InvariantQualifierStatement,
19
- LayoutQualifier,
20
- Literal,
21
- LogicalOperator,
22
- ParameterQualifier,
23
- PrecisionQualifier,
24
- PrecisionQualifierStatement,
25
- PreprocessorStatement,
26
- Program,
27
- ReturnStatement,
28
- Statement,
29
- StorageQualifier,
30
- StructDeclaration,
31
- SwitchCase,
32
- SwitchStatement,
33
- UnaryOperator,
34
- StructuredBufferDeclaration,
35
- UpdateOperator,
36
- VariableDeclaration,
37
- VariableDeclarator,
38
- WhileStatement,
39
- LayoutQualifierStatement,
40
- } from './ast.js'
41
- import { type Token, tokenize } from './tokenizer.js'
42
-
43
- // https://engineering.desmos.com/articles/pratt-parser
44
- // https://matklad.github.io/2020/04/13/simple-but-powerful-pratt-parsing.html
45
-
46
- // 5.1 Operators
47
- enum Precedence {
48
- LOWEST,
49
- COMMA,
50
- ASSIGN,
51
- LOGICAL_OR,
52
- LOGICAL_XOR,
53
- LOGICAL_AND,
54
- BITWISE_OR,
55
- BITWISE_XOR,
56
- BITWISE_AND,
57
- TERNARY,
58
- COMPARE,
59
- SHIFT,
60
- ADD,
61
- MULTIPLY,
62
- UNARY_PREFIX,
63
- UNARY_POSTFIX,
64
- CALL,
65
- MEMBER,
66
- }
67
-
68
- const PREFIX_OPERATOR_PRECEDENCE: Record<string, Precedence> = {
69
- '~': Precedence.UNARY_PREFIX,
70
- '!': Precedence.UNARY_PREFIX,
71
- '--': Precedence.UNARY_PREFIX,
72
- '++': Precedence.UNARY_PREFIX,
73
- '-': Precedence.UNARY_PREFIX,
74
- '+': Precedence.UNARY_PREFIX,
75
- }
76
-
77
- const POSTFIX_OPERATOR_PRECEDENCE: Record<string, Precedence> = {
78
- '--': Precedence.UNARY_POSTFIX,
79
- '++': Precedence.UNARY_POSTFIX,
80
- '(': Precedence.CALL,
81
- '[': Precedence.MEMBER,
82
- '.': Precedence.MEMBER,
83
- }
84
-
85
- const INFIX_OPERATOR_PRECEDENCE_LEFT: Record<string, Precedence> = {
86
- ',': Precedence.COMMA,
87
- '||': Precedence.LOGICAL_OR,
88
- '^^': Precedence.LOGICAL_XOR,
89
- '&&': Precedence.LOGICAL_AND,
90
- '|': Precedence.BITWISE_OR,
91
- '^': Precedence.BITWISE_XOR,
92
- '&': Precedence.BITWISE_AND,
93
- '==': Precedence.COMPARE,
94
- '>': Precedence.COMPARE,
95
- '>=': Precedence.COMPARE,
96
- '<': Precedence.COMPARE,
97
- '<=': Precedence.COMPARE,
98
- '!=': Precedence.COMPARE,
99
- '<<': Precedence.SHIFT,
100
- '>>': Precedence.SHIFT,
101
- '+': Precedence.ADD,
102
- '-': Precedence.ADD,
103
- '*': Precedence.MULTIPLY,
104
- '/': Precedence.MULTIPLY,
105
- '%': Precedence.MULTIPLY,
106
- }
107
-
108
- const INFIX_OPERATOR_PRECEDENCE_RIGHT: Record<string, Precedence> = {
109
- '=': Precedence.ASSIGN,
110
- '+=': Precedence.ASSIGN,
111
- '&=': Precedence.ASSIGN,
112
- '|=': Precedence.ASSIGN,
113
- '^=': Precedence.ASSIGN,
114
- '/=': Precedence.ASSIGN,
115
- '*=': Precedence.ASSIGN,
116
- '%=': Precedence.ASSIGN,
117
- '<<=': Precedence.ASSIGN,
118
- '>>=': Precedence.ASSIGN,
119
- '-=': Precedence.ASSIGN,
120
- '?': Precedence.TERNARY,
121
- }
122
-
123
- const QUALIFIER_REGEX =
124
- /^(const|buffer|uniform|in|out|inout|centroid|flat|smooth|invariant|lowp|mediump|highp|coherent|volatile|restrict|readonly|writeonly|attribute|varying)$/
125
-
126
- const SCOPE_DELTAS: Record<string, number> = {
127
- // Open
128
- '(': 1,
129
- '[': 1,
130
- '{': 1,
131
- // Close
132
- ')': -1,
133
- ']': -1,
134
- '}': -1,
135
- }
136
- function getScopeDelta(token: Token): number {
137
- return SCOPE_DELTAS[token.value] ?? 0
138
- }
139
-
140
- function peek(tokens: Token[], offset: number = 0): Token | null {
141
- for (let i = 0; i < tokens.length; i++) {
142
- const token = tokens[i]
143
- if (token.type !== 'whitespace' && token.type !== 'comment') {
144
- if (offset === 0) return token
145
- else offset--
146
- }
147
- }
148
-
149
- return null
150
- }
151
-
152
- function consume(tokens: Token[], expected?: string): Token {
153
- // TODO: use token cursor for performance and store for sourcemaps
154
- let token = tokens.shift()
155
- while (token && (token.type === 'whitespace' || token.type === 'comment')) {
156
- token = tokens.shift()
157
- }
158
-
159
- if (token === undefined && expected !== undefined) {
160
- throw new SyntaxError(`Expected "${expected}"`)
161
- } else if (token === undefined) {
162
- throw new SyntaxError('Unexpected end of input')
163
- } else if (expected !== undefined && token.value !== expected) {
164
- throw new SyntaxError(`Expected "${expected}" got "${token.value}"`)
165
- }
166
-
167
- return token
168
- }
169
-
170
- function parseExpression(tokens: Token[], minBindingPower: number = 0): Expression {
171
- let token = consume(tokens)
172
-
173
- let lhs: Expression
174
- if (token.type === 'identifier' || token.type === 'keyword') {
175
- lhs = { type: 'Identifier', name: token.value }
176
- } else if (token.type === 'bool' || token.type === 'float' || token.type === 'int') {
177
- lhs = { type: 'Literal', value: token.value }
178
- } else if (token.type === 'symbol' && token.value === '(') {
179
- lhs = parseExpression(tokens, 0)
180
- consume(tokens, ')')
181
- } else if (token.type === 'symbol' && token.value in PREFIX_OPERATOR_PRECEDENCE) {
182
- const rightBindingPower = PREFIX_OPERATOR_PRECEDENCE[token.value]
183
- const rhs = parseExpression(tokens, rightBindingPower)
184
- if (token.value === '--' || token.value === '++') {
185
- lhs = { type: 'UpdateExpression', operator: token.value, prefix: true, argument: rhs }
186
- } else {
187
- lhs = { type: 'UnaryExpression', operator: token.value as UnaryOperator, prefix: true, argument: rhs }
188
- }
189
- } else {
190
- throw new SyntaxError(`Unexpected token: "${token.value}"`)
191
- }
192
-
193
- while (tokens.length) {
194
- token = peek(tokens)!
195
-
196
- if (token.value in POSTFIX_OPERATOR_PRECEDENCE) {
197
- const leftBindingPower = POSTFIX_OPERATOR_PRECEDENCE[token.value]
198
- if (leftBindingPower < minBindingPower) break
199
-
200
- consume(tokens)
201
-
202
- if (token.value === '(') {
203
- const args: Expression[] = []
204
-
205
- while (peek(tokens)?.value !== ')') {
206
- args.push(parseExpression(tokens, Precedence.COMMA))
207
- if (peek(tokens)?.value !== ')') consume(tokens, ',')
208
- }
209
- consume(tokens, ')')
210
-
211
- lhs = { type: 'CallExpression', callee: lhs, arguments: args }
212
- } else if (token.value === '[') {
213
- const rhs = peek(tokens)?.value !== ']' ? parseExpression(tokens, 0) : null
214
- consume(tokens, ']')
215
-
216
- if (peek(tokens)?.value === '(') {
217
- consume(tokens, '(')
218
- const elements: Expression[] = []
219
-
220
- while (peek(tokens)?.value !== ')') {
221
- elements.push(parseExpression(tokens, Precedence.COMMA))
222
- if (peek(tokens)?.value !== ')') consume(tokens, ',')
223
- }
224
- consume(tokens, ')')
225
-
226
- const typeSpecifier: ArraySpecifier = {
227
- type: 'ArraySpecifier',
228
- typeSpecifier: lhs as Identifier,
229
- dimensions: [rhs as Literal | null],
230
- }
231
- lhs = { type: 'ArrayExpression', typeSpecifier, elements }
232
- } else {
233
- lhs = { type: 'MemberExpression', object: lhs, property: rhs!, computed: true }
234
- }
235
- } else if (token.value === '.') {
236
- const rhs = parseExpression(tokens, leftBindingPower)
237
- lhs = { type: 'MemberExpression', object: lhs, property: rhs, computed: false }
238
- } else if (token.value === '--' || token.value === '++') {
239
- lhs = { type: 'UpdateExpression', operator: token.value as UpdateOperator, prefix: false, argument: lhs }
240
- } else {
241
- lhs = { type: 'UnaryExpression', operator: token.value as UnaryOperator, prefix: false, argument: lhs }
242
- }
243
- } else if (token.value in INFIX_OPERATOR_PRECEDENCE_LEFT) {
244
- const precedence = INFIX_OPERATOR_PRECEDENCE_LEFT[token.value]
245
- const leftBindingPower = precedence - 1
246
- const rightBindingPower = precedence
247
-
248
- if (leftBindingPower < minBindingPower) break
249
-
250
- consume(tokens)
251
-
252
- if (token.value === '||' || token.value === '&&' || token.value === '^^') {
253
- const rhs = parseExpression(tokens, rightBindingPower)
254
- lhs = { type: 'LogicalExpression', operator: token.value, left: lhs, right: rhs }
255
- } else {
256
- const rhs = parseExpression(tokens, rightBindingPower)
257
- lhs = { type: 'BinaryExpression', operator: token.value as BinaryOperator, left: lhs, right: rhs }
258
- }
259
- } else if (token.value in INFIX_OPERATOR_PRECEDENCE_RIGHT) {
260
- const precedence = INFIX_OPERATOR_PRECEDENCE_RIGHT[token.value]
261
- const leftBindingPower = precedence
262
- const rightBindingPower = precedence - 1
263
-
264
- if (leftBindingPower < minBindingPower) break
265
-
266
- consume(tokens)
267
-
268
- if (token.value === '?') {
269
- const mhs = parseExpression(tokens, 0)
270
- consume(tokens, ':')
271
- const rhs = parseExpression(tokens, rightBindingPower)
272
- lhs = { type: 'ConditionalExpression', test: lhs, alternate: mhs, consequent: rhs }
273
- } else {
274
- const rhs = parseExpression(tokens, rightBindingPower)
275
- lhs = { type: 'AssignmentExpression', operator: token.value as AssignmentOperator, left: lhs, right: rhs }
276
- }
277
- } else {
278
- break
279
- }
280
- }
281
-
282
- return lhs
283
- }
284
-
285
- function parseTypeSpecifier(tokens: Token[]): Identifier | ArraySpecifier {
286
- let typeSpecifier: Identifier | ArraySpecifier = { type: 'Identifier', name: consume(tokens).value }
287
-
288
- if (peek(tokens)?.value === '[') {
289
- const dimensions: (Literal | Identifier | null)[] = []
290
-
291
- while (peek(tokens)?.value === '[') {
292
- consume(tokens, '[')
293
-
294
- if (peek(tokens)?.value !== ']') {
295
- dimensions.push(parseExpression(tokens) as Literal | Identifier)
296
- } else {
297
- dimensions.push(null)
298
- }
299
-
300
- consume(tokens, ']')
301
- }
302
-
303
- typeSpecifier = {
304
- type: 'ArraySpecifier',
305
- typeSpecifier,
306
- dimensions,
307
- }
308
- }
309
-
310
- return typeSpecifier
311
- }
312
-
313
- function parseVariableDeclarator(
314
- tokens: Token[],
315
- typeSpecifier: Identifier | ArraySpecifier,
316
- qualifiers: (ConstantQualifier | InterpolationQualifier | StorageQualifier | PrecisionQualifier)[],
317
- layout: Record<string, string | boolean> | null,
318
- ): VariableDeclarator {
319
- const id = parseTypeSpecifier(tokens) as Identifier
320
-
321
- let init: Expression | null = null
322
-
323
- if (peek(tokens)?.value === '=') {
324
- consume(tokens, '=')
325
- init = parseExpression(tokens, Precedence.COMMA)
326
- }
327
-
328
- return { type: 'VariableDeclarator', id, qualifiers, typeSpecifier, layout, init }
329
- }
330
-
331
- function parseVariable(
332
- tokens: Token[],
333
- typeSpecifier: Identifier | ArraySpecifier,
334
- qualifiers: (ConstantQualifier | InterpolationQualifier | StorageQualifier | PrecisionQualifier)[] = [],
335
- layout: Record<string, string | boolean> | null = null,
336
- ): VariableDeclaration {
337
- const declarations: VariableDeclarator[] = []
338
-
339
- if (peek(tokens)?.value !== ';') {
340
- while (tokens.length) {
341
- declarations.push(parseVariableDeclarator(tokens, typeSpecifier, qualifiers, layout))
342
-
343
- if (peek(tokens)?.value === ',') {
344
- consume(tokens, ',')
345
- } else {
346
- break
347
- }
348
- }
349
- }
350
-
351
- consume(tokens, ';')
352
-
353
- return { type: 'VariableDeclaration', declarations }
354
- }
355
-
356
- function parseBufferInterface(
357
- tokens: Token[],
358
- typeSpecifier: Identifier | ArraySpecifier,
359
- qualifiers: LayoutQualifier[] = [],
360
- layout: Record<string, string | boolean> | null = null,
361
- ): StructuredBufferDeclaration {
362
- const members = parseBlock(tokens).body as VariableDeclaration[]
363
-
364
- let id: Identifier | null = null
365
- if (peek(tokens)?.value !== ';') id = parseExpression(tokens) as Identifier
366
- consume(tokens, ';')
367
-
368
- return { type: 'StructuredBufferDeclaration', id, qualifiers, typeSpecifier, layout, members }
369
- }
370
-
371
- function parseFunction(
372
- tokens: Token[],
373
- typeSpecifier: ArraySpecifier | Identifier,
374
- qualifiers: PrecisionQualifier[] = [],
375
- ): FunctionDeclaration {
376
- const id: Identifier = { type: 'Identifier', name: consume(tokens).value }
377
-
378
- consume(tokens, '(')
379
-
380
- const params: FunctionParameter[] = []
381
- while (true) {
382
- const token = peek(tokens)
383
- if (!token || token.value === ')') break
384
-
385
- const qualifiers: (ConstantQualifier | ParameterQualifier | PrecisionQualifier)[] = []
386
- while (peek(tokens) && QUALIFIER_REGEX.test(peek(tokens)!.value)) {
387
- qualifiers.push(consume(tokens).value as ConstantQualifier | ParameterQualifier | PrecisionQualifier)
388
- }
389
- const typeSpecifier = parseTypeSpecifier(tokens)
390
-
391
- let id: Identifier | null = null
392
- if (peek(tokens)?.type !== 'symbol') id = parseTypeSpecifier(tokens) as Identifier
393
-
394
- params.push({ type: 'FunctionParameter', id, qualifiers, typeSpecifier })
395
-
396
- if (peek(tokens)?.value === ',') consume(tokens, ',')
397
- }
398
-
399
- consume(tokens, ')')
400
-
401
- let body = null
402
- if (peek(tokens)?.value === ';') consume(tokens, ';')
403
- else body = parseBlock(tokens)
404
-
405
- return { type: 'FunctionDeclaration', id, qualifiers, typeSpecifier, params, body }
406
- }
407
-
408
- function parseLayoutQualifier(tokens: Token[], layout: Record<string, string | boolean>): LayoutQualifierStatement {
409
- const qualifier = consume(tokens).value as StorageQualifier
410
- consume(tokens, ';')
411
- return { type: 'LayoutQualifierStatement', layout, qualifier }
412
- }
413
-
414
- function parseInvariant(tokens: Token[]): InvariantQualifierStatement {
415
- consume(tokens, 'invariant')
416
- const typeSpecifier = parseExpression(tokens) as Identifier
417
- consume(tokens, ';')
418
- return { type: 'InvariantQualifierStatement', typeSpecifier }
419
- }
420
-
421
- function parseIndeterminate(
422
- tokens: Token[],
423
- ):
424
- | VariableDeclaration
425
- | FunctionDeclaration
426
- | StructuredBufferDeclaration
427
- | LayoutQualifierStatement
428
- | InvariantQualifierStatement {
429
- let layout: Record<string, string | boolean> | null = null
430
- if (peek(tokens)?.value === 'layout') {
431
- consume(tokens, 'layout')
432
- consume(tokens, '(')
433
-
434
- layout = {}
435
-
436
- while (peek(tokens) && peek(tokens)!.value !== ')') {
437
- const expression = parseExpression(tokens, Precedence.COMMA)
438
-
439
- if (
440
- expression.type === 'AssignmentExpression' &&
441
- expression.left.type === 'Identifier' &&
442
- expression.right.type === 'Literal'
443
- ) {
444
- layout[expression.left.name] = expression.right.value
445
- } else if (expression.type === 'Identifier') {
446
- layout[expression.name] = true
447
- } else {
448
- throw new TypeError('Unexpected expression')
449
- }
450
-
451
- if (peek(tokens) && peek(tokens)!.value !== ')') consume(tokens, ',')
452
- }
453
-
454
- consume(tokens, ')')
455
- }
456
-
457
- // Input qualifiers will suddenly terminate
458
- if (layout !== null && peek(tokens, 1)?.value === ';') {
459
- return parseLayoutQualifier(tokens, layout)
460
- }
461
-
462
- // Invariant qualifiers will terminate with an identifier
463
- if (
464
- layout === null &&
465
- peek(tokens)?.value === 'invariant' &&
466
- (peek(tokens, 1)?.type === 'identifier' || (peek(tokens, 1)?.type === 'keyword' && peek(tokens, 2)?.value === ';'))
467
- ) {
468
- return parseInvariant(tokens)
469
- }
470
-
471
- // TODO: only precision qualifier valid for function return type
472
- const qualifiers: string[] = []
473
- while (peek(tokens) && QUALIFIER_REGEX.test(peek(tokens)!.value)) {
474
- qualifiers.push(consume(tokens).value)
475
- }
476
-
477
- const typeSpecifier = parseTypeSpecifier(tokens)
478
-
479
- if (peek(tokens)?.value === '{') {
480
- return parseBufferInterface(tokens, typeSpecifier, qualifiers as LayoutQualifier[], layout)
481
- } else if (peek(tokens, 1)?.value === '(') {
482
- return parseFunction(tokens, typeSpecifier, qualifiers as PrecisionQualifier[])
483
- } else {
484
- return parseVariable(
485
- tokens,
486
- typeSpecifier,
487
- qualifiers as (ConstantQualifier | InterpolationQualifier | StorageQualifier | PrecisionQualifier)[],
488
- layout,
489
- )
490
- }
491
- }
492
-
493
- function parseStruct(tokens: Token[]): StructDeclaration {
494
- consume(tokens, 'struct')
495
- const id: Identifier = { type: 'Identifier', name: consume(tokens).value }
496
- consume(tokens, '{')
497
- const members: VariableDeclaration[] = []
498
- while (peek(tokens) && peek(tokens)!.value !== '}') {
499
- members.push(...(parseStatements(tokens) as unknown as VariableDeclaration[]))
500
- }
501
- consume(tokens, '}')
502
-
503
- // Hack to append a separate variable declaration in the next iterator
504
- // `struct a {} name;` is parsed as `struct a {}; a name;`
505
- if (peek(tokens)?.type === 'identifier') {
506
- const type = id.name
507
- const name = consume(tokens).value
508
- tokens.push(
509
- { type: 'identifier', value: type },
510
- { type: 'identifier', value: name },
511
- { type: 'symbol', value: ';' },
512
- )
513
- }
514
-
515
- consume(tokens, ';')
516
-
517
- return { type: 'StructDeclaration', id, members }
518
- }
519
-
520
- function parseContinue(tokens: Token[]): ContinueStatement {
521
- consume(tokens, 'continue')
522
- consume(tokens, ';')
523
-
524
- return { type: 'ContinueStatement' }
525
- }
526
-
527
- function parseBreak(tokens: Token[]): BreakStatement {
528
- consume(tokens, 'break')
529
- consume(tokens, ';')
530
-
531
- return { type: 'BreakStatement' }
532
- }
533
-
534
- function parseDiscard(tokens: Token[]): DiscardStatement {
535
- consume(tokens, 'discard')
536
- consume(tokens, ';')
537
-
538
- return { type: 'DiscardStatement' }
539
- }
540
-
541
- function parseReturn(tokens: Token[]): ReturnStatement {
542
- consume(tokens, 'return')
543
-
544
- let argument: Expression | null = null
545
- if (peek(tokens)?.value !== ';') argument = parseExpression(tokens)
546
- consume(tokens, ';')
547
-
548
- return { type: 'ReturnStatement', argument }
549
- }
550
-
551
- function parseIf(tokens: Token[]): IfStatement {
552
- consume(tokens, 'if')
553
- consume(tokens, '(')
554
- const test = parseExpression(tokens)
555
- consume(tokens, ')')
556
-
557
- const consequent = parseBlockOrStatement(tokens)
558
-
559
- let alternate = null
560
- const elseToken = peek(tokens)
561
- if (elseToken && elseToken.value === 'else') {
562
- consume(tokens, 'else')
563
-
564
- if (peek(tokens) && peek(tokens)!.value === 'if') {
565
- alternate = parseIf(tokens)
566
- } else {
567
- alternate = parseBlockOrStatement(tokens)
568
- }
569
- }
570
-
571
- return { type: 'IfStatement', test, consequent, alternate }
572
- }
573
-
574
- function parseWhile(tokens: Token[]): WhileStatement {
575
- consume(tokens, 'while')
576
- consume(tokens, '(')
577
- const test = parseExpression(tokens)
578
- consume(tokens, ')')
579
- const body = parseBlockOrStatement(tokens)
580
-
581
- return { type: 'WhileStatement', test, body }
582
- }
583
-
584
- function parseFor(tokens: Token[]): ForStatement {
585
- consume(tokens, 'for')
586
- consume(tokens, '(')
587
- const typeSpecifier = parseExpression(tokens) as Identifier | ArraySpecifier
588
- const init = parseVariable(tokens, typeSpecifier)
589
- // consume(tokens, ';')
590
- const test = parseExpression(tokens)
591
- consume(tokens, ';')
592
- const update = parseExpression(tokens)
593
- consume(tokens, ')')
594
- const body = parseBlockOrStatement(tokens)
595
-
596
- return { type: 'ForStatement', init, test, update, body }
597
- }
598
-
599
- function parseDoWhile(tokens: Token[]): DoWhileStatement {
600
- consume(tokens, 'do')
601
- const body = parseBlockOrStatement(tokens)
602
- consume(tokens, 'while')
603
- consume(tokens, '(')
604
- const test = parseExpression(tokens)
605
- consume(tokens, ')')
606
- consume(tokens, ';')
607
-
608
- return { type: 'DoWhileStatement', test, body }
609
- }
610
-
611
- function parseSwitch(tokens: Token[]): SwitchStatement {
612
- consume(tokens, 'switch')
613
- const discriminant = parseExpression(tokens)
614
-
615
- const cases: SwitchCase[] = []
616
- while (tokens.length) {
617
- const token = consume(tokens)
618
- if (token.value === '}') break
619
-
620
- if (token.value === 'case') {
621
- const test = parseExpression(tokens)
622
- consume(tokens, ':')
623
- const consequent = parseStatements(tokens)
624
- cases.push({ type: 'SwitchCase', test, consequent })
625
- } else if (token.value === 'default') {
626
- const test = null
627
- consume(tokens, ':')
628
- const consequent = parseStatements(tokens)
629
- cases.push({ type: 'SwitchCase', test, consequent })
630
- }
631
- }
632
-
633
- return { type: 'SwitchStatement', discriminant, cases }
634
- }
635
-
636
- function parsePrecision(tokens: Token[]): PrecisionQualifierStatement {
637
- consume(tokens, 'precision')
638
- const precision = consume(tokens).value as PrecisionQualifier
639
- const typeSpecifier: Identifier = { type: 'Identifier', name: consume(tokens).value }
640
- consume(tokens, ';')
641
- return { type: 'PrecisionQualifierStatement', precision, typeSpecifier }
642
- }
643
-
644
- function parsePreprocessor(tokens: Token[]): PreprocessorStatement {
645
- consume(tokens, '#')
646
-
647
- let name = '' // name can be unset for the # directive which is ignored
648
- let value: Expression[] | null = null
649
-
650
- if (peek(tokens)?.value !== '\\') {
651
- name = consume(tokens).value
652
-
653
- if (name === 'define') {
654
- const lhs = consume(tokens)
655
- let left: Expression = { type: 'Identifier', name: lhs.value }
656
- const next = peek(tokens)
657
-
658
- // Macro definition: #define foo(a, b, c) ...
659
- if (next && next.value === '(') {
660
- consume(tokens)
661
-
662
- const args: Expression[] = []
663
- while (peek(tokens)?.value !== ')') {
664
- args.push(parseExpression(tokens, Precedence.COMMA))
665
- if (peek(tokens)?.value !== ')') consume(tokens, ',')
666
- }
667
- consume(tokens, ')')
668
-
669
- left = { type: 'CallExpression', callee: left, arguments: args }
670
- }
671
-
672
- if (peek(tokens)?.value === '\\') value = [left]
673
- else value = [left, parseExpression(tokens)]
674
- } else if (name === 'extension') {
675
- // TODO: extension directives must be before declarations
676
- const left = parseExpression(tokens)
677
- consume(tokens, ':')
678
- const right = parseExpression(tokens)
679
- value = [left, right]
680
- } else if (name === 'include') {
681
- consume(tokens, '<')
682
- value = [{ type: 'Identifier', name: consume(tokens).value }]
683
- consume(tokens, '>')
684
- } else if (name !== 'else' && name !== 'endif') {
685
- value = []
686
- while (peek(tokens) && peek(tokens)!.value !== '\\') {
687
- value.push(parseExpression(tokens))
688
- }
689
- }
690
- }
691
-
692
- consume(tokens, '\\')
693
-
694
- return { type: 'PreprocessorStatement', name, value }
695
- }
696
-
697
- function isVariable(tokens: Token[]): boolean {
698
- let token = peek(tokens, 0)
699
-
700
- // Skip first token if EOF or not type qualifier/specifier
701
- if (!token || (token.type !== 'identifier' && token.type !== 'keyword')) return false
702
-
703
- // Layout qualifiers are only valid for declarations
704
- if (token.value === 'layout') return true
705
-
706
- // Skip to end of possible expression statement (e.g. callexpr -> fndecl)
707
- let i = 1
708
- let scopeIndex = 0
709
- while (true) {
710
- token = peek(tokens, i)
711
- if (!token) break
712
-
713
- const delta = getScopeDelta(token)
714
- if (scopeIndex <= 0 && delta <= 0) break
715
-
716
- scopeIndex += delta
717
- i++
718
- }
719
-
720
- // A variable declaration must follow with an identifier or type
721
- return peek(tokens, i)?.type !== 'symbol'
722
- }
723
-
724
- function parseStatement(tokens: Token[]): Statement {
725
- const token = peek(tokens)!
726
- let statement: Statement | null = null
727
-
728
- if (token.value === '#') statement = parsePreprocessor(tokens)
729
- else if (token.value === 'struct') statement = parseStruct(tokens)
730
- else if (token.value === 'continue') statement = parseContinue(tokens)
731
- else if (token.value === 'break') statement = parseBreak(tokens)
732
- else if (token.value === 'discard') statement = parseDiscard(tokens)
733
- else if (token.value === 'return') statement = parseReturn(tokens)
734
- else if (token.value === 'if') statement = parseIf(tokens)
735
- else if (token.value === 'while') statement = parseWhile(tokens)
736
- else if (token.value === 'for') statement = parseFor(tokens)
737
- else if (token.value === 'do') statement = parseDoWhile(tokens)
738
- else if (token.value === 'switch') statement = parseSwitch(tokens)
739
- else if (token.value === 'precision') statement = parsePrecision(tokens)
740
- else if (isVariable(tokens)) statement = parseIndeterminate(tokens)
741
- else if (token.value === '{') statement = parseBlock(tokens)
742
- else {
743
- const expression = parseExpression(tokens)
744
- if (peek(tokens)?.value === ',') consume(tokens, ';')
745
- else consume(tokens, ';')
746
- statement = { type: 'ExpressionStatement', expression }
747
- }
748
-
749
- return statement
750
- }
751
-
752
- function parseStatements(tokens: Token[]): Statement[] {
753
- const body: Statement[] = []
754
- let scopeIndex = 0
755
-
756
- while (true) {
757
- const token = peek(tokens)
758
- if (!token) break
759
-
760
- scopeIndex += getScopeDelta(token)
761
- if (scopeIndex < 0 || token.value === '}') break
762
-
763
- if (token.value === 'case' || token.value === 'default') break
764
- body.push(parseStatement(tokens))
765
- }
766
-
767
- return body
768
- }
769
-
770
- function parseBlock(tokens: Token[]): BlockStatement {
771
- consume(tokens, '{')
772
- const body = parseStatements(tokens)
773
- consume(tokens, '}')
774
- return { type: 'BlockStatement', body }
775
- }
776
-
777
- // TODO: validate block versus sub-statements for GLSL/WGSL
778
- function parseBlockOrStatement(tokens: Token[]): BlockStatement | Statement {
779
- if (peek(tokens)?.value === '{') {
780
- return parseBlock(tokens)
781
- } else {
782
- return parseStatement(tokens)
783
- }
784
- }
785
-
786
- const NEWLINE_REGEX = /\\\s+/gm
787
- const DIRECTIVE_REGEX = /(^\s*#[^\\]*?)(\n|\/[\/\*])/gm
788
-
789
- /**
790
- * Parses a string of GLSL (WGSL WIP) code into an [AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree).
791
- */
792
- export function parse(code: string): Program {
793
- // Fold newlines
794
- code = code.replace(NEWLINE_REGEX, '')
795
-
796
- // Escape newlines after directives, skip comments
797
- code = code.replace(DIRECTIVE_REGEX, '$1\\$2')
798
-
799
- const tokens = tokenize(code)
800
-
801
- return { type: 'Program', body: parseStatements(tokens) }
802
- }
1
+ import {
2
+ ArraySpecifier,
3
+ AssignmentOperator,
4
+ BinaryOperator,
5
+ BlockStatement,
6
+ BreakStatement,
7
+ ConstantQualifier,
8
+ ContinueStatement,
9
+ DiscardStatement,
10
+ DoWhileStatement,
11
+ Expression,
12
+ ForStatement,
13
+ FunctionDeclaration,
14
+ FunctionParameter,
15
+ Identifier,
16
+ IfStatement,
17
+ InterpolationQualifier,
18
+ InvariantQualifierStatement,
19
+ LayoutQualifier,
20
+ Literal,
21
+ LogicalOperator,
22
+ ParameterQualifier,
23
+ PrecisionQualifier,
24
+ PrecisionQualifierStatement,
25
+ PreprocessorStatement,
26
+ Program,
27
+ ReturnStatement,
28
+ Statement,
29
+ StorageQualifier,
30
+ StructDeclaration,
31
+ SwitchCase,
32
+ SwitchStatement,
33
+ UnaryOperator,
34
+ StructuredBufferDeclaration,
35
+ UpdateOperator,
36
+ VariableDeclaration,
37
+ VariableDeclarator,
38
+ WhileStatement,
39
+ LayoutQualifierStatement,
40
+ } from './ast.js'
41
+ import { hoistPreprocessorDirectives } from './hoister.js'
42
+ import { type Token, tokenize } from './tokenizer.js'
43
+
44
+ // https://engineering.desmos.com/articles/pratt-parser
45
+ // https://matklad.github.io/2020/04/13/simple-but-powerful-pratt-parsing.html
46
+
47
+ // 5.1 Operators
48
+ enum Precedence {
49
+ LOWEST,
50
+ COMMA,
51
+ ASSIGN,
52
+ LOGICAL_OR,
53
+ LOGICAL_XOR,
54
+ LOGICAL_AND,
55
+ BITWISE_OR,
56
+ BITWISE_XOR,
57
+ BITWISE_AND,
58
+ TERNARY,
59
+ COMPARE,
60
+ SHIFT,
61
+ ADD,
62
+ MULTIPLY,
63
+ UNARY_PREFIX,
64
+ UNARY_POSTFIX,
65
+ CALL,
66
+ MEMBER,
67
+ }
68
+
69
+ const PREFIX_OPERATOR_PRECEDENCE: Record<string, Precedence> = {
70
+ '~': Precedence.UNARY_PREFIX,
71
+ '!': Precedence.UNARY_PREFIX,
72
+ '--': Precedence.UNARY_PREFIX,
73
+ '++': Precedence.UNARY_PREFIX,
74
+ '-': Precedence.UNARY_PREFIX,
75
+ '+': Precedence.UNARY_PREFIX,
76
+ }
77
+
78
+ const POSTFIX_OPERATOR_PRECEDENCE: Record<string, Precedence> = {
79
+ '--': Precedence.UNARY_POSTFIX,
80
+ '++': Precedence.UNARY_POSTFIX,
81
+ '(': Precedence.CALL,
82
+ '[': Precedence.MEMBER,
83
+ '.': Precedence.MEMBER,
84
+ }
85
+
86
+ const INFIX_OPERATOR_PRECEDENCE_LEFT: Record<string, Precedence> = {
87
+ ',': Precedence.COMMA,
88
+ '||': Precedence.LOGICAL_OR,
89
+ '^^': Precedence.LOGICAL_XOR,
90
+ '&&': Precedence.LOGICAL_AND,
91
+ '|': Precedence.BITWISE_OR,
92
+ '^': Precedence.BITWISE_XOR,
93
+ '&': Precedence.BITWISE_AND,
94
+ '==': Precedence.COMPARE,
95
+ '>': Precedence.COMPARE,
96
+ '>=': Precedence.COMPARE,
97
+ '<': Precedence.COMPARE,
98
+ '<=': Precedence.COMPARE,
99
+ '!=': Precedence.COMPARE,
100
+ '<<': Precedence.SHIFT,
101
+ '>>': Precedence.SHIFT,
102
+ '+': Precedence.ADD,
103
+ '-': Precedence.ADD,
104
+ '*': Precedence.MULTIPLY,
105
+ '/': Precedence.MULTIPLY,
106
+ '%': Precedence.MULTIPLY,
107
+ }
108
+
109
+ const INFIX_OPERATOR_PRECEDENCE_RIGHT: Record<string, Precedence> = {
110
+ '=': Precedence.ASSIGN,
111
+ '+=': Precedence.ASSIGN,
112
+ '&=': Precedence.ASSIGN,
113
+ '|=': Precedence.ASSIGN,
114
+ '^=': Precedence.ASSIGN,
115
+ '/=': Precedence.ASSIGN,
116
+ '*=': Precedence.ASSIGN,
117
+ '%=': Precedence.ASSIGN,
118
+ '<<=': Precedence.ASSIGN,
119
+ '>>=': Precedence.ASSIGN,
120
+ '-=': Precedence.ASSIGN,
121
+ '?': Precedence.TERNARY,
122
+ }
123
+
124
+ const QUALIFIER_REGEX =
125
+ /^(const|buffer|uniform|in|out|inout|centroid|flat|smooth|invariant|lowp|mediump|highp|coherent|volatile|restrict|readonly|writeonly|attribute|varying)$/
126
+
127
+ const SCOPE_DELTAS: Record<string, number> = {
128
+ // Open
129
+ '(': 1,
130
+ '[': 1,
131
+ '{': 1,
132
+ // Close
133
+ ')': -1,
134
+ ']': -1,
135
+ '}': -1,
136
+ }
137
+ function getScopeDelta(token: Token): number {
138
+ return SCOPE_DELTAS[token.value] ?? 0
139
+ }
140
+
141
+ interface Tokens {
142
+ list: Token[]
143
+ /**
144
+ * Starts at 0, points at the next token yet to be consumed.
145
+ */
146
+ cursor: number
147
+ /**
148
+ * If a macro (or preprocessor statement) was encountered
149
+ * during parsing. Used to workaround expression-level macros
150
+ * and transform them into statements.
151
+ */
152
+ encounteredMacro?: boolean | undefined
153
+ }
154
+
155
+ function hasNextToken(tokens: Tokens): boolean {
156
+ return tokens.cursor < tokens.list.length
157
+ }
158
+
159
+ function skipIrrelevant(tokens: Tokens, ignorePreprocessor: boolean = true): void {
160
+ let preprocessorScope = 0
161
+ for (; hasNextToken(tokens); tokens.cursor++) {
162
+ const token = tokens.list[tokens.cursor]
163
+
164
+ if (ignorePreprocessor && token.value === '#') {
165
+ tokens.encounteredMacro = true
166
+ let name = ''
167
+ const nameToken = tokens.list[++tokens.cursor]
168
+ if (nameToken.value !== '\\') {
169
+ name = nameToken.value
170
+ }
171
+
172
+ if (name === 'if' || name === 'ifdef' || name === 'ifndef') {
173
+ preprocessorScope++
174
+ } else if (name === 'endif') {
175
+ preprocessorScope--
176
+ tokens.cursor++
177
+ } else {
178
+ // Ignoring everything up until the end of the directive
179
+ while (hasNextToken(tokens) && tokens.list[tokens.cursor].value !== '\\') tokens.cursor++
180
+ tokens.cursor++
181
+ }
182
+
183
+ continue
184
+ }
185
+
186
+ if (preprocessorScope > 0) {
187
+ continue
188
+ }
189
+
190
+ if (token.type === 'whitespace' || token.type === 'comment') {
191
+ continue
192
+ }
193
+
194
+ // Something relevant
195
+ return
196
+ }
197
+ }
198
+
199
+ function peek(tokens: Tokens, offset: number = 0, ignorePreprocessor: boolean = true): Token | null {
200
+ const prevCursor = tokens.cursor
201
+ while (hasNextToken(tokens)) {
202
+ skipIrrelevant(tokens, ignorePreprocessor)
203
+
204
+ if (offset === 0) {
205
+ const token = tokens.list[tokens.cursor]
206
+ tokens.cursor = prevCursor
207
+ return token
208
+ } else {
209
+ offset--
210
+ tokens.cursor++
211
+ }
212
+ }
213
+
214
+ tokens.cursor = prevCursor
215
+ return null
216
+ }
217
+
218
+ function consume(tokens: Tokens, expected?: string, ignorePreprocessor: boolean = true): Token {
219
+ // TODO: use store for sourcemaps
220
+ skipIrrelevant(tokens, ignorePreprocessor)
221
+ const token = tokens.list[tokens.cursor++]
222
+
223
+ if (token === undefined && expected !== undefined) {
224
+ throw new SyntaxError(`Expected "${expected}"`)
225
+ } else if (token === undefined) {
226
+ throw new SyntaxError('Unexpected end of input')
227
+ } else if (expected !== undefined && token.value !== expected) {
228
+ throw new SyntaxError(`Expected "${expected}" got "${token.value}"`)
229
+ }
230
+
231
+ return token
232
+ }
233
+
234
+ function parseExpression(tokens: Tokens, minBindingPower: number = 0): Expression {
235
+ let token = consume(tokens)
236
+
237
+ let lhs: Expression
238
+ if (token.type === 'identifier' || token.type === 'keyword') {
239
+ lhs = { type: 'Identifier', name: token.value }
240
+ } else if (token.type === 'bool' || token.type === 'float' || token.type === 'int') {
241
+ lhs = { type: 'Literal', value: token.value }
242
+ } else if (token.type === 'symbol' && token.value === '(') {
243
+ lhs = parseExpression(tokens, 0)
244
+ consume(tokens, ')')
245
+ } else if (token.type === 'symbol' && token.value in PREFIX_OPERATOR_PRECEDENCE) {
246
+ const rightBindingPower = PREFIX_OPERATOR_PRECEDENCE[token.value]
247
+ const rhs = parseExpression(tokens, rightBindingPower)
248
+ if (token.value === '--' || token.value === '++') {
249
+ lhs = { type: 'UpdateExpression', operator: token.value, prefix: true, argument: rhs }
250
+ } else {
251
+ lhs = { type: 'UnaryExpression', operator: token.value as UnaryOperator, prefix: true, argument: rhs }
252
+ }
253
+ } else {
254
+ throw new SyntaxError(`Unexpected token: "${token.value}"`)
255
+ }
256
+
257
+ while (hasNextToken(tokens)) {
258
+ token = peek(tokens)!
259
+
260
+ if (token.value in POSTFIX_OPERATOR_PRECEDENCE) {
261
+ const leftBindingPower = POSTFIX_OPERATOR_PRECEDENCE[token.value]
262
+ if (leftBindingPower < minBindingPower) break
263
+
264
+ consume(tokens)
265
+
266
+ if (token.value === '(') {
267
+ const args: Expression[] = []
268
+
269
+ while (peek(tokens)?.value !== ')') {
270
+ args.push(parseExpression(tokens, Precedence.COMMA))
271
+ if (peek(tokens)?.value !== ')') consume(tokens, ',')
272
+ }
273
+ consume(tokens, ')')
274
+
275
+ lhs = { type: 'CallExpression', callee: lhs, arguments: args }
276
+ } else if (token.value === '[') {
277
+ const rhs = peek(tokens)?.value !== ']' ? parseExpression(tokens, 0) : null
278
+ consume(tokens, ']')
279
+
280
+ if (peek(tokens)?.value === '(') {
281
+ consume(tokens, '(')
282
+ const elements: Expression[] = []
283
+
284
+ while (peek(tokens)?.value !== ')') {
285
+ elements.push(parseExpression(tokens, Precedence.COMMA))
286
+ if (peek(tokens)?.value !== ')') consume(tokens, ',')
287
+ }
288
+ consume(tokens, ')')
289
+
290
+ const typeSpecifier: ArraySpecifier = {
291
+ type: 'ArraySpecifier',
292
+ typeSpecifier: lhs as Identifier,
293
+ dimensions: [rhs as Literal | null],
294
+ }
295
+ lhs = { type: 'ArrayExpression', typeSpecifier, elements }
296
+ } else {
297
+ lhs = { type: 'MemberExpression', object: lhs, property: rhs!, computed: true }
298
+ }
299
+ } else if (token.value === '.') {
300
+ const rhs = parseExpression(tokens, leftBindingPower)
301
+ lhs = { type: 'MemberExpression', object: lhs, property: rhs, computed: false }
302
+ } else if (token.value === '--' || token.value === '++') {
303
+ lhs = { type: 'UpdateExpression', operator: token.value as UpdateOperator, prefix: false, argument: lhs }
304
+ } else {
305
+ lhs = { type: 'UnaryExpression', operator: token.value as UnaryOperator, prefix: false, argument: lhs }
306
+ }
307
+ } else if (token.value in INFIX_OPERATOR_PRECEDENCE_LEFT) {
308
+ const precedence = INFIX_OPERATOR_PRECEDENCE_LEFT[token.value]
309
+ const leftBindingPower = precedence - 1
310
+ const rightBindingPower = precedence
311
+
312
+ if (leftBindingPower < minBindingPower) break
313
+
314
+ consume(tokens)
315
+
316
+ if (token.value === '||' || token.value === '&&' || token.value === '^^') {
317
+ const rhs = parseExpression(tokens, rightBindingPower)
318
+ lhs = { type: 'LogicalExpression', operator: token.value, left: lhs, right: rhs }
319
+ } else {
320
+ const rhs = parseExpression(tokens, rightBindingPower)
321
+ lhs = { type: 'BinaryExpression', operator: token.value as BinaryOperator, left: lhs, right: rhs }
322
+ }
323
+ } else if (token.value in INFIX_OPERATOR_PRECEDENCE_RIGHT) {
324
+ const precedence = INFIX_OPERATOR_PRECEDENCE_RIGHT[token.value]
325
+ const leftBindingPower = precedence
326
+ const rightBindingPower = precedence - 1
327
+
328
+ if (leftBindingPower < minBindingPower) break
329
+
330
+ consume(tokens)
331
+
332
+ if (token.value === '?') {
333
+ const mhs = parseExpression(tokens, 0)
334
+ consume(tokens, ':')
335
+ const rhs = parseExpression(tokens, rightBindingPower)
336
+ lhs = { type: 'ConditionalExpression', test: lhs, alternate: mhs, consequent: rhs }
337
+ } else {
338
+ const rhs = parseExpression(tokens, rightBindingPower)
339
+ lhs = { type: 'AssignmentExpression', operator: token.value as AssignmentOperator, left: lhs, right: rhs }
340
+ }
341
+ } else {
342
+ break
343
+ }
344
+ }
345
+
346
+ return lhs
347
+ }
348
+
349
+ function parseTypeSpecifier(tokens: Tokens): Identifier | ArraySpecifier {
350
+ let typeSpecifier: Identifier | ArraySpecifier = { type: 'Identifier', name: consume(tokens).value }
351
+
352
+ if (peek(tokens)?.value === '[') {
353
+ const dimensions: (Literal | Identifier | null)[] = []
354
+
355
+ while (peek(tokens)?.value === '[') {
356
+ consume(tokens, '[')
357
+
358
+ if (peek(tokens)?.value !== ']') {
359
+ dimensions.push(parseExpression(tokens) as Literal | Identifier)
360
+ } else {
361
+ dimensions.push(null)
362
+ }
363
+
364
+ consume(tokens, ']')
365
+ }
366
+
367
+ typeSpecifier = {
368
+ type: 'ArraySpecifier',
369
+ typeSpecifier,
370
+ dimensions,
371
+ }
372
+ }
373
+
374
+ return typeSpecifier
375
+ }
376
+
377
+ function parseVariableDeclarator(
378
+ tokens: Tokens,
379
+ typeSpecifier: Identifier | ArraySpecifier,
380
+ qualifiers: (ConstantQualifier | InterpolationQualifier | StorageQualifier | PrecisionQualifier)[],
381
+ layout: Record<string, string | boolean> | null,
382
+ ): VariableDeclarator {
383
+ const id = parseTypeSpecifier(tokens) as Identifier
384
+
385
+ let init: Expression | null = null
386
+
387
+ if (peek(tokens)?.value === '=') {
388
+ consume(tokens, '=')
389
+ init = parseExpression(tokens, Precedence.COMMA)
390
+ }
391
+
392
+ return { type: 'VariableDeclarator', id, qualifiers, typeSpecifier, layout, init }
393
+ }
394
+
395
+ function parseVariable(
396
+ tokens: Tokens,
397
+ typeSpecifier: Identifier | ArraySpecifier,
398
+ qualifiers: (ConstantQualifier | InterpolationQualifier | StorageQualifier | PrecisionQualifier)[] = [],
399
+ layout: Record<string, string | boolean> | null = null,
400
+ ): VariableDeclaration {
401
+ const declarations: VariableDeclarator[] = []
402
+
403
+ if (peek(tokens)?.value !== ';') {
404
+ while (hasNextToken(tokens)) {
405
+ declarations.push(parseVariableDeclarator(tokens, typeSpecifier, qualifiers, layout))
406
+
407
+ if (peek(tokens)?.value === ',') {
408
+ consume(tokens, ',')
409
+ } else {
410
+ break
411
+ }
412
+ }
413
+ }
414
+
415
+ consume(tokens, ';')
416
+
417
+ return { type: 'VariableDeclaration', declarations }
418
+ }
419
+
420
+ function parseBufferInterface(
421
+ tokens: Tokens,
422
+ typeSpecifier: Identifier | ArraySpecifier,
423
+ qualifiers: LayoutQualifier[] = [],
424
+ layout: Record<string, string | boolean> | null = null,
425
+ ): StructuredBufferDeclaration {
426
+ const members = parseBlock(tokens).body as VariableDeclaration[]
427
+
428
+ let id: Identifier | null = null
429
+ if (peek(tokens)?.value !== ';') id = parseExpression(tokens) as Identifier
430
+ consume(tokens, ';')
431
+
432
+ return { type: 'StructuredBufferDeclaration', id, qualifiers, typeSpecifier, layout, members }
433
+ }
434
+
435
+ function parseFunction(
436
+ tokens: Tokens,
437
+ typeSpecifier: ArraySpecifier | Identifier,
438
+ qualifiers: PrecisionQualifier[] = [],
439
+ ): FunctionDeclaration {
440
+ const id: Identifier = { type: 'Identifier', name: consume(tokens).value }
441
+
442
+ consume(tokens, '(')
443
+
444
+ const params: FunctionParameter[] = []
445
+ while (true) {
446
+ const token = peek(tokens)
447
+ if (!token || token.value === ')') break
448
+
449
+ const qualifiers: (ConstantQualifier | ParameterQualifier | PrecisionQualifier)[] = []
450
+ while (peek(tokens) && QUALIFIER_REGEX.test(peek(tokens)!.value)) {
451
+ qualifiers.push(consume(tokens).value as ConstantQualifier | ParameterQualifier | PrecisionQualifier)
452
+ }
453
+ const typeSpecifier = parseTypeSpecifier(tokens)
454
+
455
+ let id: Identifier | null = null
456
+ if (peek(tokens)?.type !== 'symbol') id = parseTypeSpecifier(tokens) as Identifier
457
+
458
+ params.push({ type: 'FunctionParameter', id, qualifiers, typeSpecifier })
459
+
460
+ if (peek(tokens)?.value === ',') consume(tokens, ',')
461
+ }
462
+
463
+ consume(tokens, ')')
464
+
465
+ let body = null
466
+ if (peek(tokens)?.value === ';') consume(tokens, ';')
467
+ else body = parseBlock(tokens)
468
+
469
+ return { type: 'FunctionDeclaration', id, qualifiers, typeSpecifier, params, body }
470
+ }
471
+
472
+ function parseLayoutQualifier(tokens: Tokens, layout: Record<string, string | boolean>): LayoutQualifierStatement {
473
+ const qualifier = consume(tokens).value as StorageQualifier
474
+ consume(tokens, ';')
475
+ return { type: 'LayoutQualifierStatement', layout, qualifier }
476
+ }
477
+
478
+ function parseInvariant(tokens: Tokens): InvariantQualifierStatement {
479
+ consume(tokens, 'invariant')
480
+ const typeSpecifier = parseExpression(tokens) as Identifier
481
+ consume(tokens, ';')
482
+ return { type: 'InvariantQualifierStatement', typeSpecifier }
483
+ }
484
+
485
+ function parseIndeterminate(
486
+ tokens: Tokens,
487
+ ):
488
+ | VariableDeclaration
489
+ | FunctionDeclaration
490
+ | StructuredBufferDeclaration
491
+ | LayoutQualifierStatement
492
+ | InvariantQualifierStatement {
493
+ let layout: Record<string, string | boolean> | null = null
494
+ if (peek(tokens)?.value === 'layout') {
495
+ consume(tokens, 'layout')
496
+ consume(tokens, '(')
497
+
498
+ layout = {}
499
+
500
+ while (peek(tokens) && peek(tokens)!.value !== ')') {
501
+ const expression = parseExpression(tokens, Precedence.COMMA)
502
+
503
+ if (
504
+ expression.type === 'AssignmentExpression' &&
505
+ expression.left.type === 'Identifier' &&
506
+ expression.right.type === 'Literal'
507
+ ) {
508
+ layout[expression.left.name] = expression.right.value
509
+ } else if (expression.type === 'Identifier') {
510
+ layout[expression.name] = true
511
+ } else {
512
+ throw new TypeError('Unexpected expression')
513
+ }
514
+
515
+ if (peek(tokens) && peek(tokens)!.value !== ')') consume(tokens, ',')
516
+ }
517
+
518
+ consume(tokens, ')')
519
+ }
520
+
521
+ // Input qualifiers will suddenly terminate
522
+ if (layout !== null && peek(tokens, 1)?.value === ';') {
523
+ return parseLayoutQualifier(tokens, layout)
524
+ }
525
+
526
+ // Invariant qualifiers will terminate with an identifier
527
+ if (
528
+ layout === null &&
529
+ peek(tokens)?.value === 'invariant' &&
530
+ (peek(tokens, 1)?.type === 'identifier' || (peek(tokens, 1)?.type === 'keyword' && peek(tokens, 2)?.value === ';'))
531
+ ) {
532
+ return parseInvariant(tokens)
533
+ }
534
+
535
+ // TODO: only precision qualifier valid for function return type
536
+ const qualifiers: string[] = []
537
+ while (peek(tokens) && QUALIFIER_REGEX.test(peek(tokens)!.value)) {
538
+ qualifiers.push(consume(tokens).value)
539
+ }
540
+
541
+ const typeSpecifier = parseTypeSpecifier(tokens)
542
+
543
+ if (peek(tokens)?.value === '{') {
544
+ return parseBufferInterface(tokens, typeSpecifier, qualifiers as LayoutQualifier[], layout)
545
+ } else if (peek(tokens, 1)?.value === '(') {
546
+ return parseFunction(tokens, typeSpecifier, qualifiers as PrecisionQualifier[])
547
+ } else {
548
+ return parseVariable(
549
+ tokens,
550
+ typeSpecifier,
551
+ qualifiers as (ConstantQualifier | InterpolationQualifier | StorageQualifier | PrecisionQualifier)[],
552
+ layout,
553
+ )
554
+ }
555
+ }
556
+
557
+ function parseStruct(tokens: Tokens): StructDeclaration {
558
+ consume(tokens, 'struct')
559
+ const id: Identifier = { type: 'Identifier', name: consume(tokens).value }
560
+ consume(tokens, '{')
561
+ const members: VariableDeclaration[] = []
562
+ while (peek(tokens, 0, false) && peek(tokens, 0, false)!.value !== '}') {
563
+ members.push(...(parseStatements(tokens) as unknown as VariableDeclaration[]))
564
+ }
565
+ consume(tokens, '}')
566
+
567
+ // Hack to append a separate variable declaration in the next iterator
568
+ // `struct a {} name;` is parsed as `struct a {}; a name;`
569
+ if (peek(tokens)?.type === 'identifier') {
570
+ const type = id.name
571
+ const name = consume(tokens).value
572
+ tokens.list.push(
573
+ { type: 'identifier', value: type },
574
+ { type: 'identifier', value: name },
575
+ { type: 'symbol', value: ';' },
576
+ )
577
+ }
578
+
579
+ consume(tokens, ';')
580
+
581
+ return { type: 'StructDeclaration', id, members }
582
+ }
583
+
584
+ function parseContinue(tokens: Tokens): ContinueStatement {
585
+ consume(tokens, 'continue')
586
+ consume(tokens, ';')
587
+
588
+ return { type: 'ContinueStatement' }
589
+ }
590
+
591
+ function parseBreak(tokens: Tokens): BreakStatement {
592
+ consume(tokens, 'break')
593
+ consume(tokens, ';')
594
+
595
+ return { type: 'BreakStatement' }
596
+ }
597
+
598
+ function parseDiscard(tokens: Tokens): DiscardStatement {
599
+ consume(tokens, 'discard')
600
+ consume(tokens, ';')
601
+
602
+ return { type: 'DiscardStatement' }
603
+ }
604
+
605
+ function parseReturn(tokens: Tokens): ReturnStatement {
606
+ consume(tokens, 'return')
607
+
608
+ let argument: Expression | null = null
609
+ if (peek(tokens)?.value !== ';') argument = parseExpression(tokens)
610
+ consume(tokens, ';')
611
+
612
+ return { type: 'ReturnStatement', argument }
613
+ }
614
+
615
+ function parseIf(tokens: Tokens): IfStatement {
616
+ consume(tokens, 'if')
617
+ consume(tokens, '(')
618
+ const test = parseExpression(tokens)
619
+ consume(tokens, ')')
620
+
621
+ const consequent = parseBlockOrStatement(tokens)
622
+
623
+ let alternate = null
624
+ const elseToken = peek(tokens)
625
+ if (elseToken && elseToken.value === 'else') {
626
+ consume(tokens, 'else')
627
+
628
+ if (peek(tokens) && peek(tokens)!.value === 'if') {
629
+ alternate = parseIf(tokens)
630
+ } else {
631
+ alternate = parseBlockOrStatement(tokens)
632
+ }
633
+ }
634
+
635
+ return { type: 'IfStatement', test, consequent, alternate }
636
+ }
637
+
638
+ function parseWhile(tokens: Tokens): WhileStatement {
639
+ consume(tokens, 'while')
640
+ consume(tokens, '(')
641
+ const test = parseExpression(tokens)
642
+ consume(tokens, ')')
643
+ const body = parseBlockOrStatement(tokens)
644
+
645
+ return { type: 'WhileStatement', test, body }
646
+ }
647
+
648
+ function parseFor(tokens: Tokens): ForStatement {
649
+ consume(tokens, 'for')
650
+ consume(tokens, '(')
651
+ const typeSpecifier = parseExpression(tokens) as Identifier | ArraySpecifier
652
+ const init = parseVariable(tokens, typeSpecifier)
653
+ // consume(tokens, ';')
654
+ const test = parseExpression(tokens)
655
+ consume(tokens, ';')
656
+ const update = parseExpression(tokens)
657
+ consume(tokens, ')')
658
+ const body = parseBlockOrStatement(tokens)
659
+
660
+ return { type: 'ForStatement', init, test, update, body }
661
+ }
662
+
663
+ function parseDoWhile(tokens: Tokens): DoWhileStatement {
664
+ consume(tokens, 'do')
665
+ const body = parseBlockOrStatement(tokens)
666
+ consume(tokens, 'while')
667
+ consume(tokens, '(')
668
+ const test = parseExpression(tokens)
669
+ consume(tokens, ')')
670
+ consume(tokens, ';')
671
+
672
+ return { type: 'DoWhileStatement', test, body }
673
+ }
674
+
675
+ function parseSwitch(tokens: Tokens): SwitchStatement {
676
+ consume(tokens, 'switch')
677
+ const discriminant = parseExpression(tokens)
678
+
679
+ const cases: SwitchCase[] = []
680
+ while (hasNextToken(tokens)) {
681
+ const token = consume(tokens)
682
+ if (token.value === '}') break
683
+
684
+ if (token.value === 'case') {
685
+ const test = parseExpression(tokens)
686
+ consume(tokens, ':')
687
+ const consequent = parseStatements(tokens)
688
+ cases.push({ type: 'SwitchCase', test, consequent })
689
+ } else if (token.value === 'default') {
690
+ const test = null
691
+ consume(tokens, ':')
692
+ const consequent = parseStatements(tokens)
693
+ cases.push({ type: 'SwitchCase', test, consequent })
694
+ }
695
+ }
696
+
697
+ return { type: 'SwitchStatement', discriminant, cases }
698
+ }
699
+
700
+ function parsePrecision(tokens: Tokens): PrecisionQualifierStatement {
701
+ consume(tokens, 'precision')
702
+ const precision = consume(tokens).value as PrecisionQualifier
703
+ const typeSpecifier: Identifier = { type: 'Identifier', name: consume(tokens).value }
704
+ consume(tokens, ';')
705
+ return { type: 'PrecisionQualifierStatement', precision, typeSpecifier }
706
+ }
707
+
708
+ function parsePreprocessor(tokens: Tokens): PreprocessorStatement {
709
+ consume(tokens, '#', false)
710
+
711
+ let name = '' // name can be unset for the # directive which is ignored
712
+ let value: Expression[] | null = null
713
+
714
+ if (peek(tokens)?.value !== '\\') {
715
+ name = consume(tokens).value
716
+
717
+ if (name === 'define') {
718
+ const lhs = consume(tokens)
719
+ let left: Expression = { type: 'Identifier', name: lhs.value }
720
+ const next = peek(tokens)
721
+
722
+ // Macro definition: #define foo(a, b, c) ...
723
+ if (next && next.value === '(') {
724
+ consume(tokens)
725
+
726
+ const args: Expression[] = []
727
+ while (peek(tokens)?.value !== ')') {
728
+ args.push(parseExpression(tokens, Precedence.COMMA))
729
+ if (peek(tokens)?.value !== ')') consume(tokens, ',')
730
+ }
731
+ consume(tokens, ')')
732
+
733
+ left = { type: 'CallExpression', callee: left, arguments: args }
734
+ }
735
+
736
+ if (peek(tokens)?.value === '\\') value = [left]
737
+ else value = [left, parseExpression(tokens)]
738
+ } else if (name === 'extension') {
739
+ // TODO: extension directives must be before declarations
740
+ const left = parseExpression(tokens)
741
+ consume(tokens, ':')
742
+ const right = parseExpression(tokens)
743
+ value = [left, right]
744
+ } else if (name === 'include') {
745
+ consume(tokens, '<')
746
+ value = [{ type: 'Identifier', name: consume(tokens).value }]
747
+ consume(tokens, '>')
748
+ } else if (name !== 'else' && name !== 'endif') {
749
+ value = []
750
+ while (peek(tokens) && peek(tokens)!.value !== '\\') {
751
+ value.push(parseExpression(tokens))
752
+ }
753
+ }
754
+ }
755
+
756
+ consume(tokens, '\\')
757
+
758
+ return { type: 'PreprocessorStatement', name, value }
759
+ }
760
+
761
+ function isVariable(tokens: Tokens): boolean {
762
+ let token = peek(tokens, 0)
763
+
764
+ // Skip first token if EOF or not type qualifier/specifier
765
+ if (!token || (token.type !== 'identifier' && token.type !== 'keyword')) return false
766
+
767
+ // Layout qualifiers are only valid for declarations
768
+ if (token.value === 'layout') return true
769
+
770
+ // Skip to end of possible expression statement (e.g. callexpr -> fndecl)
771
+ let i = 1
772
+ let scopeIndex = 0
773
+ while (true) {
774
+ token = peek(tokens, i)
775
+ if (!token) break
776
+
777
+ const delta = getScopeDelta(token)
778
+ if (scopeIndex <= 0 && delta <= 0) break
779
+
780
+ scopeIndex += delta
781
+ i++
782
+ }
783
+
784
+ // A variable declaration must follow with an identifier or type
785
+ return peek(tokens, i)?.type !== 'symbol'
786
+ }
787
+
788
+ function parseStatement(tokens: Tokens): Statement {
789
+ const token = peek(tokens, 0, false)!
790
+ let statement: Statement | null = null
791
+
792
+ if (token.value === '#') statement = parsePreprocessor(tokens)
793
+ else if (token.value === 'struct') statement = parseStruct(tokens)
794
+ else if (token.value === 'continue') statement = parseContinue(tokens)
795
+ else if (token.value === 'break') statement = parseBreak(tokens)
796
+ else if (token.value === 'discard') statement = parseDiscard(tokens)
797
+ else if (token.value === 'return') statement = parseReturn(tokens)
798
+ else if (token.value === 'if') statement = parseIf(tokens)
799
+ else if (token.value === 'while') statement = parseWhile(tokens)
800
+ else if (token.value === 'for') statement = parseFor(tokens)
801
+ else if (token.value === 'do') statement = parseDoWhile(tokens)
802
+ else if (token.value === 'switch') statement = parseSwitch(tokens)
803
+ else if (token.value === 'precision') statement = parsePrecision(tokens)
804
+ else if (isVariable(tokens)) statement = parseIndeterminate(tokens)
805
+ else if (token.value === '{') statement = parseBlock(tokens)
806
+ else {
807
+ const expression = parseExpression(tokens)
808
+ if (peek(tokens)?.value === ',') consume(tokens, ';')
809
+ else consume(tokens, ';')
810
+ statement = { type: 'ExpressionStatement', expression }
811
+ }
812
+
813
+ return statement
814
+ }
815
+
816
+ function parseStatements(tokens: Tokens): Statement[] {
817
+ const body: Statement[] = []
818
+ let scopeIndex = 0
819
+
820
+ while (true) {
821
+ const token = peek(tokens, 0, false)
822
+ if (!token) break
823
+
824
+ scopeIndex += getScopeDelta(token)
825
+ if (scopeIndex < 0 || token.value === '}') break
826
+
827
+ if (token.value === 'case' || token.value === 'default') break
828
+
829
+ tokens.encounteredMacro = false
830
+ const start = tokens.cursor
831
+ const statement = parseStatement(tokens)
832
+ if (tokens.encounteredMacro) {
833
+ const hoistedTokens = hoistPreprocessorDirectives(tokens.list.slice(start, tokens.cursor))
834
+ body.push(...parseStatements({ list: hoistedTokens, cursor: 0 }))
835
+ } else {
836
+ body.push(statement)
837
+ }
838
+ }
839
+
840
+ return body
841
+ }
842
+
843
+ function parseBlock(tokens: Tokens): BlockStatement {
844
+ consume(tokens, '{')
845
+ const body = parseStatements(tokens)
846
+ consume(tokens, '}')
847
+ return { type: 'BlockStatement', body }
848
+ }
849
+
850
+ // TODO: validate block versus sub-statements for GLSL/WGSL
851
+ function parseBlockOrStatement(tokens: Tokens): BlockStatement | Statement {
852
+ if (peek(tokens)?.value === '{') {
853
+ return parseBlock(tokens)
854
+ } else {
855
+ return parseStatement(tokens)
856
+ }
857
+ }
858
+
859
+ const NEWLINE_REGEX = /\\\s+/gm
860
+ const DIRECTIVE_REGEX = /(^\s*#[^\\]*?)(\n|\/[\/\*])/gm
861
+
862
+ /**
863
+ * Parses a string of GLSL (WGSL WIP) code into an [AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree).
864
+ */
865
+ export function parse(code: string): Program {
866
+ // Fold newlines
867
+ code = code.replace(NEWLINE_REGEX, '')
868
+
869
+ // Escape newlines after directives, skip comments
870
+ code = code.replace(DIRECTIVE_REGEX, '$1\\$2')
871
+
872
+ const tokens = {
873
+ list: tokenize(code),
874
+ cursor: 0,
875
+ }
876
+
877
+ return { type: 'Program', body: parseStatements(tokens) }
878
+ }