UNPKG

gpu.js

Version:

GPU Accelerated JavaScript

1,520 lines (1,437 loc) 52 kB
const { utils } = require('../../utils'); const { FunctionNode } = require('../function-node'); /** * @desc [INTERNAL] Takes in a function node, and does all the AST voodoo required to toString its respective WebGL code */ class WebGLFunctionNode extends FunctionNode { constructor(source, settings) { super(source, settings); if (settings && settings.hasOwnProperty('fixIntegerDivisionAccuracy')) { this.fixIntegerDivisionAccuracy = settings.fixIntegerDivisionAccuracy; } } astConditionalExpression(ast, retArr) { if (ast.type !== 'ConditionalExpression') { throw this.astErrorOutput('Not a conditional expression', ast); } const consequentType = this.getType(ast.consequent); const alternateType = this.getType(ast.alternate); // minification handling if void if (consequentType === null && alternateType === null) { retArr.push('if ('); this.astGeneric(ast.test, retArr); retArr.push(') {'); this.astGeneric(ast.consequent, retArr); retArr.push(';'); retArr.push('} else {'); this.astGeneric(ast.alternate, retArr); retArr.push(';'); retArr.push('}'); return retArr; } retArr.push('('); this.astGeneric(ast.test, retArr); retArr.push('?'); this.astGeneric(ast.consequent, retArr); retArr.push(':'); this.astGeneric(ast.alternate, retArr); retArr.push(')'); return retArr; } /** * @desc Parses the abstract syntax tree for to its *named function* * @param {Object} ast - the AST object to parse * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astFunction(ast, retArr) { // Setup function return type and name if (this.isRootKernel) { retArr.push('void'); } else { // looking up return type, this is a little expensive, and can be avoided if returnType is set if (!this.returnType) { const lastReturn = this.findLastReturn(); if (lastReturn) { this.returnType = this.getType(ast.body); if (this.returnType === 'LiteralInteger') { this.returnType = 'Number'; } } } const { returnType } = this; if (!returnType) { retArr.push('void'); } else { const type = typeMap[returnType]; if (!type) { throw new Error(`unknown type ${returnType}`); } retArr.push(type); } } retArr.push(' '); retArr.push(this.name); retArr.push('('); if (!this.isRootKernel) { // Arguments handling for (let i = 0; i < this.argumentNames.length; ++i) { const argumentName = this.argumentNames[i]; if (i > 0) { retArr.push(', '); } let argumentType = this.argumentTypes[this.argumentNames.indexOf(argumentName)]; // The type is too loose ended, here we decide to solidify a type, lets go with float if (!argumentType) { throw this.astErrorOutput(`Unknown argument ${argumentName} type`, ast); } if (argumentType === 'LiteralInteger') { this.argumentTypes[i] = argumentType = 'Number'; } const type = typeMap[argumentType]; if (!type) { throw this.astErrorOutput('Unexpected expression', ast); } const name = utils.sanitizeName(argumentName); if (type === 'sampler2D' || type === 'sampler2DArray') { // mash needed arguments together, since now we have end to end inference retArr.push(`${type} user_${name},ivec2 user_${name}Size,ivec3 user_${name}Dim`); } else { retArr.push(`${type} user_${name}`); } } } // Function opening retArr.push(') {\n'); // Body statement iteration for (let i = 0; i < ast.body.body.length; ++i) { this.astGeneric(ast.body.body[i], retArr); retArr.push('\n'); } // Function closing retArr.push('}\n'); return retArr; } /** * @desc Parses the abstract syntax tree for to *return* statement * @param {Object} ast - the AST object to parse * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astReturnStatement(ast, retArr) { if (!ast.argument) throw this.astErrorOutput('Unexpected return statement', ast); this.pushState('skip-literal-correction'); const type = this.getType(ast.argument); this.popState('skip-literal-correction'); const result = []; if (!this.returnType) { if (type === 'LiteralInteger' || type === 'Integer') { this.returnType = 'Number'; } else { this.returnType = type; } } switch (this.returnType) { case 'LiteralInteger': case 'Number': case 'Float': switch (type) { case 'Integer': result.push('float('); this.astGeneric(ast.argument, result); result.push(')'); break; case 'LiteralInteger': this.castLiteralToFloat(ast.argument, result); // Running astGeneric forces the LiteralInteger to pick a type, and here, if we are returning a float, yet // the LiteralInteger has picked to be an integer because of constraints on it we cast it to float. if (this.getType(ast) === 'Integer') { result.unshift('float('); result.push(')'); } break; default: this.astGeneric(ast.argument, result); } break; case 'Integer': switch (type) { case 'Float': case 'Number': this.castValueToInteger(ast.argument, result); break; case 'LiteralInteger': this.castLiteralToInteger(ast.argument, result); break; default: this.astGeneric(ast.argument, result); } break; case 'Array(4)': case 'Array(3)': case 'Array(2)': case 'Matrix(2)': case 'Matrix(3)': case 'Matrix(4)': case 'Input': this.astGeneric(ast.argument, result); break; default: throw this.astErrorOutput(`unhandled return type ${this.returnType}`, ast); } if (this.isRootKernel) { retArr.push(`kernelResult = ${ result.join('') };`); retArr.push('return;'); } else if (this.isSubKernel) { retArr.push(`subKernelResult_${ this.name } = ${ result.join('') };`); retArr.push(`return subKernelResult_${ this.name };`); } else { retArr.push(`return ${ result.join('') };`); } return retArr; } /** * @desc Parses the abstract syntax tree for *literal value* * * @param {Object} ast - the AST object to parse * @param {Array} retArr - return array string * * @returns {Array} the append retArr */ astLiteral(ast, retArr) { // Reject non numeric literals if (isNaN(ast.value)) { throw this.astErrorOutput( 'Non-numeric literal not supported : ' + ast.value, ast ); } const key = this.astKey(ast); if (Number.isInteger(ast.value)) { if (this.isState('casting-to-integer') || this.isState('building-integer')) { this.literalTypes[key] = 'Integer'; retArr.push(`${ast.value}`); } else if (this.isState('casting-to-float') || this.isState('building-float')) { this.literalTypes[key] = 'Number'; retArr.push(`${ast.value}.0`); } else { this.literalTypes[key] = 'Number'; retArr.push(`${ast.value}.0`); } } else if (this.isState('casting-to-integer') || this.isState('building-integer')) { this.literalTypes[key] = 'Integer'; retArr.push(Math.round(ast.value)); } else { this.literalTypes[key] = 'Number'; retArr.push(`${ast.value}`); } return retArr; } /** * @desc Parses the abstract syntax tree for *binary* expression * @param {Object} ast - the AST object to parse * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astBinaryExpression(ast, retArr) { if (this.checkAndUpconvertOperator(ast, retArr)) { return retArr; } if (this.fixIntegerDivisionAccuracy && ast.operator === '/') { retArr.push('divWithIntCheck('); this.pushState('building-float'); switch (this.getType(ast.left)) { case 'Integer': this.castValueToFloat(ast.left, retArr); break; case 'LiteralInteger': this.castLiteralToFloat(ast.left, retArr); break; default: this.astGeneric(ast.left, retArr); } retArr.push(', '); switch (this.getType(ast.right)) { case 'Integer': this.castValueToFloat(ast.right, retArr); break; case 'LiteralInteger': this.castLiteralToFloat(ast.right, retArr); break; default: this.astGeneric(ast.right, retArr); } this.popState('building-float'); retArr.push(')'); return retArr; } retArr.push('('); const leftType = this.getType(ast.left) || 'Number'; const rightType = this.getType(ast.right) || 'Number'; if (!leftType || !rightType) { throw this.astErrorOutput(`Unhandled binary expression`, ast); } const key = leftType + ' & ' + rightType; switch (key) { case 'Integer & Integer': this.pushState('building-integer'); this.astGeneric(ast.left, retArr); retArr.push(operatorMap[ast.operator] || ast.operator); this.astGeneric(ast.right, retArr); this.popState('building-integer'); break; case 'Number & Float': case 'Float & Number': case 'Float & Float': case 'Number & Number': this.pushState('building-float'); this.astGeneric(ast.left, retArr); retArr.push(operatorMap[ast.operator] || ast.operator); this.astGeneric(ast.right, retArr); this.popState('building-float'); break; case 'LiteralInteger & LiteralInteger': if (this.isState('casting-to-integer') || this.isState('building-integer')) { this.pushState('building-integer'); this.astGeneric(ast.left, retArr); retArr.push(operatorMap[ast.operator] || ast.operator); this.astGeneric(ast.right, retArr); this.popState('building-integer'); } else { this.pushState('building-float'); this.castLiteralToFloat(ast.left, retArr); retArr.push(operatorMap[ast.operator] || ast.operator); this.castLiteralToFloat(ast.right, retArr); this.popState('building-float'); } break; case 'Integer & Float': case 'Integer & Number': if (ast.operator === '>' || ast.operator === '<' && ast.right.type === 'Literal') { // if right value is actually a float, don't loose that information, cast left to right rather than the usual right to left if (!Number.isInteger(ast.right.value)) { this.pushState('building-float'); this.castValueToFloat(ast.left, retArr); retArr.push(operatorMap[ast.operator] || ast.operator); this.astGeneric(ast.right, retArr); this.popState('building-float'); break; } } this.pushState('building-integer'); this.astGeneric(ast.left, retArr); retArr.push(operatorMap[ast.operator] || ast.operator); this.pushState('casting-to-integer'); if (ast.right.type === 'Literal') { const literalResult = []; this.astGeneric(ast.right, literalResult); const literalType = this.getType(ast.right); if (literalType === 'Integer') { retArr.push(literalResult.join('')); } else { throw this.astErrorOutput(`Unhandled binary expression with literal`, ast); } } else { retArr.push('int('); this.astGeneric(ast.right, retArr); retArr.push(')'); } this.popState('casting-to-integer'); this.popState('building-integer'); break; case 'Integer & LiteralInteger': this.pushState('building-integer'); this.astGeneric(ast.left, retArr); retArr.push(operatorMap[ast.operator] || ast.operator); this.castLiteralToInteger(ast.right, retArr); this.popState('building-integer'); break; case 'Number & Integer': this.pushState('building-float'); this.astGeneric(ast.left, retArr); retArr.push(operatorMap[ast.operator] || ast.operator); this.castValueToFloat(ast.right, retArr); this.popState('building-float'); break; case 'Float & LiteralInteger': case 'Number & LiteralInteger': this.pushState('building-float'); this.astGeneric(ast.left, retArr); retArr.push(operatorMap[ast.operator] || ast.operator); this.castLiteralToFloat(ast.right, retArr); this.popState('building-float'); break; case 'LiteralInteger & Float': case 'LiteralInteger & Number': if (this.isState('casting-to-integer')) { this.pushState('building-integer'); this.castLiteralToInteger(ast.left, retArr); retArr.push(operatorMap[ast.operator] || ast.operator); this.castValueToInteger(ast.right, retArr); this.popState('building-integer'); } else { this.pushState('building-float'); this.astGeneric(ast.left, retArr); retArr.push(operatorMap[ast.operator] || ast.operator); this.pushState('casting-to-float'); this.astGeneric(ast.right, retArr); this.popState('casting-to-float'); this.popState('building-float'); } break; case 'LiteralInteger & Integer': this.pushState('building-integer'); this.castLiteralToInteger(ast.left, retArr); retArr.push(operatorMap[ast.operator] || ast.operator); this.astGeneric(ast.right, retArr); this.popState('building-integer'); break; case 'Boolean & Boolean': this.pushState('building-boolean'); this.astGeneric(ast.left, retArr); retArr.push(operatorMap[ast.operator] || ast.operator); this.astGeneric(ast.right, retArr); this.popState('building-boolean'); break; case 'Float & Integer': this.pushState('building-float'); this.astGeneric(ast.left, retArr); retArr.push(operatorMap[ast.operator] || ast.operator); this.castValueToFloat(ast.right, retArr); this.popState('building-float'); break; default: throw this.astErrorOutput(`Unhandled binary expression between ${key}`, ast); } retArr.push(')'); return retArr; } checkAndUpconvertOperator(ast, retArr) { const bitwiseResult = this.checkAndUpconvertBitwiseOperators(ast, retArr); if (bitwiseResult) { return bitwiseResult; } const upconvertableOperators = { '%': this.fixIntegerDivisionAccuracy ? 'integerCorrectionModulo' : 'modulo', '**': 'pow', }; const foundOperator = upconvertableOperators[ast.operator]; if (!foundOperator) return null; retArr.push(foundOperator); retArr.push('('); switch (this.getType(ast.left)) { case 'Integer': this.castValueToFloat(ast.left, retArr); break; case 'LiteralInteger': this.castLiteralToFloat(ast.left, retArr); break; default: this.astGeneric(ast.left, retArr); } retArr.push(','); switch (this.getType(ast.right)) { case 'Integer': this.castValueToFloat(ast.right, retArr); break; case 'LiteralInteger': this.castLiteralToFloat(ast.right, retArr); break; default: this.astGeneric(ast.right, retArr); } retArr.push(')'); return retArr; } checkAndUpconvertBitwiseOperators(ast, retArr) { const upconvertableOperators = { '&': 'bitwiseAnd', '|': 'bitwiseOr', '^': 'bitwiseXOR', '<<': 'bitwiseZeroFillLeftShift', '>>': 'bitwiseSignedRightShift', '>>>': 'bitwiseZeroFillRightShift', }; const foundOperator = upconvertableOperators[ast.operator]; if (!foundOperator) return null; retArr.push(foundOperator); retArr.push('('); const leftType = this.getType(ast.left); switch (leftType) { case 'Number': case 'Float': this.castValueToInteger(ast.left, retArr); break; case 'LiteralInteger': this.castLiteralToInteger(ast.left, retArr); break; default: this.astGeneric(ast.left, retArr); } retArr.push(','); const rightType = this.getType(ast.right); switch (rightType) { case 'Number': case 'Float': this.castValueToInteger(ast.right, retArr); break; case 'LiteralInteger': this.castLiteralToInteger(ast.right, retArr); break; default: this.astGeneric(ast.right, retArr); } retArr.push(')'); return retArr; } checkAndUpconvertBitwiseUnary(ast, retArr) { const upconvertableOperators = { '~': 'bitwiseNot', }; const foundOperator = upconvertableOperators[ast.operator]; if (!foundOperator) return null; retArr.push(foundOperator); retArr.push('('); switch (this.getType(ast.argument)) { case 'Number': case 'Float': this.castValueToInteger(ast.argument, retArr); break; case 'LiteralInteger': this.castLiteralToInteger(ast.argument, retArr); break; default: this.astGeneric(ast.argument, retArr); } retArr.push(')'); return retArr; } /** * * @param {Object} ast * @param {Array} retArr * @return {String[]} */ castLiteralToInteger(ast, retArr) { this.pushState('casting-to-integer'); this.astGeneric(ast, retArr); this.popState('casting-to-integer'); return retArr; } /** * * @param {Object} ast * @param {Array} retArr * @return {String[]} */ castLiteralToFloat(ast, retArr) { this.pushState('casting-to-float'); this.astGeneric(ast, retArr); this.popState('casting-to-float'); return retArr; } /** * * @param {Object} ast * @param {Array} retArr * @return {String[]} */ castValueToInteger(ast, retArr) { this.pushState('casting-to-integer'); retArr.push('int('); this.astGeneric(ast, retArr); retArr.push(')'); this.popState('casting-to-integer'); return retArr; } /** * * @param {Object} ast * @param {Array} retArr * @return {String[]} */ castValueToFloat(ast, retArr) { this.pushState('casting-to-float'); retArr.push('float('); this.astGeneric(ast, retArr); retArr.push(')'); this.popState('casting-to-float'); return retArr; } /** * @desc Parses the abstract syntax tree for *identifier* expression * @param {Object} idtNode - An ast Node * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astIdentifierExpression(idtNode, retArr) { if (idtNode.type !== 'Identifier') { throw this.astErrorOutput('IdentifierExpression - not an Identifier', idtNode); } const type = this.getType(idtNode); const name = utils.sanitizeName(idtNode.name); if (idtNode.name === 'Infinity') { // https://stackoverflow.com/a/47543127/1324039 retArr.push('3.402823466e+38'); } else if (type === 'Boolean') { if (this.argumentNames.indexOf(name) > -1) { retArr.push(`bool(user_${name})`); } else { retArr.push(`user_${name}`); } } else { retArr.push(`user_${name}`); } return retArr; } /** * @desc Parses the abstract syntax tree for *for-loop* expression * @param {Object} forNode - An ast Node * @param {Array} retArr - return array string * @returns {Array} the parsed webgl string */ astForStatement(forNode, retArr) { if (forNode.type !== 'ForStatement') { throw this.astErrorOutput('Invalid for statement', forNode); } const initArr = []; const testArr = []; const updateArr = []; const bodyArr = []; let isSafe = null; if (forNode.init) { const { declarations } = forNode.init; if (declarations.length > 1) { isSafe = false; } this.astGeneric(forNode.init, initArr); for (let i = 0; i < declarations.length; i++) { if (declarations[i].init && declarations[i].init.type !== 'Literal') { isSafe = false; } } } else { isSafe = false; } if (forNode.test) { this.astGeneric(forNode.test, testArr); } else { isSafe = false; } if (forNode.update) { this.astGeneric(forNode.update, updateArr); } else { isSafe = false; } if (forNode.body) { this.pushState('loop-body'); this.astGeneric(forNode.body, bodyArr); this.popState('loop-body'); } // have all parts, now make them safe if (isSafe === null) { isSafe = this.isSafe(forNode.init) && this.isSafe(forNode.test); } if (isSafe) { const initString = initArr.join(''); const initNeedsSemiColon = initString[initString.length - 1] !== ';'; retArr.push(`for (${initString}${initNeedsSemiColon ? ';' : ''}${testArr.join('')};${updateArr.join('')}){\n`); retArr.push(bodyArr.join('')); retArr.push('}\n'); } else { const iVariableName = this.getInternalVariableName('safeI'); if (initArr.length > 0) { retArr.push(initArr.join(''), '\n'); } retArr.push(`for (int ${iVariableName}=0;${iVariableName}<LOOP_MAX;${iVariableName}++){\n`); if (testArr.length > 0) { retArr.push(`if (!${testArr.join('')}) break;\n`); } retArr.push(bodyArr.join('')); retArr.push(`\n${updateArr.join('')};`); retArr.push('}\n'); } return retArr; } /** * @desc Parses the abstract syntax tree for *while* loop * @param {Object} whileNode - An ast Node * @param {Array} retArr - return array string * @returns {Array} the parsed webgl string */ astWhileStatement(whileNode, retArr) { if (whileNode.type !== 'WhileStatement') { throw this.astErrorOutput('Invalid while statement', whileNode); } const iVariableName = this.getInternalVariableName('safeI'); retArr.push(`for (int ${iVariableName}=0;${iVariableName}<LOOP_MAX;${iVariableName}++){\n`); retArr.push('if (!'); this.astGeneric(whileNode.test, retArr); retArr.push(') break;\n'); this.astGeneric(whileNode.body, retArr); retArr.push('}\n'); return retArr; } /** * @desc Parses the abstract syntax tree for *do while* loop * @param {Object} doWhileNode - An ast Node * @param {Array} retArr - return array string * @returns {Array} the parsed webgl string */ astDoWhileStatement(doWhileNode, retArr) { if (doWhileNode.type !== 'DoWhileStatement') { throw this.astErrorOutput('Invalid while statement', doWhileNode); } const iVariableName = this.getInternalVariableName('safeI'); retArr.push(`for (int ${iVariableName}=0;${iVariableName}<LOOP_MAX;${iVariableName}++){\n`); this.astGeneric(doWhileNode.body, retArr); retArr.push('if (!'); this.astGeneric(doWhileNode.test, retArr); retArr.push(') break;\n'); retArr.push('}\n'); return retArr; } /** * @desc Parses the abstract syntax tree for *Assignment* Expression * @param {Object} assNode - An ast Node * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astAssignmentExpression(assNode, retArr) { // TODO: casting needs implemented here if (assNode.operator === '%=') { this.astGeneric(assNode.left, retArr); retArr.push('='); retArr.push('mod('); this.astGeneric(assNode.left, retArr); retArr.push(','); this.astGeneric(assNode.right, retArr); retArr.push(')'); } else if (assNode.operator === '**=') { this.astGeneric(assNode.left, retArr); retArr.push('='); retArr.push('pow('); this.astGeneric(assNode.left, retArr); retArr.push(','); this.astGeneric(assNode.right, retArr); retArr.push(')'); } else { const leftType = this.getType(assNode.left); const rightType = this.getType(assNode.right); this.astGeneric(assNode.left, retArr); retArr.push(assNode.operator); if (leftType !== 'Integer' && rightType === 'Integer') { retArr.push('float('); this.astGeneric(assNode.right, retArr); retArr.push(')'); } else { this.astGeneric(assNode.right, retArr); } return retArr; } } /** * @desc Parses the abstract syntax tree for *Block* statement * @param {Object} bNode - the AST object to parse * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astBlockStatement(bNode, retArr) { if (this.isState('loop-body')) { this.pushState('block-body'); // this prevents recursive removal of braces for (let i = 0; i < bNode.body.length; i++) { this.astGeneric(bNode.body[i], retArr); } this.popState('block-body'); } else { retArr.push('{\n'); for (let i = 0; i < bNode.body.length; i++) { this.astGeneric(bNode.body[i], retArr); } retArr.push('}\n'); } return retArr; } /** * @desc Parses the abstract syntax tree for *Variable Declaration* * @param {Object} varDecNode - An ast Node * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astVariableDeclaration(varDecNode, retArr) { const declarations = varDecNode.declarations; if (!declarations || !declarations[0] || !declarations[0].init) { throw this.astErrorOutput('Unexpected expression', varDecNode); } const result = []; let lastType = null; const declarationSets = []; let declarationSet = []; for (let i = 0; i < declarations.length; i++) { const declaration = declarations[i]; const init = declaration.init; const info = this.getDeclaration(declaration.id); const actualType = this.getType(declaration.init); let type = actualType; if (type === 'LiteralInteger') { if (info.suggestedType === 'Integer') { type = 'Integer'; } else { // We had the choice to go either float or int, choosing float type = 'Number'; } } const markupType = typeMap[type]; if (!markupType) { throw this.astErrorOutput(`Markup type ${ type } not handled`, varDecNode); } const declarationResult = []; if (actualType === 'Integer' && type === 'Integer') { // Since we are assigning to a float, ensure valueType is reset to that info.valueType = 'Number'; if (i === 0 || lastType === null) { declarationResult.push('float '); } else if (type !== lastType) { throw new Error('Unhandled declaration'); } lastType = type; declarationResult.push(`user_${utils.sanitizeName(declaration.id.name)}=`); declarationResult.push('float('); this.astGeneric(init, declarationResult); declarationResult.push(')'); } else { // Since we are assigning to a float, ensure valueType is reset to that info.valueType = type; if (i === 0 || lastType === null) { declarationResult.push(`${markupType} `); } else if (type !== lastType) { declarationSets.push(declarationSet.join(',')); declarationSet = []; declarationResult.push(`${markupType} `); } lastType = type; declarationResult.push(`user_${utils.sanitizeName(declaration.id.name)}=`); if (actualType === 'Number' && type === 'Integer') { if (init.left && init.left.type === 'Literal') { this.astGeneric(init, declarationResult); } else { declarationResult.push('int('); this.astGeneric(init, declarationResult); declarationResult.push(')'); } } else if (actualType === 'LiteralInteger' && type === 'Integer') { this.castLiteralToInteger(init, declarationResult); } else { this.astGeneric(init, declarationResult); } } declarationSet.push(declarationResult.join('')); } if (declarationSet.length > 0) { declarationSets.push(declarationSet.join(',')); } result.push(declarationSets.join(';')); retArr.push(result.join('')); retArr.push(';'); return retArr; } /** * @desc Parses the abstract syntax tree for *If* Statement * @param {Object} ifNode - An ast Node * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astIfStatement(ifNode, retArr) { retArr.push('if ('); this.astGeneric(ifNode.test, retArr); retArr.push(')'); if (ifNode.consequent.type === 'BlockStatement') { this.astGeneric(ifNode.consequent, retArr); } else { retArr.push(' {\n'); this.astGeneric(ifNode.consequent, retArr); retArr.push('\n}\n'); } if (ifNode.alternate) { retArr.push('else '); if (ifNode.alternate.type === 'BlockStatement' || ifNode.alternate.type === 'IfStatement') { this.astGeneric(ifNode.alternate, retArr); } else { retArr.push(' {\n'); this.astGeneric(ifNode.alternate, retArr); retArr.push('\n}\n'); } } return retArr; } astSwitchStatement(ast, retArr) { if (ast.type !== 'SwitchStatement') { throw this.astErrorOutput('Invalid switch statement', ast); } const { discriminant, cases } = ast; const type = this.getType(discriminant); const varName = `switchDiscriminant${this.astKey(ast, '_')}`; switch (type) { case 'Float': case 'Number': retArr.push(`float ${varName} = `); this.astGeneric(discriminant, retArr); retArr.push(';\n'); break; case 'Integer': retArr.push(`int ${varName} = `); this.astGeneric(discriminant, retArr); retArr.push(';\n'); break; } // switch with just a default: if (cases.length === 1 && !cases[0].test) { this.astGeneric(cases[0].consequent, retArr); return retArr; } // regular switches: let fallingThrough = false; let defaultResult = []; let movingDefaultToEnd = false; let pastFirstIf = false; for (let i = 0; i < cases.length; i++) { // default if (!cases[i].test) { if (cases.length > i + 1) { movingDefaultToEnd = true; this.astGeneric(cases[i].consequent, defaultResult); continue; } else { retArr.push(' else {\n'); } } else { // all others if (i === 0 || !pastFirstIf) { pastFirstIf = true; retArr.push(`if (${varName} == `); } else { if (fallingThrough) { retArr.push(`${varName} == `); fallingThrough = false; } else { retArr.push(` else if (${varName} == `); } } if (type === 'Integer') { const testType = this.getType(cases[i].test); switch (testType) { case 'Number': case 'Float': this.castValueToInteger(cases[i].test, retArr); break; case 'LiteralInteger': this.castLiteralToInteger(cases[i].test, retArr); break; } } else if (type === 'Float') { const testType = this.getType(cases[i].test); switch (testType) { case 'LiteralInteger': this.castLiteralToFloat(cases[i].test, retArr); break; case 'Integer': this.castValueToFloat(cases[i].test, retArr); break; } } else { throw new Error('unhanlded'); } if (!cases[i].consequent || cases[i].consequent.length === 0) { fallingThrough = true; retArr.push(' || '); continue; } retArr.push(`) {\n`); } this.astGeneric(cases[i].consequent, retArr); retArr.push('\n}'); } if (movingDefaultToEnd) { retArr.push(' else {'); retArr.push(defaultResult.join('')); retArr.push('}'); } return retArr; } /** * @desc Parses the abstract syntax tree for *This* expression * @param {Object} tNode - An ast Node * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astThisExpression(tNode, retArr) { retArr.push('this'); return retArr; } /** * @desc Parses the abstract syntax tree for *Member* Expression * @param {Object} mNode - An ast Node * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astMemberExpression(mNode, retArr) { const { property, name, signature, origin, type, xProperty, yProperty, zProperty } = this.getMemberExpressionDetails(mNode); switch (signature) { case 'value.thread.value': case 'this.thread.value': if (name !== 'x' && name !== 'y' && name !== 'z') { throw this.astErrorOutput('Unexpected expression, expected `this.thread.x`, `this.thread.y`, or `this.thread.z`', mNode); } retArr.push(`threadId.${name}`); return retArr; case 'this.output.value': if (this.dynamicOutput) { switch (name) { case 'x': if (this.isState('casting-to-float')) { retArr.push('float(uOutputDim.x)'); } else { retArr.push('uOutputDim.x'); } break; case 'y': if (this.isState('casting-to-float')) { retArr.push('float(uOutputDim.y)'); } else { retArr.push('uOutputDim.y'); } break; case 'z': if (this.isState('casting-to-float')) { retArr.push('float(uOutputDim.z)'); } else { retArr.push('uOutputDim.z'); } break; default: throw this.astErrorOutput('Unexpected expression', mNode); } } else { switch (name) { case 'x': if (this.isState('casting-to-integer')) { retArr.push(this.output[0]); } else { retArr.push(this.output[0], '.0'); } break; case 'y': if (this.isState('casting-to-integer')) { retArr.push(this.output[1]); } else { retArr.push(this.output[1], '.0'); } break; case 'z': if (this.isState('casting-to-integer')) { retArr.push(this.output[2]); } else { retArr.push(this.output[2], '.0'); } break; default: throw this.astErrorOutput('Unexpected expression', mNode); } } return retArr; case 'value': throw this.astErrorOutput('Unexpected expression', mNode); case 'value[]': case 'value[][]': case 'value[][][]': case 'value[][][][]': case 'value.value': if (origin === 'Math') { retArr.push(Math[name]); return retArr; } const cleanName = utils.sanitizeName(name); switch (property) { case 'r': retArr.push(`user_${ cleanName }.r`); return retArr; case 'g': retArr.push(`user_${ cleanName }.g`); return retArr; case 'b': retArr.push(`user_${ cleanName }.b`); return retArr; case 'a': retArr.push(`user_${ cleanName }.a`); return retArr; } break; case 'this.constants.value': if (typeof xProperty === 'undefined') { switch (type) { case 'Array(2)': case 'Array(3)': case 'Array(4)': retArr.push(`constants_${ utils.sanitizeName(name) }`); return retArr; } } case 'this.constants.value[]': case 'this.constants.value[][]': case 'this.constants.value[][][]': case 'this.constants.value[][][][]': break; case 'fn()[]': this.astCallExpression(mNode.object, retArr); retArr.push('['); retArr.push(this.memberExpressionPropertyMarkup(property)); retArr.push(']'); return retArr; case 'fn()[][]': this.astCallExpression(mNode.object.object, retArr); retArr.push('['); retArr.push(this.memberExpressionPropertyMarkup(mNode.object.property)); retArr.push(']'); retArr.push('['); retArr.push(this.memberExpressionPropertyMarkup(mNode.property)); retArr.push(']'); return retArr; case '[][]': this.astArrayExpression(mNode.object, retArr); retArr.push('['); retArr.push(this.memberExpressionPropertyMarkup(property)); retArr.push(']'); return retArr; default: throw this.astErrorOutput('Unexpected expression', mNode); } if (mNode.computed === false) { // handle simple types switch (type) { case 'Number': case 'Integer': case 'Float': case 'Boolean': retArr.push(`${origin}_${utils.sanitizeName(name)}`); return retArr; } } // handle more complex types // argument may have come from a parent const markupName = `${origin}_${utils.sanitizeName(name)}`; switch (type) { case 'Array(2)': case 'Array(3)': case 'Array(4)': // Get from local vec4 this.astGeneric(mNode.object, retArr); retArr.push('['); retArr.push(this.memberExpressionPropertyMarkup(xProperty)); retArr.push(']'); break; case 'HTMLImageArray': retArr.push(`getImage3D(${ markupName }, ${ markupName }Size, ${ markupName }Dim, `); this.memberExpressionXYZ(xProperty, yProperty, zProperty, retArr); retArr.push(')'); break; case 'ArrayTexture(1)': retArr.push(`getFloatFromSampler2D(${ markupName }, ${ markupName }Size, ${ markupName }Dim, `); this.memberExpressionXYZ(xProperty, yProperty, zProperty, retArr); retArr.push(')'); break; case 'Array1D(2)': case 'Array2D(2)': case 'Array3D(2)': retArr.push(`getMemoryOptimizedVec2(${ markupName }, ${ markupName }Size, ${ markupName }Dim, `); this.memberExpressionXYZ(xProperty, yProperty, zProperty, retArr); retArr.push(')'); break; case 'ArrayTexture(2)': retArr.push(`getVec2FromSampler2D(${ markupName }, ${ markupName }Size, ${ markupName }Dim, `); this.memberExpressionXYZ(xProperty, yProperty, zProperty, retArr); retArr.push(')'); break; case 'Array1D(3)': case 'Array2D(3)': case 'Array3D(3)': retArr.push(`getMemoryOptimizedVec3(${ markupName }, ${ markupName }Size, ${ markupName }Dim, `); this.memberExpressionXYZ(xProperty, yProperty, zProperty, retArr); retArr.push(')'); break; case 'ArrayTexture(3)': retArr.push(`getVec3FromSampler2D(${ markupName }, ${ markupName }Size, ${ markupName }Dim, `); this.memberExpressionXYZ(xProperty, yProperty, zProperty, retArr); retArr.push(')'); break; case 'Array1D(4)': case 'Array2D(4)': case 'Array3D(4)': retArr.push(`getMemoryOptimizedVec4(${ markupName }, ${ markupName }Size, ${ markupName }Dim, `); this.memberExpressionXYZ(xProperty, yProperty, zProperty, retArr); retArr.push(')'); break; case 'ArrayTexture(4)': case 'HTMLCanvas': case 'OffscreenCanvas': case 'HTMLImage': case 'ImageBitmap': case 'ImageData': case 'HTMLVideo': retArr.push(`getVec4FromSampler2D(${ markupName }, ${ markupName }Size, ${ markupName }Dim, `); this.memberExpressionXYZ(xProperty, yProperty, zProperty, retArr); retArr.push(')'); break; case 'NumberTexture': case 'Array': case 'Array2D': case 'Array3D': case 'Array4D': case 'Input': case 'Number': case 'Float': case 'Integer': if (this.precision === 'single') { // bitRatio is always 4 here, javascript doesn't yet have 8 or 16 bit support // TODO: make 8 or 16 bit work anyway! retArr.push(`getMemoryOptimized32(${markupName}, ${markupName}Size, ${markupName}Dim, `); this.memberExpressionXYZ(xProperty, yProperty, zProperty, retArr); retArr.push(')'); } else { const bitRatio = (origin === 'user' ? this.lookupFunctionArgumentBitRatio(this.name, name) : this.constantBitRatios[name] ); switch (bitRatio) { case 1: retArr.push(`get8(${markupName}, ${markupName}Size, ${markupName}Dim, `); break; case 2: retArr.push(`get16(${markupName}, ${markupName}Size, ${markupName}Dim, `); break; case 4: case 0: retArr.push(`get32(${markupName}, ${markupName}Size, ${markupName}Dim, `); break; default: throw new Error(`unhandled bit ratio of ${bitRatio}`); } this.memberExpressionXYZ(xProperty, yProperty, zProperty, retArr); retArr.push(')'); } break; case 'MemoryOptimizedNumberTexture': retArr.push(`getMemoryOptimized32(${ markupName }, ${ markupName }Size, ${ markupName }Dim, `); this.memberExpressionXYZ(xProperty, yProperty, zProperty, retArr); retArr.push(')'); break; case 'Matrix(2)': case 'Matrix(3)': case 'Matrix(4)': retArr.push(`${markupName}[${this.memberExpressionPropertyMarkup(yProperty)}]`); if (yProperty) { retArr.push(`[${this.memberExpressionPropertyMarkup(xProperty)}]`); } break; default: throw new Error(`unhandled member expression "${ type }"`); } return retArr; } /** * @desc Parses the abstract syntax tree for *call* expression * @param {Object} ast - the AST object to parse * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astCallExpression(ast, retArr) { if (!ast.callee) { throw this.astErrorOutput('Unknown CallExpression', ast); } let functionName = null; const isMathFunction = this.isAstMathFunction(ast); // Its a math operator or this.something(), remove the prefix if (isMathFunction || (ast.callee.object && ast.callee.object.type === 'ThisExpression')) { functionName = ast.callee.property.name; } // Issue #212, BABEL! else if (ast.callee.type === 'SequenceExpression' && ast.callee.expressions[0].type === 'Literal' && !isNaN(ast.callee.expressions[0].raw)) { functionName = ast.callee.expressions[1].property.name; } else { functionName = ast.callee.name; } if (!functionName) { throw this.astErrorOutput(`Unhandled function, couldn't find name`, ast); } // if this if grows to more than one, lets use a switch switch (functionName) { case 'pow': functionName = '_pow'; break; case 'round': functionName = '_round'; break; } // Register the function into the called registry if (this.calledFunctions.indexOf(functionName) < 0) { this.calledFunctions.push(functionName); } if (functionName === 'random' && this.plugins && this.plugins.length > 0) { for (let i = 0; i < this.plugins.length; i++) { const plugin = this.plugins[i]; if (plugin.functionMatch === 'Math.random()' && plugin.functionReplace) { retArr.push(plugin.functionReplace); return retArr; } } } // track the function was called if (this.onFunctionCall) { this.onFunctionCall(this.name, functionName, ast.arguments); } // Call the function retArr.push(functionName); // Open arguments space retArr.push('('); // Add the arguments if (isMathFunction) { for (let i = 0; i < ast.arguments.length; ++i) { const argument = ast.arguments[i]; const argumentType = this.getType(argument); if (i > 0) { retArr.push(', '); } switch (argumentType) { case 'Integer': this.castValueToFloat(argument, retArr); break; default: this.astGeneric(argument, retArr); break; } } } else { const targetTypes = this.lookupFunctionArgumentTypes(functionName) || []; for (let i = 0; i < ast.arguments.length; ++i) { const argument = ast.arguments[i]; let targetType = targetTypes[i]; if (i > 0) { retArr.push(', '); } const argumentType = this.getType(argument); if (!targetType) { this.triggerImplyArgumentType(functionName, i, argumentType, this); targetType = argumentType; } switch (argumentType) { case 'Boolean': this.astGeneric(argument, retArr); continue; case 'Number': case 'Float': if (targetType === 'Integer') { retArr.push('int('); this.astGeneric(argument, retArr); retArr.push(')'); continue; } else if (targetType === 'Number' || targetType === 'Float') { this.astGeneric(argument, retArr); continue; } else if (targetType === 'LiteralInteger') { this.castLiteralToFloat(argument, retArr); continue; } break; case 'Integer': if (targetType === 'Number' || targetType === 'Float') { retArr.push('float('); this.astGeneric(argument, retArr); retArr.push(')'); continue; } else if (targetType === 'Integer') { this.astGeneric(argument, retArr); continue; } break; case 'LiteralInteger': if (targetType === 'Integer') { this.castLiteralToInteger(argument, retArr); continue; } else if (targetType === 'Number' || targetType === 'Float') { this.castLiteralToFloat(argument, retArr); continue; } else if (targetType === 'LiteralInteger') { this.astGeneric(argument, retArr); continue; } break; case 'Array(2)': case 'Array(3)': case 'Array(4)': if (targetType === argumentType) { if (argument.type === 'Identifier') { retArr.push(`user_${utils.sanitizeName(argument.name)}`); } else if (argument.type === 'ArrayExpression' || argument.type === 'MemberExpression' || argument.type === 'CallExpression') { this.astGeneric(argument, retArr); } else { throw this.astErrorOutput(`Unhandled argument type ${ argument.type }`, ast); } continue; } break; case 'HTMLCanvas': case 'OffscreenCanvas': case 'HTMLImage': case 'ImageBitmap': case 'ImageData': case 'HTMLImageArray': case 'HTMLVideo': case 'ArrayTexture(1)': case 'ArrayTexture(2)': case 'ArrayTexture(3)': case 'ArrayTexture(4)': case 'Array': case 'Input': if (targetType === argumentType) { if (argument.type !== 'Identifier') throw this.astErrorOutput(`Unhandled argument type ${ argument.type }`, ast); this.triggerImplyArgumentBitRatio(this.name, argument.name, functionName, i); const name = utils.sanitizeName(argument.name); retArr.push(`user_${name},user_${name}Size,user_${name}Dim`); continue; } break; } throw this.astErrorOutput(`Unhandled argument combination of ${ argumentType } and ${ targetType } for argument named "${ argument.name }"`, ast); } } // Close arguments space retArr.push(')'); return retArr; } /** * @desc Parses the abstract syntax tree for *Array* Expression * @param {Object} arrNode - the AST object to parse * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astArrayExpression(arrNode, retArr) { const returnType = this.getType(arrNode); const arrLen = arrNode.elements.length; switch (returnType) { case 'Matrix(2)': case 'Matrix(3)': case 'Matrix(4)': retArr.push(`mat${arrLen}(`); break; default: retArr.push(`vec${arrLen}(`); } for (let i = 0; i < arrLen; ++i)