diff --git a/README.md b/README.md index bc12c3a..72747b9 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Tools and IntelliSense for GLSL and WGSL. - [Identifier](#identifier) - [Literal](#literal) - [ArraySpecifier](#arrayspecifier) + - [TypeSpecifier](#typespecifier) - [Program](#program) - Statements - [ExpressionStatement](#expressionstatement) @@ -252,7 +253,7 @@ minify(`#version 300 es\nin vec2 c;out vec4 data[gl_MaxDrawBuffers];void main(){ ## Parse -Parses a string of GLSL (WGSL is WIP) code into an [AST](#ast). +Parses a string of GLSL or WGSL code into an [AST](#ast). ```ts const ast: Program = parse(code: string) @@ -260,11 +261,11 @@ const ast: Program = parse(code: string) ## Generate -Generates a string of GLSL (WGSL is WIP) code from an [AST](#ast). +Generates a string of GLSL or WGSL code from an [AST](#ast). ```ts const code: string = generate(program: Program, { - target: 'GLSL' // | 'WGSL' + target: 'GLSL' | 'WGSL' }) ``` @@ -341,6 +342,18 @@ interface ArraySpecifier extends Node { } ``` +### TypeSpecifier + +A type specifier and optional shader layout. + +```ts +interface TypeSpecifier extends Node { + type: 'TypeSpecifier' + typeSpecifier: Identifier | ArraySpecifier + layout: Record | null +} +``` + ### Program Represents the root of an AST. @@ -544,9 +557,9 @@ A function declaration. `body` is null for overloads. ```ts interface FunctionDeclaration extends Node { type: 'FunctionDeclaration' - id: Identifier + id: TypeSpecifier qualifiers: PrecisionQualifier[] - typeSpecifier: Identifier | ArraySpecifier + typeSpecifier: TypeSpecifier params: FunctionParameter[] body: BlockStatement | null } @@ -559,9 +572,9 @@ A function parameter within a function declaration. ```ts interface FunctionParameter extends Node { type: 'FunctionParameter' - id: Identifier | null + id: TypeSpecifier | null qualifiers: (ConstantQualifier | ParameterQualifier | PrecisionQualifier)[] - typeSpecifier: Identifier | ArraySpecifier + typeSpecifier: TypeSpecifier } ``` @@ -583,10 +596,9 @@ A variable declarator within a variable declaration. ```ts interface VariableDeclarator extends Node { type: 'VariableDeclarator' - id: Identifier + id: TypeSpecifier qualifiers: (ConstantQualifier | InterpolationQualifier | StorageQualifier | PrecisionQualifier)[] - typeSpecifier: Identifier | ArraySpecifier - layout: Record | null + typeSpecifier: TypeSpecifier init: Expression | null } ``` @@ -600,8 +612,7 @@ interface StructuredBufferDeclaration extends Node { type: 'StructuredBufferDeclaration' id: Identifier | null qualifiers: (InterfaceStorageQualifier | MemoryQualifier | LayoutQualifier)[] - typeSpecifier: Identifier | ArraySpecifier - layout: Record | null + typeSpecifier: TypeSpecifier members: VariableDeclaration[] } ``` diff --git a/src/ast.ts b/src/ast.ts index d3e3079..871e795 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -56,6 +56,15 @@ export interface ArraySpecifier extends Node { dimensions: (Literal | Identifier | null)[] } +/** + * A type specifier and optional shader layout. + */ +export interface TypeSpecifier extends Node { + type: 'TypeSpecifier' + typeSpecifier: Identifier | ArraySpecifier + layout: Record | null +} + /** * An array initialization expression. */ @@ -287,9 +296,9 @@ export type PrecisionQualifier = 'highp' | 'mediump' | 'lowp' */ export interface FunctionDeclaration extends Node { type: 'FunctionDeclaration' - id: Identifier + id: TypeSpecifier qualifiers: PrecisionQualifier[] - typeSpecifier: Identifier | ArraySpecifier + typeSpecifier: TypeSpecifier | null params: FunctionParameter[] body: BlockStatement | null } @@ -299,9 +308,9 @@ export interface FunctionDeclaration extends Node { */ export interface FunctionParameter extends Node { type: 'FunctionParameter' - id: Identifier | null + id: TypeSpecifier | null qualifiers: (ConstantQualifier | ParameterQualifier | PrecisionQualifier)[] - typeSpecifier: Identifier | ArraySpecifier + typeSpecifier: TypeSpecifier } /** @@ -317,10 +326,9 @@ export interface VariableDeclaration extends Node { */ export interface VariableDeclarator extends Node { type: 'VariableDeclarator' - id: Identifier + id: TypeSpecifier qualifiers: (ConstantQualifier | InterpolationQualifier | StorageQualifier | PrecisionQualifier)[] - typeSpecifier: Identifier | ArraySpecifier - layout: Record | null + typeSpecifier: TypeSpecifier | null init: Expression | null } @@ -331,8 +339,7 @@ export interface StructuredBufferDeclaration extends Node { type: 'StructuredBufferDeclaration' id: Identifier | null qualifiers: (InterfaceStorageQualifier | MemoryQualifier | LayoutQualifier)[] - typeSpecifier: Identifier | ArraySpecifier - layout: Record | null + typeSpecifier: TypeSpecifier members: VariableDeclaration[] } @@ -383,7 +390,9 @@ export interface LayoutQualifierStatement extends Node { export type Expression = | Literal | Identifier + | ArraySpecifier | ArrayExpression + | TypeSpecifier | UnaryExpression | UpdateExpression | BinaryExpression diff --git a/src/generator.ts b/src/generator.ts index 482baec..8ee59c2 100644 --- a/src/generator.ts +++ b/src/generator.ts @@ -1,128 +1,187 @@ -import { type AST, type Program } from './ast.js' +import { Identifier, type AST, type Program } from './ast.js' -function formatLayout(layout: Record | null): string { - if (!layout) return '' - - return `layout(${Object.entries(layout) - .map(([k, v]) => (v === true ? k : `${k}=${v}`)) - .join(',')})` +export interface GenerateOptions { + target: 'GLSL' | 'WGSL' } -// TODO: restore comments/whitespace with sourcemaps, WGSL support -function format(node: AST | null): string { - if (!node) return '' +/** + * Generates a string of GLSL or WGSL code from an [AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree). + */ +export function generate(program: Program, options: GenerateOptions): string { + function formatLayout(layout: Record | null): string { + if (!layout) return '' + + if (options.target === 'GLSL') { + return `layout(${Object.entries(layout) + .map(([k, v]) => (v === true ? k : `${k}=${v}`)) + .join(',')})` + } else { + return Object.entries(layout) + .map(([k, v]) => (v === true ? `@${k} ` : `@${k}(${v})`)) + .join('') + .replaceAll(' @', '@') + } + } + + // TODO: restore comments/whitespace with sourcemaps + function format(node: AST | null): string { + if (!node) return '' + + switch (node.type) { + case 'Identifier': + return node.name + case 'Literal': + return node.value + case 'TypeSpecifier': + return format(node.typeSpecifier) + case 'ArraySpecifier': + if (options.target === 'GLSL') { + return `${node.typeSpecifier.name}${node.dimensions.map((d) => `[${format(d)}]`).join('')}` + } else { + return `${node.typeSpecifier.name}<${node.dimensions.map(format).join(',')}>` + } + case 'ExpressionStatement': + return `${format(node.expression)};` + case 'BlockStatement': + return `{${node.body.map(format).join('')}}` + case 'DiscardStatement': + return 'discard;' + case 'PreprocessorStatement': { + let value = '' + if (node.value) { + if (node.name === 'include') value = ` <${format(node.value[0])}>` // three is whitespace sensitive + else if (node.name === 'extension') value = ` ${node.value.map(format).join(':')}` + else if (node.value.length) value = ` ${node.value.map(format).join(' ')}` + } - switch (node.type) { - case 'Identifier': - return node.name - case 'Literal': - return node.value - case 'ArraySpecifier': - return `${node.typeSpecifier.name}${node.dimensions.map((d) => `[${format(d)}]`).join('')}` - case 'ExpressionStatement': - return `${format(node.expression)};` - case 'BlockStatement': - return `{${node.body.map(format).join('')}}` - case 'DiscardStatement': - return 'discard;' - case 'PreprocessorStatement': { - let value = '' - if (node.value) { - if (node.name === 'include') value = ` <${format(node.value[0])}>` // three is whitespace sensitive - else if (node.name === 'extension') value = ` ${node.value.map(format).join(':')}` - else if (node.value.length) value = ` ${node.value.map(format).join(' ')}` + return `\n#${node.name}${value}\n` + } + case 'PrecisionQualifierStatement': + return `precision ${node.precision} ${node.typeSpecifier.name};` + case 'InvariantQualifierStatement': + return `invariant ${format(node.typeSpecifier)};` + case 'LayoutQualifierStatement': + return `${formatLayout(node.layout)}${node.qualifier};` + case 'ReturnStatement': + return node.argument ? `return ${format(node.argument)};` : 'return;' + case 'BreakStatement': + return 'break;' + case 'ContinueStatement': + return 'continue;' + case 'IfStatement': { + const alternate = node.alternate ? ` else${format(node.consequent)}` : '' + return `if(${format(node.test)})${format(node.consequent)}${alternate}` + } + case 'SwitchStatement': + return `switch(${format(node.discriminant)}){${node.cases.map(format).join('')}}` + case 'SwitchCase': + return `case ${node.test ? format(node.test) : 'default'}:{${node.consequent.map(format).join(';')}}` + case 'WhileStatement': + return `while (${format(node.test)}) ${format(node.body)}` + case 'DoWhileStatement': + return `do ${format(node.body)}while(${format(node.test)})` + case 'ForStatement': + return `for(${format(node.init)}${format(node.test)};${format(node.update)})${format(node.body)}` + case 'FunctionDeclaration': { + if (options.target === 'GLSL') { + const qualifiers = node.qualifiers.length ? `${node.qualifiers.join(' ')} ` : '' // precision + const body = node.body ? format(node.body) : ';' + return `${qualifiers}${format(node.typeSpecifier)} ${format(node.id)}(${node.params + .map(format) + .join(',')})${body}` + } else { + const attributes = node.id.layout ? formatLayout(node.id.layout) : '' + const params = node.params.map(format).join(',') + const typeSpecifier = node.typeSpecifier ? `->${format(node.typeSpecifier)}` : '' + return `${attributes}fn ${format(node.id)}(${params})${typeSpecifier}${format(node.body)}` + } } + case 'FunctionParameter': { + if (options.target === 'GLSL') { + const qualifiers = node.qualifiers.length ? `${node.qualifiers.join(' ')} ` : '' + const id = node.id ? ` ${format(node.id)}` : '' + return `${qualifiers}${format(node.typeSpecifier)}${id}` + } else { + const attributes = node.id!.layout ? formatLayout(node.id!.layout) : '' + return `${attributes}${format(node.id)}:${format(node.typeSpecifier)}` + } + } + case 'VariableDeclaration': { + if (options.target === 'GLSL') { + const head = node.declarations[0] + const layout = formatLayout(head.id.layout) + const qualifiers = head.qualifiers.length ? `${head.qualifiers.join(' ')} ` : '' + return `${layout}${qualifiers}${format(head.typeSpecifier)} ${node.declarations.map(format).join(',')};` + } else { + const head = node.declarations[0] - return `\n#${node.name}${value}\n` - } - case 'PrecisionQualifierStatement': - return `precision ${node.precision} ${node.typeSpecifier.name};` - case 'InvariantQualifierStatement': - return `invariant ${format(node.typeSpecifier)};` - case 'LayoutQualifierStatement': - return `${formatLayout(node.layout)}${node.qualifier};` - case 'ReturnStatement': - return node.argument ? `return ${format(node.argument)};` : 'return;' - case 'BreakStatement': - return 'break;' - case 'ContinueStatement': - return 'continue;' - case 'IfStatement': { - const alternate = node.alternate ? ` else${format(node.consequent)}` : '' - return `if(${format(node.test)})${format(node.consequent)}${alternate}` - } - case 'SwitchStatement': - return `switch(${format(node.discriminant)}){${node.cases.map(format).join('')}}` - case 'SwitchCase': - return `case ${node.test ? format(node.test) : 'default'}:{${node.consequent.map(format).join(';')}}` - case 'WhileStatement': - return `while (${format(node.test)}) ${format(node.body)}` - case 'DoWhileStatement': - return `do ${format(node.body)}while(${format(node.test)})` - case 'ForStatement': - return `for(${format(node.init)};${format(node.test)};${format(node.update)})${format(node.body)}` - case 'FunctionDeclaration': { - const qualifiers = node.qualifiers.length ? `${node.qualifiers.join(' ')} ` : '' // precision - const body = node.body ? format(node.body) : ';' - return `${qualifiers}${format(node.typeSpecifier)} ${format(node.id)}(${node.params - .map(format) - .join(',')})${body}` - } - case 'FunctionParameter': { - const qualifiers = node.qualifiers.length ? `${node.qualifiers.join(' ')} ` : '' - const id = node.id ? ` ${format(node.id)}` : '' - return `${qualifiers}${format(node.typeSpecifier)}${id}` - } - case 'VariableDeclaration': { - const head = node.declarations[0] - const layout = formatLayout(head.layout) - const qualifiers = head.qualifiers.length ? `${head.qualifiers.join(' ')} ` : '' - return `${layout}${qualifiers}${format(head.typeSpecifier)} ${node.declarations.map(format).join(',')};` - } - case 'VariableDeclarator': { - const init = node.init ? `=${format(node.init)}` : '' - return `${format(node.id)}${init}` - } - case 'StructuredBufferDeclaration': { - const layout = formatLayout(node.layout) - const scope = node.id ? `${format(node.id)}` : '' - return `${layout}${node.qualifiers.join(' ')} ${format(node.typeSpecifier)}{${node.members - .map(format) - .join('')}}${scope};` + const attributes = formatLayout(head.id.layout) + + let kind = '' + if (head.qualifiers.length) { + kind = head.qualifiers[0] + const params = head.qualifiers.slice(1) + kind = params.length ? `${kind}<${params.join(',')}>` : `${kind} ` + } + + return `${attributes}${kind}${node.declarations.map(format).join(',')};` // TODO + } + } + case 'VariableDeclarator': { + const init = node.init ? `=${format(node.init)}` : '' + if (options.target === 'GLSL') { + return `${format(node.id)}${init}` + } else { + const typeSpecifier = node.typeSpecifier ? `:${format(node.typeSpecifier)}` : '' + return `${format(node.id)}${typeSpecifier}${init}` + } + } + case 'StructuredBufferDeclaration': { + const layout = formatLayout(node.typeSpecifier.layout) + const scope = node.id ? format(node.id) : '' + return `${layout}${node.qualifiers.join(' ')} ${format(node.typeSpecifier)}{${node.members + .map(format) + .join('')}}${scope};` + } + case 'StructDeclaration': + if (options.target === 'GLSL') { + return `struct ${format(node.id)}{${node.members.map(format).join('')}};` + } else { + return `struct ${format(node.id)}{${node.members.map(format).join('')}}` + .replaceAll(';', ',') + .replace(',}', '}') + } + case 'ArrayExpression': + if (options.target === 'GLSL') { + return `${format(node.typeSpecifier)}(${node.elements.map(format).join(',')})` + } else { + return '' // TODO + } + case 'UnaryExpression': + case 'UpdateExpression': + return node.prefix ? `${node.operator}${format(node.argument)}` : `${format(node.argument)}${node.operator}` + case 'BinaryExpression': + case 'AssignmentExpression': + case 'LogicalExpression': + return `${format(node.left)}${node.operator}${format(node.right)}` + case 'MemberExpression': + return node.computed + ? `${format(node.object)}[${format(node.property)}]` + : `${format(node.object)}.${format(node.property)}` + case 'ConditionalExpression': + if (options.target === 'GLSL') { + return `${format(node.test)}?${format(node.consequent)}:${format(node.alternate)}` + } else { + return `select(${format(node.alternate)},${format(node.consequent)},${format(node.test)})` + } + case 'CallExpression': + return `${format(node.callee)}(${node.arguments.map(format).join(',')})` + case 'Program': + return `${node.body.map(format).join('')}` + default: + return node satisfies never } - case 'StructDeclaration': - return `struct ${format(node.id)}{${node.members.map(format).join('')}};` - case 'ArrayExpression': - return `${format(node.typeSpecifier)}(${node.elements.map(format).join(',')})` - case 'UnaryExpression': - case 'UpdateExpression': - return node.prefix ? `${node.operator}${format(node.argument)}` : `${format(node.argument)}${node.operator}` - case 'BinaryExpression': - case 'AssignmentExpression': - case 'LogicalExpression': - return `${format(node.left)}${node.operator}${format(node.right)}` - case 'MemberExpression': - return node.computed - ? `${format(node.object)}[${format(node.property)}]` - : `${format(node.object)}.${format(node.property)}` - case 'ConditionalExpression': - return `${format(node.test)}?${format(node.consequent)}:${format(node.alternate)}` - case 'CallExpression': - return `${format(node.callee)}(${node.arguments.map(format).join(',')})` - case 'Program': - return `${node.body.map(format).join('')}` - default: - return node satisfies never } -} - -export interface GenerateOptions { - target: 'GLSL' // | 'WGSL' -} -/** - * Generates a string of GLSL (WGSL WIP) code from an [AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree). - */ -export function generate(program: Program, options: GenerateOptions): string { return format(program).replaceAll('\n\n', '\n').replaceAll('] ', ']').trim() } diff --git a/src/parser.ts b/src/parser.ts index 3d0509e..3568a03 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -37,6 +37,7 @@ import { VariableDeclarator, WhileStatement, LayoutQualifierStatement, + TypeSpecifier, } from './ast.js' import { type Token, tokenize } from './tokenizer.js' @@ -149,12 +150,80 @@ function consume(tokens: Token[], expected?: string): Token { return token } +// WGSL-only +function parseGenerics(tokens: Token[]): (Identifier | Literal)[] | null { + let generics: (Identifier | Literal)[] | null = null + + // Check whether token stream is formatted for generics + if (tokens[0]?.value === '<') { + let i = 1 + while (i < tokens.length) { + const token = tokens[i++] + + if (token.type === 'symbol') { + if (token.value === '>') generics = [] + if (token.value !== ',') break + } + } + } + + // Parse generic arguments if found + if (generics) { + consume(tokens, '<') + while (tokens[0]?.value !== '>') { + if (tokens[0]?.type === 'identifier') { + generics.push({ type: 'Identifier', name: consume(tokens).value }) + } else { + generics.push({ type: 'Literal', value: consume(tokens).value }) + } + if (tokens[0]?.value === ',') consume(tokens, ',') + } + consume(tokens, '>') + } + + return generics +} + +// WGSL-only +function parseAttributes(tokens: Token[]): Record | null { + let attributes: Record | null = null + + while (tokens[0]?.value === '@') { + attributes ??= {} + + consume(tokens, '@') + const key = consume(tokens).value + + // TODO: workgroup size (x, y, z) + let value: string | boolean = true + if ((tokens[0]?.value as string) === '(') { + consume(tokens, '(') + value = consume(tokens).value + consume(tokens, ')') + } + + attributes[key] = value + } + + return attributes +} + function parseExpression(tokens: Token[], minBindingPower: number = 0): Expression { let token = consume(tokens) let lhs: Expression if (token.type === 'identifier' || token.type === 'keyword') { lhs = { type: 'Identifier', name: token.value } + + // WGSL-only + const generics = parseGenerics(tokens) + if (generics) { + lhs = { + type: 'ArraySpecifier', + typeSpecifier: lhs, + dimensions: generics, + } + } } else if (token.type === 'bool' || token.type === 'float' || token.type === 'int') { lhs = { type: 'Literal', value: token.value } } else if (token.type === 'symbol' && token.value === '(') { @@ -264,10 +333,51 @@ function parseExpression(tokens: Token[], minBindingPower: number = 0): Expressi return lhs } -function parseTypeSpecifier(tokens: Token[]): Identifier | ArraySpecifier { +function parseLayout(tokens: Token[]): Record | null { + let layout: Record | null = null + + if (tokens[0].value === 'layout') { + consume(tokens, 'layout') + consume(tokens, '(') + + layout = {} + + while (tokens[0] && (tokens[0] as Token).value !== ')') { + const expression = parseExpression(tokens) + + if ( + expression.type === 'AssignmentExpression' && + expression.left.type === 'Identifier' && + expression.right.type === 'Literal' + ) { + layout[expression.left.name] = expression.right.value + } else if (expression.type === 'Identifier') { + layout[expression.name] = true + } else { + throw new TypeError('Unexpected expression') + } + + if (tokens[0] && (tokens[0] as Token).value !== ')') consume(tokens, ',') + } + + consume(tokens, ')') + } + + return layout +} + +function parseTypeSpecifier(tokens: Token[], layout: Record | null): TypeSpecifier { let typeSpecifier: Identifier | ArraySpecifier = { type: 'Identifier', name: consume(tokens).value } - if (tokens[0]?.value === '[') { + // WGSL-only + const generics = parseGenerics(tokens) + if (generics) { + typeSpecifier = { + type: 'ArraySpecifier', + typeSpecifier, + dimensions: generics, + } + } else if (tokens[0]?.value === '[') { const dimensions: (Literal | Identifier | null)[] = [] while (tokens[0]?.value === '[') { @@ -289,16 +399,16 @@ function parseTypeSpecifier(tokens: Token[]): Identifier | ArraySpecifier { } } - return typeSpecifier + return { type: 'TypeSpecifier', typeSpecifier, layout } } function parseVariableDeclarator( tokens: Token[], - typeSpecifier: Identifier | ArraySpecifier, + typeSpecifier: TypeSpecifier, qualifiers: (ConstantQualifier | InterpolationQualifier | StorageQualifier | PrecisionQualifier)[], layout: Record | null, ): VariableDeclarator { - const id = parseTypeSpecifier(tokens) as Identifier + const id = parseTypeSpecifier(tokens, layout) let init: Expression | null = null @@ -307,14 +417,14 @@ function parseVariableDeclarator( init = parseExpression(tokens) } - return { type: 'VariableDeclarator', id, qualifiers, typeSpecifier, layout, init } + return { type: 'VariableDeclarator', id, qualifiers, typeSpecifier, init } } function parseVariable( tokens: Token[], - typeSpecifier: Identifier | ArraySpecifier, + typeSpecifier: TypeSpecifier, qualifiers: (ConstantQualifier | InterpolationQualifier | StorageQualifier | PrecisionQualifier)[] = [], - layout: Record | null = null, + layout: Record | null, ): VariableDeclaration { const declarations: VariableDeclarator[] = [] @@ -337,25 +447,28 @@ function parseVariable( function parseBufferInterface( tokens: Token[], - typeSpecifier: Identifier | ArraySpecifier, + typeSpecifier: TypeSpecifier, + layout: Record | null, qualifiers: LayoutQualifier[] = [], - layout: Record | null = null, ): StructuredBufferDeclaration { + typeSpecifier.layout = layout // Identifiers are optional, so store on type name + const members = parseBlock(tokens).body as VariableDeclaration[] let id: Identifier | null = null if (tokens[0]?.value !== ';') id = parseExpression(tokens) as Identifier consume(tokens, ';') - return { type: 'StructuredBufferDeclaration', id, qualifiers, typeSpecifier, layout, members } + return { type: 'StructuredBufferDeclaration', id, qualifiers, typeSpecifier, members } } function parseFunction( tokens: Token[], - typeSpecifier: ArraySpecifier | Identifier, + typeSpecifier: TypeSpecifier, + layout: Record | null, qualifiers: PrecisionQualifier[] = [], ): FunctionDeclaration { - const id: Identifier = { type: 'Identifier', name: consume(tokens).value } + const id = parseTypeSpecifier(tokens, layout) consume(tokens, '(') @@ -365,10 +478,10 @@ function parseFunction( while (tokens[0] && QUALIFIER_REGEX.test(tokens[0].value)) { qualifiers.push(consume(tokens).value as ConstantQualifier | ParameterQualifier | PrecisionQualifier) } - const typeSpecifier = parseTypeSpecifier(tokens) + const typeSpecifier = parseTypeSpecifier(tokens, null) - let id: Identifier | null = null - if (tokens[0]?.type !== 'symbol') id = parseTypeSpecifier(tokens) as Identifier + let id: TypeSpecifier | null = null + if (tokens[0]?.type !== 'symbol') id = parseTypeSpecifier(tokens, null) params.push({ type: 'FunctionParameter', id, qualifiers, typeSpecifier }) @@ -405,33 +518,7 @@ function parseIndeterminate( | StructuredBufferDeclaration | LayoutQualifierStatement | InvariantQualifierStatement { - let layout: Record | null = null - if (tokens[0].value === 'layout') { - consume(tokens, 'layout') - consume(tokens, '(') - - layout = {} - - while (tokens[0] && (tokens[0] as Token).value !== ')') { - const expression = parseExpression(tokens) - - if ( - expression.type === 'AssignmentExpression' && - expression.left.type === 'Identifier' && - expression.right.type === 'Literal' - ) { - layout[expression.left.name] = expression.right.value - } else if (expression.type === 'Identifier') { - layout[expression.name] = true - } else { - throw new TypeError('Unexpected expression') - } - - if (tokens[0] && (tokens[0] as Token).value !== ')') consume(tokens, ',') - } - - consume(tokens, ')') - } + const layout = parseLayout(tokens) // Input qualifiers will suddenly terminate if (layout !== null && tokens[1]?.value === ';') { @@ -449,12 +536,12 @@ function parseIndeterminate( qualifiers.push(consume(tokens).value) } - const typeSpecifier = parseTypeSpecifier(tokens) + const typeSpecifier = parseTypeSpecifier(tokens, null) if (tokens[0]?.value === '{') { - return parseBufferInterface(tokens, typeSpecifier, qualifiers as LayoutQualifier[], layout) + return parseBufferInterface(tokens, typeSpecifier, layout, qualifiers as LayoutQualifier[]) } else if (tokens[1]?.value === '(') { - return parseFunction(tokens, typeSpecifier, qualifiers as PrecisionQualifier[]) + return parseFunction(tokens, typeSpecifier, layout, qualifiers as PrecisionQualifier[]) } else { return parseVariable( tokens, @@ -470,8 +557,39 @@ function parseStruct(tokens: Token[]): StructDeclaration { const id: Identifier = { type: 'Identifier', name: consume(tokens).value } consume(tokens, '{') const members: VariableDeclaration[] = [] + const isWGSL = tokens[0].value === '@' || tokens[1]?.value === '<' || tokens[1]?.value === ':' while (tokens[0] && tokens[0].value !== '}') { - members.push(...(parseStatements(tokens) as unknown as VariableDeclaration[])) + if (isWGSL) { + const layout = parseAttributes(tokens) + const id = parseTypeSpecifier(tokens, layout) + + let typeSpecifier: TypeSpecifier | null = null + if ((tokens[0]?.value as string) === ':') { + consume(tokens, ':') + typeSpecifier = parseTypeSpecifier(tokens, null) + } + + // TODO: infer from return type or ptr usage + const qualifiers: (ConstantQualifier | InterpolationQualifier | StorageQualifier | PrecisionQualifier)[] = [] + + members.push({ + type: 'VariableDeclaration', + declarations: [ + { + type: 'VariableDeclarator', + id, + qualifiers, + typeSpecifier, + init: null, + }, + ], + }) + + if (tokens[0]?.value === ',') consume(tokens, ',') + else if (tokens[0]?.value === ';') consume(tokens, ';') + } else { + members.push(...(parseStatements(tokens) as unknown as VariableDeclaration[])) + } } consume(tokens, '}') @@ -487,7 +605,7 @@ function parseStruct(tokens: Token[]): StructDeclaration { ) } - consume(tokens, ';') + if (!isWGSL || tokens[0]?.value === ';') consume(tokens, ';') return { type: 'StructDeclaration', id, members } } @@ -558,9 +676,12 @@ function parseWhile(tokens: Token[]): WhileStatement { function parseFor(tokens: Token[]): ForStatement { consume(tokens, 'for') consume(tokens, '(') - const typeSpecifier = parseExpression(tokens) as Identifier | ArraySpecifier - const init = parseVariable(tokens, typeSpecifier) - // consume(tokens, ';') + let init: VariableDeclaration + if (isWGSLVariable(tokens)) { + init = parseWGSLIndeterminate(tokens) as VariableDeclaration + } else { + init = parseIndeterminate(tokens) as VariableDeclaration + } const test = parseExpression(tokens) consume(tokens, ';') const update = parseExpression(tokens) @@ -582,8 +703,10 @@ function parseDoWhile(tokens: Token[]): DoWhileStatement { return { type: 'DoWhileStatement', test, body } } +// TODO: https://w3.org/TR/WGSL/#switch-statement function parseSwitch(tokens: Token[]): SwitchStatement { consume(tokens, 'switch') + const isWGSL = tokens[0]?.value !== '(' const discriminant = parseExpression(tokens) const cases: SwitchCase[] = [] @@ -671,6 +794,111 @@ function isVariable(tokens: Token[]): boolean { return tokens[i]?.type !== 'symbol' } +function parseWGSLIndeterminate(tokens: Token[]): FunctionDeclaration | VariableDeclaration { + const layout = parseAttributes(tokens) + + if (tokens[0]?.value === 'fn') { + consume(tokens, 'fn') + const id = parseTypeSpecifier(tokens, layout) + + consume(tokens, '(') + const params: FunctionParameter[] = [] + + while (tokens.length && (tokens[0]?.value as string) !== ')') { + const layout = parseAttributes(tokens) + const id = parseTypeSpecifier(tokens, layout) + + consume(tokens, ':') + + // TODO: infer qualifiers from return type or ptr usage + const qualifiers: (ConstantQualifier | ParameterQualifier | PrecisionQualifier)[] = [] + const typeSpecifier = parseTypeSpecifier(tokens, null) + + params.push({ type: 'FunctionParameter', id, qualifiers, typeSpecifier }) + + if ((tokens[0]?.value as string) === ',') consume(tokens, ',') + } + + consume(tokens, ')') + + // TODO: infer precision qualifier from type + let typeSpecifier: TypeSpecifier | null = null + if ((tokens[0]?.value as string) === '->') { + consume(tokens, '->') + typeSpecifier = parseTypeSpecifier(tokens, parseLayout(tokens)) + } + + const body = parseBlock(tokens) + + return { type: 'FunctionDeclaration', id, qualifiers: [], params, typeSpecifier, body } + } else if (tokens[0]?.value === 'var' || tokens[0]?.value === 'let' || tokens[0]?.value === 'const') { + const kind = consume(tokens).value // var | let | const + + const storageQualifiers = parseGenerics(tokens) + + const id = parseTypeSpecifier(tokens, layout) + + let typeSpecifier: TypeSpecifier | null = null + if ((tokens[0]?.value as string) === ':') { + consume(tokens, ':') + typeSpecifier = parseTypeSpecifier(tokens, null) + } + + // TODO: infer from return type or ptr usage + const qualifiers: (ConstantQualifier | InterpolationQualifier | StorageQualifier | PrecisionQualifier)[] = [ + kind as any, + ] + if (storageQualifiers !== null) { + for (const expression of storageQualifiers) { + if (expression.type === 'Identifier') { + qualifiers.push(expression.name as StorageQualifier) + } else if (expression.type === 'Literal') { + qualifiers.push(expression.value as StorageQualifier) + } + } + } + + let init: Expression | null = null + if ((tokens[0]?.value as string) === '=' || kind === 'const') { + consume(tokens, '=') + init = parseExpression(tokens) + } + + consume(tokens, ';') + + return { + type: 'VariableDeclaration', + declarations: [ + // TODO: are declaration lists allowed? + { + type: 'VariableDeclarator', + id, + qualifiers, + typeSpecifier, + init, + }, + ], + } + } + + // unreachable + return null! +} + +function isWGSLVariable(tokens: Token[]) { + const token = tokens[0] + + // Attribute; indeterminate + if (token.value === '@') return true + // Function declaration + if (token.value === 'fn') return true + // Variable declaration + if (token.value === 'var' || token.value === 'let' || (token.value === 'const' && tokens[1]?.type === 'identifier')) + return true + + return false +} + function parseStatements(tokens: Token[]): Statement[] { const body: Statement[] = [] let scopeIndex = 0 @@ -696,6 +924,7 @@ function parseStatements(tokens: Token[]): Statement[] { else if (token.value === 'do') statement = parseDoWhile(tokens) else if (token.value === 'switch') statement = parseSwitch(tokens) else if (token.value === 'precision') statement = parsePrecision(tokens) + else if (isWGSLVariable(tokens)) statement = parseWGSLIndeterminate(tokens) else if (isVariable(tokens)) statement = parseIndeterminate(tokens) else { const expression = parseExpression(tokens) @@ -721,7 +950,7 @@ const NEWLINE_REGEX = /\\\s+/gm const DIRECTIVE_REGEX = /(^\s*#[^\\]*?)(\n|\/[\/\*])/gm /** - * Parses a string of GLSL (WGSL WIP) code into an [AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree). + * Parses a string of GLSL or WGSL code into an [AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree). */ export function parse(code: string): Program { // Fold newlines diff --git a/src/visitor.ts b/src/visitor.ts index 4b37952..a5ad260 100644 --- a/src/visitor.ts +++ b/src/visitor.ts @@ -26,6 +26,9 @@ export function visit(node: AST, visitors: Visitors, ancestors: AST[] = []): voi visit(node.typeSpecifier, visitors, ancestors) for (const dimension of node.dimensions) if (dimension) visit(dimension, visitors, ancestors) break + case 'TypeSpecifier': + visit(node.typeSpecifier, visitors, ancestors) + break case 'ExpressionStatement': visit(node.expression, visitors, ancestors) break @@ -69,7 +72,7 @@ export function visit(node: AST, visitors: Visitors, ancestors: AST[] = []): voi visit(node.body, visitors, ancestors) break case 'FunctionDeclaration': - visit(node.typeSpecifier, visitors, ancestors) + if (node.typeSpecifier) visit(node.typeSpecifier, visitors, ancestors) visit(node.id, visitors, ancestors) if (node.body) visit(node.body, visitors, ancestors) break @@ -81,7 +84,7 @@ export function visit(node: AST, visitors: Visitors, ancestors: AST[] = []): voi for (const declaration of node.declarations) visit(declaration, visitors, ancestors) break case 'VariableDeclarator': - visit(node.typeSpecifier, visitors, ancestors) + if (node.typeSpecifier) visit(node.typeSpecifier, visitors, ancestors) visit(node.id, visitors, ancestors) if (node.init) visit(node.init, visitors, ancestors) break diff --git a/tests/generator.test.ts b/tests/generator.test.ts index c3a53b1..9c630ee 100644 --- a/tests/generator.test.ts +++ b/tests/generator.test.ts @@ -248,6 +248,68 @@ const compute = /* glsl */ `#version 310 es } ` +// TODO: switch statement, pointers, loop/continuing, if statement optional parenthesis +// fn foo () -> @location(0) vec4 +// @compute @workgroup_size(16, 1, 1) +// var bigger_stride: array, 8>; +// block statements +// override expressions +const wgsl = /* wgsl */ ` + // single line + + /* + multiline + */ + + struct LightData { + intensity: f32, + position: vec3, + one: f32, + two: f32, + }; + + struct Uniforms { + projectionMatrix: mat4x4, + modelViewMatrix: mat4x4, + normalMatrix: mat3x3, + one: f32, + two: f32, + lights: array, + }; + @binding(0) @group(0) var uniforms: Uniforms; + + @binding(1) @group(0) var sample: sampler; + @binding(2) @group(0) var map: texture_2d; + + struct VertexIn { + @location(0) position: vec4, + @location(1) uv: vec2, + }; + + struct VertexOut { + @builtin(position) position: vec4, + @location(0) uv: vec2, + }; + + @vertex + fn vert_main(input: VertexIn) -> VertexOut { + var output: VertexOut; + output.position = input.position; + output.uv = input.uv; + return output; + } + + @fragment + fn frag_main(uv: vec2) -> vec4 { + var lightNormal = vec4(uniforms.lights[0].position * uniforms.lights[0].intensity, 0.0); + var clipPosition = uniforms.projectionMatrix * uniforms.modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0); + + var color = textureSample(map, sample, uv); + color.a += 1.0; + return color; + } +` + describe('generator', () => { it('can handle GLSL', () => { const program = parse(glsl) @@ -260,4 +322,10 @@ describe('generator', () => { const output = generate(program, { target: 'GLSL' }) expect(output).toBe(minify(compute)) }) + + it('can handle WGSL', () => { + const program = parse(wgsl) + const output = generate(program, { target: 'WGSL' }) + expect(output).toBe(minify(wgsl)) + }) }) diff --git a/tests/parser.test.ts b/tests/parser.test.ts index 918429f..fdfdc84 100644 --- a/tests/parser.test.ts +++ b/tests/parser.test.ts @@ -301,16 +301,23 @@ describe('parser', () => { declarations: [ { id: { - name: 'foo', - type: 'Identifier', + type: 'TypeSpecifier', + typeSpecifier: { + name: 'foo', + type: 'Identifier', + }, + layout: null, }, init: null, - layout: null, qualifiers: ['uniform'], type: 'VariableDeclarator', typeSpecifier: { - name: 'Type', - type: 'Identifier', + type: 'TypeSpecifier', + typeSpecifier: { + name: 'Type', + type: 'Identifier', + }, + layout: null, }, }, ], @@ -323,8 +330,12 @@ describe('parser', () => { declarations: [ { id: { - name: 'foo', - type: 'Identifier', + type: 'TypeSpecifier', + typeSpecifier: { + name: 'foo', + type: 'Identifier', + }, + layout: null, }, init: { arguments: [ @@ -351,12 +362,15 @@ describe('parser', () => { }, type: 'CallExpression', }, - layout: null, qualifiers: ['const'], type: 'VariableDeclarator', typeSpecifier: { - name: 'vec4', - type: 'Identifier', + type: 'TypeSpecifier', + typeSpecifier: { + name: 'vec4', + type: 'Identifier', + }, + layout: null, }, }, ], @@ -371,20 +385,27 @@ describe('parser', () => { declarations: [ { id: { - name: 'test', - type: 'Identifier', + type: 'TypeSpecifier', + typeSpecifier: { + name: 'test', + type: 'Identifier', + }, + layout: { + column_major: true, + component: '1', + location: '0', + }, }, init: null, - layout: { - column_major: true, - component: '1', - location: '0', - }, qualifiers: ['flat', 'in'], type: 'VariableDeclarator', typeSpecifier: { - name: 'mat4', - type: 'Identifier', + type: 'TypeSpecifier', + typeSpecifier: { + name: 'mat4', + type: 'Identifier', + }, + layout: null, }, }, ], @@ -397,25 +418,36 @@ describe('parser', () => { declarations: [ { id: { - name: 'foo', - type: 'Identifier', + type: 'TypeSpecifier', + typeSpecifier: { + name: 'foo', + type: 'Identifier', + }, + layout: null, }, init: { type: 'Literal', value: '0.0', }, - layout: null, qualifiers: [], type: 'VariableDeclarator', typeSpecifier: { - name: 'float', - type: 'Identifier', + type: 'TypeSpecifier', + typeSpecifier: { + name: 'float', + type: 'Identifier', + }, + layout: null, }, }, { id: { - name: 'bar', - type: 'Identifier', + type: 'TypeSpecifier', + typeSpecifier: { + name: 'bar', + type: 'Identifier', + }, + layout: null, }, init: { left: { @@ -429,35 +461,45 @@ describe('parser', () => { }, type: 'BinaryExpression', }, - layout: null, qualifiers: [], type: 'VariableDeclarator', typeSpecifier: { - name: 'float', - type: 'Identifier', + type: 'TypeSpecifier', + typeSpecifier: { + name: 'float', + type: 'Identifier', + }, + layout: null, }, }, { id: { - type: 'ArraySpecifier', + type: 'TypeSpecifier', typeSpecifier: { - type: 'Identifier', - name: 'baz', - }, - dimensions: [ - { - type: 'Literal', - value: '3', + type: 'ArraySpecifier', + typeSpecifier: { + type: 'Identifier', + name: 'baz', }, - ], - } satisfies ArraySpecifier as unknown as Identifier, // TODO: revisit VariableDeclarator AST + dimensions: [ + { + type: 'Literal', + value: '3', + }, + ], + }, + layout: null, + }, init: null, - layout: null, qualifiers: [], type: 'VariableDeclarator', typeSpecifier: { - name: 'float', - type: 'Identifier', + type: 'TypeSpecifier', + typeSpecifier: { + name: 'float', + type: 'Identifier', + }, + layout: null, }, }, ], @@ -478,19 +520,26 @@ describe('parser', () => { declarations: [ { id: { - name: 'bar', - type: 'Identifier', + type: 'TypeSpecifier', + typeSpecifier: { + name: 'bar', + type: 'Identifier', + }, + layout: null, }, init: { type: 'Literal', value: 'true', }, - layout: null, qualifiers: ['const'], type: 'VariableDeclarator', typeSpecifier: { - name: 'bool', - type: 'Identifier', + type: 'TypeSpecifier', + typeSpecifier: { + name: 'bool', + type: 'Identifier', + }, + layout: null, }, }, ], @@ -515,15 +564,22 @@ describe('parser', () => { declarations: [ { type: 'VariableDeclarator', - layout: null, qualifiers: [], id: { - type: 'Identifier', - name: 'b', + type: 'TypeSpecifier', + typeSpecifier: { + type: 'Identifier', + name: 'b', + }, + layout: null, }, typeSpecifier: { - type: 'Identifier', - name: 'a', + type: 'TypeSpecifier', + typeSpecifier: { + type: 'Identifier', + name: 'a', + }, + layout: null, }, init: null, }, @@ -537,15 +593,23 @@ describe('parser', () => { { body: null, id: { - name: 'main', - type: 'Identifier', + type: 'TypeSpecifier', + typeSpecifier: { + name: 'main', + type: 'Identifier', + }, + layout: null, }, params: [], qualifiers: [], type: 'FunctionDeclaration', typeSpecifier: { - name: 'void', - type: 'Identifier', + type: 'TypeSpecifier', + typeSpecifier: { + name: 'void', + type: 'Identifier', + }, + layout: null, }, }, ]) @@ -557,40 +621,64 @@ describe('parser', () => { type: 'BlockStatement', }, id: { - name: 'main', - type: 'Identifier', + type: 'TypeSpecifier', + typeSpecifier: { + name: 'main', + type: 'Identifier', + }, + layout: null, }, params: [ { id: { - name: 'enabled', - type: 'Identifier', + type: 'TypeSpecifier', + typeSpecifier: { + name: 'enabled', + type: 'Identifier', + }, + layout: null, }, qualifiers: ['const'], type: 'FunctionParameter', typeSpecifier: { - name: 'bool', - type: 'Identifier', + type: 'TypeSpecifier', + typeSpecifier: { + name: 'bool', + type: 'Identifier', + }, + layout: null, }, }, { id: { - name: 'disabled', - type: 'Identifier', + type: 'TypeSpecifier', + typeSpecifier: { + name: 'disabled', + type: 'Identifier', + }, + layout: null, }, qualifiers: [], type: 'FunctionParameter', typeSpecifier: { - name: 'bool', - type: 'Identifier', + type: 'TypeSpecifier', + typeSpecifier: { + name: 'bool', + type: 'Identifier', + }, + layout: null, }, }, ], qualifiers: ['highp'], type: 'FunctionDeclaration', typeSpecifier: { - name: 'vec4', - type: 'Identifier', + type: 'TypeSpecifier', + typeSpecifier: { + name: 'vec4', + type: 'Identifier', + }, + layout: null, }, }, ]) @@ -696,19 +784,26 @@ describe('parser', () => { declarations: [ { id: { - name: 'i', - type: 'Identifier', + type: 'TypeSpecifier', + typeSpecifier: { + name: 'i', + type: 'Identifier', + }, + layout: null, }, init: { type: 'Literal', value: '0', }, - layout: null, qualifiers: [], type: 'VariableDeclarator', typeSpecifier: { - name: 'int', - type: 'Identifier', + type: 'TypeSpecifier', + typeSpecifier: { + name: 'int', + type: 'Identifier', + }, + layout: null, }, }, ],