gpu.js
Version:
GPU Accelerated JavaScript
1,520 lines (1,437 loc) • 52 kB
JavaScript
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)