Repository: josdejong/mathjs (js)

Issue:
Additional arguments of OperatorNode's are silently ignored in derivative.
`OperatorNode`'s that only support two arguments (such as '-', '/' or '^') silently drop any additional arguments when calculating the derivative.

The `derivative` function should throw an error instead.

See also #1003 

Pull request:
Implemented OperatorNode.isUnary and OperatorNode.isBinary
This PR:

- Implements two new methods for `OperatorNode`: `isUnary()`  `isBinary()`
- Adds explicit checks for number of arguments everywhere working with `OperatorNode` and `FunctionNode`.
- Fixed #1014: derivative silently ignoring additional arguments.

@HarrySarson can you review this PR? 

Whilst implementing these changes, I saw that the exact same situation with dropping additional nodes from an OperatorNode can possibly happen with FunctionNodes, which has a list with arguments too. `derivative` has checks for that (see `funcArgsCheck`), but this is a bit indirect and could forget cases. Therefore I added explicit checks for function arguments too everywhere in `derivative`.

Related discussion: #1025 

Patch:
diff --git a/HISTORY.md b/HISTORY.md
index f372d9579..da6b74f0a 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -49,7 +49,11 @@ Breaking changes:
 
 Non breaking changes:
 
+- Implemented additional methods `isUnary()` and `isBinary()` on
+  `OperatorNode`. See #1025.
 - Improved error messages for statistical functions.
+- Fixed #1014: `derivative` silently dropping additional arguments
+  from operator nodes with more than two arguments.
 
 
 ## not-yet-released, version 3.20.2
diff --git a/docs/expressions/expression_trees.md b/docs/expressions/expression_trees.md
index 0b80b09b7..13db3a3b0 100644
--- a/docs/expressions/expression_trees.md
+++ b/docs/expressions/expression_trees.md
@@ -528,7 +528,7 @@ var node1 = math.parse('{a: 1, b: 2, c: 3}');
 var a = new math.expression.node.ConstantNode(1);
 var b = new math.expression.node.ConstantNode(2);
 var c = new math.expression.node.ConstantNode(3);
-var node2  = new math.expression.node.ObjectNode({a: a, b: b, c: c});
+var node2 = new math.expression.node.ObjectNode({a: a, b: b, c: c});
 ```
 
 
@@ -540,6 +540,31 @@ Construction:
 new OperatorNode(op: string, fn: string, args: Node[])
 ```
 
+Additional methods:
+
+-   `isUnary() : boolean`
+
+    Returns true when the `OperatorNode` contains exactly one argument,
+    like with a unary minus:
+
+    ```js
+    var a = new math.expression.node.ConstantNode(2);
+    var b = new math.expression.node.OperatorNode('-', 'unaryMinus', [a]);`
+    b.isUnary(); // true
+    ```
+
+-   `isBinary() : boolean`
+
+    Returns true when the `OperatorNode` contains exactly two arguments,
+    like with most regular operators:
+
+    ```js
+    var a = new math.expression.node.ConstantNode(2);
+    var b = new math.expression.node.ConstantNode(3);
+    var c = new math.expression.node.OperatorNode('+', 'add', [a, b]);`
+    c.isBinary(); // true
+    ```
+
 Properties:
 
 - `op: string`
diff --git a/lib/expression/node/OperatorNode.js b/lib/expression/node/OperatorNode.js
index 276b29ecb..adc0bcbe9 100644
--- a/lib/expression/node/OperatorNode.js
+++ b/lib/expression/node/OperatorNode.js
@@ -131,6 +131,24 @@ function factory (type, config, load, typed) {
     return new OperatorNode(this.op, this.fn, this.args.slice(0), this.implicit);
   };
 
+  /**
+   * Check whether this is an unary OperatorNode:
+   * has exactly one argument, like `-a`.
+   * @return {boolean} Returns true when an unary operator node, false otherwise.
+   */
+  OperatorNode.prototype.isUnary = function () {
+    return this.args.length === 1;
+  };
+
+  /**
+   * Check whether this is a binary OperatorNode:
+   * has exactly two arguments, like `a + b`.
+   * @return {boolean} Returns true when a binary operator node, false otherwise.
+   */
+  OperatorNode.prototype.isBinary = function () {
+    return this.args.length === 2;
+  };
+
   /**
    * Calculate which parentheses are necessary. Gets an OperatorNode
    * (which is the root of the tree) and an Array of Nodes
diff --git a/lib/function/algebra/derivative.js b/lib/function/algebra/derivative.js
index bcb7c2476..d1d15bc60 100644
--- a/lib/function/algebra/derivative.js
+++ b/lib/function/algebra/derivative.js
@@ -163,7 +163,7 @@ function factory (type, config, load, typed) {
     },
 
     'Object, FunctionNode | OperatorNode, string': function (constNodes, node, varName) {
-      if (node.args.length !== 0) {
+      if (node.args.length > 0) {
         var isConst = constTag(constNodes, node.args[0], varName);
         for (var i = 1; i < node.args.length; ++i) {
           isConst = constTag(constNodes, node.args[i], varName) && isConst;
@@ -247,19 +247,20 @@ function factory (type, config, load, typed) {
               createConstantNode(2),
               new FunctionNode('sqrt', [arg1])
             ]);
-            break;
           }
+          else if (node.args.length === 2) {
+            // Rearrange from nthRoot(x, a) -> x^(1/a)
+            arg2 = new OperatorNode('/', 'divide', [
+              createConstantNode(1),
+              node.args[1]
+            ]);
 
-          // Rearrange from nthRoot(x, a) -> x^(1/a)
-          arg2 = new OperatorNode('/', 'divide', [
-            createConstantNode(1),
-            node.args[1]
-          ]);
-
-          // Is a variable?
-          constNodes[arg2] = constNodes[node.args[1]];
+            // Is a variable?
+            constNodes[arg2] = constNodes[node.args[1]];
 
-          return _derivative(new OperatorNode('^', 'pow', [arg1, arg2]), constNodes);
+            return _derivative(new OperatorNode('^', 'pow', [arg1, arg2]), constNodes);
+          }
+          break;
         case 'log10':
           arg2 = createConstantNode(10);
           /* fall through! */
@@ -267,21 +268,22 @@ function factory (type, config, load, typed) {
           if (!arg2 && node.args.length === 1) {
             // d/dx(log(x)) = 1 / x
             funcDerivative = arg1.clone();
-          } else if (arg2 || constNodes[node.args[1]] !== undefined) {
+            div = true;
+          } else if ((node.args.length === 1 && arg2) ||
+              (node.args.length === 2 && constNodes[node.args[1]] !== undefined)) {
             // d/dx(log(x, c)) = 1 / (x*ln(c))
             funcDerivative = new OperatorNode('*', 'multiply', [
               arg1.clone(),
               new FunctionNode('log', [arg2 || node.args[1]])
             ]);
-          } else {
+            div = true;
+          } else if (node.args.length === 2) {
             // d/dx(log(f(x), g(x))) = d/dx(log(f(x)) / log(g(x)))
             return _derivative(new OperatorNode('/', 'divide', [
                 new FunctionNode('log', [arg1]),
                 new FunctionNode('log', [node.args[1]])
               ]), constNodes);
           }
-
-          div = true;
           break;
         case 'exp':
           // d/dx(e^x) = e^x
@@ -540,7 +542,7 @@ function factory (type, config, load, typed) {
           ]);
           break;
         case 'gamma':  // Needs digamma function, d/dx(gamma(x)) = gamma(x)digamma(x)
-        default: throw new Error('Function "' + node.name + '" not supported by derivative');
+        default: throw new Error('Function "' + node.name + '" is not supported by derivative, or a wrong number of arguments is passed');
       }
 
       var op, func;
@@ -567,96 +569,108 @@ function factory (type, config, load, typed) {
         return createConstantNode(0);
       }
 
-      var arg1 = node.args[0];
-      var arg2 = node.args[1];
+      if (node.op === '+') {
+        // d/dx(sum(f(x)) = sum(f'(x))
+        return new OperatorNode(node.op, node.fn, node.args.map(function(arg) {
+          return _derivative(arg, constNodes);
+        }));
+      }
 
-      switch (node.op) {
-        case '+':
-          // d/dx(sum(f(x)) = sum(f'(x))
-          return new OperatorNode(node.op, node.fn, node.args.map(function(arg) {
-            return _derivative(arg, constNodes);
-          }));
-        case '-':
-          // d/dx(+/-f(x)) = +/-f'(x)
-          if (node.args.length === 1) {
-            return new OperatorNode(node.op, node.fn, [_derivative(arg1, constNodes)]);
-          }
+      if (node.op === '-') {
+        // d/dx(+/-f(x)) = +/-f'(x)
+        if (node.isUnary()) {
+          return new OperatorNode(node.op, node.fn, [_derivative(arg1, constNodes)]);
+        }
 
-          // Linearity of differentiation, d/dx(f(x) +/- g(x)) = f'(x) +/- g'(x)
+        // Linearity of differentiation, d/dx(f(x) +/- g(x)) = f'(x) +/- g'(x)
+        if (node.isBinary()) {
           return new OperatorNode(node.op, node.fn, [
-            _derivative(arg1, constNodes),
-            _derivative(arg2, constNodes)
+            _derivative(node.args[0], constNodes),
+            _derivative(node.args[1], constNodes)
           ]);
-        case '*':
-          // d/dx(c*f(x)) = c*f'(x)
-          var constantTerms = node.args.filter(function(arg) {
-            return constNodes[arg] !== undefined;
-          });
+        }
+      }
 
-          if (constantTerms.length > 0) {
-            var nonConstantTerms = node.args.filter(function(arg) {
-              return constNodes[arg] === undefined;
-            });
+      if (node.op === '*') {
+        // d/dx(c*f(x)) = c*f'(x)
+        var constantTerms = node.args.filter(function(arg) {
+          return constNodes[arg] !== undefined;
+        });
 
-            var nonConstantNode = nonConstantTerms.length === 1
+        if (constantTerms.length > 0) {
+          var nonConstantTerms = node.args.filter(function(arg) {
+            return constNodes[arg] === undefined;
+          });
+
+          var nonConstantNode = nonConstantTerms.length === 1
               ? nonConstantTerms[0]
               : new OperatorNode('*', 'multiply', nonConstantTerms);
 
-            var newArgs = constantTerms.concat(_derivative(nonConstantNode, constNodes));
+          var newArgs = constantTerms.concat(_derivative(nonConstantNode, constNodes));
 
-            return new OperatorNode('*', 'multiply', newArgs);
-          }
+          return new OperatorNode('*', 'multiply', newArgs);
+        }
 
-          // Product Rule, d/dx(f(x)*g(x)) = f'(x)*g(x) + f(x)*g'(x)
-          return new OperatorNode('+', 'add', node.args.map(function(argOuter) {
-            return new OperatorNode('*', 'multiply', node.args.map(function(argInner) {
-              return (argInner === argOuter)
+        // Product Rule, d/dx(f(x)*g(x)) = f'(x)*g(x) + f(x)*g'(x)
+        return new OperatorNode('+', 'add', node.args.map(function(argOuter) {
+          return new OperatorNode('*', 'multiply', node.args.map(function(argInner) {
+            return (argInner === argOuter)
                 ? _derivative(argInner, constNodes)
                 : argInner.clone();
-            }));
           }));
-        case '/':
-          // d/dx(f(x) / c) = f'(x) / c
-          if (constNodes[arg2] !== undefined) {
-            return new OperatorNode('/', 'divide', [_derivative(arg1, constNodes), arg2]);
-          }
+        }));
+      }
 
-          // Reciprocal Rule, d/dx(c / f(x)) = -c(f'(x)/f(x)^2)
-          if (constNodes[arg1] !== undefined) {
-            return new OperatorNode('*', 'multiply', [
-              new OperatorNode('-', 'unaryMinus', [arg1]),
-              new OperatorNode('/', 'divide', [
-                _derivative(arg2, constNodes),
-                new OperatorNode('^', 'pow', [arg2.clone(), createConstantNode(2)])
-              ])
-            ]);
-          }
+      if (node.op === '/' && node.isBinary()) {
+        var arg1 = node.args[0];
+        var arg2 = node.args[1];
 
-          // Quotient rule, d/dx(f(x) / g(x)) = (f'(x)g(x) - f(x)g'(x)) / g(x)^2
-          return new OperatorNode('/', 'divide', [
-            new OperatorNode('-', 'subtract', [
-              new OperatorNode('*', 'multiply', [_derivative(arg1, constNodes), arg2.clone()]),
-              new OperatorNode('*', 'multiply', [arg1.clone(), _derivative(arg2, constNodes)])
-            ]),
-            new OperatorNode('^', 'pow', [arg2.clone(), createConstantNode(2)])
+        // d/dx(f(x) / c) = f'(x) / c
+        if (constNodes[arg2] !== undefined) {
+          return new OperatorNode('/', 'divide', [_derivative(arg1, constNodes), arg2]);
+        }
+
+        // Reciprocal Rule, d/dx(c / f(x)) = -c(f'(x)/f(x)^2)
+        if (constNodes[arg1] !== undefined) {
+          return new OperatorNode('*', 'multiply', [
+            new OperatorNode('-', 'unaryMinus', [arg1]),
+            new OperatorNode('/', 'divide', [
+              _derivative(arg2, constNodes),
+              new OperatorNode('^', 'pow', [arg2.clone(), createConstantNode(2)])
+            ])
           ]);
-        case '^':
-          if (constNodes[arg1] !== undefined) {
-            // If is secretly constant; 0^f(x) = 1 (in JS), 1^f(x) = 1
-            if (type.isConstantNode(arg1) && (isZero(arg1.value) || equal(arg1.value, 1))) {
-              return createConstantNode(0);
-            }
+        }
 
-            // d/dx(c^f(x)) = c^f(x)*ln(c)*f'(x)
-            return new OperatorNode('*', 'multiply', [
-              node,
-              new OperatorNode('*', 'multiply', [
-                new FunctionNode('log', [arg1.clone()]),
-                _derivative(arg2.clone(), constNodes)
-              ])
-            ]);
+        // Quotient rule, d/dx(f(x) / g(x)) = (f'(x)g(x) - f(x)g'(x)) / g(x)^2
+        return new OperatorNode('/', 'divide', [
+          new OperatorNode('-', 'subtract', [
+            new OperatorNode('*', 'multiply', [_derivative(arg1, constNodes), arg2.clone()]),
+            new OperatorNode('*', 'multiply', [arg1.clone(), _derivative(arg2, constNodes)])
+          ]),
+          new OperatorNode('^', 'pow', [arg2.clone(), createConstantNode(2)])
+        ]);
+      }
+
+      if (node.op === '^' && node.isBinary()) {
+        var arg1 = node.args[0];
+        var arg2 = node.args[1];
+
+        if (constNodes[arg1] !== undefined) {
+          // If is secretly constant; 0^f(x) = 1 (in JS), 1^f(x) = 1
+          if (type.isConstantNode(arg1) && (isZero(arg1.value) || equal(arg1.value, 1))) {
+            return createConstantNode(0);
           }
 
+          // d/dx(c^f(x)) = c^f(x)*ln(c)*f'(x)
+          return new OperatorNode('*', 'multiply', [
+            node,
+            new OperatorNode('*', 'multiply', [
+              new FunctionNode('log', [arg1.clone()]),
+              _derivative(arg2.clone(), constNodes)
+            ])
+          ]);
+        }
+
           if (constNodes[arg2] !== undefined) {
             if (type.isConstantNode(arg2)) {
               // If is secretly constant; f(x)^0 = 1 -> d/dx(1) = 0
@@ -664,7 +678,7 @@ function factory (type, config, load, typed) {
                 return createConstantNode(0);
               }
               // Ignore exponent; f(x)^1 = f(x)
-              if (equal(arg2.value, 1)) {
+              if (equal(arg2.value,1)) {
                 return _derivative(arg1, constNodes);
               }
             }
@@ -678,33 +692,32 @@ function factory (type, config, load, typed) {
               ])
             ]);
 
-            return new OperatorNode('*', 'multiply', [
-              arg2.clone(),
-              new OperatorNode('*', 'multiply', [
-                _derivative(arg1, constNodes),
-                powMinusOne
-              ])
-            ]);
-          }
-
-          // Functional Power Rule, d/dx(f^g) = f^g*[f'*(g/f) + g'ln(f)]
           return new OperatorNode('*', 'multiply', [
-            new OperatorNode('^', 'pow', [arg1.clone(), arg2.clone()]),
-            new OperatorNode('+', 'add', [
-              new OperatorNode('*', 'multiply', [
-                _derivative(arg1, constNodes),
-                new OperatorNode('/', 'divide', [arg2.clone(), arg1.clone()])
-              ]),
-              new OperatorNode('*', 'multiply', [
-                _derivative(arg2, constNodes),
-                new FunctionNode('log', [arg1.clone()])
-              ])
+            arg2.clone(),
+            new OperatorNode('*', 'multiply', [
+              _derivative(arg1, constNodes),
+              powMinusOne
             ])
           ]);
-        case '%':
-        case 'mod':
-        default: throw new Error('Operator "' + node.op + '" not supported by derivative');
+        }
+
+        // Functional Power Rule, d/dx(f^g) = f^g*[f'*(g/f) + g'ln(f)]
+        return new OperatorNode('*', 'multiply', [
+          new OperatorNode('^', 'pow', [arg1.clone(), arg2.clone()]),
+          new OperatorNode('+', 'add', [
+            new OperatorNode('*', 'multiply', [
+              _derivative(arg1, constNodes),
+              new OperatorNode('/', 'divide', [arg2.clone(), arg1.clone()])
+            ]),
+            new OperatorNode('*', 'multiply', [
+              _derivative(arg2, constNodes),
+              new FunctionNode('log', [arg1.clone()])
+            ])
+          ])
+        ]);
       }
+
+      throw new Error('Operator "' + node.op + '" is not supported by derivative, or a wrong number of arguments is passed');
     }
   });
 
diff --git a/lib/function/algebra/rationalize.js b/lib/function/algebra/rationalize.js
index ede1fafa9..2d989c5ec 100644
--- a/lib/function/algebra/rationalize.js
+++ b/lib/function/algebra/rationalize.js
@@ -131,7 +131,7 @@ function factory (type, config, load, typed) {
       var coefficients=[];
       var retRationalize = {};
 
-      if (expr.type==='OperatorNode'  &&  expr.op==='/')  {   // Separate numerator from denominator
+      if (expr.type === 'OperatorNode' && expr.isBinary() && expr.op === '/')  {   // Separate numerator from denominator
           if (nVars==1)  {
              expr.args[0] = polyToCanonical(expr.args[0],coefficients);
              expr.args[1] = polyToCanonical(expr.args[1]);
@@ -210,7 +210,7 @@ function factory (type, config, load, typed) {
       if (tp==='FunctionNode') 
         throw new ArgumentsError('There is an unsolved function call')   // No function call in polynomial expression
       else if (tp==='OperatorNode')  {
-        if (node.op==='^')  {
+        if (node.op === '^' && node.isBinary())  {
           if (node.args[1].type!=='ConstantNode' ||  ! number.isInteger(parseFloat(node.args[1].value)))
             throw new ArgumentsError('There is a non-integer exponent');
           else
@@ -349,7 +349,7 @@ function factory (type, config, load, typed) {
     var tp = node.type; 
     var internal = (arguments.length>1)   // TRUE in internal calls
 
-    if (tp==='OperatorNode') { 
+    if (tp === 'OperatorNode' && node.isBinary()) {
       var does = false;
       if (node.op==='^')  {   // First operator: Parenthesis or UnaryMinus
         if ( ( node.args[0].type==='ParenthesisNode' ||  
@@ -392,7 +392,7 @@ function factory (type, config, load, typed) {
           else
             parent.args[indParent] = node
       } // does
-    } // Operator Node
+    } // binary OperatorNode
 
     if (tp==='ParenthesisNode' )  // Recursion 
            expandPower(node.content,node,'content');  
diff --git a/lib/function/algebra/simplify/simplifyConstant.js b/lib/function/algebra/simplify/simplifyConstant.js
index 8950e26bf..33548c017 100644
--- a/lib/function/algebra/simplify/simplifyConstant.js
+++ b/lib/function/algebra/simplify/simplifyConstant.js
@@ -189,7 +189,7 @@ function factory(type, config, load, typed, math) {
         var args;
         var res;
         var makeNode = createMakeNodeFunction(node);
-        if (node.args.length === 1) {
+        if (node.isUnary()) {
           args = [foldFraction(node.args[0])];
           if (!type.isNode(args[0])) {
             res = _eval(fn, args);
diff --git a/lib/function/algebra/simplify/simplifyCore.js b/lib/function/algebra/simplify/simplifyCore.js
index a1ca57424..68de37102 100644
--- a/lib/function/algebra/simplify/simplifyCore.js
+++ b/lib/function/algebra/simplify/simplifyCore.js
@@ -42,13 +42,29 @@ function factory(type, config, load, typed, math) {
    *     The expression to be simplified
    */
   function simplifyCore(node) {
-    if (type.isOperatorNode(node) && node.args.length <= 2) {
+    if (type.isOperatorNode(node) && node.isUnary()) {
       var a0 = simplifyCore(node.args[0]);
-      var a1 = node.args[1] && simplifyCore(node.args[1]);
-      if (node.op === "+") {
-          if (node.args.length === 1) {
-            return node.args[0];
+
+      if (node.op === '+') { // unary plus
+        return a0;
+      }
+
+      if (node.op === '-') { // unary minus
+        if (type.isOperatorNode(a0)) {
+          if (a0.isUnary() && a0.op === '-') {
+            return a0.args[0];
+          } else if (a0.isBinary() && a0.fn === 'subtract') {
+            return new OperatorNode('-', 'subtract', [a0.args[1], a0.args[0]]);
           }
+        }
+        return new OperatorNode(node.op, node.fn, [a0]);
+      }
+    }
+    else if (type.isOperatorNode(node) && node.isBinary()) {
+      var a0 = simplifyCore(node.args[0]);
+      var a1 = simplifyCore(node.args[1]);
+
+      if (node.op === "+") {
           if (type.isConstantNode(a0)) {
               if (isZero(a0.value)) {
                   return a1;
@@ -59,7 +75,7 @@ function factory(type, config, load, typed, math) {
           if (type.isConstantNode(a1) && isZero(a1.value)) {
               return a0;
           }
-          if (node.args.length === 2 && type.isOperatorNode(a1) && a1.op === '-' && a1.fn === 'unaryMinus') {
+          if (type.isOperatorNode(a1) && a1.isUnary() && a1.op === '-') {
               return new OperatorNode('-', 'subtract', [a0,a1.args[0]]);
           }
           return new OperatorNode(node.op, node.fn, a1 ? [a0,a1] : [a0]);
@@ -71,25 +87,16 @@ function factory(type, config, load, typed, math) {
                   return new OperatorNode("-", "unaryMinus", [a1]);
               }
           }
-          if (node.fn === "subtract" && node.args.length === 2) {
+          // if (node.fn === "subtract" && node.args.length === 2) {
+          if (node.fn === "subtract") {
               if (type.isConstantNode(a1) && isZero(a1.value)) {
                   return a0;
               }
-              if (type.isOperatorNode(a1) && a1.fn === "unaryMinus") {
+              if (type.isOperatorNode(a1) && a1.isUnary() && a1.op === '-') {
                   return simplifyCore(new OperatorNode("+", "add", [a0, a1.args[0]]));
               }
               return new OperatorNode(node.op, node.fn, [a0,a1]);
-          } else if (node.fn === "unaryMinus") {
-              if (type.isOperatorNode(a0)) {
-                  if (a0.fn === 'unaryMinus') {
-                      return a0.args[0];
-                  } else if (a0.fn === 'subtract') {
-                      return new OperatorNode('-', 'subtract', [a0.args[1], a0.args[0]]);
-                  }
-              }
-              return new OperatorNode(node.op, node.fn, [a0]);
           }
-          throw new Error('never happens');
       } else if (node.op === "*") {
           if (type.isConstantNode(a0)) {
               if (isZero(a0.value)) {
@@ -105,7 +112,7 @@ function factory(type, config, load, typed, math) {
                   return node0;
               } else if (equal(a1.value, 1)) {
                   return a0;
-              } else if (type.isOperatorNode(a0) && a0.op === node.op) {
+              } else if (type.isOperatorNode(a0) && a0.isBinary() && a0.op === node.op) {
                   var a00 = a0.args[0];
                   if (type.isConstantNode(a00)) {
                       var a00_a1 =  new ConstantNode(multiply(a00.value, a1.value));
@@ -135,7 +142,7 @@ function factory(type, config, load, typed, math) {
                   if (type.isConstantNode(a0)) {
                       // fold constant
                       return new ConstantNode(pow(a0.value, a1.value));
-                  } else if (type.isOperatorNode(a0) && a0.op === "^") {
+                  } else if (type.isOperatorNode(a0) && a0.isBinary() && a0.op === "^") {
                       var a01 = a0.args[1];
                       if (type.isConstantNode(a01)) {
                           return new OperatorNode(node.op, node.fn, [
@@ -155,12 +162,11 @@ function factory(type, config, load, typed, math) {
         }
         return new ParenthesisNode(c);
     } else if (type.isFunctionNode(node)) {
-          var args = node.args.map(simplifyCore);
-          if (args.length === 1) {
-              if (type.isParenthesisNode(args[0])) {
-                  args[0] = args[0].content;
-              }
-          }
+          var args = node.args
+              .map(simplifyCore)
+              .map(function (arg) {
+                return type.isParenthesisNode(arg) ? arg.content : arg;
+              });
           return new FunctionNode(simplifyCore(node.fn), args);
     } else {
         // cannot simplify


Test patch:
diff --git a/test/expression/node/OperatorNode.test.js b/test/expression/node/OperatorNode.test.js
index 549e96b1f..17079cc88 100644
--- a/test/expression/node/OperatorNode.test.js
+++ b/test/expression/node/OperatorNode.test.js
@@ -38,6 +38,30 @@ describe('OperatorNode', function() {
     assert.equal(expr.eval(), 5);
   });
 
+  it ('should test whether a unary or binary operator', function () {
+    var n1 = new OperatorNode('-', 'unaryMinus', [new ConstantNode(2)]);
+    assert.strictEqual(n1.isUnary(), true);
+    assert.strictEqual(n1.isBinary(), false);
+
+    // change the args of an operator node (bad practice, but should keep working correctly)
+    n1.args.push(new ConstantNode(3))
+    assert.strictEqual(n1.isUnary(), false);
+    assert.strictEqual(n1.isBinary(), true);
+
+    var n2 = new OperatorNode('+', 'add', [new ConstantNode(2), new ConstantNode(3)]);
+    assert.strictEqual(n2.isUnary(), false);
+    assert.strictEqual(n2.isBinary(), true);
+
+    var n3 = new OperatorNode('+', 'add', [new ConstantNode(2), new ConstantNode(3), new ConstantNode(4)]);
+    assert.strictEqual(n3.isUnary(), false);
+    assert.strictEqual(n3.isBinary(), false);
+
+    // change the args of an operator node (bad practice, but should keep working correctly)
+    n3.args.splice(2, 1);
+    assert.strictEqual(n3.isUnary(), false);
+    assert.strictEqual(n3.isBinary(), true);
+  });
+
   it ('should throw an error in case of unresolved operator function', function () {
     var a = new ConstantNode(2);
     var b = new ConstantNode(3);
diff --git a/test/function/algebra/derivative.test.js b/test/function/algebra/derivative.test.js
index 138f55917..0bd09a9d3 100644
--- a/test/function/algebra/derivative.test.js
+++ b/test/function/algebra/derivative.test.js
@@ -2,6 +2,8 @@
 var assert = require('assert');
 var math = require('../../../index');
 var OperatorNode = math.expression.node.OperatorNode;
+var ConstantNode = math.expression.node.ConstantNode;
+var SymbolNode = math.expression.node.SymbolNode;
 var derivative = math.derivative;
 
 describe('derivative', function() {
@@ -207,9 +209,28 @@ describe('derivative', function() {
 
   });
 
+  it ('should not drop any additional operator arguments', function() {
+    // This case cannot happen when parsing via the expression parser,
+    // but it can when you create your own operator nodes. See #1014
+
+    var c12 = new ConstantNode(12);
+    var c4 = new ConstantNode(4);
+    var x = new SymbolNode('x');
+
+    assert.throws(function () {
+      var node = new OperatorNode('/', 'myDivide', [c12, c4, x]);
+      derivative(node, 'x')
+    }, /Error: Operator "\/" is not supported by derivative, or a wrong number of arguments is passed/ );
+
+    assert.throws(function () {
+      var node = new OperatorNode('^', 'myPow', [c12, c4, x]);
+      derivative(node, 'x')
+    }, /Error: Operator "\^" is not supported by derivative, or a wrong number of arguments is passed/ );
+  });
+
   it('should throw error if expressions contain unsupported operators or functions', function() {
-    assert.throws(function () { derivative('x << 2', 'x'); }, /Error: Operator "<<" not supported by derivative/);
-    assert.throws(function () { derivative('subset(x)', 'x'); }, /Error: Function "subset" not supported by derivative/);
+    assert.throws(function () { derivative('x << 2', 'x'); }, /Error: Operator "<<" is not supported by derivative/);
+    assert.throws(function () { derivative('subset(x)', 'x'); }, /Error: Function "subset" is not supported by derivative/);
   });
 
   it('should have controlled behavior on arguments errors', function() {