Skip to content

Commit 3a3c581

Browse files
refactor: preprocessor statements
1 parent 734686c commit 3a3c581

File tree

4 files changed

+132
-2
lines changed

4 files changed

+132
-2
lines changed

src/ast.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ export class BlockStatement extends Node {
1818
}
1919
}
2020

21+
export class PreprocessorStatement extends Node {
22+
constructor(public name: string, public value: Node[] | null) {
23+
super()
24+
}
25+
}
26+
2127
export class Type extends Node {
2228
constructor(public name: string, public parameters: (Type | Literal | Identifier)[] | null) {
2329
super()
@@ -173,3 +179,4 @@ export type AST =
173179
| ContinueStatement
174180
| BreakStatement
175181
| DiscardStatement
182+
| PreprocessorStatement

src/generator.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
ContinueStatement,
2525
BreakStatement,
2626
DiscardStatement,
27+
PreprocessorStatement,
2728
} from './ast'
2829

2930
const EOL_REGEX = /\n$/
@@ -133,6 +134,19 @@ function format(node: AST | null): string {
133134
line = 'break;\n'
134135
} else if (node instanceof DiscardStatement) {
135136
line = 'discard;\n'
137+
} else if (node instanceof PreprocessorStatement) {
138+
let value = ''
139+
if (node.value) {
140+
if (node.name === 'include') {
141+
value = ` <${format(node.value[0])}>`
142+
} else if (node.name === 'extension') {
143+
value = ` ${node.value.map(format).join(': ')}`
144+
} else {
145+
value = ` ${node.value.map(format).join(' ')}`
146+
}
147+
}
148+
149+
line = `#${node.name}${value}\n`
136150
}
137151

138152
return line

src/parser.ts

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
StructDeclaration,
2626
PrecisionStatement,
2727
ArrayExpression,
28+
PreprocessorStatement,
2829
} from './ast'
2930
import { generate } from './generator'
3031
import { type Token, tokenize } from './tokenizer'
@@ -455,6 +456,34 @@ function parsePrecision(): PrecisionStatement {
455456
return new PrecisionStatement(precision as any, type)
456457
}
457458

459+
function parsePreprocessor(): PreprocessorStatement {
460+
const name = tokens[i++].value
461+
462+
const body = consumeUntil('\\').slice(0, -1)
463+
let value: AST[] | null = null
464+
465+
if (name !== 'else' && name !== 'endif') {
466+
value = []
467+
468+
if (name === 'define') {
469+
const left = parseExpression([body.shift()!])!
470+
const right = parseExpression(body)!
471+
value.push(left, right)
472+
} else if (name === 'extension') {
473+
const left = parseExpression([body.shift()!])!
474+
body.shift() // skip :
475+
const right = parseExpression(body)!
476+
value.push(left, right)
477+
} else if (name === 'include') {
478+
value.push(parseExpression(body.slice(1, -1))!)
479+
} else {
480+
value.push(parseExpression(body)!)
481+
}
482+
}
483+
484+
return new PreprocessorStatement(name, value)
485+
}
486+
458487
function parseStatements(): AST[] {
459488
const body: AST[] = []
460489
let scopeIndex = 0
@@ -467,7 +496,9 @@ function parseStatements(): AST[] {
467496

468497
let statement: AST | null = null
469498

470-
if (token.type === 'keyword') {
499+
if (token.value === '#') {
500+
statement = parsePreprocessor()
501+
} else if (token.type === 'keyword') {
471502
if (token.value === 'case' || token.value === 'default') {
472503
i--
473504
break
@@ -504,12 +535,19 @@ function parseBlock(): BlockStatement {
504535
return new BlockStatement(body)
505536
}
506537

538+
const DIRECTIVE_REGEX = /(^\s*#[^\\]*?)(\n|\/[\/\*])/gm
539+
507540
/**
508541
* Parses a string of GLSL or WGSL code into an [AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree).
509542
*/
510543
export function parse(code: string): AST[] {
511-
// TODO: preserve
544+
// Remove (implicit) version header
512545
code = code.replace('#version 300 es', '')
546+
547+
// Escape newlines after directives, skip comments
548+
code = code.replace(DIRECTIVE_REGEX, '$1\\$2')
549+
550+
// TODO: preserve
513551
tokens = tokenize(code).filter((token) => token.type !== 'whitespace' && token.type !== 'comment')
514552
i = 0
515553

@@ -553,6 +591,17 @@ void main() {
553591
gl_FragColor = vec4(1, 0, 0, 1);
554592
}
555593
594+
#line 0
595+
#define TEST 1
596+
#extension all: disable
597+
#error test
598+
#pragma optimize(on)
599+
#include <chunk>
600+
601+
#ifdef TEST
602+
test;
603+
#endif
604+
556605
method(true);
557606
558607
foo.bar();

tests/parser.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
SwitchCase,
2626
PrecisionStatement,
2727
ArrayExpression,
28+
PreprocessorStatement,
2829
} from 'shaderkit'
2930

3031
describe('parser', () => {
@@ -371,4 +372,63 @@ describe('parser', () => {
371372
expect(statement.members[2]).toBeInstanceOf(Literal)
372373
expect((statement.members[2] as Literal).value).toBe('1.5')
373374
})
375+
376+
it('parses preprocessor statements', () => {
377+
{
378+
const statement = parse('#define TEST 1\n')[0] as PreprocessorStatement
379+
expect(statement).toBeInstanceOf(PreprocessorStatement)
380+
expect(statement.name).toBe('define')
381+
console.log(statement.value)
382+
expect(statement.value!.length).toBe(2)
383+
expect(statement.value![0]).toBeInstanceOf(Identifier)
384+
expect((statement.value![0] as Identifier).value).toBe('TEST')
385+
expect(statement.value![1]).toBeInstanceOf(Literal)
386+
expect((statement.value![1] as Literal).value).toBe('1')
387+
}
388+
389+
{
390+
const statement = parse('#extension all: disable\n')[0] as PreprocessorStatement
391+
expect(statement).toBeInstanceOf(PreprocessorStatement)
392+
expect(statement.name).toBe('extension')
393+
expect(statement.value!.length).toBe(2)
394+
}
395+
396+
{
397+
const statement = parse('#pragma optimize(on)\n')[0] as PreprocessorStatement
398+
expect(statement).toBeInstanceOf(PreprocessorStatement)
399+
expect(statement.name).toBe('pragma')
400+
expect(statement.value!.length).toBe(1)
401+
expect(statement.value![0]).toBeInstanceOf(CallExpression)
402+
expect((statement.value![0] as CallExpression).callee).toBeInstanceOf(Identifier)
403+
expect(((statement.value![0] as CallExpression).callee as Identifier).value).toBe('optimize')
404+
expect((statement.value![0] as CallExpression).args.length).toBe(1)
405+
expect((statement.value![0] as CallExpression).args[0]).toBeInstanceOf(Identifier)
406+
expect(((statement.value![0] as CallExpression).args[0] as Identifier).value).toBe('on')
407+
}
408+
409+
{
410+
const statement = parse('#include <chunk>\n')[0] as PreprocessorStatement
411+
expect(statement).toBeInstanceOf(PreprocessorStatement)
412+
expect(statement.name).toBe('include')
413+
expect(statement.value!.length).toBe(1)
414+
expect(statement.value![0]).toBeInstanceOf(Identifier)
415+
expect((statement.value![0] as Identifier).value).toBe('chunk')
416+
}
417+
418+
{
419+
const statement = parse('#ifdef TEST\n')[0] as PreprocessorStatement
420+
expect(statement).toBeInstanceOf(PreprocessorStatement)
421+
expect(statement.name).toBe('ifdef')
422+
expect(statement.value!.length).toBe(1)
423+
expect(statement.value![0]).toBeInstanceOf(Identifier)
424+
expect((statement.value![0] as Identifier).value).toBe('TEST')
425+
}
426+
427+
{
428+
const statement = parse('#endif\n')[0] as PreprocessorStatement
429+
expect(statement).toBeInstanceOf(PreprocessorStatement)
430+
expect(statement.name).toBe('endif')
431+
expect(statement.value).toBe(null)
432+
}
433+
})
374434
})

0 commit comments

Comments
 (0)