shaderkit 0.2.0 → 0.3.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,568 +1,660 @@
1
1
  import {
2
- type AST,
3
- Type,
4
- Identifier,
5
- Literal,
6
- CallExpression,
7
- UnaryExpression,
8
- MemberExpression,
9
- TernaryExpression,
10
- BinaryExpression,
2
+ ArraySpecifier,
3
+ AssignmentOperator,
4
+ BinaryOperator,
11
5
  BlockStatement,
12
- FunctionDeclaration,
13
- VariableDeclaration,
14
- VariableDeclarator,
15
- ContinueStatement,
16
6
  BreakStatement,
7
+ ConstantQualifier,
8
+ ContinueStatement,
17
9
  DiscardStatement,
18
- ReturnStatement,
19
- IfStatement,
20
- WhileStatement,
21
- ForStatement,
22
10
  DoWhileStatement,
23
- SwitchStatement,
24
- SwitchCase,
25
- StructDeclaration,
11
+ Expression,
12
+ ForStatement,
13
+ FunctionDeclaration,
14
+ FunctionParameter,
15
+ Identifier,
16
+ IfStatement,
17
+ InterpolationQualifier,
18
+ LayoutQualifier,
19
+ Literal,
20
+ LogicalOperator,
21
+ ParameterQualifier,
22
+ PrecisionQualifier,
26
23
  PrecisionStatement,
27
- ArrayExpression,
28
24
  PreprocessorStatement,
29
- } from './ast'
30
- import { type Token, tokenize } from './tokenizer'
31
-
32
- const UNARY_OPERATORS = ['+', '-', '~', '!', '++', '--']
33
-
34
- const BINARY_OPERATORS = [
35
- '>>=',
36
- '<<=',
37
- '|=',
38
- '&=',
39
- '^=',
40
- '%=',
41
- '/=',
42
- '*=',
43
- '-=',
44
- '+=',
45
- '=',
46
- '?',
47
- '||',
48
- '^^',
49
- '&&',
50
- '|',
51
- '^',
52
- '&',
53
- '!=',
54
- '==',
55
- '>=',
56
- '<=',
57
- '>',
58
- '<',
59
- '>>',
60
- '<<',
61
- '+',
62
- '-',
63
- '%',
64
- '/',
65
- '*',
66
- ]
25
+ Program,
26
+ ReturnStatement,
27
+ Statement,
28
+ StorageQualifier,
29
+ StructDeclaration,
30
+ SwitchCase,
31
+ SwitchStatement,
32
+ UnaryOperator,
33
+ UniformDeclarationBlock,
34
+ UpdateOperator,
35
+ VariableDeclaration,
36
+ VariableDeclarator,
37
+ WhileStatement,
38
+ } from './ast.js'
39
+ import { type Token, tokenize } from './tokenizer.js'
40
+
41
+ // https://engineering.desmos.com/articles/pratt-parser
42
+ // https://matklad.github.io/2020/04/13/simple-but-powerful-pratt-parsing.html
43
+
44
+ // 5.1 Operators
45
+ enum Precedence {
46
+ LOWEST,
47
+ COMMA,
48
+ ASSIGN,
49
+ LOGICAL_OR,
50
+ LOGICAL_XOR,
51
+ LOGICAL_AND,
52
+ BITWISE_OR,
53
+ BITWISE_XOR,
54
+ BITWISE_AND,
55
+ TERNARY,
56
+ COMPARE,
57
+ SHIFT,
58
+ ADD,
59
+ MULTIPLY,
60
+ UNARY_PREFIX,
61
+ UNARY_POSTFIX,
62
+ MEMBER,
63
+ }
64
+
65
+ const PREFIX_OPERATOR_PRECEDENCE: Record<string, Precedence> = {
66
+ '~': Precedence.UNARY_PREFIX,
67
+ '!': Precedence.UNARY_PREFIX,
68
+ '--': Precedence.UNARY_PREFIX,
69
+ '++': Precedence.UNARY_PREFIX,
70
+ '-': Precedence.UNARY_PREFIX,
71
+ '+': Precedence.UNARY_PREFIX,
72
+ }
73
+
74
+ const POSTFIX_OPERATOR_PRECEDENCE: Record<string, Precedence> = {
75
+ '--': Precedence.UNARY_POSTFIX,
76
+ '++': Precedence.UNARY_POSTFIX,
77
+ '(': Precedence.LOWEST,
78
+ '[': Precedence.MEMBER,
79
+ '.': Precedence.MEMBER,
80
+ }
81
+
82
+ const INFIX_OPERATOR_PRECEDENCE_LEFT: Record<string, Precedence> = {
83
+ '||': Precedence.LOGICAL_OR,
84
+ '^^': Precedence.LOGICAL_XOR,
85
+ '&&': Precedence.LOGICAL_AND,
86
+ '|': Precedence.BITWISE_OR,
87
+ '^': Precedence.BITWISE_XOR,
88
+ '&': Precedence.BITWISE_AND,
89
+ '==': Precedence.COMPARE,
90
+ '>': Precedence.COMPARE,
91
+ '>=': Precedence.COMPARE,
92
+ '<': Precedence.COMPARE,
93
+ '<=': Precedence.COMPARE,
94
+ '!=': Precedence.COMPARE,
95
+ '<<': Precedence.SHIFT,
96
+ '>>': Precedence.SHIFT,
97
+ '+': Precedence.ADD,
98
+ '-': Precedence.ADD,
99
+ '*': Precedence.MULTIPLY,
100
+ '/': Precedence.MULTIPLY,
101
+ '%': Precedence.MULTIPLY,
102
+ }
103
+
104
+ const INFIX_OPERATOR_PRECEDENCE_RIGHT: Record<string, Precedence> = {
105
+ '=': Precedence.ASSIGN,
106
+ '+=': Precedence.ASSIGN,
107
+ '&=': Precedence.ASSIGN,
108
+ '|=': Precedence.ASSIGN,
109
+ '^=': Precedence.ASSIGN,
110
+ '/=': Precedence.ASSIGN,
111
+ '*=': Precedence.ASSIGN,
112
+ '%=': Precedence.ASSIGN,
113
+ '<<=': Precedence.ASSIGN,
114
+ '>>=': Precedence.ASSIGN,
115
+ '-=': Precedence.ASSIGN,
116
+ '?': Precedence.TERNARY,
117
+ }
67
118
 
68
119
  // TODO: this is GLSL-only, separate language constants
69
120
  const TYPE_REGEX = /^(void|bool|float|u?int|[uib]?vec\d|mat\d(x\d)?)$/
121
+ // TODO: must be ordered: invariant interpolation storage precision storage parameter precision
122
+ // const cannot be used with storage or parameter qualifiers
70
123
  const QUALIFIER_REGEX = /^(const|uniform|in|out|inout|centroid|flat|smooth|invariant|lowp|mediump|highp)$/
71
124
  const VARIABLE_REGEX = new RegExp(`${TYPE_REGEX.source}|${QUALIFIER_REGEX.source}|layout`)
72
125
 
73
- const isDeclaration = RegExp.prototype.test.bind(VARIABLE_REGEX)
74
-
75
- const isOpen = RegExp.prototype.test.bind(/^[\(\[\{]$/)
76
- const isClose = RegExp.prototype.test.bind(/^[\)\]\}]$/)
77
-
126
+ const SCOPE_DELTAS: Record<string, number> = {
127
+ // Open
128
+ '(': 1,
129
+ '[': 1,
130
+ '{': 1,
131
+ // Close
132
+ ')': -1,
133
+ ']': -1,
134
+ '}': -1,
135
+ }
78
136
  function getScopeDelta(token: Token): number {
79
- if (isOpen(token.value)) return 1
80
- if (isClose(token.value)) return -1
81
- return 0
137
+ return SCOPE_DELTAS[token.value] ?? 0
82
138
  }
83
139
 
84
- let tokens: Token[] = []
85
- let i: number = 0
140
+ function consume(tokens: Token[], expected?: string): Token {
141
+ const token = tokens.shift()
86
142
 
87
- function readUntil(value: string, body: Token[], offset: number = 0): Token[] {
88
- const output: Token[] = []
89
- let scopeIndex = 0
90
-
91
- while (offset < body.length) {
92
- const token = body[offset++]
93
- output.push(token)
94
-
95
- scopeIndex += getScopeDelta(token)
96
- if (scopeIndex === 0 && token.value === value) break
143
+ if (token === undefined && expected !== undefined) {
144
+ throw new SyntaxError(`Expected "${expected}"`)
145
+ } else if (token === undefined) {
146
+ throw new SyntaxError('Unexpected end of input')
147
+ } else if (expected !== undefined && token.value !== expected) {
148
+ throw new SyntaxError(`Expected "${expected}" got "${token.value}"`)
97
149
  }
98
150
 
99
- return output
151
+ return token
100
152
  }
101
153
 
102
- function consumeUntil(value: string): Token[] {
103
- const output = readUntil(value, tokens, i)
104
- i += output.length
105
- return output
106
- }
107
-
108
- function parseExpression(body: Token[]): AST | null {
109
- if (body.length === 0) return null
110
-
111
- const first = body[0]
112
- const last = body[body.length - 1]
113
- if (UNARY_OPERATORS.includes(first.value)) {
114
- const right = parseExpression(body.slice(1))!
115
- return new UnaryExpression(first.value, null, right)
116
- } else if (UNARY_OPERATORS.includes(last.value)) {
117
- const left = parseExpression(body.slice(0, body.length - 1))!
118
- return new UnaryExpression(last.value, left, null)
119
- }
120
-
121
- if (first.value === '(') {
122
- const leftBody = readUntil(')', body)
123
- const left = parseExpression(leftBody.slice(1, leftBody.length - 1))!
124
-
125
- const operator = body[leftBody.length]
126
- if (operator) {
127
- const rightBody = body.slice(leftBody.length + 1)
128
- const right = parseExpression(rightBody)!
129
-
130
- return new BinaryExpression(operator.value, left, right)
154
+ function parseExpression(tokens: Token[], minBindingPower: number = 0): Expression {
155
+ let token = consume(tokens)
156
+
157
+ let lhs: Expression
158
+ if (token.type === 'identifier' || token.type === 'keyword') {
159
+ lhs = { type: 'Identifier', name: token.value }
160
+ } else if (token.type === 'bool' || token.type === 'float' || token.type === 'int') {
161
+ lhs = { type: 'Literal', value: token.value }
162
+ } else if (token.type === 'symbol' && token.value === '(') {
163
+ lhs = parseExpression(tokens, 0)
164
+ consume(tokens, ')')
165
+ } else if (token.type === 'symbol' && token.value in PREFIX_OPERATOR_PRECEDENCE) {
166
+ const rightBindingPower = PREFIX_OPERATOR_PRECEDENCE[token.value]
167
+ const rhs = parseExpression(tokens, rightBindingPower)
168
+ if (token.value === '--' || token.value === '++') {
169
+ lhs = { type: 'UpdateExpression', operator: token.value, prefix: true, argument: rhs }
170
+ } else {
171
+ lhs = { type: 'UnaryExpression', operator: token.value as UnaryOperator, prefix: true, argument: rhs }
131
172
  }
132
-
133
- return left
173
+ } else {
174
+ throw new SyntaxError(`Unexpected token: "${token.value}"`)
134
175
  }
135
176
 
136
- let scopeIndex = 0
137
-
138
- for (const operator of BINARY_OPERATORS) {
139
- for (let i = 0; i < body.length; i++) {
140
- const token = body[i]
141
- if (token.type !== 'symbol') continue
177
+ while (tokens.length) {
178
+ token = tokens[0]
142
179
 
143
- scopeIndex += getScopeDelta(token)
180
+ if (token.value in POSTFIX_OPERATOR_PRECEDENCE) {
181
+ const leftBindingPower = POSTFIX_OPERATOR_PRECEDENCE[token.value]
182
+ if (leftBindingPower < minBindingPower) break
144
183
 
145
- if (scopeIndex === 0 && token.value === operator) {
146
- if (operator === '?') {
147
- const testBody = body.slice(0, i)
148
- const consequentBody = readUntil(':', body, i + 1).slice(0, -1)
149
- const alternateBody = body.slice(i + consequentBody.length + 2)
184
+ consume(tokens)
150
185
 
151
- const test = parseExpression(testBody)!
152
- const consequent = parseExpression(consequentBody)!
153
- const alternate = parseExpression(alternateBody)!
186
+ if (token.value === '(') {
187
+ const args: Expression[] = []
154
188
 
155
- return new TernaryExpression(test, consequent, alternate)
189
+ while (tokens[0]?.value !== ')') {
190
+ args.push(parseExpression(tokens, 0))
191
+ if (tokens[0]?.value !== ')') consume(tokens, ',')
192
+ }
193
+ consume(tokens, ')')
194
+
195
+ lhs = { type: 'CallExpression', callee: lhs, arguments: args }
196
+ } else if (token.value === '[') {
197
+ const rhs = tokens[0]?.value !== ']' ? parseExpression(tokens, 0) : null
198
+ consume(tokens, ']')
199
+
200
+ if (tokens[0]?.value === '(') {
201
+ consume(tokens, '(')
202
+ const elements: Expression[] = []
203
+
204
+ while ((tokens[0] as Token | undefined)?.value !== ')') {
205
+ elements.push(parseExpression(tokens, 0))
206
+ if ((tokens[0] as Token | undefined)?.value !== ')') consume(tokens, ',')
207
+ }
208
+ consume(tokens, ')')
209
+
210
+ const typeSpecifier: ArraySpecifier = {
211
+ type: 'ArraySpecifier',
212
+ typeSpecifier: lhs as Identifier,
213
+ dimensions: [rhs as Literal | null],
214
+ }
215
+ lhs = { type: 'ArrayExpression', typeSpecifier, elements }
156
216
  } else {
157
- const left = parseExpression(body.slice(0, i))!
158
- const right = parseExpression(body.slice(i + 1, body.length))!
159
-
160
- return new BinaryExpression(operator, left, right)
217
+ lhs = { type: 'MemberExpression', object: lhs, property: rhs!, computed: true }
161
218
  }
219
+ } else if (token.value === '.') {
220
+ const rhs = parseExpression(tokens, 0)
221
+ lhs = { type: 'MemberExpression', object: lhs, property: rhs, computed: false }
222
+ } else if (token.value === '--' || token.value === '++') {
223
+ lhs = { type: 'UpdateExpression', operator: token.value as UpdateOperator, prefix: false, argument: lhs }
224
+ } else {
225
+ lhs = { type: 'UnaryExpression', operator: token.value as UnaryOperator, prefix: false, argument: lhs }
162
226
  }
227
+ } else if (token.value in INFIX_OPERATOR_PRECEDENCE_LEFT) {
228
+ const precedence = INFIX_OPERATOR_PRECEDENCE_LEFT[token.value]
229
+ const leftBindingPower = precedence - 1
230
+ const rightBindingPower = precedence
163
231
 
164
- if (scopeIndex < 0) {
165
- return parseExpression(body.slice(0, i))
166
- }
167
- }
168
- }
169
-
170
- if (first.type === 'bool' || first.type === 'int' || first.type === 'float') {
171
- return new Literal(first.value)
172
- } else if (first.type === 'identifier' || first.type === 'keyword') {
173
- const second = body[1]
174
-
175
- if (!second) {
176
- return new Identifier(first.value)
177
- } else if (second.value === '(') {
178
- const callee = new Identifier(first.value)
179
- const args: AST[] = []
232
+ if (leftBindingPower < minBindingPower) break
180
233
 
181
- const scope = readUntil(')', body, 1).slice(1, -1)
234
+ consume(tokens)
182
235
 
183
- let j = 0
184
- while (j < scope.length) {
185
- const line = readUntil(',', scope, j)
186
- j += line.length
187
- if (line[line.length - 1]?.value === ',') line.pop() // skip ,
188
-
189
- const arg = parseExpression(line)
190
- if (arg) args.push(arg)
236
+ if (token.value === '||' || token.value === '&&' || token.value === '^^') {
237
+ const rhs = parseExpression(tokens, rightBindingPower)
238
+ lhs = { type: 'LogicalExpression', operator: token.value, left: lhs, right: rhs }
239
+ } else {
240
+ const rhs = parseExpression(tokens, rightBindingPower)
241
+ lhs = { type: 'BinaryExpression', operator: token.value as BinaryOperator, left: lhs, right: rhs }
191
242
  }
243
+ } else if (token.value in INFIX_OPERATOR_PRECEDENCE_RIGHT) {
244
+ const precedence = INFIX_OPERATOR_PRECEDENCE_RIGHT[token.value]
245
+ const leftBindingPower = precedence
246
+ const rightBindingPower = precedence - 1
192
247
 
193
- const expression = new CallExpression(callee, args)
248
+ if (leftBindingPower < minBindingPower) break
194
249
 
195
- // e.g. texture().rgb
196
- let i = 3 + j
197
- if (body[i]?.value === '.') {
198
- const right = parseExpression([first, ...body.slice(i)])! as MemberExpression
199
- right.object = expression
200
- return right
201
- }
250
+ consume(tokens)
202
251
 
203
- return expression
204
- } else if (second.value === '.') {
205
- const object = new Identifier(first.value)
206
- const property = parseExpression([body[2]])!
207
- const left = new MemberExpression(object, property)
208
-
209
- // e.g. array.length()
210
- if (body[3]?.value === '(' && last.value === ')') {
211
- const right = parseExpression(body.slice(2))! as CallExpression
212
- right.callee = left
213
- return right
252
+ if (token.value === '?') {
253
+ const mhs = parseExpression(tokens, 0)
254
+ consume(tokens, ':')
255
+ const rhs = parseExpression(tokens, rightBindingPower)
256
+ lhs = { type: 'ConditionalExpression', test: lhs, alternate: mhs, consequent: rhs }
257
+ } else {
258
+ const rhs = parseExpression(tokens, rightBindingPower)
259
+ lhs = { type: 'AssignmentExpression', operator: token.value as AssignmentOperator, left: lhs, right: rhs }
214
260
  }
261
+ } else {
262
+ break
263
+ }
264
+ }
215
265
 
216
- return left
217
- } else if (second.value === '[') {
218
- let i = 2
219
-
220
- const type = new Type(first.value, [])
221
-
222
- if (body[i].value !== ']') type.parameters!.push(parseExpression([body[i++]]) as any)
223
- i++ // skip ]
224
-
225
- const scope = readUntil(')', body, i).slice(1, -1)
226
-
227
- const members: AST[] = []
228
-
229
- let j = 0
230
- while (j < scope.length) {
231
- const next = readUntil(',', scope, j)
232
- j += next.length
266
+ return lhs
267
+ }
233
268
 
234
- if (next[next.length - 1].value === ',') next.pop()
269
+ function parseVariableDeclarator(
270
+ tokens: Token[],
271
+ typeSpecifier: Identifier | ArraySpecifier,
272
+ qualifiers: (ConstantQualifier | InterpolationQualifier | StorageQualifier | PrecisionQualifier)[],
273
+ layout: Record<string, string | boolean> | null,
274
+ ): VariableDeclarator {
275
+ let id: Identifier = { type: 'Identifier', name: consume(tokens).value }
276
+
277
+ if (tokens[0]?.value === '[') {
278
+ consume(tokens, '[')
279
+ id = {
280
+ type: 'ArraySpecifier',
281
+ typeSpecifier: id,
282
+ dimensions: [parseExpression(tokens) as Literal | Identifier],
283
+ } satisfies ArraySpecifier as unknown as Identifier
284
+ consume(tokens, ']')
285
+ }
235
286
 
236
- members.push(parseExpression(next)!)
237
- }
287
+ let init: Expression | null = null
238
288
 
239
- return new ArrayExpression(type, members)
240
- }
289
+ if (tokens[0]?.value === '=') {
290
+ consume(tokens, '=')
291
+ init = parseExpression(tokens)
241
292
  }
242
293
 
243
- return null
294
+ return { type: 'VariableDeclarator', id, qualifiers, typeSpecifier, layout, init }
244
295
  }
245
296
 
246
297
  function parseVariable(
247
- qualifiers: string[] = [],
298
+ tokens: Token[],
299
+ typeSpecifier: Identifier | ArraySpecifier,
300
+ qualifiers: (ConstantQualifier | InterpolationQualifier | StorageQualifier | PrecisionQualifier)[] = [],
248
301
  layout: Record<string, string | boolean> | null = null,
249
302
  ): VariableDeclaration {
250
- i-- // TODO: remove backtrack hack
251
-
252
- const kind = null // TODO: WGSL
253
- const type = new Type(tokens[i++].value, null)
254
-
255
303
  const declarations: VariableDeclarator[] = []
256
304
 
257
- const body = consumeUntil(';')
258
- let j = 0
259
-
260
- while (j < body.length) {
261
- const name = body[j++].value
262
-
263
- let prefix: AST | null = null
264
- if (body[j].value === '[') {
265
- j++ // skip [
266
- prefix = new ArrayExpression(new Type(type.name, [parseExpression([body[j++]]) as any]), [])
267
- j++ // skip ]
268
- }
269
-
270
- let value: AST | null = null
271
-
272
- const delimiter = body[j++]
273
- if (delimiter?.value === '=') {
274
- const right = readUntil(',', body, j)
275
- j += right.length
305
+ if (tokens[0]?.value !== ';') {
306
+ while (tokens.length) {
307
+ declarations.push(parseVariableDeclarator(tokens, typeSpecifier, qualifiers, layout))
276
308
 
277
- value = parseExpression(right.slice(0, -1))
309
+ if (tokens[0]?.value === ',') {
310
+ consume(tokens, ',')
311
+ } else {
312
+ break
313
+ }
278
314
  }
279
-
280
- declarations.push(new VariableDeclarator(name, value ?? prefix))
281
315
  }
282
316
 
283
- return new VariableDeclaration(layout, qualifiers, kind, type, declarations)
317
+ consume(tokens, ';')
318
+
319
+ return { type: 'VariableDeclaration', declarations }
284
320
  }
285
321
 
286
- function parseFunction(qualifiers: string[]): FunctionDeclaration {
287
- i-- // TODO: remove backtrack hack
322
+ function parseUniformBlock(
323
+ tokens: Token[],
324
+ typeSpecifier: Identifier | ArraySpecifier,
325
+ qualifiers: LayoutQualifier[] = [],
326
+ layout: Record<string, string | boolean> | null = null,
327
+ ): UniformDeclarationBlock {
328
+ const members = parseBlock(tokens).body as VariableDeclaration[]
288
329
 
289
- const type = new Type(tokens[i++].value, null)
290
- const name = tokens[i++].value
291
- const args: VariableDeclaration[] = []
330
+ let id: Identifier | null = null
331
+ if (tokens[0]?.value !== ';') id = parseExpression(tokens) as Identifier
332
+ consume(tokens, ';')
292
333
 
293
- // TODO: merge with parseVariable
294
- const header = consumeUntil(')').slice(1, -1)
295
- let j = 0
296
- while (j < header.length) {
297
- const qualifiers: string[] = []
298
- while (header[j] && header[j].type !== 'identifier') {
299
- qualifiers.push(header[j++].value)
300
- }
301
- const type = new Type(qualifiers.pop()!, null)
334
+ return { type: 'UniformDeclarationBlock', id, qualifiers, typeSpecifier, layout, members }
335
+ }
302
336
 
303
- const line = readUntil(',', header, j)
304
- j += line.length
337
+ function parseFunction(
338
+ tokens: Token[],
339
+ typeSpecifier: ArraySpecifier | Identifier,
340
+ qualifiers: PrecisionQualifier[] = [],
341
+ ): FunctionDeclaration {
342
+ const id: Identifier = { type: 'Identifier', name: consume(tokens).value }
305
343
 
306
- const name = line.shift()!.value
344
+ consume(tokens, '(')
307
345
 
308
- let prefix: AST | null = null
309
- if (line[0]?.value === '[') {
310
- line.shift() // skip [
311
- prefix = new ArrayExpression(new Type(type.name, [parseExpression([line.shift()!]) as any]), [])
312
- line.shift() // skip ]
346
+ const params: FunctionParameter[] = []
347
+ while (tokens[0] && tokens[0].value !== ')') {
348
+ const qualifiers: (ConstantQualifier | ParameterQualifier | PrecisionQualifier)[] = []
349
+ while (tokens[0] && QUALIFIER_REGEX.test(tokens[0].value)) {
350
+ qualifiers.push(consume(tokens).value as ConstantQualifier | ParameterQualifier | PrecisionQualifier)
313
351
  }
352
+ const typeSpecifier = parseExpression(tokens) as ArraySpecifier | Identifier
353
+ const id: Identifier = { type: 'Identifier', name: consume(tokens).value }
314
354
 
315
- if (line[line.length - 1]?.value === ',') line.pop() // skip ,
316
-
317
- const value = parseExpression(line) ?? prefix
355
+ params.push({ type: 'FunctionParameter', id, qualifiers, typeSpecifier })
318
356
 
319
- const declarations: VariableDeclarator[] = [new VariableDeclarator(name, value)]
320
-
321
- args.push(new VariableDeclaration(null, qualifiers, null, type, declarations))
357
+ if (tokens[0]?.value === ',') consume(tokens, ',')
322
358
  }
323
359
 
360
+ consume(tokens, ')')
361
+
324
362
  let body = null
325
- if (tokens[i].value === ';') i++ // skip ;
326
- else body = parseBlock()
363
+ if (tokens[0].value === ';') consume(tokens, ';')
364
+ else body = parseBlock(tokens)
327
365
 
328
- return new FunctionDeclaration(name, type, qualifiers, args, body)
366
+ return { type: 'FunctionDeclaration', id, qualifiers, typeSpecifier, params, body }
329
367
  }
330
368
 
331
- function parseIndeterminate(): VariableDeclaration | FunctionDeclaration {
332
- i-- // TODO: remove backtrack hack
333
-
369
+ function parseIndeterminate(tokens: Token[]): VariableDeclaration | FunctionDeclaration | UniformDeclarationBlock {
334
370
  let layout: Record<string, string | boolean> | null = null
335
- if (tokens[i].value === 'layout') {
336
- i++ // skip layout
371
+ if (tokens[0].value === 'layout') {
372
+ consume(tokens, 'layout')
373
+ consume(tokens, '(')
337
374
 
338
375
  layout = {}
339
376
 
340
- let key: string | null = null
341
- while (tokens[i] && tokens[i].value !== ')') {
342
- const token = tokens[i++]
343
-
344
- if (token.value === ',') key = null
345
- if (token.type === 'symbol') continue
346
-
347
- if (!key) {
348
- key = token.value
349
- layout[key] = true
377
+ while (tokens[0] && (tokens[0] as Token).value !== ')') {
378
+ const expression = parseExpression(tokens)
379
+
380
+ if (
381
+ expression.type === 'AssignmentExpression' &&
382
+ expression.left.type === 'Identifier' &&
383
+ expression.right.type === 'Literal'
384
+ ) {
385
+ layout[expression.left.name] = expression.right.value
386
+ } else if (expression.type === 'Identifier') {
387
+ layout[expression.name] = true
350
388
  } else {
351
- layout[key] = token.value
389
+ throw new TypeError('Unexpected expression')
352
390
  }
391
+
392
+ if (tokens[0] && (tokens[0] as Token).value !== ')') consume(tokens, ',')
353
393
  }
354
394
 
355
- i++ // skip )
395
+ consume(tokens, ')')
356
396
  }
357
397
 
398
+ // TODO: only precision qualifier valid for function return type
358
399
  const qualifiers: string[] = []
359
- while (tokens[i] && QUALIFIER_REGEX.test(tokens[i].value)) {
360
- qualifiers.push(tokens[i++].value)
400
+ while (tokens[0] && QUALIFIER_REGEX.test(tokens[0].value)) {
401
+ qualifiers.push(consume(tokens).value)
361
402
  }
362
- i++
363
403
 
364
- return tokens[i + 1]?.value === '(' ? parseFunction(qualifiers) : parseVariable(qualifiers, layout)
404
+ // TODO: cleanup handling of typeSpecifier
405
+ if (tokens[2]?.value === '(')
406
+ return parseFunction(
407
+ tokens,
408
+ parseExpression(tokens) as Identifier | ArraySpecifier,
409
+ qualifiers as PrecisionQualifier[],
410
+ )
411
+
412
+ const typeSpecifier = parseExpression(tokens) as Identifier | ArraySpecifier
413
+
414
+ if (tokens[0]?.value === '{') return parseUniformBlock(tokens, typeSpecifier, qualifiers as LayoutQualifier[], layout)
415
+ else
416
+ return parseVariable(
417
+ tokens,
418
+ typeSpecifier,
419
+ qualifiers as (ConstantQualifier | InterpolationQualifier | StorageQualifier | PrecisionQualifier)[],
420
+ layout,
421
+ )
365
422
  }
366
423
 
367
- function parseStruct(): StructDeclaration {
368
- const name = tokens[i++].value
369
- i++ // skip {
424
+ function parseStruct(tokens: Token[]): StructDeclaration {
425
+ consume(tokens, 'struct')
426
+ const id: Identifier = { type: 'Identifier', name: consume(tokens).value }
427
+ consume(tokens, '{')
370
428
  const members: VariableDeclaration[] = []
371
- while (tokens[i] && tokens[i].value !== '}') {
372
- i++ // TODO: remove backtrack hack
373
- members.push(parseIndeterminate() as VariableDeclaration)
429
+ while (tokens[0] && tokens[0].value !== '}') {
430
+ members.push(parseIndeterminate(tokens) as VariableDeclaration)
374
431
  }
375
- i++ // skip }
376
- i++ // skip ;
432
+ consume(tokens, '}')
433
+ consume(tokens, ';')
377
434
 
378
- return new StructDeclaration(name, members)
435
+ return { type: 'StructDeclaration', id, members }
379
436
  }
380
437
 
381
- function parseReturn(): ReturnStatement {
382
- const body = consumeUntil(';')
383
- body.pop() // skip ;
438
+ function parseContinue(tokens: Token[]): ContinueStatement {
439
+ consume(tokens, 'continue')
440
+ consume(tokens, ';')
384
441
 
385
- const argument = parseExpression(body)
386
-
387
- return new ReturnStatement(argument as any)
442
+ return { type: 'ContinueStatement' }
388
443
  }
389
444
 
390
- function parseIf(): IfStatement {
391
- const test = parseExpression(consumeUntil(')'))!
392
- const consequent = parseBlock()
445
+ function parseBreak(tokens: Token[]): BreakStatement {
446
+ consume(tokens, 'break')
447
+ consume(tokens, ';')
393
448
 
394
- let alternate = null
395
- if (tokens[i].value === 'else') {
396
- i++ // TODO: remove backtrack hack
449
+ return { type: 'BreakStatement' }
450
+ }
397
451
 
398
- if (tokens[i].value === 'if') {
399
- i++
400
- alternate = parseIf()
401
- } else {
402
- alternate = parseBlock()
403
- }
404
- }
452
+ function parseDiscard(tokens: Token[]): DiscardStatement {
453
+ consume(tokens, 'discard')
454
+ consume(tokens, ';')
405
455
 
406
- return new IfStatement(test, consequent, alternate)
456
+ return { type: 'DiscardStatement' }
407
457
  }
408
458
 
409
- function parseWhile(): WhileStatement {
410
- const test = parseExpression(consumeUntil(')'))!
411
- const body = parseBlock()
459
+ function parseReturn(tokens: Token[]): ReturnStatement {
460
+ consume(tokens, 'return')
461
+
462
+ let argument: Expression | null = null
463
+ if (tokens[0]?.value !== ';') argument = parseExpression(tokens)
464
+ consume(tokens, ';')
412
465
 
413
- return new WhileStatement(test, body)
466
+ return { type: 'ReturnStatement', argument }
414
467
  }
415
468
 
416
- function parseFor(): ForStatement {
417
- const delimiterIndex = i + (readUntil(')', tokens, i).length - 1)
469
+ function parseIf(tokens: Token[]): IfStatement {
470
+ consume(tokens, 'if')
471
+ consume(tokens, '(')
472
+ const test = parseExpression(tokens)
473
+ consume(tokens, ')')
418
474
 
419
- i++ // skip (
420
- i++ // TODO: remove backtrack hack
475
+ const consequent = parseBlock(tokens)
421
476
 
422
- const init = parseVariable()
423
- const test = parseExpression(consumeUntil(';').slice(0, -1))
424
- const update = parseExpression(tokens.slice(i, delimiterIndex))
477
+ let alternate = null
478
+ if (tokens[0] && tokens[0].value === 'else') {
479
+ consume(tokens, 'else')
480
+
481
+ if (tokens[0] && (tokens[0] as Token).value === 'if') {
482
+ alternate = parseIf(tokens)
483
+ } else {
484
+ alternate = parseBlock(tokens)
485
+ }
486
+ }
487
+
488
+ return { type: 'IfStatement', test, consequent, alternate }
489
+ }
425
490
 
426
- i = delimiterIndex
427
- i++ // skip )
491
+ function parseWhile(tokens: Token[]): WhileStatement {
492
+ consume(tokens, 'while')
493
+ consume(tokens, '(')
494
+ const test = parseExpression(tokens)
495
+ consume(tokens, ')')
496
+ const body = parseBlock(tokens)
428
497
 
429
- const body = parseBlock()
498
+ return { type: 'WhileStatement', test, body }
499
+ }
430
500
 
431
- return new ForStatement(init, test, update, body)
501
+ function parseFor(tokens: Token[]): ForStatement {
502
+ consume(tokens, 'for')
503
+ consume(tokens, '(')
504
+ const typeSpecifier = parseExpression(tokens) as Identifier | ArraySpecifier
505
+ const init = parseVariable(tokens, typeSpecifier)
506
+ // consume(tokens, ';')
507
+ const test = parseExpression(tokens)
508
+ consume(tokens, ';')
509
+ const update = parseExpression(tokens)
510
+ consume(tokens, ')')
511
+ const body = parseBlock(tokens)
512
+
513
+ return { type: 'ForStatement', init, test, update, body }
432
514
  }
433
515
 
434
- function parseDoWhile(): DoWhileStatement {
435
- const body = parseBlock()
436
- i++ // skip while
437
- const test = parseExpression(consumeUntil(')'))!
438
- i++ // skip ;
516
+ function parseDoWhile(tokens: Token[]): DoWhileStatement {
517
+ consume(tokens, 'do')
518
+ const body = parseBlock(tokens)
519
+ consume(tokens, 'while')
520
+ consume(tokens, '(')
521
+ const test = parseExpression(tokens)
522
+ consume(tokens, ')')
523
+ consume(tokens, ';')
439
524
 
440
- return new DoWhileStatement(test, body)
525
+ return { type: 'DoWhileStatement', test, body }
441
526
  }
442
527
 
443
- function parseSwitch(): SwitchStatement {
444
- const discriminant = parseExpression(consumeUntil(')'))
445
- const delimiterIndex = i + readUntil('}', tokens, i).length - 1
528
+ function parseSwitch(tokens: Token[]): SwitchStatement {
529
+ consume(tokens, 'switch')
530
+ const discriminant = parseExpression(tokens)
446
531
 
447
532
  const cases: SwitchCase[] = []
448
- while (i < delimiterIndex) {
449
- const token = tokens[i++]
533
+ while (tokens.length) {
534
+ const token = consume(tokens)
535
+ if (token.value === '}') break
450
536
 
451
537
  if (token.value === 'case') {
452
- const test = parseExpression(consumeUntil(':').slice(0, -1))
453
- const consequent = parseStatements()
454
- cases.push(new SwitchCase(test, consequent))
538
+ const test = parseExpression(tokens)
539
+ consume(tokens, ':')
540
+ const consequent = parseStatements(tokens)
541
+ cases.push({ type: 'SwitchCase', test, consequent })
455
542
  } else if (token.value === 'default') {
456
- i++ // skip :
457
- const consequent = parseStatements()
458
- cases.push(new SwitchCase(null, consequent))
543
+ const test = null
544
+ consume(tokens, ':')
545
+ const consequent = parseStatements(tokens)
546
+ cases.push({ type: 'SwitchCase', test, consequent })
459
547
  }
460
548
  }
461
549
 
462
- return new SwitchStatement(discriminant!, cases)
550
+ return { type: 'SwitchStatement', discriminant, cases }
463
551
  }
464
552
 
465
- function parsePrecision(): PrecisionStatement {
466
- const precision = tokens[i++].value
467
- const type = new Type(tokens[i++].value, null)
468
- i++ // skip ;
469
- return new PrecisionStatement(precision as any, type)
553
+ function parsePrecision(tokens: Token[]): PrecisionStatement {
554
+ consume(tokens, 'precision')
555
+ const precision = consume(tokens).value as PrecisionQualifier
556
+ const typeSpecifier: Identifier = { type: 'Identifier', name: consume(tokens).value }
557
+ consume(tokens, ';')
558
+ return { type: 'PrecisionStatement', precision, typeSpecifier }
470
559
  }
471
560
 
472
- function parsePreprocessor(): PreprocessorStatement {
473
- const name = tokens[i++].value
561
+ function parsePreprocessor(tokens: Token[]): PreprocessorStatement {
562
+ consume(tokens, '#')
474
563
 
475
- const body = consumeUntil('\\').slice(0, -1)
476
- let value: AST[] | null = null
564
+ let name = '' // name can be unset for the # directive which is ignored
565
+ let value: Expression[] | null = null
477
566
 
478
- if (name !== 'else' && name !== 'endif') {
479
- value = []
567
+ if (tokens[0]?.value !== '\\') {
568
+ name = consume(tokens).value
480
569
 
481
570
  if (name === 'define') {
482
- const left = parseExpression([body.shift()!])!
483
- const right = parseExpression(body)!
484
- value.push(left, right)
571
+ const left = parseExpression(tokens)
572
+ if (tokens[0]?.value === '\\') value = [left]
573
+ else value = [left, parseExpression(tokens)]
485
574
  } else if (name === 'extension') {
486
- const left = parseExpression([body.shift()!])!
487
- body.shift() // skip :
488
- const right = parseExpression(body)!
489
- value.push(left, right)
575
+ // TODO: extension directives must be before declarations
576
+ const left = parseExpression(tokens)
577
+ consume(tokens, ':')
578
+ const right = parseExpression(tokens)
579
+ value = [left, right]
490
580
  } else if (name === 'include') {
491
- value.push(parseExpression(body.slice(1, -1))!)
492
- } else {
493
- value.push(parseExpression(body)!)
581
+ consume(tokens, '<')
582
+ value = [{ type: 'Identifier', name: consume(tokens).value }]
583
+ consume(tokens, '>')
584
+ } else if (name !== 'else' && name !== 'endif') {
585
+ value = []
586
+ while (tokens.length && tokens[0].value !== '\\') {
587
+ value.push(parseExpression(tokens))
588
+ }
494
589
  }
495
590
  }
496
591
 
497
- return new PreprocessorStatement(name, value)
592
+ consume(tokens, '\\')
593
+
594
+ return { type: 'PreprocessorStatement', name, value }
498
595
  }
499
596
 
500
- function parseStatements(): AST[] {
501
- const body: AST[] = []
597
+ function parseStatements(tokens: Token[]): Statement[] {
598
+ const body: Statement[] = []
502
599
  let scopeIndex = 0
503
600
 
504
- while (i < tokens.length) {
505
- const token = tokens[i++]
601
+ while (tokens.length) {
602
+ const token = tokens[0]
506
603
 
507
604
  scopeIndex += getScopeDelta(token)
508
605
  if (scopeIndex < 0) break
509
606
 
510
- let statement: AST | null = null
511
-
512
- if (token.value === '#') {
513
- statement = parsePreprocessor()
514
- } else if (token.type === 'keyword') {
515
- if (token.value === 'case' || token.value === 'default') {
516
- i--
517
- break
518
- } else if (token.value === 'struct') statement = parseStruct()
519
- else if (token.value === 'continue') (statement = new ContinueStatement()), i++
520
- else if (token.value === 'break') (statement = new BreakStatement()), i++
521
- else if (token.value === 'discard') (statement = new DiscardStatement()), i++
522
- else if (token.value === 'return') statement = parseReturn()
523
- else if (token.value === 'if') statement = parseIf()
524
- else if (token.value === 'while') statement = parseWhile()
525
- else if (token.value === 'for') statement = parseFor()
526
- else if (token.value === 'do') statement = parseDoWhile()
527
- else if (token.value === 'switch') statement = parseSwitch()
528
- else if (token.value === 'precision') statement = parsePrecision()
529
- else if (isDeclaration(token.value) && tokens[i].value !== '[') statement = parseIndeterminate()
607
+ let statement: Statement | null = null
608
+
609
+ if (token.value === 'case' || token.value === 'default') break
610
+ else if (token.value === '#') statement = parsePreprocessor(tokens)
611
+ else if (token.value === 'struct') statement = parseStruct(tokens)
612
+ else if (token.value === 'continue') statement = parseContinue(tokens)
613
+ else if (token.value === 'break') statement = parseBreak(tokens)
614
+ else if (token.value === 'discard') statement = parseDiscard(tokens)
615
+ else if (token.value === 'return') statement = parseReturn(tokens)
616
+ else if (token.value === 'if') statement = parseIf(tokens)
617
+ else if (token.value === 'while') statement = parseWhile(tokens)
618
+ else if (token.value === 'for') statement = parseFor(tokens)
619
+ else if (token.value === 'do') statement = parseDoWhile(tokens)
620
+ else if (token.value === 'switch') statement = parseSwitch(tokens)
621
+ else if (token.value === 'precision') statement = parsePrecision(tokens)
622
+ else if (VARIABLE_REGEX.test(token.value) && tokens[1].value !== '[') statement = parseIndeterminate(tokens)
623
+ else {
624
+ const expression = parseExpression(tokens)
625
+ consume(tokens, ';')
626
+ statement = { type: 'ExpressionStatement', expression }
530
627
  }
531
628
 
532
- if (statement) {
533
- body.push(statement)
534
- } else {
535
- const line = [token, ...consumeUntil(';')]
536
- if (line[line.length - 1].value === ';') line.pop()
537
- const expression = parseExpression(line)
538
- if (expression) body.push(expression)
539
- }
629
+ body.push(statement)
540
630
  }
541
631
 
542
632
  return body
543
633
  }
544
634
 
545
- function parseBlock(): BlockStatement {
546
- i++ // skip {
547
- const body = parseStatements()
548
- return new BlockStatement(body)
635
+ // TODO: allow block versus sub-statements for GLSL/WGSL
636
+ function parseBlock(tokens: Token[]): BlockStatement {
637
+ consume(tokens, '{')
638
+ const body = parseStatements(tokens)
639
+ consume(tokens, '}')
640
+ return { type: 'BlockStatement', body }
549
641
  }
550
642
 
643
+ const NEWLINE_REGEX = /\\\n/gm
551
644
  const DIRECTIVE_REGEX = /(^\s*#[^\\]*?)(\n|\/[\/\*])/gm
552
645
 
553
646
  /**
554
647
  * Parses a string of GLSL (WGSL WIP) code into an [AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree).
555
648
  */
556
- export function parse(code: string): AST[] {
557
- // Remove (implicit) version header
558
- code = code.replace('#version 300 es', '')
649
+ export function parse(code: string): Program {
650
+ // Fold newlines
651
+ code = code.replace(NEWLINE_REGEX, '')
559
652
 
560
653
  // Escape newlines after directives, skip comments
561
654
  code = code.replace(DIRECTIVE_REGEX, '$1\\$2')
562
655
 
563
656
  // TODO: preserve
564
- tokens = tokenize(code).filter((token) => token.type !== 'whitespace' && token.type !== 'comment')
565
- i = 0
657
+ const tokens = tokenize(code).filter((token) => token.type !== 'whitespace' && token.type !== 'comment')
566
658
 
567
- return parseStatements()
659
+ return { type: 'Program', body: parseStatements(tokens) }
568
660
  }