UNPKG

gpu.js

Version:

GPU Accelerated JavaScript

1,498 lines (1,426 loc) 47 kB
const acorn = require('acorn'); const { utils } = require('../utils'); const { FunctionTracer } = require('./function-tracer'); /** * * @desc Represents a single function, inside JS, webGL, or openGL. * <p>This handles all the raw state, converted state, etc. Of a single function.</p> */ class FunctionNode { /** * * @param {string|object} source * @param {IFunctionSettings} [settings] */ constructor(source, settings) { if (!source && !settings.ast) { throw new Error('source parameter is missing'); } settings = settings || {}; this.source = source; this.ast = null; this.name = typeof source === 'string' ? settings.isRootKernel ? 'kernel' : (settings.name || utils.getFunctionNameFromString(source)) : null; this.calledFunctions = []; this.constants = {}; this.constantTypes = {}; this.constantBitRatios = {}; this.isRootKernel = false; this.isSubKernel = false; this.debug = null; this.functions = null; this.identifiers = null; this.contexts = null; this.functionCalls = null; this.states = []; this.needsArgumentType = null; this.assignArgumentType = null; this.lookupReturnType = null; this.lookupFunctionArgumentTypes = null; this.lookupFunctionArgumentBitRatio = null; this.triggerImplyArgumentType = null; this.triggerImplyArgumentBitRatio = null; this.onNestedFunction = null; this.onFunctionCall = null; this.optimizeFloatMemory = null; this.precision = null; this.loopMaxIterations = null; this.argumentNames = (typeof this.source === 'string' ? utils.getArgumentNamesFromString(this.source) : null); this.argumentTypes = []; this.argumentSizes = []; this.argumentBitRatios = null; this.returnType = null; this.output = []; this.plugins = null; this.leadingReturnStatement = null; this.followingReturnStatement = null; this.dynamicOutput = null; this.dynamicArguments = null; this.strictTypingChecking = false; this.fixIntegerDivisionAccuracy = null; if (settings) { for (const p in settings) { if (!settings.hasOwnProperty(p)) continue; if (!this.hasOwnProperty(p)) continue; this[p] = settings[p]; } } this.literalTypes = {}; this.validate(); this._string = null; this._internalVariableNames = {}; } validate() { if (typeof this.source !== 'string' && !this.ast) { throw new Error('this.source not a string'); } if (!this.ast && !utils.isFunctionString(this.source)) { throw new Error('this.source not a function string'); } if (!this.name) { throw new Error('this.name could not be set'); } if (this.argumentTypes.length > 0 && this.argumentTypes.length !== this.argumentNames.length) { throw new Error(`argumentTypes count of ${ this.argumentTypes.length } exceeds ${ this.argumentNames.length }`); } if (this.output.length < 1) { throw new Error('this.output is not big enough'); } } /** * @param {String} name * @returns {boolean} */ isIdentifierConstant(name) { if (!this.constants) return false; return this.constants.hasOwnProperty(name); } isInput(argumentName) { return this.argumentTypes[this.argumentNames.indexOf(argumentName)] === 'Input'; } pushState(state) { this.states.push(state); } popState(state) { if (this.state !== state) { throw new Error(`Cannot popState ${ state } when in ${ this.state }`); } this.states.pop(); } isState(state) { return this.state === state; } get state() { return this.states[this.states.length - 1]; } /** * @function * @name astMemberExpressionUnroll * @desc Parses the abstract syntax tree for binary expression. * * <p>Utility function for astCallExpression.</p> * * @param {Object} ast - the AST object to parse * * @returns {String} the function namespace call, unrolled */ astMemberExpressionUnroll(ast) { if (ast.type === 'Identifier') { return ast.name; } else if (ast.type === 'ThisExpression') { return 'this'; } if (ast.type === 'MemberExpression') { if (ast.object && ast.property) { //babel sniffing if (ast.object.hasOwnProperty('name') && ast.object.name !== 'Math') { return this.astMemberExpressionUnroll(ast.property); } return ( this.astMemberExpressionUnroll(ast.object) + '.' + this.astMemberExpressionUnroll(ast.property) ); } } //babel sniffing if (ast.hasOwnProperty('expressions')) { const firstExpression = ast.expressions[0]; if (firstExpression.type === 'Literal' && firstExpression.value === 0 && ast.expressions.length === 2) { return this.astMemberExpressionUnroll(ast.expressions[1]); } } // Failure, unknown expression throw this.astErrorOutput('Unknown astMemberExpressionUnroll', ast); } /** * @desc Parses the class function JS, and returns its Abstract Syntax Tree object. * This is used internally to convert to shader code * * @param {Object} [inParser] - Parser to use, assumes in scope 'parser' if null or undefined * * @returns {Object} The function AST Object, note that result is cached under this.ast; */ getJsAST(inParser) { if (this.ast) { return this.ast; } if (typeof this.source === 'object') { this.traceFunctionAST(this.source); return this.ast = this.source; } inParser = inParser || acorn; if (inParser === null) { throw new Error('Missing JS to AST parser'); } const ast = Object.freeze(inParser.parse(`const parser_${ this.name } = ${ this.source };`, { locations: true })); // take out the function object, outside the var declarations const functionAST = ast.body[0].declarations[0].init; this.traceFunctionAST(functionAST); if (!ast) { throw new Error('Failed to parse JS code'); } return this.ast = functionAST; } traceFunctionAST(ast) { const { contexts, declarations, functions, identifiers, functionCalls } = new FunctionTracer(ast); this.contexts = contexts; this.identifiers = identifiers; this.functionCalls = functionCalls; this.functions = functions; for (let i = 0; i < declarations.length; i++) { const declaration = declarations[i]; const { ast, inForLoopInit, inForLoopTest } = declaration; const { init } = ast; const dependencies = this.getDependencies(init); let valueType = null; if (inForLoopInit && inForLoopTest) { valueType = 'Integer'; } else { if (init) { const realType = this.getType(init); switch (realType) { case 'Integer': case 'Float': case 'Number': if (init.type === 'MemberExpression') { valueType = realType; } else { valueType = 'Number'; } break; case 'LiteralInteger': valueType = 'Number'; break; default: valueType = realType; } } } declaration.valueType = valueType; declaration.dependencies = dependencies; declaration.isSafe = this.isSafeDependencies(dependencies); } for (let i = 0; i < functions.length; i++) { this.onNestedFunction(functions[i], this.source); } } getDeclaration(ast) { for (let i = 0; i < this.identifiers.length; i++) { const identifier = this.identifiers[i]; if (ast === identifier.ast) { return identifier.declaration; } } return null; } /** * @desc Return the type of parameter sent to subKernel/Kernel. * @param {Object} ast - Identifier * @returns {String} Type of the parameter */ getVariableType(ast) { if (ast.type !== 'Identifier') { throw new Error(`ast of ${ast.type} not "Identifier"`); } let type = null; const argumentIndex = this.argumentNames.indexOf(ast.name); if (argumentIndex === -1) { const declaration = this.getDeclaration(ast); if (declaration) { return declaration.valueType; } } else { const argumentType = this.argumentTypes[argumentIndex]; if (argumentType) { type = argumentType; } } if (!type && this.strictTypingChecking) { throw new Error(`Declaration of ${name} not found`); } return type; } /** * Generally used to lookup the value type returned from a member expressions * @param {String} type * @return {String} */ getLookupType(type) { if (!typeLookupMap.hasOwnProperty(type)) { throw new Error(`unknown typeLookupMap ${ type }`); } return typeLookupMap[type]; } getConstantType(constantName) { if (this.constantTypes[constantName]) { const type = this.constantTypes[constantName]; if (type === 'Float') { return 'Number'; } else { return type; } } throw new Error(`Type for constant "${ constantName }" not declared`); } toString() { if (this._string) return this._string; return this._string = this.astGeneric(this.getJsAST(), []).join('').trim(); } toJSON() { const settings = { source: this.source, name: this.name, constants: this.constants, constantTypes: this.constantTypes, isRootKernel: this.isRootKernel, isSubKernel: this.isSubKernel, debug: this.debug, output: this.output, loopMaxIterations: this.loopMaxIterations, argumentNames: this.argumentNames, argumentTypes: this.argumentTypes, argumentSizes: this.argumentSizes, returnType: this.returnType, leadingReturnStatement: this.leadingReturnStatement, followingReturnStatement: this.followingReturnStatement, }; return { ast: this.ast, settings }; } /** * Recursively looks up type for ast expression until it's found * @param ast * @returns {String|null} */ getType(ast) { if (Array.isArray(ast)) { return this.getType(ast[ast.length - 1]); } switch (ast.type) { case 'BlockStatement': return this.getType(ast.body); case 'ArrayExpression': const childType = this.getType(ast.elements[0]); switch (childType) { case 'Array(2)': case 'Array(3)': case 'Array(4)': return `Matrix(${ast.elements.length})`; } return `Array(${ ast.elements.length })`; case 'Literal': const literalKey = this.astKey(ast); if (this.literalTypes[literalKey]) { return this.literalTypes[literalKey]; } if (Number.isInteger(ast.value)) { return 'LiteralInteger'; } else if (ast.value === true || ast.value === false) { return 'Boolean'; } else { return 'Number'; } case 'AssignmentExpression': return this.getType(ast.left); case 'CallExpression': if (this.isAstMathFunction(ast)) { return 'Number'; } if (!ast.callee || !ast.callee.name) { if (ast.callee.type === 'SequenceExpression' && ast.callee.expressions[ast.callee.expressions.length - 1].property.name) { const functionName = ast.callee.expressions[ast.callee.expressions.length - 1].property.name; this.inferArgumentTypesIfNeeded(functionName, ast.arguments); return this.lookupReturnType(functionName, ast, this); } if (this.getVariableSignature(ast.callee, true) === 'this.color') { return null; } if (ast.callee.type === 'MemberExpression' && ast.callee.object && ast.callee.property && ast.callee.property.name && ast.arguments) { const functionName = ast.callee.property.name; this.inferArgumentTypesIfNeeded(functionName, ast.arguments); return this.lookupReturnType(functionName, ast, this); } throw this.astErrorOutput('Unknown call expression', ast); } if (ast.callee && ast.callee.name) { const functionName = ast.callee.name; this.inferArgumentTypesIfNeeded(functionName, ast.arguments); return this.lookupReturnType(functionName, ast, this); } throw this.astErrorOutput(`Unhandled getType Type "${ ast.type }"`, ast); case 'LogicalExpression': return 'Boolean'; case 'BinaryExpression': // modulos is Number switch (ast.operator) { case '%': case '/': if (this.fixIntegerDivisionAccuracy) { return 'Number'; } else { break; } case '>': case '<': return 'Boolean'; case '&': case '|': case '^': case '<<': case '>>': case '>>>': return 'Integer'; } const type = this.getType(ast.left); if (this.isState('skip-literal-correction')) return type; if (type === 'LiteralInteger') { const rightType = this.getType(ast.right); if (rightType === 'LiteralInteger') { if (ast.left.value % 1 === 0) { return 'Integer'; } else { return 'Float'; } } return rightType; } return typeLookupMap[type] || type; case 'UpdateExpression': return this.getType(ast.argument); case 'UnaryExpression': if (ast.operator === '~') { return 'Integer'; } return this.getType(ast.argument); case 'VariableDeclaration': { const declarations = ast.declarations; let lastType; for (let i = 0; i < declarations.length; i++) { const declaration = declarations[i]; lastType = this.getType(declaration); } if (!lastType) { throw this.astErrorOutput(`Unable to find type for declaration`, ast); } return lastType; } case 'VariableDeclarator': const declaration = this.getDeclaration(ast.id); if (!declaration) { throw this.astErrorOutput(`Unable to find declarator`, ast); } if (!declaration.valueType) { throw this.astErrorOutput(`Unable to find declarator valueType`, ast); } return declaration.valueType; case 'Identifier': if (ast.name === 'Infinity') { return 'Number'; } if (this.isAstVariable(ast)) { const signature = this.getVariableSignature(ast); if (signature === 'value') { return this.getCheckVariableType(ast); } } const origin = this.findIdentifierOrigin(ast); if (origin && origin.init) { return this.getType(origin.init); } return null; case 'ReturnStatement': return this.getType(ast.argument); case 'MemberExpression': if (this.isAstMathFunction(ast)) { switch (ast.property.name) { case 'ceil': return 'Integer'; case 'floor': return 'Integer'; case 'round': return 'Integer'; } return 'Number'; } if (this.isAstVariable(ast)) { const variableSignature = this.getVariableSignature(ast); switch (variableSignature) { case 'value[]': return this.getLookupType(this.getCheckVariableType(ast.object)); case 'value[][]': return this.getLookupType(this.getCheckVariableType(ast.object.object)); case 'value[][][]': return this.getLookupType(this.getCheckVariableType(ast.object.object.object)); case 'value[][][][]': return this.getLookupType(this.getCheckVariableType(ast.object.object.object.object)); case 'value.thread.value': case 'this.thread.value': return 'Integer'; case 'this.output.value': return this.dynamicOutput ? 'Integer' : 'LiteralInteger'; case 'this.constants.value': return this.getConstantType(ast.property.name); case 'this.constants.value[]': return this.getLookupType(this.getConstantType(ast.object.property.name)); case 'this.constants.value[][]': return this.getLookupType(this.getConstantType(ast.object.object.property.name)); case 'this.constants.value[][][]': return this.getLookupType(this.getConstantType(ast.object.object.object.property.name)); case 'this.constants.value[][][][]': return this.getLookupType(this.getConstantType(ast.object.object.object.object.property.name)); case 'fn()[]': case 'fn()[][]': case 'fn()[][][]': return this.getLookupType(this.getType(ast.object)); case 'value.value': if (this.isAstMathVariable(ast)) { return 'Number'; } switch (ast.property.name) { case 'r': case 'g': case 'b': case 'a': return this.getLookupType(this.getCheckVariableType(ast.object)); } case '[][]': return 'Number'; } throw this.astErrorOutput('Unhandled getType MemberExpression', ast); } throw this.astErrorOutput('Unhandled getType MemberExpression', ast); case 'ConditionalExpression': return this.getType(ast.consequent); case 'FunctionDeclaration': case 'FunctionExpression': const lastReturn = this.findLastReturn(ast.body); if (lastReturn) { return this.getType(lastReturn); } return null; case 'IfStatement': return this.getType(ast.consequent); case 'SequenceExpression': return this.getType(ast.expressions[ast.expressions.length - 1]); default: throw this.astErrorOutput(`Unhandled getType Type "${ ast.type }"`, ast); } } getCheckVariableType(ast) { const type = this.getVariableType(ast); if (!type) { throw this.astErrorOutput(`${ast.type} is not defined`, ast); } return type; } inferArgumentTypesIfNeeded(functionName, args) { // ensure arguments are filled in, so when we lookup return type, we already can infer it for (let i = 0; i < args.length; i++) { if (!this.needsArgumentType(functionName, i)) continue; const type = this.getType(args[i]); if (!type) { throw this.astErrorOutput(`Unable to infer argument ${i}`, args[i]); } this.assignArgumentType(functionName, i, type); } } isAstMathVariable(ast) { const mathProperties = [ 'E', 'PI', 'SQRT2', 'SQRT1_2', 'LN2', 'LN10', 'LOG2E', 'LOG10E', ]; return ast.type === 'MemberExpression' && ast.object && ast.object.type === 'Identifier' && ast.object.name === 'Math' && ast.property && ast.property.type === 'Identifier' && mathProperties.indexOf(ast.property.name) > -1; } isAstMathFunction(ast) { const mathFunctions = [ 'abs', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh', 'cbrt', 'ceil', 'clz32', 'cos', 'cosh', 'expm1', 'exp', 'floor', 'fround', 'imul', 'log', 'log2', 'log10', 'log1p', 'max', 'min', 'pow', 'random', 'round', 'sign', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'trunc', ]; return ast.type === 'CallExpression' && ast.callee && ast.callee.type === 'MemberExpression' && ast.callee.object && ast.callee.object.type === 'Identifier' && ast.callee.object.name === 'Math' && ast.callee.property && ast.callee.property.type === 'Identifier' && mathFunctions.indexOf(ast.callee.property.name) > -1; } isAstVariable(ast) { return ast.type === 'Identifier' || ast.type === 'MemberExpression'; } isSafe(ast) { return this.isSafeDependencies(this.getDependencies(ast)); } isSafeDependencies(dependencies) { return dependencies && dependencies.every ? dependencies.every(dependency => dependency.isSafe) : true; } /** * * @param ast * @param dependencies * @param isNotSafe * @return {Array} */ getDependencies(ast, dependencies, isNotSafe) { if (!dependencies) { dependencies = []; } if (!ast) return null; if (Array.isArray(ast)) { for (let i = 0; i < ast.length; i++) { this.getDependencies(ast[i], dependencies, isNotSafe); } return dependencies; } switch (ast.type) { case 'AssignmentExpression': this.getDependencies(ast.left, dependencies, isNotSafe); this.getDependencies(ast.right, dependencies, isNotSafe); return dependencies; case 'ConditionalExpression': this.getDependencies(ast.test, dependencies, isNotSafe); this.getDependencies(ast.alternate, dependencies, isNotSafe); this.getDependencies(ast.consequent, dependencies, isNotSafe); return dependencies; case 'Literal': dependencies.push({ origin: 'literal', value: ast.value, isSafe: isNotSafe === true ? false : ast.value > -Infinity && ast.value < Infinity && !isNaN(ast.value) }); break; case 'VariableDeclarator': return this.getDependencies(ast.init, dependencies, isNotSafe); case 'Identifier': const declaration = this.getDeclaration(ast); if (declaration) { dependencies.push({ name: ast.name, origin: 'declaration', isSafe: isNotSafe ? false : this.isSafeDependencies(declaration.dependencies), }); } else if (this.argumentNames.indexOf(ast.name) > -1) { dependencies.push({ name: ast.name, origin: 'argument', isSafe: false, }); } else if (this.strictTypingChecking) { throw new Error(`Cannot find identifier origin "${ast.name}"`); } break; case 'FunctionDeclaration': return this.getDependencies(ast.body.body[ast.body.body.length - 1], dependencies, isNotSafe); case 'ReturnStatement': return this.getDependencies(ast.argument, dependencies); case 'BinaryExpression': case 'LogicalExpression': isNotSafe = (ast.operator === '/' || ast.operator === '*'); this.getDependencies(ast.left, dependencies, isNotSafe); this.getDependencies(ast.right, dependencies, isNotSafe); return dependencies; case 'UnaryExpression': case 'UpdateExpression': return this.getDependencies(ast.argument, dependencies, isNotSafe); case 'VariableDeclaration': return this.getDependencies(ast.declarations, dependencies, isNotSafe); case 'ArrayExpression': dependencies.push({ origin: 'declaration', isSafe: true, }); return dependencies; case 'CallExpression': dependencies.push({ origin: 'function', isSafe: true, }); return dependencies; case 'MemberExpression': const details = this.getMemberExpressionDetails(ast); switch (details.signature) { case 'value[]': this.getDependencies(ast.object, dependencies, isNotSafe); break; case 'value[][]': this.getDependencies(ast.object.object, dependencies, isNotSafe); break; case 'value[][][]': this.getDependencies(ast.object.object.object, dependencies, isNotSafe); break; case 'this.output.value': if (this.dynamicOutput) { dependencies.push({ name: details.name, origin: 'output', isSafe: false, }); } break; } if (details) { if (details.property) { this.getDependencies(details.property, dependencies, isNotSafe); } if (details.xProperty) { this.getDependencies(details.xProperty, dependencies, isNotSafe); } if (details.yProperty) { this.getDependencies(details.yProperty, dependencies, isNotSafe); } if (details.zProperty) { this.getDependencies(details.zProperty, dependencies, isNotSafe); } return dependencies; } case 'SequenceExpression': return this.getDependencies(ast.expressions, dependencies, isNotSafe); default: throw this.astErrorOutput(`Unhandled type ${ ast.type } in getDependencies`, ast); } return dependencies; } getVariableSignature(ast, returnRawValue) { if (!this.isAstVariable(ast)) { throw new Error(`ast of type "${ ast.type }" is not a variable signature`); } if (ast.type === 'Identifier') { return 'value'; } const signature = []; while (true) { if (!ast) break; if (ast.computed) { signature.push('[]'); } else if (ast.type === 'ThisExpression') { signature.unshift('this'); } else if (ast.property && ast.property.name) { if ( ast.property.name === 'x' || ast.property.name === 'y' || ast.property.name === 'z' ) { signature.unshift(returnRawValue ? '.' + ast.property.name : '.value'); } else if ( ast.property.name === 'constants' || ast.property.name === 'thread' || ast.property.name === 'output' ) { signature.unshift('.' + ast.property.name); } else { signature.unshift(returnRawValue ? '.' + ast.property.name : '.value'); } } else if (ast.name) { signature.unshift(returnRawValue ? ast.name : 'value'); } else if (ast.callee && ast.callee.name) { signature.unshift(returnRawValue ? ast.callee.name + '()' : 'fn()'); } else if (ast.elements) { signature.unshift('[]'); } else { signature.unshift('unknown'); } ast = ast.object; } const signatureString = signature.join(''); if (returnRawValue) { return signatureString; } const allowedExpressions = [ 'value', 'value[]', 'value[][]', 'value[][][]', 'value[][][][]', 'value.value', 'value.thread.value', 'this.thread.value', 'this.output.value', 'this.constants.value', 'this.constants.value[]', 'this.constants.value[][]', 'this.constants.value[][][]', 'this.constants.value[][][][]', 'fn()[]', 'fn()[][]', 'fn()[][][]', '[][]', ]; if (allowedExpressions.indexOf(signatureString) > -1) { return signatureString; } return null; } build() { return this.toString().length > 0; } /** * @desc Parses the abstract syntax tree for generically to its respective function * @param {Object} ast - the AST object to parse * @param {Array} retArr - return array string * @returns {Array} the parsed string array */ astGeneric(ast, retArr) { if (ast === null) { throw this.astErrorOutput('NULL ast', ast); } else { if (Array.isArray(ast)) { for (let i = 0; i < ast.length; i++) { this.astGeneric(ast[i], retArr); } return retArr; } switch (ast.type) { case 'FunctionDeclaration': return this.astFunctionDeclaration(ast, retArr); case 'FunctionExpression': return this.astFunctionExpression(ast, retArr); case 'ReturnStatement': return this.astReturnStatement(ast, retArr); case 'Literal': return this.astLiteral(ast, retArr); case 'BinaryExpression': return this.astBinaryExpression(ast, retArr); case 'Identifier': return this.astIdentifierExpression(ast, retArr); case 'AssignmentExpression': return this.astAssignmentExpression(ast, retArr); case 'ExpressionStatement': return this.astExpressionStatement(ast, retArr); case 'EmptyStatement': return this.astEmptyStatement(ast, retArr); case 'BlockStatement': return this.astBlockStatement(ast, retArr); case 'IfStatement': return this.astIfStatement(ast, retArr); case 'SwitchStatement': return this.astSwitchStatement(ast, retArr); case 'BreakStatement': return this.astBreakStatement(ast, retArr); case 'ContinueStatement': return this.astContinueStatement(ast, retArr); case 'ForStatement': return this.astForStatement(ast, retArr); case 'WhileStatement': return this.astWhileStatement(ast, retArr); case 'DoWhileStatement': return this.astDoWhileStatement(ast, retArr); case 'VariableDeclaration': return this.astVariableDeclaration(ast, retArr); case 'VariableDeclarator': return this.astVariableDeclarator(ast, retArr); case 'ThisExpression': return this.astThisExpression(ast, retArr); case 'SequenceExpression': return this.astSequenceExpression(ast, retArr); case 'UnaryExpression': return this.astUnaryExpression(ast, retArr); case 'UpdateExpression': return this.astUpdateExpression(ast, retArr); case 'LogicalExpression': return this.astLogicalExpression(ast, retArr); case 'MemberExpression': return this.astMemberExpression(ast, retArr); case 'CallExpression': return this.astCallExpression(ast, retArr); case 'ArrayExpression': return this.astArrayExpression(ast, retArr); case 'DebuggerStatement': return this.astDebuggerStatement(ast, retArr); case 'ConditionalExpression': return this.astConditionalExpression(ast, retArr); } throw this.astErrorOutput('Unknown ast type : ' + ast.type, ast); } } /** * @desc To throw the AST error, with its location. * @param {string} error - the error message output * @param {Object} ast - the AST object where the error is */ astErrorOutput(error, ast) { if (typeof this.source !== 'string') { return new Error(error); } const debugString = utils.getAstString(this.source, ast); const leadingSource = this.source.substr(ast.start); const splitLines = leadingSource.split(/\n/); const lineBefore = splitLines.length > 0 ? splitLines[splitLines.length - 1] : 0; return new Error(`${error} on line ${ splitLines.length }, position ${ lineBefore.length }:\n ${ debugString }`); } astDebuggerStatement(arrNode, retArr) { return retArr; } astConditionalExpression(ast, retArr) { if (ast.type !== 'ConditionalExpression') { throw this.astErrorOutput('Not a conditional expression', ast); } 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; } /** * @abstract * @param {Object} ast * @param {String[]} retArr * @returns {String[]} */ astFunction(ast, retArr) { throw new Error(`"astFunction" not defined on ${ this.constructor.name }`); } /** * @desc Parses the abstract syntax tree for to its *named function declaration* * @param {Object} ast - the AST object to parse * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astFunctionDeclaration(ast, retArr) { if (this.isChildFunction(ast)) { return retArr; } return this.astFunction(ast, retArr); } astFunctionExpression(ast, retArr) { if (this.isChildFunction(ast)) { return retArr; } return this.astFunction(ast, retArr); } isChildFunction(ast) { for (let i = 0; i < this.functions.length; i++) { if (this.functions[i] === ast) { return true; } } return false; } astReturnStatement(ast, retArr) { return retArr; } astLiteral(ast, retArr) { this.literalTypes[this.astKey(ast)] = 'Number'; return retArr; } astBinaryExpression(ast, retArr) { return retArr; } astIdentifierExpression(ast, retArr) { return retArr; } astAssignmentExpression(ast, retArr) { return retArr; } /** * @desc Parses the abstract syntax tree for *generic expression* statement * @param {Object} esNode - An ast Node * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astExpressionStatement(esNode, retArr) { this.astGeneric(esNode.expression, retArr); retArr.push(';'); return retArr; } /** * @desc Parses the abstract syntax tree for an *Empty* Statement * @param {Object} eNode - An ast Node * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astEmptyStatement(eNode, retArr) { return retArr; } astBlockStatement(ast, retArr) { return retArr; } astIfStatement(ast, retArr) { return retArr; } astSwitchStatement(ast, retArr) { return retArr; } /** * @desc Parses the abstract syntax tree for *Break* Statement * @param {Object} brNode - An ast Node * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astBreakStatement(brNode, retArr) { retArr.push('break;'); return retArr; } /** * @desc Parses the abstract syntax tree for *Continue* Statement * @param {Object} crNode - An ast Node * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astContinueStatement(crNode, retArr) { retArr.push('continue;\n'); return retArr; } astForStatement(ast, retArr) { return retArr; } astWhileStatement(ast, retArr) { return retArr; } astDoWhileStatement(ast, retArr) { return retArr; } /** * @desc Parses the abstract syntax tree for *Variable Declarator* * @param {Object} iVarDecNode - An ast Node * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astVariableDeclarator(iVarDecNode, retArr) { this.astGeneric(iVarDecNode.id, retArr); if (iVarDecNode.init !== null) { retArr.push('='); this.astGeneric(iVarDecNode.init, retArr); } return retArr; } astThisExpression(ast, retArr) { return retArr; } astSequenceExpression(sNode, retArr) { const { expressions } = sNode; const sequenceResult = []; for (let i = 0; i < expressions.length; i++) { const expression = expressions[i]; const expressionResult = []; this.astGeneric(expression, expressionResult); sequenceResult.push(expressionResult.join('')); } if (sequenceResult.length > 1) { retArr.push('(', sequenceResult.join(','), ')'); } else { retArr.push(sequenceResult[0]); } return retArr; } /** * @desc Parses the abstract syntax tree for *Unary* Expression * @param {Object} uNode - An ast Node * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astUnaryExpression(uNode, retArr) { const unaryResult = this.checkAndUpconvertBitwiseUnary(uNode, retArr); if (unaryResult) { return retArr; } if (uNode.prefix) { retArr.push(uNode.operator); this.astGeneric(uNode.argument, retArr); } else { this.astGeneric(uNode.argument, retArr); retArr.push(uNode.operator); } return retArr; } checkAndUpconvertBitwiseUnary(uNode, retArr) {} /** * @desc Parses the abstract syntax tree for *Update* Expression * @param {Object} uNode - An ast Node * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astUpdateExpression(uNode, retArr) { if (uNode.prefix) { retArr.push(uNode.operator); this.astGeneric(uNode.argument, retArr); } else { this.astGeneric(uNode.argument, retArr); retArr.push(uNode.operator); } return retArr; } /** * @desc Parses the abstract syntax tree for *Logical* Expression * @param {Object} logNode - An ast Node * @param {Array} retArr - return array string * @returns {Array} the append retArr */ astLogicalExpression(logNode, retArr) { retArr.push('('); this.astGeneric(logNode.left, retArr); retArr.push(logNode.operator); this.astGeneric(logNode.right, retArr); retArr.push(')'); return retArr; } astMemberExpression(ast, retArr) { return retArr; } astCallExpression(ast, retArr) { return retArr; } astArrayExpression(ast, retArr) { return retArr; } /** * * @param ast * @return {IFunctionNodeMemberExpressionDetails} */ getMemberExpressionDetails(ast) { if (ast.type !== 'MemberExpression') { throw this.astErrorOutput(`Expression ${ ast.type } not a MemberExpression`, ast); } let name = null; let type = null; const variableSignature = this.getVariableSignature(ast); switch (variableSignature) { case 'value': return null; case 'value.thread.value': case 'this.thread.value': case 'this.output.value': return { signature: variableSignature, type: 'Integer', name: ast.property.name }; case 'value[]': if (typeof ast.object.name !== 'string') { throw this.astErrorOutput('Unexpected expression', ast); } name = ast.object.name; return { name, origin: 'user', signature: variableSignature, type: this.getVariableType(ast.object), xProperty: ast.property }; case 'value[][]': if (typeof ast.object.object.name !== 'string') { throw this.astErrorOutput('Unexpected expression', ast); } name = ast.object.object.name; return { name, origin: 'user', signature: variableSignature, type: this.getVariableType(ast.object.object), yProperty: ast.object.property, xProperty: ast.property, }; case 'value[][][]': if (typeof ast.object.object.object.name !== 'string') { throw this.astErrorOutput('Unexpected expression', ast); } name = ast.object.object.object.name; return { name, origin: 'user', signature: variableSignature, type: this.getVariableType(ast.object.object.object), zProperty: ast.object.object.property, yProperty: ast.object.property, xProperty: ast.property, }; case 'value[][][][]': if (typeof ast.object.object.object.object.name !== 'string') { throw this.astErrorOutput('Unexpected expression', ast); } name = ast.object.object.object.object.name; return { name, origin: 'user', signature: variableSignature, type: this.getVariableType(ast.object.object.object.object), zProperty: ast.object.object.property, yProperty: ast.object.property, xProperty: ast.property, }; case 'value.value': if (typeof ast.property.name !== 'string') { throw this.astErrorOutput('Unexpected expression', ast); } if (this.isAstMathVariable(ast)) { name = ast.property.name; return { name, origin: 'Math', type: 'Number', signature: variableSignature, }; } switch (ast.property.name) { case 'r': case 'g': case 'b': case 'a': name = ast.object.name; return { name, property: ast.property.name, origin: 'user', signature: variableSignature, type: 'Number' }; default: throw this.astErrorOutput('Unexpected expression', ast); } case 'this.constants.value': if (typeof ast.property.name !== 'string') { throw this.astErrorOutput('Unexpected expression', ast); } name = ast.property.name; type = this.getConstantType(name); if (!type) { throw this.astErrorOutput('Constant has no type', ast); } return { name, type, origin: 'constants', signature: variableSignature, }; case 'this.constants.value[]': if (typeof ast.object.property.name !== 'string') { throw this.astErrorOutput('Unexpected expression', ast); } name = ast.object.property.name; type = this.getConstantType(name); if (!type) { throw this.astErrorOutput('Constant has no type', ast); } return { name, type, origin: 'constants', signature: variableSignature, xProperty: ast.property, }; case 'this.constants.value[][]': { if (typeof ast.object.object.property.name !== 'string') { throw this.astErrorOutput('Unexpected expression', ast); } name = ast.object.object.property.name; type = this.getConstantType(name); if (!type) { throw this.astErrorOutput('Constant has no type', ast); } return { name, type, origin: 'constants', signature: variableSignature, yProperty: ast.object.property, xProperty: ast.property, }; } case 'this.constants.value[][][]': { if (typeof ast.object.object.object.property.name !== 'string') { throw this.astErrorOutput('Unexpected expression', ast); } name = ast.object.object.object.property.name; type = this.getConstantType(name); if (!type) { throw this.astErrorOutput('Constant has no type', ast); } return { name, type, origin: 'constants', signature: variableSignature, zProperty: ast.object.object.property, yProperty: ast.object.property, xProperty: ast.property, }; } case 'fn()[]': case 'fn()[][]': case '[][]': return { signature: variableSignature, property: ast.property, }; default: throw this.astErrorOutput('Unexpected expression', ast); } } findIdentifierOrigin(astToFind) { const stack = [this.ast]; while (stack.length > 0) { const atNode = stack[0]; if (atNode.type === 'VariableDeclarator' && atNode.id && atNode.id.name && atNode.id.name === astToFind.name) { return atNode; } stack.shift(); if (atNode.argument) { stack.push(atNode.argument); } else if (atNode.body) { stack.push(atNode.body); } else if (atNode.declarations) { stack.push(atNode.declarations); } else if (Array.isArray(atNode)) { for (let i = 0; i < atNode.length; i++) { stack.push(atNode[i]); } } } return null; } findLastReturn(ast) { const stack = [ast || this.ast]; while (stack.length > 0) { const atNode = stack.pop(); if (atNode.type === 'ReturnStatement') { return atNode; } if (atNode.type === 'FunctionDeclaration') { continue; } if (atNode.argument) { stack.push(atNode.argument); } else if (atNode.body) { stack.push(atNode.body); } else if (atNode.declarations) { stack.push(atNode.declarations); } else if (Array.isArray(atNode)) { for (let i = 0; i < atNode.length; i++) { stack.push(atNode[i]); } } else if (atNode.consequent) { stack.push(atNode.consequent); } else if (atNode.cases) { stack.push(atNode.cases); } } return null; } getInternalVariableName(name) { if (!this._internalVariableNames.hasOwnProperty(name)) { this._internalVariableNames[name] = 0; } this._internalVariableNames[name]++; if (this._internalVariableNames[name] === 1) { return name; } return name + this._internalVariableNames[name]; } astKey(ast, separator = ',') { if (!ast.start || !ast.end) throw new Error('AST start and end needed'); return `${ast.start}${separator}${ast.end}`; } } const typeLookupMap = { 'Number': 'Number', 'Float': 'Float', 'Integer': 'Integer', 'Array': 'Number', 'Array(2)': 'Number', 'Array(3)': 'Number', 'Array(4)': 'Number', 'Matrix(2)': 'Number', 'Matrix(3)': 'Number', 'Matrix(4)': 'Number', 'Array2D': 'Number', 'Array3D': 'Number', 'Input': 'Number', 'HTMLCanvas': 'Array(4)', 'OffscreenCanvas': 'Array(4)', 'HTMLImage': 'Array(4)', 'ImageBitmap': 'Array(4)', 'ImageData': 'Array(4)', 'HTMLVideo': 'Array(4)', 'HTMLImageArray': 'Array(4)', 'NumberTexture': 'Number', 'MemoryOptimizedNumberTexture': 'Number', 'Array1D(2)': 'Array(2)', 'Array1D(3)': 'Array(3)', 'Array1D(4)': 'Array(4)', 'Array2D(2)': 'Array(2)', 'Array2D(3)': 'Array(3)', 'Array2D(4)': 'Array(4)', 'Array3D(2)': 'Array(2)', 'Array3D(3)': 'Array(3)', 'Array3D(4)': 'Array(4)', 'ArrayTexture(1)': 'Number', 'ArrayTexture(2)': 'Array(2)', 'ArrayTexture(3)': 'Array(3)', 'ArrayTexture(4)': 'Array(4)', }; module.exports = { FunctionNode };