You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

942 lines
17 KiB

import { Program, FunctionDeclaration, For, AccessorElements, Ternary, Varying, DynamicElement, StaticElement, FunctionParameter, Unary, Conditional, VariableDeclaration, Operator, Number, String, FunctionCall, Return, Accessor, Uniform } from './AST.js';
const unaryOperators = [
'+', '-', '~', '!', '++', '--'
];
const precedenceOperators = [
'*', '/', '%',
'-', '+',
'<<', '>>',
'<', '>', '<=', '>=',
'==', '!=',
'&',
'^',
'|',
'&&',
'^^',
'||',
'?',
'=',
'+=', '-=', '*=', '/=', '%=', '^=', '&=', '|=', '<<=', '>>=',
','
].reverse();
const spaceRegExp = /^((\t| )\n*)+/;
const lineRegExp = /^\n+/;
const commentRegExp = /^\/\*[\s\S]*?\*\//;
const inlineCommentRegExp = /^\/\/.*?(\n|$)/;
const numberRegExp = /^((0x\w+)|(\.?\d+\.?\d*((e-?\d+)|\w)?))/;
const stringDoubleRegExp = /^(\"((?:[^"\\]|\\.)*)\")/;
const stringSingleRegExp = /^(\'((?:[^'\\]|\\.)*)\')/;
const literalRegExp = /^[A-Za-z](\w|\.)*/;
const operatorsRegExp = new RegExp( '^(\\' + [
'<<=', '>>=', '++', '--', '<<', '>>', '+=', '-=', '*=', '/=', '%=', '&=', '^^', '^=', '|=',
'<=', '>=', '==', '!=', '&&', '||',
'(', ')', '[', ']', '{', '}',
'.', ',', ';', '!', '=', '~', '*', '/', '%', '+', '-', '<', '>', '&', '^', '|', '?', ':', '#'
].join( '$' ).split( '' ).join( '\\' ).replace( /\\\$/g, '|' ) + ')' );
function getGroupDelta( str ) {
if ( str === '(' || str === '[' || str === '{' ) return 1;
if ( str === ')' || str === ']' || str === '}' ) return - 1;
return 0;
}
class Token {
constructor( tokenizer, type, str, pos ) {
this.tokenizer = tokenizer;
this.type = type;
this.str = str;
this.pos = pos;
this.tag = null;
}
get endPos() {
return this.pos + this.str.length;
}
get isNumber() {
return this.type === Token.NUMBER;
}
get isString() {
return this.type === Token.STRING;
}
get isLiteral() {
return this.type === Token.LITERAL;
}
get isOperator() {
return this.type === Token.OPERATOR;
}
}
Token.LINE = 'line';
Token.COMMENT = 'comment';
Token.NUMBER = 'number';
Token.STRING = 'string';
Token.LITERAL = 'literal';
Token.OPERATOR = 'operator';
const TokenParserList = [
{ type: Token.LINE, regexp: lineRegExp, isTag: true },
{ type: Token.COMMENT, regexp: commentRegExp, isTag: true },
{ type: Token.COMMENT, regexp: inlineCommentRegExp, isTag: true },
{ type: Token.NUMBER, regexp: numberRegExp },
{ type: Token.STRING, regexp: stringDoubleRegExp, group: 2 },
{ type: Token.STRING, regexp: stringSingleRegExp, group: 2 },
{ type: Token.LITERAL, regexp: literalRegExp },
{ type: Token.OPERATOR, regexp: operatorsRegExp }
];
class Tokenizer {
constructor( source ) {
this.source = source;
this.position = 0;
this.tokens = [];
}
tokenize() {
let token = this.readToken();
while ( token ) {
this.tokens.push( token );
token = this.readToken();
}
return this;
}
skip( ...params ) {
let remainingCode = this.source.substr( this.position );
let i = params.length;
while ( i -- ) {
const skip = params[ i ].exec( remainingCode );
const skipLength = skip ? skip[ 0 ].length : 0;
if ( skipLength > 0 ) {
this.position += skipLength;
remainingCode = this.source.substr( this.position );
// re-skip, new remainingCode is generated
// maybe exist previous regexp non detected
i = params.length;
}
}
return remainingCode;
}
readToken() {
const remainingCode = this.skip( spaceRegExp );
for ( var i = 0; i < TokenParserList.length; i ++ ) {
const parser = TokenParserList[ i ];
const result = parser.regexp.exec( remainingCode );
if ( result ) {
const token = new Token( this, parser.type, result[ parser.group || 0 ], this.position );
this.position += result[ 0 ].length;
if ( parser.isTag ) {
const nextToken = this.readToken();
if ( nextToken ) {
nextToken.tag = token;
}
return nextToken;
}
return token;
}
}
}
}
const isType = ( str ) => /void|bool|float|u?int|(u|i)?vec[234]/.test( str );
class GLSLDecoder {
constructor() {
this.index = 0;
this.tokenizer = null;
this.keywords = [];
this._currentFunction = null;
this.addPolyfill( 'gl_FragCoord', 'vec3 gl_FragCoord = vec3( viewportCoordinate.x, viewportCoordinate.y.oneMinus(), viewportCoordinate.z );' );
}
addPolyfill( name, polyfill ) {
this.keywords.push( { name, polyfill } );
return this;
}
get tokens() {
return this.tokenizer.tokens;
}
readToken() {
return this.tokens[ this.index ++ ];
}
getToken( offset = 0 ) {
return this.tokens[ this.index + offset ];
}
getTokensUntil( str, tokens, offset = 0 ) {
const output = [];
let groupIndex = 0;
for ( let i = offset; i < tokens.length; i ++ ) {
const token = tokens[ i ];
groupIndex += getGroupDelta( token.str );
output.push( token );
if ( groupIndex === 0 && token.str === str ) {
break;
}
}
return output;
}
readTokensUntil( str ) {
const tokens = this.getTokensUntil( str, this.tokens, this.index );
this.index += tokens.length;
return tokens;
}
parseExpressionFromTokens( tokens ) {
if ( tokens.length === 0 ) return null;
const firstToken = tokens[ 0 ];
const lastToken = tokens[ tokens.length - 1 ];
// precedence operators
let groupIndex = 0;
for ( const operator of precedenceOperators ) {
for ( let i = 0; i < tokens.length; i ++ ) {
const token = tokens[ i ];
groupIndex += getGroupDelta( token.str );
if ( ! token.isOperator || i === 0 || i === tokens.length - 1 ) continue;
if ( groupIndex === 0 && token.str === operator ) {
if ( operator === '?' ) {
const conditionTokens = tokens.slice( 0, i );
const leftTokens = this.getTokensUntil( ':', tokens, i + 1 ).slice( 0, - 1 );
const rightTokens = tokens.slice( i + leftTokens.length + 2 );
const condition = this.parseExpressionFromTokens( conditionTokens );
const left = this.parseExpressionFromTokens( leftTokens );
const right = this.parseExpressionFromTokens( rightTokens );
return new Ternary( condition, left, right );
} else {
const left = this.parseExpressionFromTokens( tokens.slice( 0, i ) );
const right = this.parseExpressionFromTokens( tokens.slice( i + 1, tokens.length ) );
return this._evalOperator( new Operator( operator, left, right ) );
}
}
if ( groupIndex < 0 ) {
return this.parseExpressionFromTokens( tokens.slice( 0, i ) );
}
}
}
// unary operators (before)
if ( firstToken.isOperator ) {
for ( const operator of unaryOperators ) {
if ( firstToken.str === operator ) {
const right = this.parseExpressionFromTokens( tokens.slice( 1 ) );
return new Unary( operator, right );
}
}
}
// unary operators (after)
if ( lastToken.isOperator ) {
for ( const operator of unaryOperators ) {
if ( lastToken.str === operator ) {
const left = this.parseExpressionFromTokens( tokens.slice( 0, tokens.length - 1 ) );
return new Unary( operator, left, true );
}
}
}
// groups
if ( firstToken.str === '(' ) {
const leftTokens = this.getTokensUntil( ')', tokens );
const left = this.parseExpressionFromTokens( leftTokens.slice( 1, leftTokens.length - 1 ) );
const operator = tokens[ leftTokens.length ];
if ( operator ) {
const rightTokens = tokens.slice( leftTokens.length + 1 );
const right = this.parseExpressionFromTokens( rightTokens );
return this._evalOperator( new Operator( operator.str, left, right ) );
}
return left;
}
// primitives and accessors
if ( firstToken.isNumber ) {
let type;
const isHex = /^(0x)/.test( firstToken.str );
if ( isHex ) type = 'int';
else if ( /u$/.test( firstToken.str ) ) type = 'uint';
else if ( /f|e|\./.test( firstToken.str ) ) type = 'float';
else type = 'int';
let str = firstToken.str.replace( /u|i$/, '' );
if ( isHex === false ) {
str = str.replace( /f$/, '' );
}
return new Number( str, type );
} else if ( firstToken.isString ) {
return new String( firstToken.str );
} else if ( firstToken.isLiteral ) {
if ( firstToken.str === 'return' ) {
return new Return( this.parseExpressionFromTokens( tokens.slice( 1 ) ) );
}
const secondToken = tokens[ 1 ];
if ( secondToken ) {
if ( secondToken.str === '(' ) {
// function call
const paramsTokens = this.parseFunctionParametersFromTokens( tokens.slice( 2, tokens.length - 1 ) );
return new FunctionCall( firstToken.str, paramsTokens );
} else if ( secondToken.str === '[' ) {
// array accessor
const elements = [];
let currentTokens = tokens.slice( 1 );
while ( currentTokens.length > 0 ) {
const token = currentTokens[ 0 ];
if ( token.str === '[' ) {
const accessorTokens = this.getTokensUntil( ']', currentTokens );
const element = this.parseExpressionFromTokens( accessorTokens.slice( 1, accessorTokens.length - 1 ) );
currentTokens = currentTokens.slice( accessorTokens.length );
elements.push( new DynamicElement( element ) );
} else if ( token.str === '.' ) {
const accessorTokens = currentTokens.slice( 1, 2 );
const element = this.parseExpressionFromTokens( accessorTokens );
currentTokens = currentTokens.slice( 2 );
elements.push( new StaticElement( element ) );
} else {
console.error( 'Unknown accessor expression', token );
break;
}
}
return new AccessorElements( firstToken.str, elements );
}
}
return new Accessor( firstToken.str );
}
}
parseFunctionParametersFromTokens( tokens ) {
if ( tokens.length === 0 ) return [];
const expression = this.parseExpressionFromTokens( tokens );
const params = [];
let current = expression;
while ( current.type === ',' ) {
params.push( current.left );
current = current.right;
}
params.push( current );
return params;
}
parseExpression() {
const tokens = this.readTokensUntil( ';' );
const exp = this.parseExpressionFromTokens( tokens.slice( 0, tokens.length - 1 ) );
return exp;
}
parseFunctionParams( tokens ) {
const params = [];
for ( let i = 0; i < tokens.length; i ++ ) {
const immutable = tokens[ i ].str === 'const';
if ( immutable ) i ++;
let qualifier = tokens[ i ].str;
if ( /^(in|out|inout)$/.test( qualifier ) ) {
i ++;
} else {
qualifier = null;
}
const type = tokens[ i ++ ].str;
const name = tokens[ i ++ ].str;
params.push( new FunctionParameter( type, name, qualifier, immutable ) );
if ( tokens[ i ] && tokens[ i ].str !== ',' ) throw new Error( 'Expected ","' );
}
return params;
}
parseFunction() {
const type = this.readToken().str;
const name = this.readToken().str;
const paramsTokens = this.readTokensUntil( ')' );
const params = this.parseFunctionParams( paramsTokens.slice( 1, paramsTokens.length - 1 ) );
const func = new FunctionDeclaration( type, name, params );
this._currentFunction = func;
this.parseBlock( func );
this._currentFunction = null;
return func;
}
parseVariablesFromToken( tokens, type ) {
let index = 0;
const immutable = tokens[ 0 ].str === 'const';
if ( immutable ) index ++;
type = type || tokens[ index ++ ].str;
const name = tokens[ index ++ ].str;
const token = tokens[ index ];
let init = null;
let next = null;
if ( token ) {
const initTokens = this.getTokensUntil( ',', tokens, index );
if ( initTokens[ 0 ].str === '=' ) {
const expressionTokens = initTokens.slice( 1 );
if ( expressionTokens[ expressionTokens.length - 1 ].str === ',' ) expressionTokens.pop();
init = this.parseExpressionFromTokens( expressionTokens );
}
const nextTokens = tokens.slice( initTokens.length + ( index - 1 ) );
if ( nextTokens[ 0 ] && nextTokens[ 0 ].str === ',' ) {
next = this.parseVariablesFromToken( nextTokens.slice( 1 ), type );
}
}
const variable = new VariableDeclaration( type, name, init, next, immutable );
return variable;
}
parseVariables() {
const tokens = this.readTokensUntil( ';' );
return this.parseVariablesFromToken( tokens.slice( 0, tokens.length - 1 ) );
}
parseUniform() {
const tokens = this.readTokensUntil( ';' );
const type = tokens[ 1 ].str;
const name = tokens[ 2 ].str;
return new Uniform( type, name );
}
parseVarying() {
const tokens = this.readTokensUntil( ';' );
const type = tokens[ 1 ].str;
const name = tokens[ 2 ].str;
return new Varying( type, name );
}
parseReturn() {
this.readToken(); // skip 'return'
const expression = this.parseExpression();
return new Return( expression );
}
parseFor() {
this.readToken(); // skip 'for'
const forTokens = this.readTokensUntil( ')' ).slice( 1, - 1 );
const initializationTokens = this.getTokensUntil( ';', forTokens, 0 ).slice( 0, - 1 );
const conditionTokens = this.getTokensUntil( ';', forTokens, initializationTokens.length + 1 ).slice( 0, - 1 );
const afterthoughtTokens = forTokens.slice( initializationTokens.length + conditionTokens.length + 2 );
let initialization;
if ( initializationTokens[ 0 ] && isType( initializationTokens[ 0 ].str ) ) {
initialization = this.parseVariablesFromToken( initializationTokens );
} else {
initialization = this.parseExpressionFromTokens( initializationTokens );
}
const condition = this.parseExpressionFromTokens( conditionTokens );
const afterthought = this.parseExpressionFromTokens( afterthoughtTokens );
const statement = new For( initialization, condition, afterthought );
if ( this.getToken().str === '{' ) {
this.parseBlock( statement );
} else {
statement.body.push( this.parseExpression() );
}
return statement;
}
parseIf() {
const parseIfExpression = () => {
this.readToken(); // skip 'if'
const condTokens = this.readTokensUntil( ')' );
return this.parseExpressionFromTokens( condTokens.slice( 1, condTokens.length - 1 ) );
};
const parseIfBlock = ( cond ) => {
if ( this.getToken().str === '{' ) {
this.parseBlock( cond );
} else {
cond.body.push( this.parseExpression() );
}
};
//
const conditional = new Conditional( parseIfExpression() );
parseIfBlock( conditional );
//
let current = conditional;
while ( this.getToken() && this.getToken().str === 'else' ) {
this.readToken(); // skip 'else'
const previous = current;
if ( this.getToken().str === 'if' ) {
current = new Conditional( parseIfExpression() );
} else {
current = new Conditional();
}
previous.elseConditional = current;
parseIfBlock( current );
}
return conditional;
}
parseBlock( scope ) {
const firstToken = this.getToken();
if ( firstToken.str === '{' ) {
this.readToken(); // skip '{'
}
let groupIndex = 0;
while ( this.index < this.tokens.length ) {
const token = this.getToken();
let statement = null;
groupIndex += getGroupDelta( token.str );
if ( groupIndex < 0 ) {
this.readToken(); // skip '}'
break;
}
//
if ( token.isLiteral ) {
if ( token.str === 'const' ) {
statement = this.parseVariables();
} else if ( token.str === 'uniform' ) {
statement = this.parseUniform();
} else if ( token.str === 'varying' ) {
statement = this.parseVarying();
} else if ( isType( token.str ) ) {
if ( this.getToken( 2 ).str === '(' ) {
statement = this.parseFunction();
} else {
statement = this.parseVariables();
}
} else if ( token.str === 'return' ) {
statement = this.parseReturn();
} else if ( token.str === 'if' ) {
statement = this.parseIf();
} else if ( token.str === 'for' ) {
statement = this.parseFor();
} else {
statement = this.parseExpression();
}
}
if ( statement ) {
scope.body.push( statement );
} else {
this.index ++;
}
}
}
_evalOperator( operator ) {
if ( operator.type.includes( '=' ) ) {
const parameter = this._getFunctionParameter( operator.left.property );
if ( parameter !== undefined ) {
// Parameters are immutable in WGSL
parameter.immutable = false;
}
}
return operator;
}
_getFunctionParameter( name ) {
if ( this._currentFunction ) {
for ( const param of this._currentFunction.params ) {
if ( param.name === name ) {
return param;
}
}
}
}
parse( source ) {
let polyfill = '';
for ( const keyword of this.keywords ) {
if ( new RegExp( `(^|\\b)${ keyword.name }($|\\b)`, 'gm' ).test( source ) ) {
polyfill += keyword.polyfill + '\n';
}
}
if ( polyfill ) {
polyfill = '// Polyfills\n\n' + polyfill + '\n';
}
this.index = 0;
this.tokenizer = new Tokenizer( polyfill + source ).tokenize();
const program = new Program();
this.parseBlock( program );
return program;
}
}
export default GLSLDecoder;