text: 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;
          case 'value.value[]':
          case 'value.value[][]':
            if (this.removeIstanbulCoverage) {
              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 'HTMLImage':
        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 'HTMLImage':
            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) {
        if (i > 0) {
          retArr.push(', ');
        }
        const subNode = arrNode.elements[i];
        this.astGeneric(subNode, retArr)
      }
      retArr.push(')');
  
      return retArr;
    }
  
    memberExpressionXYZ(x, y, z, retArr) {
      if (z) {
        retArr.push(this.memberExpressionPropertyMarkup(z), ', ');
      } else {
        retArr.push('0, ');
      }
      if (y) {
        retArr.push(this.memberExpressionPropertyMarkup(y), ', ');
      } else {
        retArr.push('0, ');
      }
      retArr.push(this.memberExpressionPropertyMarkup(x));
      return retArr;
    }
  
    memberExpressionPropertyMarkup(property) {
      if (!property) {
        throw new Error('Property not set');
      }
      const type = this.getType(property);
      const result = [];
      switch (type) {
        case 'Number':
        case 'Float':
          this.castValueToInteger(property, result);
          break;
        case 'LiteralInteger':
          this.castLiteralToInteger(property, result);
          break;
        default:
          this.astGeneric(property, result);
      }
      return result.join('');
    }
  }
  
  const typeMap = {
    'Array': 'sampler2D',
    'Array(2)': 'vec2',
    'Array(3)': 'vec3',
    'Array(4)': 'vec4',
    'Matrix(2)': 'mat2',
    'Matrix(3)': 'mat3',
    'Matrix(4)': 'mat4',
    'Array2D': 'sampler2D',
    'Array3D': 'sampler2D',
    'Boolean': 'bool',
    'Float': 'float',
    'Input': 'sampler2D',
    'Integer': 'int',
    'Number': 'float',
    'LiteralInteger': 'float',
    'NumberTexture': 'sampler2D',
    'MemoryOptimizedNumberTexture': 'sampler2D',
    'ArrayTexture(1)': 'sampler2D',
    'ArrayTexture(2)': 'sampler2D',
    'ArrayTexture(3)': 'sampler2D',
    'ArrayTexture(4)': 'sampler2D',
    'HTMLVideo': 'sampler2D',
    'HTMLCanvas': 'sampler2D',
    'HTMLImage': 'sampler2D',
    'HTMLImageArray': 'sampler2DArray',
  };
  
  const operatorMap = {
    '===': '==',
    '!==': '!='
  };
  
  module.exports = {
    WebGLFunctionNode
  };