shaderkit 0.1.14 → 0.2.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 ADDED
@@ -0,0 +1,568 @@
1
+ import {
2
+ type AST,
3
+ Type,
4
+ Identifier,
5
+ Literal,
6
+ CallExpression,
7
+ UnaryExpression,
8
+ MemberExpression,
9
+ TernaryExpression,
10
+ BinaryExpression,
11
+ BlockStatement,
12
+ FunctionDeclaration,
13
+ VariableDeclaration,
14
+ VariableDeclarator,
15
+ ContinueStatement,
16
+ BreakStatement,
17
+ DiscardStatement,
18
+ ReturnStatement,
19
+ IfStatement,
20
+ WhileStatement,
21
+ ForStatement,
22
+ DoWhileStatement,
23
+ SwitchStatement,
24
+ SwitchCase,
25
+ StructDeclaration,
26
+ PrecisionStatement,
27
+ ArrayExpression,
28
+ 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
+ ]
67
+
68
+ // TODO: this is GLSL-only, separate language constants
69
+ const TYPE_REGEX = /^(void|bool|float|u?int|[uib]?vec\d|mat\d(x\d)?)$/
70
+ const QUALIFIER_REGEX = /^(const|uniform|in|out|inout|centroid|flat|smooth|invariant|lowp|mediump|highp)$/
71
+ const VARIABLE_REGEX = new RegExp(`${TYPE_REGEX.source}|${QUALIFIER_REGEX.source}|layout`)
72
+
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
+
78
+ function getScopeDelta(token: Token): number {
79
+ if (isOpen(token.value)) return 1
80
+ if (isClose(token.value)) return -1
81
+ return 0
82
+ }
83
+
84
+ let tokens: Token[] = []
85
+ let i: number = 0
86
+
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
97
+ }
98
+
99
+ return output
100
+ }
101
+
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)
131
+ }
132
+
133
+ return left
134
+ }
135
+
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
142
+
143
+ scopeIndex += getScopeDelta(token)
144
+
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)
150
+
151
+ const test = parseExpression(testBody)!
152
+ const consequent = parseExpression(consequentBody)!
153
+ const alternate = parseExpression(alternateBody)!
154
+
155
+ return new TernaryExpression(test, consequent, alternate)
156
+ } 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)
161
+ }
162
+ }
163
+
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[] = []
180
+
181
+ const scope = readUntil(')', body, 1).slice(1, -1)
182
+
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)
191
+ }
192
+
193
+ const expression = new CallExpression(callee, args)
194
+
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
+ }
202
+
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
214
+ }
215
+
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
233
+
234
+ if (next[next.length - 1].value === ',') next.pop()
235
+
236
+ members.push(parseExpression(next)!)
237
+ }
238
+
239
+ return new ArrayExpression(type, members)
240
+ }
241
+ }
242
+
243
+ return null
244
+ }
245
+
246
+ function parseVariable(
247
+ qualifiers: string[] = [],
248
+ layout: Record<string, string | boolean> | null = null,
249
+ ): 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
+ const declarations: VariableDeclarator[] = []
256
+
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
276
+
277
+ value = parseExpression(right.slice(0, -1))
278
+ }
279
+
280
+ declarations.push(new VariableDeclarator(name, value ?? prefix))
281
+ }
282
+
283
+ return new VariableDeclaration(layout, qualifiers, kind, type, declarations)
284
+ }
285
+
286
+ function parseFunction(qualifiers: string[]): FunctionDeclaration {
287
+ i-- // TODO: remove backtrack hack
288
+
289
+ const type = new Type(tokens[i++].value, null)
290
+ const name = tokens[i++].value
291
+ const args: VariableDeclaration[] = []
292
+
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)
302
+
303
+ const line = readUntil(',', header, j)
304
+ j += line.length
305
+
306
+ const name = line.shift()!.value
307
+
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 ]
313
+ }
314
+
315
+ if (line[line.length - 1]?.value === ',') line.pop() // skip ,
316
+
317
+ const value = parseExpression(line) ?? prefix
318
+
319
+ const declarations: VariableDeclarator[] = [new VariableDeclarator(name, value)]
320
+
321
+ args.push(new VariableDeclaration(null, qualifiers, null, type, declarations))
322
+ }
323
+
324
+ let body = null
325
+ if (tokens[i].value === ';') i++ // skip ;
326
+ else body = parseBlock()
327
+
328
+ return new FunctionDeclaration(name, type, qualifiers, args, body)
329
+ }
330
+
331
+ function parseIndeterminate(): VariableDeclaration | FunctionDeclaration {
332
+ i-- // TODO: remove backtrack hack
333
+
334
+ let layout: Record<string, string | boolean> | null = null
335
+ if (tokens[i].value === 'layout') {
336
+ i++ // skip layout
337
+
338
+ layout = {}
339
+
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
350
+ } else {
351
+ layout[key] = token.value
352
+ }
353
+ }
354
+
355
+ i++ // skip )
356
+ }
357
+
358
+ const qualifiers: string[] = []
359
+ while (tokens[i] && QUALIFIER_REGEX.test(tokens[i].value)) {
360
+ qualifiers.push(tokens[i++].value)
361
+ }
362
+ i++
363
+
364
+ return tokens[i + 1]?.value === '(' ? parseFunction(qualifiers) : parseVariable(qualifiers, layout)
365
+ }
366
+
367
+ function parseStruct(): StructDeclaration {
368
+ const name = tokens[i++].value
369
+ i++ // skip {
370
+ const members: VariableDeclaration[] = []
371
+ while (tokens[i] && tokens[i].value !== '}') {
372
+ i++ // TODO: remove backtrack hack
373
+ members.push(parseIndeterminate() as VariableDeclaration)
374
+ }
375
+ i++ // skip }
376
+ i++ // skip ;
377
+
378
+ return new StructDeclaration(name, members)
379
+ }
380
+
381
+ function parseReturn(): ReturnStatement {
382
+ const body = consumeUntil(';')
383
+ body.pop() // skip ;
384
+
385
+ const argument = parseExpression(body)
386
+
387
+ return new ReturnStatement(argument as any)
388
+ }
389
+
390
+ function parseIf(): IfStatement {
391
+ const test = parseExpression(consumeUntil(')'))!
392
+ const consequent = parseBlock()
393
+
394
+ let alternate = null
395
+ if (tokens[i].value === 'else') {
396
+ i++ // TODO: remove backtrack hack
397
+
398
+ if (tokens[i].value === 'if') {
399
+ i++
400
+ alternate = parseIf()
401
+ } else {
402
+ alternate = parseBlock()
403
+ }
404
+ }
405
+
406
+ return new IfStatement(test, consequent, alternate)
407
+ }
408
+
409
+ function parseWhile(): WhileStatement {
410
+ const test = parseExpression(consumeUntil(')'))!
411
+ const body = parseBlock()
412
+
413
+ return new WhileStatement(test, body)
414
+ }
415
+
416
+ function parseFor(): ForStatement {
417
+ const delimiterIndex = i + (readUntil(')', tokens, i).length - 1)
418
+
419
+ i++ // skip (
420
+ i++ // TODO: remove backtrack hack
421
+
422
+ const init = parseVariable()
423
+ const test = parseExpression(consumeUntil(';').slice(0, -1))
424
+ const update = parseExpression(tokens.slice(i, delimiterIndex))
425
+
426
+ i = delimiterIndex
427
+ i++ // skip )
428
+
429
+ const body = parseBlock()
430
+
431
+ return new ForStatement(init, test, update, body)
432
+ }
433
+
434
+ function parseDoWhile(): DoWhileStatement {
435
+ const body = parseBlock()
436
+ i++ // skip while
437
+ const test = parseExpression(consumeUntil(')'))!
438
+ i++ // skip ;
439
+
440
+ return new DoWhileStatement(test, body)
441
+ }
442
+
443
+ function parseSwitch(): SwitchStatement {
444
+ const discriminant = parseExpression(consumeUntil(')'))
445
+ const delimiterIndex = i + readUntil('}', tokens, i).length - 1
446
+
447
+ const cases: SwitchCase[] = []
448
+ while (i < delimiterIndex) {
449
+ const token = tokens[i++]
450
+
451
+ if (token.value === 'case') {
452
+ const test = parseExpression(consumeUntil(':').slice(0, -1))
453
+ const consequent = parseStatements()
454
+ cases.push(new SwitchCase(test, consequent))
455
+ } else if (token.value === 'default') {
456
+ i++ // skip :
457
+ const consequent = parseStatements()
458
+ cases.push(new SwitchCase(null, consequent))
459
+ }
460
+ }
461
+
462
+ return new SwitchStatement(discriminant!, cases)
463
+ }
464
+
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)
470
+ }
471
+
472
+ function parsePreprocessor(): PreprocessorStatement {
473
+ const name = tokens[i++].value
474
+
475
+ const body = consumeUntil('\\').slice(0, -1)
476
+ let value: AST[] | null = null
477
+
478
+ if (name !== 'else' && name !== 'endif') {
479
+ value = []
480
+
481
+ if (name === 'define') {
482
+ const left = parseExpression([body.shift()!])!
483
+ const right = parseExpression(body)!
484
+ value.push(left, right)
485
+ } else if (name === 'extension') {
486
+ const left = parseExpression([body.shift()!])!
487
+ body.shift() // skip :
488
+ const right = parseExpression(body)!
489
+ value.push(left, right)
490
+ } else if (name === 'include') {
491
+ value.push(parseExpression(body.slice(1, -1))!)
492
+ } else {
493
+ value.push(parseExpression(body)!)
494
+ }
495
+ }
496
+
497
+ return new PreprocessorStatement(name, value)
498
+ }
499
+
500
+ function parseStatements(): AST[] {
501
+ const body: AST[] = []
502
+ let scopeIndex = 0
503
+
504
+ while (i < tokens.length) {
505
+ const token = tokens[i++]
506
+
507
+ scopeIndex += getScopeDelta(token)
508
+ if (scopeIndex < 0) break
509
+
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()
530
+ }
531
+
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
+ }
540
+ }
541
+
542
+ return body
543
+ }
544
+
545
+ function parseBlock(): BlockStatement {
546
+ i++ // skip {
547
+ const body = parseStatements()
548
+ return new BlockStatement(body)
549
+ }
550
+
551
+ const DIRECTIVE_REGEX = /(^\s*#[^\\]*?)(\n|\/[\/\*])/gm
552
+
553
+ /**
554
+ * Parses a string of GLSL (WGSL WIP) code into an [AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree).
555
+ */
556
+ export function parse(code: string): AST[] {
557
+ // Remove (implicit) version header
558
+ code = code.replace('#version 300 es', '')
559
+
560
+ // Escape newlines after directives, skip comments
561
+ code = code.replace(DIRECTIVE_REGEX, '$1\\$2')
562
+
563
+ // TODO: preserve
564
+ tokens = tokenize(code).filter((token) => token.type !== 'whitespace' && token.type !== 'comment')
565
+ i = 0
566
+
567
+ return parseStatements()
568
+ }