simple-squiggle

A restricted subset of Squiggle
Log | Files | Refs | README

simplifyConstant.js (14268B)


      1 // TODO this could be improved by simplifying seperated constants under associative and commutative operators
      2 import { isFraction, isMatrix, isNode, isArrayNode, isConstantNode, isIndexNode, isObjectNode, isOperatorNode } from '../../../utils/is.js';
      3 import { factory } from '../../../utils/factory.js';
      4 import { createUtil } from './util.js';
      5 import { noBignumber, noFraction } from '../../../utils/noop.js';
      6 var name = 'simplifyConstant';
      7 var dependencies = ['typed', 'config', 'mathWithTransform', 'matrix', '?fraction', '?bignumber', 'AccessorNode', 'ArrayNode', 'ConstantNode', 'FunctionNode', 'IndexNode', 'ObjectNode', 'OperatorNode', 'SymbolNode'];
      8 export var createSimplifyConstant = /* #__PURE__ */factory(name, dependencies, _ref => {
      9   var {
     10     typed,
     11     config,
     12     mathWithTransform,
     13     matrix,
     14     fraction,
     15     bignumber,
     16     AccessorNode,
     17     ArrayNode,
     18     ConstantNode,
     19     FunctionNode,
     20     IndexNode,
     21     ObjectNode,
     22     OperatorNode,
     23     SymbolNode
     24   } = _ref;
     25   var {
     26     isCommutative,
     27     isAssociative,
     28     allChildren,
     29     createMakeNodeFunction
     30   } = createUtil({
     31     FunctionNode,
     32     OperatorNode,
     33     SymbolNode
     34   });
     35 
     36   function simplifyConstant(expr, options) {
     37     return _ensureNode(foldFraction(expr, options));
     38   }
     39 
     40   function _removeFractions(thing) {
     41     if (isFraction(thing)) {
     42       return thing.valueOf();
     43     }
     44 
     45     if (thing instanceof Array) {
     46       return thing.map(_removeFractions);
     47     }
     48 
     49     if (isMatrix(thing)) {
     50       return matrix(_removeFractions(thing.valueOf()));
     51     }
     52 
     53     return thing;
     54   }
     55 
     56   function _eval(fnname, args, options) {
     57     try {
     58       return mathWithTransform[fnname].apply(null, args);
     59     } catch (ignore) {
     60       // sometimes the implicit type conversion causes the evaluation to fail, so we'll try again after removing Fractions
     61       args = args.map(_removeFractions);
     62       return _toNumber(mathWithTransform[fnname].apply(null, args), options);
     63     }
     64   }
     65 
     66   var _toNode = typed({
     67     Fraction: _fractionToNode,
     68     number: function number(n) {
     69       if (n < 0) {
     70         return unaryMinusNode(new ConstantNode(-n));
     71       }
     72 
     73       return new ConstantNode(n);
     74     },
     75     BigNumber: function BigNumber(n) {
     76       if (n < 0) {
     77         return unaryMinusNode(new ConstantNode(-n));
     78       }
     79 
     80       return new ConstantNode(n); // old parameters: (n.toString(), 'number')
     81     },
     82     Complex: function Complex(s) {
     83       throw new Error('Cannot convert Complex number to Node');
     84     },
     85     string: function string(s) {
     86       return new ConstantNode(s);
     87     },
     88     Matrix: function Matrix(m) {
     89       return new ArrayNode(m.valueOf().map(e => _toNode(e)));
     90     }
     91   });
     92 
     93   function _ensureNode(thing) {
     94     if (isNode(thing)) {
     95       return thing;
     96     }
     97 
     98     return _toNode(thing);
     99   } // convert a number to a fraction only if it can be expressed exactly,
    100   // and when both numerator and denominator are small enough
    101 
    102 
    103   function _exactFraction(n, options) {
    104     var exactFractions = options && options.exactFractions !== false;
    105 
    106     if (exactFractions && isFinite(n) && fraction) {
    107       var f = fraction(n);
    108       var fractionsLimit = options && typeof options.fractionsLimit === 'number' ? options.fractionsLimit : Infinity; // no limit by default
    109 
    110       if (f.valueOf() === n && f.n < fractionsLimit && f.d < fractionsLimit) {
    111         return f;
    112       }
    113     }
    114 
    115     return n;
    116   } // Convert numbers to a preferred number type in preference order: Fraction, number, Complex
    117   // BigNumbers are left alone
    118 
    119 
    120   var _toNumber = typed({
    121     'string, Object': function stringObject(s, options) {
    122       if (config.number === 'BigNumber') {
    123         if (bignumber === undefined) {
    124           noBignumber();
    125         }
    126 
    127         return bignumber(s);
    128       } else if (config.number === 'Fraction') {
    129         if (fraction === undefined) {
    130           noFraction();
    131         }
    132 
    133         return fraction(s);
    134       } else {
    135         var n = parseFloat(s);
    136         return _exactFraction(n, options);
    137       }
    138     },
    139     'Fraction, Object': function FractionObject(s, options) {
    140       return s;
    141     },
    142     // we don't need options here
    143     'BigNumber, Object': function BigNumberObject(s, options) {
    144       return s;
    145     },
    146     // we don't need options here
    147     'number, Object': function numberObject(s, options) {
    148       return _exactFraction(s, options);
    149     },
    150     'Complex, Object': function ComplexObject(s, options) {
    151       if (s.im !== 0) {
    152         return s;
    153       }
    154 
    155       return _exactFraction(s.re, options);
    156     },
    157     'Matrix, Object': function MatrixObject(s, options) {
    158       return matrix(_exactFraction(s.valueOf()));
    159     },
    160     'Array, Object': function ArrayObject(s, options) {
    161       return s.map(_exactFraction);
    162     }
    163   });
    164 
    165   function unaryMinusNode(n) {
    166     return new OperatorNode('-', 'unaryMinus', [n]);
    167   }
    168 
    169   function _fractionToNode(f) {
    170     var n;
    171     var vn = f.s * f.n;
    172 
    173     if (vn < 0) {
    174       n = new OperatorNode('-', 'unaryMinus', [new ConstantNode(-vn)]);
    175     } else {
    176       n = new ConstantNode(vn);
    177     }
    178 
    179     if (f.d === 1) {
    180       return n;
    181     }
    182 
    183     return new OperatorNode('/', 'divide', [n, new ConstantNode(f.d)]);
    184   }
    185   /* Handles constant indexing of ArrayNodes, matrices, and ObjectNodes */
    186 
    187 
    188   function _foldAccessor(obj, index, options) {
    189     if (!isIndexNode(index)) {
    190       // don't know what to do with that...
    191       return new AccessorNode(_ensureNode(obj), _ensureNode(index));
    192     }
    193 
    194     if (isArrayNode(obj) || isMatrix(obj)) {
    195       var remainingDims = Array.from(index.dimensions);
    196       /* We will resolve constant indices one at a time, looking
    197        * just in the first or second dimensions because (a) arrays
    198        * of more than two dimensions are likely rare, and (b) pulling
    199        * out the third or higher dimension would be pretty intricate.
    200        * The price is that we miss simplifying [..3d array][x,y,1]
    201        */
    202 
    203       while (remainingDims.length > 0) {
    204         if (isConstantNode(remainingDims[0]) && typeof remainingDims[0].value !== 'string') {
    205           var first = _toNumber(remainingDims.shift().value, options);
    206 
    207           if (isArrayNode(obj)) {
    208             obj = obj.items[first - 1];
    209           } else {
    210             // matrix
    211             obj = obj.valueOf()[first - 1];
    212 
    213             if (obj instanceof Array) {
    214               obj = matrix(obj);
    215             }
    216           }
    217         } else if (remainingDims.length > 1 && isConstantNode(remainingDims[1]) && typeof remainingDims[1].value !== 'string') {
    218           var second = _toNumber(remainingDims[1].value, options);
    219 
    220           var tryItems = [];
    221           var fromItems = isArrayNode(obj) ? obj.items : obj.valueOf();
    222 
    223           for (var item of fromItems) {
    224             if (isArrayNode(item)) {
    225               tryItems.push(item.items[second - 1]);
    226             } else if (isMatrix(obj)) {
    227               tryItems.push(item[second - 1]);
    228             } else {
    229               break;
    230             }
    231           }
    232 
    233           if (tryItems.length === fromItems.length) {
    234             if (isArrayNode(obj)) {
    235               obj = new ArrayNode(tryItems);
    236             } else {
    237               // matrix
    238               obj = matrix(tryItems);
    239             }
    240 
    241             remainingDims.splice(1, 1);
    242           } else {
    243             // extracting slice along 2nd dimension failed, give up
    244             break;
    245           }
    246         } else {
    247           // neither 1st or 2nd dimension is constant, give up
    248           break;
    249         }
    250       }
    251 
    252       if (remainingDims.length === index.dimensions.length) {
    253         /* No successful constant indexing */
    254         return new AccessorNode(_ensureNode(obj), index);
    255       }
    256 
    257       if (remainingDims.length > 0) {
    258         /* Indexed some but not all dimensions */
    259         index = new IndexNode(remainingDims);
    260         return new AccessorNode(_ensureNode(obj), index);
    261       }
    262       /* All dimensions were constant, access completely resolved */
    263 
    264 
    265       return obj;
    266     }
    267 
    268     if (isObjectNode(obj) && index.dimensions.length === 1 && isConstantNode(index.dimensions[0])) {
    269       var key = index.dimensions[0].value;
    270 
    271       if (key in obj.properties) {
    272         return obj.properties[key];
    273       }
    274 
    275       return new ConstantNode(); // undefined
    276     }
    277     /* Don't know how to index this sort of obj, at least not with this index */
    278 
    279 
    280     return new AccessorNode(_ensureNode(obj), index);
    281   }
    282   /*
    283    * Create a binary tree from a list of Fractions and Nodes.
    284    * Tries to fold Fractions by evaluating them until the first Node in the list is hit, so
    285    * `args` should be sorted to have the Fractions at the start (if the operator is commutative).
    286    * @param args - list of Fractions and Nodes
    287    * @param fn - evaluator for the binary operation evaluator that accepts two Fractions
    288    * @param makeNode - creates a binary OperatorNode/FunctionNode from a list of child Nodes
    289    * if args.length is 1, returns args[0]
    290    * @return - Either a Node representing a binary expression or Fraction
    291    */
    292 
    293 
    294   function foldOp(fn, args, makeNode, options) {
    295     return args.reduce(function (a, b) {
    296       if (!isNode(a) && !isNode(b)) {
    297         try {
    298           return _eval(fn, [a, b], options);
    299         } catch (ignoreandcontinue) {}
    300 
    301         a = _toNode(a);
    302         b = _toNode(b);
    303       } else if (!isNode(a)) {
    304         a = _toNode(a);
    305       } else if (!isNode(b)) {
    306         b = _toNode(b);
    307       }
    308 
    309       return makeNode([a, b]);
    310     });
    311   } // destroys the original node and returns a folded one
    312 
    313 
    314   function foldFraction(node, options) {
    315     switch (node.type) {
    316       case 'SymbolNode':
    317         return node;
    318 
    319       case 'ConstantNode':
    320         switch (typeof node.value) {
    321           case 'number':
    322             return _toNumber(node.value, options);
    323 
    324           case 'string':
    325             return node.value;
    326 
    327           default:
    328             if (!isNaN(node.value)) return _toNumber(node.value, options);
    329         }
    330 
    331         return node;
    332 
    333       case 'FunctionNode':
    334         if (mathWithTransform[node.name] && mathWithTransform[node.name].rawArgs) {
    335           return node;
    336         }
    337 
    338         {
    339           // Process operators as OperatorNode
    340           var operatorFunctions = ['add', 'multiply'];
    341 
    342           if (operatorFunctions.indexOf(node.name) === -1) {
    343             var args = node.args.map(arg => foldFraction(arg, options)); // If all args are numbers
    344 
    345             if (!args.some(isNode)) {
    346               try {
    347                 return _eval(node.name, args, options);
    348               } catch (ignoreandcontinue) {}
    349             } // Size of a matrix does not depend on entries
    350 
    351 
    352             if (node.name === 'size' && args.length === 1 && isArrayNode(args[0])) {
    353               var sz = [];
    354               var section = args[0];
    355 
    356               while (isArrayNode(section)) {
    357                 sz.push(section.items.length);
    358                 section = section.items[0];
    359               }
    360 
    361               return matrix(sz);
    362             } // Convert all args to nodes and construct a symbolic function call
    363 
    364 
    365             return new FunctionNode(node.name, args.map(_ensureNode));
    366           } else {// treat as operator
    367           }
    368         }
    369 
    370       /* falls through */
    371 
    372       case 'OperatorNode':
    373         {
    374           var fn = node.fn.toString();
    375 
    376           var _args;
    377 
    378           var res;
    379           var makeNode = createMakeNodeFunction(node);
    380 
    381           if (isOperatorNode(node) && node.isUnary()) {
    382             _args = [foldFraction(node.args[0], options)];
    383 
    384             if (!isNode(_args[0])) {
    385               res = _eval(fn, _args, options);
    386             } else {
    387               res = makeNode(_args);
    388             }
    389           } else if (isAssociative(node, options.context)) {
    390             _args = allChildren(node, options.context);
    391             _args = _args.map(arg => foldFraction(arg, options));
    392 
    393             if (isCommutative(fn, options.context)) {
    394               // commutative binary operator
    395               var consts = [];
    396               var vars = [];
    397 
    398               for (var i = 0; i < _args.length; i++) {
    399                 if (!isNode(_args[i])) {
    400                   consts.push(_args[i]);
    401                 } else {
    402                   vars.push(_args[i]);
    403                 }
    404               }
    405 
    406               if (consts.length > 1) {
    407                 res = foldOp(fn, consts, makeNode, options);
    408                 vars.unshift(res);
    409                 res = foldOp(fn, vars, makeNode, options);
    410               } else {
    411                 // we won't change the children order since it's not neccessary
    412                 res = foldOp(fn, _args, makeNode, options);
    413               }
    414             } else {
    415               // non-commutative binary operator
    416               res = foldOp(fn, _args, makeNode, options);
    417             }
    418           } else {
    419             // non-associative binary operator
    420             _args = node.args.map(arg => foldFraction(arg, options));
    421             res = foldOp(fn, _args, makeNode, options);
    422           }
    423 
    424           return res;
    425         }
    426 
    427       case 'ParenthesisNode':
    428         // remove the uneccessary parenthesis
    429         return foldFraction(node.content, options);
    430 
    431       case 'AccessorNode':
    432         return _foldAccessor(foldFraction(node.object, options), foldFraction(node.index, options), options);
    433 
    434       case 'ArrayNode':
    435         {
    436           var foldItems = node.items.map(item => foldFraction(item, options));
    437 
    438           if (foldItems.some(isNode)) {
    439             return new ArrayNode(foldItems.map(_ensureNode));
    440           }
    441           /* All literals -- return a Matrix so we can operate on it */
    442 
    443 
    444           return matrix(foldItems);
    445         }
    446 
    447       case 'IndexNode':
    448         {
    449           return new IndexNode(node.dimensions.map(n => simplifyConstant(n, options)));
    450         }
    451 
    452       case 'ObjectNode':
    453         {
    454           var foldProps = {};
    455 
    456           for (var prop in node.properties) {
    457             foldProps[prop] = simplifyConstant(node.properties[prop], options);
    458           }
    459 
    460           return new ObjectNode(foldProps);
    461         }
    462 
    463       case 'AssignmentNode':
    464       /* falls through */
    465 
    466       case 'BlockNode':
    467       /* falls through */
    468 
    469       case 'FunctionAssignmentNode':
    470       /* falls through */
    471 
    472       case 'RangeNode':
    473       /* falls through */
    474 
    475       case 'ConditionalNode':
    476       /* falls through */
    477 
    478       default:
    479         throw new Error("Unimplemented node type in simplifyConstant: ".concat(node.type));
    480     }
    481   }
    482 
    483   return simplifyConstant;
    484 });