whole_func_string: function(expr, context, subIntegral) {
        if(expr.type === "OperatorNode" && expr.op === '*') {
          var argsAsPower = expr.args.map(getExprInPowerForm);
  
          // Collect common bases (this is O(n^2) worst case)
          var reducedArgs = argsAsPower.reduce(function(acc, exprPower) {
            for(var i = 0; i < acc.length; i++) {
              if(acc[i].base.equals(exprPower.base)) {
                acc[i].power = new OperatorNode('+', 'add', [
                  acc[i].power,
                  exprPower.power
                ]);
                return acc;
              }
            }
  
            // Did not share a common base with any other factor so far
            acc.push(exprPower);
            return acc;
          }, []);
  
          if(reducedArgs.length < expr.args.length) {
            // We combined some things
            var reducedExpr = powerFactorsToNode(reducedArgs);
  
            return subIntegral(reducedExpr, context, "combining powers");
          }
        }
  
        function getExprInPowerForm(expr) {
          if(expr.type === "OperatorNode" && expr.op === '^') {
            return {
              base: expr.args[0],
              power: expr.args[1]
            };
          } else {
            return {
              base: expr,
              power: new ConstantNode(1)
            };
          }
        }
  
        function powerFactorsToNode(factors) {
          if(factors.length === 1) {
            return powerToNode(factors[0]);
          } else {
            return new OperatorNode('*', 'multiply', factors.map(powerToNode));
          }
  
          function powerToNode(powerExpr) {
            return new OperatorNode('^', 'pow', [powerExpr.base, powerExpr.power]);
          }
        }
      }
func_code_string: function(expr, context, subIntegral) {
        if(expr.type === "OperatorNode" && expr.op === '*') {
          var argsAsPower = expr.args.map(getExprInPowerForm);
  
          // Collect common bases (this is O(n^2) worst case)
          var reducedArgs = argsAsPower.reduce(function(acc, exprPower) {
            for(var i = 0; i < acc.length; i++) {
              if(acc[i].base.equals(exprPower.base)) {
                acc[i].power = new OperatorNode('+', 'add', [
                  acc[i].power,
                  exprPower.power
                ]);
                return acc;
              }
            }
  
            // Did not share a common base with any other factor so far
            acc.push(exprPower);
            return acc;
          }, []);
  
          if(reducedArgs.length < expr.args.length) {
            // We combined some things
            var reducedExpr = powerFactorsToNode(reducedArgs);
  
            return subIntegral(reducedExpr, context, "combining powers");
          }
        }
  
        function getExprInPowerForm(expr) {
          if(expr.type === "OperatorNode" && expr.op === '^') {
            return {
              base: expr.args[0],
              power: expr.args[1]
            };
          } else {
            return {
              base: expr,
              power: new ConstantNode(1)
            };
          }
        }
  
        function powerFactorsToNode(factors) {
          if(factors.length === 1) {
            return powerToNode(factors[0]);
          } else {
            return new OperatorNode('*', 'multiply', factors.map(powerToNode));
          }
  
          function powerToNode(powerExpr) {
            return new OperatorNode('^', 'pow', [powerExpr.base, powerExpr.power]);
          }
        }
      }
func_documentation_string: integral(f(x)^g(x) * f(x)^h(x), x) = integral(f(x)^(g(x)+h(x)), x)