UNPKG

foam-framework

Version:
759 lines (649 loc) 21.2 kB
/** * @license * Copyright 2012 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // TODO: add type-checking in partialEval // (type-checking is a subset of partial-eval) function compile_(a) { return /*Expr.isInstance(a) || Property.isInstance(a)*/ a.f ? a : a === true ? TRUE : a === false ? FALSE : ConstantExpr.create({arg1:a}); } function compileArray_(args) { var b = []; for ( var i = 0 ; i < args.length ; i++ ) { var a = args[i]; if ( a !== null && a !== undefined ) b.push(compile_(a)); } return b; } CLASS({ name: 'Expr', documentation: 'Parent model for all mLang expressions. Contains default implementations for many methods.', methods: [ function toMQL() { /* Outputs MQL for this expression. */ return this.label_; }, function toSQL() { /* Outputs SQL for this expression. */ return this.label_; }, function toBQL() { /* Outputs yet another query language for this expression. */ return this.label_; }, function toString() { /* Converts to a string form for debugging; defaults to $$DOC{ref: ".toMQL", text: "MQL"}. */ return this.toMQL(); }, function exprClone() { /* Expression-friendly cloning method. Basically a deepClone(), but many expressions override it with custom logic. */ var c = Object.create(this.__proto__); c.instance_ = {}; c.X = this.X; for ( var key in this.instance_ ) { var value = this[key]; if ( value !== undefined ) { if ( typeof value.exprClone === 'function' ) c.instance_[key] = value.exprClone(); else c.instance_[key] = value; } } return c; }, function collectInputs(terms) { /* Recursively adds all inputs of an expression to an array. */ terms.push(this); }, function partialEval() { /* <p>Simplifies the expression by eliminating unnecessary clauses and combining others.</p> <p>Can sometimes reduce whole (sub)expressions to TRUE or FALSE.</p> */ return this; }, function minterm(index, term) { // True if this bit is set in the minterm number. return !!((term >>> index[0]--) & 1 ); }, function normalize() { return this; // Each input term to the expression. var inputs = []; this.collectInputs(inputs); // Truth table for every minterm (combination of inputs). var minterms = new Array(Math.pow(2, inputs.length)); for ( var i = 0; i < minterms.length; i++ ) { minterms[i] = this.minterm([inputs.length - 1], i); } // TODO: Calculate prime implicants and reduce to minimal set. var terms = []; for ( i = 0; i < minterms.length; i++ ) { if ( minterms[i] ) { var subterms = []; for ( var j = 0; j < inputs.length; j++ ) { if ( i & (1 << (inputs.length - j - 1))) subterms.push(inputs[j]); } terms.push(AndExpr.create({ args: subterms })); } } var ret = OrExpr.create({ args: terms }).partialEval(); console.log(this.toMQL(),' normalize-> ', ret.toMQL()); return ret; }, function pipe(sink) { /* Returns a $$DOC{ref: "Sink"} which applies this expression to every value <tt>put</tt> or <tt>remove</tt>d, calling the provided <tt>sink</tt> only for those values which match the expression. */ var expr = this; return { __proto__: sink, put: function(obj) { if ( expr.f(obj) ) sink.put(obj); }, remove: function(obj) { if ( expr.f(obj) ) sink.remove(obj); } }; } ] }); var TRUE = (FOAM({ model_: 'Model', name: 'TrueExpr', extends: 'Expr', documentation: 'Model for the primitive true value.', methods: [ function clone() { return this; }, function deepClone() { return this; }, function exprClone() { return this; }, function toString() { return '<true>'; }, function toSQL() { return '( 1 = 1 )'; }, function toMQL() { return ''; }, function toBQL() { return ''; }, function f() { return true; } ] })).create(); var FALSE = (FOAM({ model_: 'Model', name: 'FalseExpr', extends: 'Expr', documentation: 'Model for the primitive false value.', methods: [ function clone() { return this; }, function deepClone() { return this; }, function exprClone() { return this; }, function toSQL(out) { return '( 1 <> 1 )'; }, function toMQL(out) { return '<false>'; }, function toBQL(out) { return '<false>'; }, function f() { return false; } ] })).create(); var IDENTITY = (FOAM({ model_: 'Model', name: 'IdentityExpr', extends: 'Expr', documentation: 'The identity expression, which passes through its input unchanged.', methods: { clone: function() { return this; }, deepClone: function() { return this; }, exprClone: function() { return this; }, f: function(obj) { return obj; }, toString: function() { return 'IDENTITY'; } } })).create(); /** An n-ary function. **/ CLASS({ name: 'NARY', extends: 'Expr', abstract: true, documentation: 'Parent model for expressions which take an arbitrary number of arguments.', properties: [ { name: 'args', label: 'Arguments', type: 'Expr[]', help: 'Sub-expressions', documentation: 'An array of subexpressions which are the arguments to this n-ary expression.', factory: function() { return []; } } ], methods: { toString: function() { var s = this.name_ + '('; for ( var i = 0 ; i < this.args.length ; i++ ) { var a = this.args[i]; s += a.toString(); if ( i < this.args.length-1 ) s += (', '); } return s + ')'; }, toSQL: function() { var s; s = this.model_.label; s += '('; for ( var i = 0 ; i < this.args.length ; i++ ) { var a = this.args[i]; s += a.toSQL(); if ( i < this.args.length-1 ) out.push(','); } s += ')'; return s; }, toMQL: function() { var s; s = this.model_.label; s += '('; for ( var i = 0 ; i < this.args.length ; i++ ) { var a = this.args[i]; s += a.toMQL(); if ( i < this.args.length-1 ) out.push(','); } s += ')'; return str; }, toBQL: function() { var s; s = this.model_.label; s += '('; for ( var i = 0 ; i < this.args.length ; i++ ) { var a = this.args[i]; s += a.toBQL(); if ( i < this.args.length-1 ) out.push(','); } s += ')'; return str; } } }); // Allow Singleton mLang's to be desearialized to properly. var TrueExpr = { finished__: true, arequire: function(ret) { return afuture().set(this).get; }, create: function() { return TRUE; } }; var FalseExpr = { finished__: true, arequire: function(ret) { return afuture().set(this).get; }, create: function() { return FALSE; } }; var IdentityExpr = { finished__: true, arequire: function(ret) { return afuture().set(this).get; }, create: function() { return IDENTITY; } }; /** An unary function. **/ CLASS({ name: 'UNARY', extends: 'Expr', abstract: true, documentation: 'Parent model for one-argument expressions.', properties: [ { name: 'arg1', label: 'Argument', type: 'Expr', help: 'Sub-expression', documentation: 'The first argument to the expression.', defaultValue: TRUE } ], methods: { toSQL: function() { return this.label_ + '(' + this.arg1.toSQL() + ')'; }, toMQL: function() { return this.label_ + '(' + this.arg1.toMQL() + ')'; }, toBQL: function() { return this.label_ + '(' + this.arg1.toBQL() + ')'; } } }); /** An unary function. **/ CLASS({ name: 'BINARY', extends: 'UNARY', abstract: true, documentation: 'Parent model for two-argument expressions. Extends $$DOC{ref: "UNARY"} to include $$DOC{ref: ".arg2"}.', properties: [ { name: 'arg2', label: 'Argument', type: 'Expr', help: 'Sub-expression', documentation: 'Second argument to the expression.', defaultValue: TRUE } ], methods: { toSQL: function() { return this.arg1.toSQL() + ' ' + this.label_ + ' ' + this.arg2.toSQL(); }, toMQL: function() { return this.arg1.toMQL() + ' ' + this.label_ + ' ' + this.arg2.toMQL(); }, toBQL: function() { return this.arg1.toBQL() + ' ' + this.label_ + ' ' + this.arg2.toBQL(); } } }); CLASS({ name: 'CountExpr', extends: 'Expr', properties: [ { name: 'count', type: 'int', defaultValue: 0 }, { name: 'value', getter: function() { return this.count; } } ], methods: { reduce: function(other) { return CountExpr.create({count: this.count + other.count}); }, reduceI: function(other) { this.count = this.count + other.count; }, pipe: function(sink) { sink.put(this); }, put: function(obj) { this.count++; }, remove: function(obj) { this.count--; }, toString: function() { return this.count; } } }); function COUNT() { return CountExpr.create(); } CLASS({ name: 'EqExpr', extends: 'BINARY', abstract: true, documentation: function() { /* <p>Binary expression that compares its arguments for equality.</p> <p>When evaluated in Javascript, uses <tt>==</tt>.</p> <p>If the first argument is an array, returns true if any of its value match the second argument.</p> */}, methods: { toSQL: function() { return this.arg1.toSQL() + '=' + this.arg2.toSQL(); }, toMQL: function() { if ( ! this.arg1.toMQL || ! this.arg2.toMQL ) return ''; return this.arg2 === TRUE ? 'is:' + this.arg1.toMQL() : this.arg2.f() == '' ? '-has:' + this.arg1.toMQL() : this.arg1.toMQL() + '=' + this.arg2.toMQL() ; }, toBQL: function() { if ( ! this.arg1.toBQL || ! this.arg2.toBQL ) return ''; return this.arg2 === TRUE ? this.arg1.toBQL() + ':true' : this.arg1.toBQL() + ':' + this.arg2.toBQL() ; }, partialEval: function() { var newArg1 = this.arg1.partialEval(); var newArg2 = this.arg2.partialEval(); if ( ConstantExpr.isInstance(newArg1) && ConstantExpr.isInstance(newArg2) ) return compile_(this.f()); return this.arg1 !== newArg1 || this.arg2 !== newArg2 ? EqExpr.create({arg1: newArg1, arg2: newArg2}) : this ; }, f: function(obj) { var arg1 = this.arg1.f(obj); var arg2 = this.arg2.f(obj); if ( Array.isArray(arg1) ) { if ( ! Array.isArray(arg2) ) { return arg1.some(function(arg) { return arg == arg2; }); } if ( arg1.length !== arg2.length ) return false; for ( var i = 0; i < arg1.length; i++ ) { if ( arg1[i] != arg2[i] ) return false; } return true; } if ( arg2 === TRUE ) return !! arg1; if ( arg2 === FALSE ) return ! arg1; return equals(arg1, arg2); } } }); function EQ(arg1, arg2) { var eq = EqExpr.create(); eq.instance_.arg1 = compile_(arg1); eq.instance_.arg2 = compile_(arg2); return eq; // return EqExpr.create({arg1: compile_(arg1), arg2: compile_(arg2)}); } CLASS({ name: 'ConstantExpr', extends: 'UNARY', methods: { escapeSQLString: function(str) { return "'" + str.replace(/\\/g, "\\\\").replace(/'/g, "\\'") + "'"; }, escapeMQLString: function(str) { if ( str.length > 0 && str.indexOf(' ') == -1 && str.indexOf('"') == -1 && str.indexOf(',') == -1 ) return str; return '"' + str.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"'; }, toSQL: function() { return ( typeof this.arg1 === 'string' ) ? this.escapeSQLString(this.arg1) : this.arg1.toString() ; }, toMQL: function() { return ( typeof this.arg1 === 'string' ) ? this.escapeMQLString(this.arg1) : (this.arg1.toMQL ? this.arg1.toMQL() : this.arg1.toString()); }, toBQL: function() { return ( typeof this.arg1 === 'string' ) ? this.escapeMQLString(this.arg1) : (this.arg1.toBQL ? this.arg1.toBQL() : this.arg1.toString()); }, f: function(obj) { return this.arg1; } } }); CLASS({ name: 'AndExpr', extends: 'NARY', abstract: true, documentation: 'N-ary expression which is true only if each of its 0 or more arguments is true. AND() === TRUE', methods: { // AND has a higher precedence than OR so doesn't need parenthesis toSQL: function() { var s = ''; for ( var i = 0 ; i < this.args.length ; i++ ) { var a = this.args[i]; s += a.toSQL(); if ( i < this.args.length-1 ) s += (' AND '); } return s; }, toMQL: function() { var s = ''; for ( var i = 0 ; i < this.args.length ; i++ ) { var a = this.args[i]; var sub = a.toMQL(); if ( OrExpr.isInstance(a) ) { sub = '(' + sub + ')'; } s += sub; if ( i < this.args.length-1 ) s += (' '); } return s; }, toBQL: function() { var s = ''; for ( var i = 0 ; i < this.args.length ; i++ ) { var a = this.args[i]; var sub = a.toBQL(); if ( OrExpr.isInstance(a) ) { sub = '(' + sub + ')'; } s += sub; if ( i < this.args.length-1 ) s += (' '); } return s; }, collectInputs: function(terms) { for ( var i = 0; i < this.args.length; i++ ) { this.args[i].collectInputs(terms); } }, minterm: function(index, term) { var out = true; for ( var i = 0; i < this.args.length; i++ ) { out = this.args[i].minterm(index, term) && out; } return out; } }, constants: { PARTIAL_AND_RULES: [ [ 'EqExpr', 'EqExpr', function(e1, e2) { return e1.arg1.exclusive ? e1.arg2.f() == e2.arg2.f() ? e1 : FALSE : e1.arg2.f() == e2.arg2.f() ? e1 : null ; } ], [ 'InExpr', 'InExpr', function(e1, e2) { var i = e1.arg1.exclusive ? e1.arg2.intersection(e2.arg2) : e1.arg2.union(e2.arg2) ; return i.length ? IN(e1.arg1, i) : FALSE; } ], [ 'InExpr', 'ContainedInICExpr', function(e1, e2) { if ( ! e1.arg1.exclusive ) return null; var i = e1.arg2.filter(function(o) { o = o.toUpperCase(); return e2.arg2.some(function(o2) { return o.indexOf(o2) != -1; }); }); return i.length ? IN(e1.arg1, i) : FALSE; } ], [ 'ContainedInICExpr', 'ContainedInICExpr', function(e1, e2) { console.assert(false, 'AND.partialEval: ContainedInICExpr has no partialEval rule'); } ], [ 'InExpr', 'ContainsICExpr', function(e1, e2) { if ( ! e1.arg1.exclusive ) return; var i = e1.arg2.filter(function(o) { return o.indexOfIC(e2.arg2.f()) !== -1; }); } ], [ 'InExpr', 'ContainsExpr', function(e1, e2) { if ( ! e1.arg1.exclusive ) return; var i = e1.arg2.filter(function(o) { return o.indexOf(e2.arg2.f()) !== -1; }); return i.length ? IN(e1.arg1, i) : FALSE; } ], [ 'EqExpr', 'InExpr', function(e1, e2) { if ( ! e1.arg1.exclusive ) return; return e2.arg2.indexOf(e1.arg2.f()) === -1 ? FALSE : e1; } ] ], partialAnd: function(e1, e2) { if ( OrExpr.isInstance(e2) ) { var tmp = e1; e1 = e2; e2 = tmp; } if ( OrExpr.isInstance(e1) ) { var args = []; for ( var i = 0 ; i < e1.args.length ; i++ ) { args.push(AND(e2, e1.args[i])); } return OrExpr.create({args: args}).partialEval(); } if ( ! BINARY.isInstance(e1) ) return null; if ( ! BINARY.isInstance(e2) ) return null; if ( e1.arg1 != e2.arg1 ) return null; var RULES = this.PARTIAL_AND_RULES; for ( var i = 0 ; i < RULES.length ; i++ ) { if ( e1.model_.name == RULES[i][0] && e2.model_.name == RULES[i][1] ) return RULES[i][2](e1, e2); if ( e2.model_.name == RULES[i][0] && e1.model_.name == RULES[i][1] ) return RULES[i][2](e2, e1); } if ( DEBUG ) console.log('Unknown partialAnd combination: ', e1.name_, e2.name_); return null; }, partialEval: function() { var newArgs = []; var updated = false; for ( var i = 0 ; i < this.args.length ; i++ ) { var a = this.args[i]; var newA = this.args[i].partialEval(); if ( newA === FALSE ) return FALSE; if ( AndExpr.isInstance(newA) ) { // In-line nested AND clauses for ( var j = 0 ; j < newA.args.length ; j++ ) { newArgs.push(newA.args[j]); } updated = true; } else { if ( newA === TRUE ) { updated = true; } else { newArgs.push(newA); if ( a !== newA ) updated = true; } } } for ( var i = 0 ; i < newArgs.length-1 ; i++ ) { for ( var j = i+1 ; j < newArgs.length ; j++ ) { var a = this.partialAnd(newArgs[i], newArgs[j]); if ( a ) { console.log('***************** ', newArgs[i].toMQL(), ' <PartialAnd> ', newArgs[j].toMQL(), ' -> ', a.toMQL()); if ( a === FALSE ) return FALSE; newArgs[i] = a; newArgs.splice(j, 1); } } } if ( newArgs.length == 0 ) return TRUE; if ( newArgs.length == 1 ) return newArgs[0]; return updated ? AndExpr.create({args: newArgs}) : this; }, f: function(obj) { return this.args.every(function(arg) { return arg.f(obj); }); } } }); function AND() { return AndExpr.create({args: compileArray_.call(null, arguments)}); } CLASS({ name: 'NeqExpr', extends: 'BINARY', abstract: true, methods: { toSQL: function() { return this.arg1.toSQL() + '<>' + this.arg2.toSQL(); }, toMQL: function() { return '-' + this.arg1.toMQL() + '=' + this.arg2.toMQL(); }, toBQL: function() { return '-' + this.arg1.toBQL() + ':' + this.arg2.toBQL(); }, partialEval: function() { var newArg1 = this.arg1.partialEval(); var newArg2 = this.arg2.partialEval(); if ( ConstantExpr.isInstance(newArg1) && ConstantExpr.isInstance(newArg2) ) return compile_(this.f()); return this.arg1 !== newArg1 || this.arg2 != newArg2 ? NeqExpr.create({arg1: newArg1, arg2: newArg2}) : this ; }, f: function(obj) { var arg1 = this.arg1.f(obj); var arg2 = this.arg2.f(obj); if ( Array.isArray(arg1) ) { if ( ! Array.isArray(arg2) ) { return ! arg1.some(function(arg) { return arg == arg2; }); } if ( arg1.length !== arg2.length ) return true; for ( var i = 0; i < arg1.length; i++ ) { if ( arg1[i] != arg2[i] ) return true; } return false; } if ( arg2 === TRUE ) return ! arg1; if ( arg2 === FALSE ) return !! arg1; return ! equals(arg1, arg2); } } }); function NEQ(arg1, arg2) { return NeqExpr.create({arg1: compile_(arg1), arg2: compile_(arg2)}); } CLASS({ name: 'UpperExpr', extends: 'UNARY', properties: [ { name: 'label_', defaultValue: 'UPPER' } ], methods: [ function partialEval() { var newArg1 = this.arg1.partialEval(); if ( ConstantExpr.isInstance(newArg1) ) { var val = newArg1.f(); if ( typeof val === 'string' ) return compile_(val.toUpperCase()); } else if ( Array.isArray(newArg1) ) { debugger; } return this; }, function f(obj) { var a = this.arg1.f(obj); if ( Array.isArray(a) ) return a.map(function(s) { return s.toUpperCase ? s.toUpperCase() : s; }); return a && a.toUpperCase ? a.toUpperCase() : a ; }, function toMQL() { if ( ConstantExpr.isInstance(this.arg1) && typeof this.arg1.arg1 == 'string' ) return this.arg1.arg1.toUpperCase(); return this.arg1.toMQL(); } ] }); function UPPER(arg1) { return UpperExpr.create({arg1: compile_(arg1)}); } function EQ_IC(arg1, arg2) { return EQ(UPPER(arg1), UPPER(arg2)); } function IN_IC(arg1, arg2) { return IN(UPPER(arg1), arg2.map(UPPER)); }