getType(ast) {
    if (Array.isArray(ast)) {
      return this.getType(ast[ast.length - 1]);
    }
    switch (ast.type) {
      case 'BlockStatement':
        return this.getType(ast.body);
      case 'ArrayExpression':
        const childType = this.getType(ast.elements[0]);
        switch (childType) {
          case 'Array(2)':
          case 'Array(3)':
          case 'Array(4)':
            return `Matrix(${ast.elements.length})`;
        }
        return `Array(${ ast.elements.length })`;
      case 'Literal':
        const literalKey = this.astKey(ast);
        if (this.literalTypes[literalKey]) {
          return this.literalTypes[literalKey];
        }
        if (Number.isInteger(ast.value)) {
          return 'LiteralInteger';
        } else if (ast.value === true || ast.value === false) {
          return 'Boolean';
        } else {
          return 'Number';
        }
      case 'AssignmentExpression':
        return this.getType(ast.left);
      case 'CallExpression':
        if (this.isAstMathFunction(ast)) {
          return 'Number';
        }
        if (!ast.callee || !ast.callee.name) {
          if (ast.callee.type === 'SequenceExpression' && ast.callee.expressions[ast.callee.expressions.length - 1].property.name) {
            const functionName = ast.callee.expressions[ast.callee.expressions.length - 1].property.name;
            this.inferArgumentTypesIfNeeded(functionName, ast.arguments);
            return this.lookupReturnType(functionName, ast, this);
          }
          if (this.getVariableSignature(ast.callee, true) === 'this.color') {
            return null;
          }
          if (ast.callee.type === 'MemberExpression' && ast.callee.object && ast.callee.property && ast.callee.property.name && ast.arguments) {
            const functionName = ast.callee.property.name;
            this.inferArgumentTypesIfNeeded(functionName, ast.arguments);
            return this.lookupReturnType(functionName, ast, this);
          }
          throw this.astErrorOutput('Unknown call expression', ast);
        }
        if (ast.callee && ast.callee.name) {
          const functionName = ast.callee.name;
          this.inferArgumentTypesIfNeeded(functionName, ast.arguments);
          return this.lookupReturnType(functionName, ast, this);
        }
        throw this.astErrorOutput(`Unhandled getType Type "${ ast.type }"`, ast);
      case 'LogicalExpression':
        return 'Boolean';
      case 'BinaryExpression':
        // modulos is Number
        switch (ast.operator) {
          case '%':
          case '/':
            if (this.fixIntegerDivisionAccuracy) {
              return 'Number';
            } else {
              break;
            }
          case '>':
          case '<':
            return 'Boolean';
          case '&':
          case '|':
          case '^':
          case '<<':
          case '>>':
          case '>>>':
            return 'Integer';
        }
        const type = this.getType(ast.left);
        if (this.isState('skip-literal-correction')) return type;
        if (type === 'LiteralInteger') {
          const rightType = this.getType(ast.right);
          if (rightType === 'LiteralInteger') {
            if (ast.left.value % 1 === 0) {
              return 'Integer';
            } else {
              return 'Float';
            }
          }
          return rightType;
        }
        return typeLookupMap[type] || type;
      case 'UpdateExpression':
        return this.getType(ast.argument);
      case 'UnaryExpression':
        if (ast.operator === '~') {
          return 'Integer';
        }
        return this.getType(ast.argument);
      case 'VariableDeclaration': {
        const declarations = ast.declarations;
        let lastType;
        for (let i = 0; i < declarations.length; i++) {
          const declaration = declarations[i];
          lastType = this.getType(declaration);
        }
        if (!lastType) {
          throw this.astErrorOutput(`Unable to find type for declaration`, ast);
        }
        return lastType;
      }
      case 'VariableDeclarator':
        const declaration = this.getDeclaration(ast.id);
        if (!declaration) {
          throw this.astErrorOutput(`Unable to find declarator`, ast);
        }

        if (!declaration.valueType) {
          throw this.astErrorOutput(`Unable to find declarator valueType`, ast);
        }

        return declaration.valueType;
      case 'Identifier':
        if (ast.name === 'Infinity') {
          return 'Number';
        }
        if (this.isAstVariable(ast)) {
          const signature = this.getVariableSignature(ast);
          if (signature === 'value') {
            return this.getCheckVariableType(ast);
          }
        }
        const origin = this.findIdentifierOrigin(ast);
        if (origin && origin.init) {
          return this.getType(origin.init);
        }
        return null;
      case 'ReturnStatement':
        return this.getType(ast.argument);
      case 'MemberExpression':
        if (this.isAstMathFunction(ast)) {
          switch (ast.property.name) {
            case 'ceil':
              return 'Integer';
            case 'floor':
              return 'Integer';
            case 'round':
              return 'Integer';
          }
          return 'Number';
        }
        if (this.isAstVariable(ast)) {
          const variableSignature = this.getVariableSignature(ast);
          switch (variableSignature) {
            case 'value[]':
              return this.getLookupType(this.getCheckVariableType(ast.object));
            case 'value[][]':
              return this.getLookupType(this.getCheckVariableType(ast.object.object));
            case 'value[][][]':
              return this.getLookupType(this.getCheckVariableType(ast.object.object.object));
            case 'value[][][][]':
              return this.getLookupType(this.getCheckVariableType(ast.object.object.object.object));
            case 'value.thread.value':
            case 'this.thread.value':
              return 'Integer';
            case 'this.output.value':
              return this.dynamicOutput ? 'Integer' : 'LiteralInteger';
            case 'this.constants.value':
              return this.getConstantType(ast.property.name);
            case 'this.constants.value[]':
              return this.getLookupType(this.getConstantType(ast.object.property.name));
            case 'this.constants.value[][]':
              return this.getLookupType(this.getConstantType(ast.object.object.property.name));
            case 'this.constants.value[][][]':
              return this.getLookupType(this.getConstantType(ast.object.object.object.property.name));
            case 'this.constants.value[][][][]':
              return this.getLookupType(this.getConstantType(ast.object.object.object.object.property.name));
            case 'fn()[]':
            case 'fn()[][]':
            case 'fn()[][][]':
              return this.getLookupType(this.getType(ast.object));
            case 'value.value':
              if (this.isAstMathVariable(ast)) {
                return 'Number';
              }
              switch (ast.property.name) {
                case 'r':
                case 'g':
                case 'b':
                case 'a':
                  return this.getLookupType(this.getCheckVariableType(ast.object));
              }
            case '[][]':
              return 'Number';
          }
          throw this.astErrorOutput('Unhandled getType MemberExpression', ast);
        }
        throw this.astErrorOutput('Unhandled getType MemberExpression', ast);
      case 'ConditionalExpression':
        return this.getType(ast.consequent);
      case 'FunctionDeclaration':
      case 'FunctionExpression':
        const lastReturn = this.findLastReturn(ast.body);
        if (lastReturn) {
          return this.getType(lastReturn);
        }
        return null;
      case 'IfStatement':
        return this.getType(ast.consequent);
      case 'SequenceExpression':
        return this.getType(ast.expressions[ast.expressions.length - 1]);
      default:
        throw this.astErrorOutput(`Unhandled getType Type "${ ast.type }"`, ast);
    }
  }
Recursively looks up type for ast expression until it's found
@param ast
@returns {String|null}