From 3a3c58167bd3be863aaefd4ea6385b65755f1b1c Mon Sep 17 00:00:00 2001 From: Cody Bennett Date: Sun, 19 Nov 2023 07:25:09 -0600 Subject: [PATCH] refactor: preprocessor statements --- src/ast.ts | 7 ++++++ src/generator.ts | 14 +++++++++++ src/parser.ts | 53 ++++++++++++++++++++++++++++++++++++-- tests/parser.test.ts | 60 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 132 insertions(+), 2 deletions(-) diff --git a/src/ast.ts b/src/ast.ts index bc3511d..14ef40b 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -18,6 +18,12 @@ export class BlockStatement extends Node { } } +export class PreprocessorStatement extends Node { + constructor(public name: string, public value: Node[] | null) { + super() + } +} + export class Type extends Node { constructor(public name: string, public parameters: (Type | Literal | Identifier)[] | null) { super() @@ -173,3 +179,4 @@ export type AST = | ContinueStatement | BreakStatement | DiscardStatement + | PreprocessorStatement diff --git a/src/generator.ts b/src/generator.ts index 22b9b50..7883796 100644 --- a/src/generator.ts +++ b/src/generator.ts @@ -24,6 +24,7 @@ import { ContinueStatement, BreakStatement, DiscardStatement, + PreprocessorStatement, } from './ast' const EOL_REGEX = /\n$/ @@ -133,6 +134,19 @@ function format(node: AST | null): string { line = 'break;\n' } else if (node instanceof DiscardStatement) { line = 'discard;\n' + } else if (node instanceof PreprocessorStatement) { + let value = '' + if (node.value) { + if (node.name === 'include') { + value = ` <${format(node.value[0])}>` + } else if (node.name === 'extension') { + value = ` ${node.value.map(format).join(': ')}` + } else { + value = ` ${node.value.map(format).join(' ')}` + } + } + + line = `#${node.name}${value}\n` } return line diff --git a/src/parser.ts b/src/parser.ts index b26008f..f59c8c0 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -25,6 +25,7 @@ import { StructDeclaration, PrecisionStatement, ArrayExpression, + PreprocessorStatement, } from './ast' import { generate } from './generator' import { type Token, tokenize } from './tokenizer' @@ -455,6 +456,34 @@ function parsePrecision(): PrecisionStatement { return new PrecisionStatement(precision as any, type) } +function parsePreprocessor(): PreprocessorStatement { + const name = tokens[i++].value + + const body = consumeUntil('\\').slice(0, -1) + let value: AST[] | null = null + + if (name !== 'else' && name !== 'endif') { + value = [] + + if (name === 'define') { + const left = parseExpression([body.shift()!])! + const right = parseExpression(body)! + value.push(left, right) + } else if (name === 'extension') { + const left = parseExpression([body.shift()!])! + body.shift() // skip : + const right = parseExpression(body)! + value.push(left, right) + } else if (name === 'include') { + value.push(parseExpression(body.slice(1, -1))!) + } else { + value.push(parseExpression(body)!) + } + } + + return new PreprocessorStatement(name, value) +} + function parseStatements(): AST[] { const body: AST[] = [] let scopeIndex = 0 @@ -467,7 +496,9 @@ function parseStatements(): AST[] { let statement: AST | null = null - if (token.type === 'keyword') { + if (token.value === '#') { + statement = parsePreprocessor() + } else if (token.type === 'keyword') { if (token.value === 'case' || token.value === 'default') { i-- break @@ -504,12 +535,19 @@ function parseBlock(): BlockStatement { return new BlockStatement(body) } +const DIRECTIVE_REGEX = /(^\s*#[^\\]*?)(\n|\/[\/\*])/gm + /** * Parses a string of GLSL or WGSL code into an [AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree). */ export function parse(code: string): AST[] { - // TODO: preserve + // Remove (implicit) version header code = code.replace('#version 300 es', '') + + // Escape newlines after directives, skip comments + code = code.replace(DIRECTIVE_REGEX, '$1\\$2') + + // TODO: preserve tokens = tokenize(code).filter((token) => token.type !== 'whitespace' && token.type !== 'comment') i = 0 @@ -553,6 +591,17 @@ void main() { gl_FragColor = vec4(1, 0, 0, 1); } +#line 0 +#define TEST 1 +#extension all: disable +#error test +#pragma optimize(on) +#include + +#ifdef TEST +test; +#endif + method(true); foo.bar(); diff --git a/tests/parser.test.ts b/tests/parser.test.ts index d2ba311..674eb95 100644 --- a/tests/parser.test.ts +++ b/tests/parser.test.ts @@ -25,6 +25,7 @@ import { SwitchCase, PrecisionStatement, ArrayExpression, + PreprocessorStatement, } from 'shaderkit' describe('parser', () => { @@ -371,4 +372,63 @@ describe('parser', () => { expect(statement.members[2]).toBeInstanceOf(Literal) expect((statement.members[2] as Literal).value).toBe('1.5') }) + + it('parses preprocessor statements', () => { + { + const statement = parse('#define TEST 1\n')[0] as PreprocessorStatement + expect(statement).toBeInstanceOf(PreprocessorStatement) + expect(statement.name).toBe('define') + console.log(statement.value) + expect(statement.value!.length).toBe(2) + expect(statement.value![0]).toBeInstanceOf(Identifier) + expect((statement.value![0] as Identifier).value).toBe('TEST') + expect(statement.value![1]).toBeInstanceOf(Literal) + expect((statement.value![1] as Literal).value).toBe('1') + } + + { + const statement = parse('#extension all: disable\n')[0] as PreprocessorStatement + expect(statement).toBeInstanceOf(PreprocessorStatement) + expect(statement.name).toBe('extension') + expect(statement.value!.length).toBe(2) + } + + { + const statement = parse('#pragma optimize(on)\n')[0] as PreprocessorStatement + expect(statement).toBeInstanceOf(PreprocessorStatement) + expect(statement.name).toBe('pragma') + expect(statement.value!.length).toBe(1) + expect(statement.value![0]).toBeInstanceOf(CallExpression) + expect((statement.value![0] as CallExpression).callee).toBeInstanceOf(Identifier) + expect(((statement.value![0] as CallExpression).callee as Identifier).value).toBe('optimize') + expect((statement.value![0] as CallExpression).args.length).toBe(1) + expect((statement.value![0] as CallExpression).args[0]).toBeInstanceOf(Identifier) + expect(((statement.value![0] as CallExpression).args[0] as Identifier).value).toBe('on') + } + + { + const statement = parse('#include \n')[0] as PreprocessorStatement + expect(statement).toBeInstanceOf(PreprocessorStatement) + expect(statement.name).toBe('include') + expect(statement.value!.length).toBe(1) + expect(statement.value![0]).toBeInstanceOf(Identifier) + expect((statement.value![0] as Identifier).value).toBe('chunk') + } + + { + const statement = parse('#ifdef TEST\n')[0] as PreprocessorStatement + expect(statement).toBeInstanceOf(PreprocessorStatement) + expect(statement.name).toBe('ifdef') + expect(statement.value!.length).toBe(1) + expect(statement.value![0]).toBeInstanceOf(Identifier) + expect((statement.value![0] as Identifier).value).toBe('TEST') + } + + { + const statement = parse('#endif\n')[0] as PreprocessorStatement + expect(statement).toBeInstanceOf(PreprocessorStatement) + expect(statement.name).toBe('endif') + expect(statement.value).toBe(null) + } + }) })