diff --git a/lib/compile/index.js b/lib/compile/index.js index 234975c..095488e 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -94,7 +94,7 @@ var createRuleFromObject = (function () { function parseRule(rule, conditions, identifiers, defined, name) { if (rule.length) { var r0 = rule[0]; - if (r0 === "not" || r0 === "exists") { + if (r0 === "not" || r0 === "exists" || r0 === "collect" ) { var temp = []; rule.shift(); __resolveRule(rule, identifiers, temp, defined, name); @@ -135,7 +135,10 @@ var createRuleFromObject = (function () { forEach(constraints, function (rule) { parseRule(rule, conditions, identifiers, defined, name); }); - return rules.createRule(name, options, conditions, parseAction(action, identifiers, defined, scope)); + var ruleScope = merge(defined, scope); + var copyOptions = merge({}, options); + copyOptions.scope = ruleScope; + return rules.createRule(name, copyOptions, conditions, parseAction(action, identifiers, defined, scope)); }; })(); @@ -158,7 +161,7 @@ exports.compile = function (flowObj, options, cb, Container) { throw new Error("Name must be present in JSON or options"); } var flow = new Container(name); - var defined = merge({Array: Array, String: String, Number: Number, Boolean: Boolean, RegExp: RegExp, Date: Date, Object: Object}, options.define || {}); + var defined = merge({Array: Array, String: String, Number: Number, Boolean: Boolean, RegExp: RegExp, Date: Date, Object: Object}, ( (options.define || options.defines || options.defined) || {})); if (typeof Buffer !== "undefined") { defined.Buffer = Buffer; } diff --git a/lib/context.js b/lib/context.js index cc876aa..28b8ac9 100644 --- a/lib/context.js +++ b/lib/context.js @@ -5,6 +5,8 @@ var extd = require("./extended"), indexOf = extd.indexOf, pPush = Array.prototype.push; +var Match = require('./match.js'); + function createContextHash(paths, hashCode) { var ret = "", i = -1, @@ -16,77 +18,28 @@ function createContextHash(paths, hashCode) { return ret; } -function merge(h1, h2, aliases) { - var i = -1, l = aliases.length, alias; - while (++i < l) { - alias = aliases[i]; - h1[alias] = h2[alias]; - } -} - -function unionRecency(arr, arr1, arr2) { - pPush.apply(arr, arr1); - var i = -1, l = arr2.length, val, j = arr.length; +function createPathstHash(paths, hashCode) { + var ret = "", + i = -1, + l = paths.length; while (++i < l) { - val = arr2[i]; - if (indexOf(arr, val) === -1) { - arr[j++] = val; - } + ret += paths[i].id + ":"; } + return ret; } -var Match = declare({ - instance: { - isMatch: true, - hashCode: "", - facts: null, - factIds: null, - factHash: null, - recency: null, - aliases: null, - constructor: function () { - this.facts = []; - this.factIds = []; - this.factHash = {}; - this.recency = []; - this.aliases = []; - }, - - addFact: function (assertable) { - pPush.call(this.facts, assertable); - pPush.call(this.recency, assertable.recency); - pPush.call(this.factIds, assertable.id); - this.hashCode = this.factIds.join(":"); - return this; - }, - - merge: function (mr) { - var ret = new Match(); - ret.isMatch = mr.isMatch; - pPush.apply(ret.facts, this.facts); - pPush.apply(ret.facts, mr.facts); - pPush.apply(ret.aliases, this.aliases); - pPush.apply(ret.aliases, mr.aliases); - ret.hashCode = this.hashCode + ":" + mr.hashCode; - merge(ret.factHash, this.factHash, this.aliases); - merge(ret.factHash, mr.factHash, mr.aliases); - unionRecency(ret.recency, this.recency, mr.recency); - return ret; - } - } -}); var Context = declare({ instance: { - match: null, - factHash: null, - aliases: null, - fact: null, - hashCode: null, - paths: null, - pathsHash: null, + match: null, + factHash: null, + aliases: null, + fact: null, + hashCode: null, + paths: null, + pathsHash: null, constructor: function (fact, paths, mr) { this.fact = fact; diff --git a/lib/extended.js b/lib/extended.js index 3468567..9e800d9 100644 --- a/lib/extended.js +++ b/lib/extended.js @@ -1,4 +1,5 @@ var arr = require("array-extended"), + is = require("is-extended"), unique = arr.unique, indexOf = arr.indexOf, map = arr.map, @@ -126,6 +127,27 @@ function diffHash(h1, h2) { function union(arr1, arr2) { return unique(arr1.concat(arr2)); } + +function findIndex(coll, item) { + var target = -1; + if( 'function' === typeof item ) { + coll.some(function(e, i) { + if(item(e,i)) { + target = i; + return true; + }; + }); + } + else { + coll.some(function(e, i) { + if(is.deepEqual(e, item)) { + target = i; + return true; + }; + }); + } + return target; +} module.exports = require("extended")() .register(require("date-extended")) @@ -145,5 +167,6 @@ module.exports = require("extended")() .register("HashTable", require("ht")) .register("declare", require("declare.js")) .register(require("leafy")) + .register('findIndex', findIndex) .register("LinkedList", require("./linkedList")); diff --git a/lib/match.js b/lib/match.js new file mode 100644 index 0000000..c819cbb --- /dev/null +++ b/lib/match.js @@ -0,0 +1,73 @@ +"use strict"; +var extd = require("./extended"), + isBoolean = extd.isBoolean, + declare = extd.declare, + indexOf = extd.indexOf, + pPush = Array.prototype.push; + + +function merge(h1, h2, aliases) { + var i = -1, l = aliases.length, alias; + while (++i < l) { + alias = aliases[i]; + h1[alias] = h2[alias]; + } +} + +function unionRecency(arr, arr1, arr2) { + pPush.apply(arr, arr1); + var i = -1, l = arr2.length, val, j = arr.length; + while (++i < l) { + val = arr2[i]; + if (indexOf(arr, val) === -1) { + arr[j++] = val; + } + } +} + +var Match = declare({ + instance: { + + isMatch: true, + hashCode: "", + facts: null, + factIds: null, + factHash: null, + recency: null, + aliases: null, + + constructor: function () { + this.facts = []; + this.factIds = []; + this.factHash = {}; + this.recency = []; + this.aliases = []; + }, + + addFact: function (assertable) { + pPush.call(this.facts, assertable); + pPush.call(this.recency, assertable.recency); + pPush.call(this.factIds, assertable.id); + this.hashCode = this.factIds.join(":"); + return this; + }, + merge: function (mr) { + var ret = new Match(); + ret.isMatch = mr.isMatch; + pPush.apply(ret.facts, this.facts); + pPush.apply(ret.facts, mr.facts); + pPush.apply(ret.factIds, this.factIds); + pPush.apply(ret.factIds, mr.factIds); + pPush.apply(ret.aliases, this.aliases); + pPush.apply(ret.aliases, mr.aliases); + ret.hashCode = this.hashCode + ":" + mr.hashCode; + merge(ret.factHash, this.factHash, this.aliases); + merge(ret.factHash, mr.factHash, mr.aliases); + unionRecency(ret.recency, this.recency, mr.recency); + return ret; + } + } +}).as(module); + + + diff --git a/lib/nodes/collectNode.js b/lib/nodes/collectNode.js new file mode 100644 index 0000000..258dddb --- /dev/null +++ b/lib/nodes/collectNode.js @@ -0,0 +1,264 @@ +var extd = require("../extended"); +var FromNode = require("./fromNode"); +var Context = require('../context.js'); +var Match = require('../match.js'); +var extdObj = require("object-extended"); + +var DEFAULT_MATCH = { + isMatch: function () { + return false; + } +}; + +/** +*/ +FromNode.extend({ + instance: { + nodeType: "CollectNode", + // + constructor: function (pattern, wm) { + this._super([pattern, wm]); + this.setAliases = pattern.setAliases; + this.collectionHash = {}; + this.isCollectionObject = this.type({}) ? true : false + this.fnCollectionSrc = pattern.fnCollectionSrc; + }, + + __createMatches: function (lc) { + var me = this + ,fc = me.getFilteredContext(lc) + ,rc = me.collectionHash[fc.hashCode] + ,lcFh = lc.factHash + ,verb = 'assert' + ,createdContext, collection, collFact, match, setHash; + // + if(!rc) { + // either an array [obj, obj,...] or { key: [], key2: [] } + if( me.fnCollectionSrc ) { + collFact = me.fnCollectionSrc(me.workingMemory); + collection = collFact.object; + } + else { + collection = me.isCollectionObject ? {} : []; + collFact = me.workingMemory.getFactHandle(collection); + } + rc = new Context(collFact, null, null); + rc.set(me.alias, collection); + rc.match = fc.match.merge(rc.match); // the match doesn't change + rc.hashCode = rc.match.hashCode; + // + me.collectionHash[fc.hashCode] = rc; + // + var fm = this.fromMemory[collFact.id]; // some bookeeping for modify / retract, revisit, not sure we need this + if (!fm) { + fm = this.fromMemory[collFact.id] = {}; + } + } + else { + verb = 'modify' + collection = rc.fact.object; + } + // + // build up the collection, the simplest case an array that accumulates a single var + me.setAliases.forEach( function(alias, i) { + var lcValue = lcFh[alias], idx; + // + if( me.isCollectionObject ) { // { key1: [values], key2: [values], ... } + collection[alias] = collection[alias] || []; + idx = extd.findIndex(collection[alias], lcValue); + -1 === idx ? collection[alias].push(lcValue) : undefined; + } + else { // the collection's an array + idx = extd.findIndex(collection, lcValue); // don't reproduce values already in the collection + if( 1 === me.setAliases.length ) { + -1 === idx ? collection.push(lcValue) : undefined; + } + else { // [ {alias:val, key2: val}, {...}, ... ] + if( !setHash ) { + setHash = {}; + setHash.filteredHashCode = fc.hashCode; // tag this so we can find it on retractions + collection.push(setHash); // [{...}, {...}...] + } + setHash[alias] = lcValue; + } + } + }); + // + createdContext = me._createMatch(lc, fc, rc); + if (createdContext.isMatch() ) { + this.__propagate(verb, createdContext.clone()); + } + } + /* + We are called with the original lc, the 'pruned' version of the lc and the rc. + */ + ,_createMatch: function (lc, fc, rc) { + var me = this + ,match = rc.match //fc.match.merge(rc.match) // the derived match + ,collFact = rc.fact + ,collection = rc.fact.object + ,eqConstraints = this.__equalityConstraints + ,i = -1,l = eqConstraints.length + ,createdContext, fh; + // + if( match.hashCode !== (fc.hashCode + ':' + rc.fact.id) ) { + throw new Error('invalid match hashCode'); + } + fh = Object.create(lc.factHash); //_.merge({}, lc.factHash); + fh[me.alias] = collection; + // check to see if any condition expressions pass before we propagate anything + while (++i < l) { + if (!eqConstraints[i](fh, fh)) { // fh: all the 'regular' bindings plus a single collection binding + createdContext = DEFAULT_MATCH; // this is a non-match; e.g. lc.isMatch() => false + break; + } + } + if (!createdContext) { + lc.fromMatches[fc.hashCode] = createdContext = lc.clone(rc.fact, null, match); + } + this.fromMemory[fc.hashCode] = [lc, createdContext]; + return createdContext; + } + // + ,modifyLeft: function (lc, retract) { + var me = this + ,ctx = this.removeFromLeftMemory(lc) + ,fc = this.getFilteredContext(lc) + ,rc = me.collectionHash[fc.hashCode] // a collection for each combo of non-collection binding(s) + ,match = rc.match // fc.match.merge(rc.match) // the rc.fact is the collection + ,collFact = rc.fact + ,collection = collFact.object + ,empty = true + ,oldLcFh, newLcFh, lcOldValue, setHash, matchContext; + if (ctx) { + ctx = ctx.data; + if(!retract) { + this.__addToLeftMemory(lc); + lc.fromMatches = {}; + } + oldLcFh = ctx.factHash; + newLcFh = lc.factHash; + rightMatches = ctx.fromMatches; + // + // it's a modify all i have to do is replace the set variables in the collection with their modified values + try { + me.setAliases.forEach(function(alias) { + var theArray, idx; + lcOldValue = oldLcFh[alias]; + lcNewValue = newLcFh[alias]; + if( me.isCollectionObject ) { // a hash of sets -> { key: [values], key2: [values], ... } + theArray = collection[alias]; + idx = extd.findIndex(theArray, lcOldValue); + retract ? delete theArray[idx] : theArray[idx] = lcNewValue; + if(retract && empty) { empty = theArray.length ? false : true } + } + else { // an array either of single values OR [{hash of single values}, {...},...] + if( 1 === me.setAliases.length ) { // a simple array + idx = extd.findIndex(collection, lcOldValue); + retract ? delete collection[idx] : collection[idx] = lcNewValue; + } + else { // we have a hash remove the entire thing from the array + collection.some(function(x,i) { if( x.filteredHashCode === fc.hashCode ) { idx = i; return true }}); + collection.splice(idx); + if(!retract) { + if(!setHash) { + setHash = {}; setHash.filteredHashCode = fc.hashCode; + collection[idx] = setHash; + setHash[alias] = lcNewValue; + } + else { + setHash[alias] = lcNewValue; + } + } + else { + if( empty ) { empty = collection.length ? false : true } + } + } + } + }); + // + if( retract && empty ) { + for (var i in ctx.fromMatches) { + matchContext = ctx.fromMatches[i]; + this.removeFromFromMemory(matchContext); + this.__propagate("retract", matchContext.clone()); + } + } + else { + lc.fromMatches[fc.hashCode] = createdContext = lc.clone(collFact, null, match); + this.fromMemory[fc.hashCode] = [lc, createdContext]; + this.__propagate("modify", createdContext.clone()); + } + } + catch(e) { + throw new Error(e); + } + } + else { + this.assertLeft(lc); + } + } + // + ,retractLeft: function(lc) { + return this.modifyLeft(lc, true); + } + // + // clone the (left) context and remove the set oriented variables + // + ,getFilteredContext: function(lc) { + var me = this + ,match = new Match() + ,clone; + // the aliases array is in order so the filtered aliases are also in order + lc.match.aliases.forEach(function(alias, i) { + var idx = extd.findIndex(me.setAliases, function(alias) { return function(x) { return x == alias; }}(alias)) + ,fact; + if( idx === -1 ) { + fact = lc.match.facts[i]; + match.addFact(fact); + match.factHash[alias] = fact.object; + } + }); + clone = lc.clone(null, null, match); // with set variables removed + extdObj(match.factHash).forEach( function(val, key) {clone.set(key, val);}); + return clone; + } + // + // clone the (left) context and remove the set oriented variables + // + ,getFilteredHashCode: function(context) { + var me = this + ,facts = [] + ,tmp; + // + context.match.aliases.forEach(function(alias, i) { + if( extd.findIndex(me.setAliases, function(x) { return x == alias; } ) === -1) { + facts.push(context.factHash[alias]); + } + }); + return facts.map(function(x){ return x.id; }).reduce(function(prev, cur, idx, array) { return prev ? ( prev + ':' + cur ) : ('' + cur) }); + } + + } +}).as(module); + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/nodes/index.js b/lib/nodes/index.js index 2f4e6ed..0a23294 100644 --- a/lib/nodes/index.js +++ b/lib/nodes/index.js @@ -11,7 +11,8 @@ var extd = require("../extended"), FromExistsPattern = pattern.FromExistsPattern, NotPattern = pattern.NotPattern, CompositePattern = pattern.CompositePattern, - InitialFactPattern = pattern.InitialFactPattern, + InitialFactPattern = pattern.InitialFactPattern, + CollectPattern = pattern.CollectPattern, constraints = require("../constraint"), HashConstraint = constraints.HashConstraint, ReferenceConstraint = constraints.ReferenceConstraint, @@ -28,7 +29,9 @@ var extd = require("../extended"), RightAdapterNode = require("./rightAdapterNode"), TypeNode = require("./typeNode"), TerminalNode = require("./terminalNode"), - PropertyNode = require("./propertyNode"); + PropertyNode = require("./propertyNode"), + CollectNode = require("./collectNode"); + function hasRefernceConstraints(pattern) { return some(pattern.constraints || [], function (c) { @@ -113,7 +116,7 @@ declare({ } } }, - + __checkEqual: function (node) { var constraints = this.constraints, i = constraints.length - 1; for (; i >= 0; i--) { @@ -163,6 +166,8 @@ declare({ joinNode = new ExistsFromNode(pattern.rightPattern, this.workingMemory); } else if (pattern.rightPattern instanceof ExistsPattern) { joinNode = new ExistsNode(); + } else if (pattern.rightPattern instanceof CollectPattern ) { + joinNode = new CollectNode(pattern.rightPattern, this.workingMemory); } else if (pattern.rightPattern instanceof FromNotPattern) { joinNode = new FromNotNode(pattern.rightPattern, this.workingMemory); } else if (pattern.rightPattern instanceof FromPattern) { diff --git a/lib/parser/nools/nool.parser.js b/lib/parser/nools/nool.parser.js index 71d4012..3b6b571 100644 --- a/lib/parser/nools/nool.parser.js +++ b/lib/parser/nools/nool.parser.js @@ -1,6 +1,6 @@ "use strict"; -var tokens = require("./tokens.js"), +var tokens = require("./tokens.js").topLevelTokens, extd = require("../../extended"), keys = extd.hash.keys, utils = require("./util.js"); diff --git a/lib/parser/nools/tokens.js b/lib/parser/nools/tokens.js index 3ac7080..2ead85f 100644 --- a/lib/parser/nools/tokens.js +++ b/lib/parser/nools/tokens.js @@ -5,7 +5,7 @@ var utils = require("./util.js"), extd = require("../../extended"), filter = extd.filter, indexOf = extd.indexOf, - predicates = ["not", "or", "exists"], + predicates = ["not", "or", "exists"], predicateRegExp = new RegExp("^(" + predicates.join("|") + ") *\\((.*)\\)$", "m"), predicateBeginExp = new RegExp(" *(" + predicates.join("|") + ") *\\(", "g"); @@ -38,8 +38,82 @@ var splitRuleLineByPredicateExpressions = function (ruleLine) { return ret.join(";"); }; -var ruleTokens = { +/** +*/ +var ruleRegExp = /^(\$?\w+)\s*:\s*(\w+)(.*)/; +var collectRuleRegExp = /^(\$?\w+)\s*:\s*(\w+)\s*(.*)from\s*collect\s*\((.*)\)/; +var hashRegExp = /(\{ *(?:["']?\$?\w+["']?\s*:\s*["']?\$?\w+["']? *(?:, *["']?\$?\w+["']?\s*:\s*["']?\$?\w+["']?)*)+ *\})/; +var fromRegExp = /(\bfrom\s+.*)/; +var parseRules = function (str) { + var rules = []; + var ruleLines = str.split(";"), l = ruleLines.length, ruleLine, alias, constraints, parts, collectRule; + for (var i = 0; i < l && (ruleLine = ruleLines[i].replace(/^\s*|\s*$/g, "").replace(/\n/g, "")); i++) { + if (!isWhiteSpace(ruleLine)) { + var rule = []; + if (predicateRegExp.test(ruleLine)) { + var m = ruleLine.match(predicateRegExp); + var pred = m[1].replace(/^\s*|\s*$/g, ""); + rule.push(pred); + ruleLine = m[2].replace(/^\s*|\s*$/g, ""); + if (pred === "or") { + rule = rule.concat(parseRules(splitRuleLineByPredicateExpressions(ruleLine))); + rules.push(rule); + continue; + } + } + else if( collectRuleRegExp.test(ruleLine) ) { + parts = ruleLine.match(collectRuleRegExp); + if(parts && parts.length ) { + rule.push('collect'); + rule.push(parts[2], parts[1]); + constraints = parts[3].trim(); + rule.push( (constraints && !isWhiteSpace(constraints)) ? constraints : undefined ); + collectRule = parseRules(parts[4]); + rule.push(collectRule[0]); + } + rules.push(rule); + continue; + } + parts = ruleLine.match(ruleRegExp); + if (parts && parts.length) { + rule.push(parts[2], parts[1]); + constraints = parts[3].replace(/^\s*|\s*$/g, ""); + var hashParts = constraints.match(hashRegExp), from = null, fromMatch; + if (hashParts) { + var hash = hashParts[1], constraint = constraints.replace(hash, ""); + if (fromRegExp.test(constraint)) { + fromMatch = constraint.match(fromRegExp); + from = fromMatch[0]; + constraint = constraint.replace(fromMatch[0], ""); + } + if (constraint) { + rule.push(constraint.replace(/^\s*|\s*$/g, "")); + } + if (hash) { + rule.push(eval("(" + hash.replace(/(\$?\w+)\s*:\s*(\$?\w+)/g, '"$1" : "$2"') + ")")); + } + } else if (constraints && !isWhiteSpace(constraints)) { + if (fromRegExp.test(constraints)) { + fromMatch = constraints.match(fromRegExp); + from = fromMatch[0]; + constraints = constraints.replace(fromMatch[0], ""); + } + rule.push(constraints); + } + if (from) { + rule.push(from); + } + rules.push(rule); + } else { + throw new Error("Invalid constraint " + ruleLine); + } + } + } + return rules; +}; +exports.parseRules = parseRules; +var ruleTokens = { salience: (function () { var salienceRegexp = /^(salience|priority)\s*:\s*(-?\d+)\s*[,;]?/; return function (src, context) { @@ -93,7 +167,7 @@ var ruleTokens = { } }; })(), - + "agenda-group": function () { return this.agendaGroup.apply(this, arguments); }, @@ -109,66 +183,6 @@ var ruleTokens = { when: (function () { /*jshint evil:true*/ - var ruleRegExp = /^(\$?\w+) *: *(\w+)(.*)/; - - var constraintRegExp = /(\{ *(?:["']?\$?\w+["']?\s*:\s*["']?\$?\w+["']? *(?:, *["']?\$?\w+["']?\s*:\s*["']?\$?\w+["']?)*)+ *\})/; - var fromRegExp = /(\bfrom\s+.*)/; - var parseRules = function (str) { - var rules = []; - var ruleLines = str.split(";"), l = ruleLines.length, ruleLine; - for (var i = 0; i < l && (ruleLine = ruleLines[i].replace(/^\s*|\s*$/g, "").replace(/\n/g, "")); i++) { - if (!isWhiteSpace(ruleLine)) { - var rule = []; - if (predicateRegExp.test(ruleLine)) { - var m = ruleLine.match(predicateRegExp); - var pred = m[1].replace(/^\s*|\s*$/g, ""); - rule.push(pred); - ruleLine = m[2].replace(/^\s*|\s*$/g, ""); - if (pred === "or") { - rule = rule.concat(parseRules(splitRuleLineByPredicateExpressions(ruleLine))); - rules.push(rule); - continue; - } - - } - var parts = ruleLine.match(ruleRegExp); - if (parts && parts.length) { - rule.push(parts[2], parts[1]); - var constraints = parts[3].replace(/^\s*|\s*$/g, ""); - var hashParts = constraints.match(constraintRegExp), from = null, fromMatch; - if (hashParts) { - var hash = hashParts[1], constraint = constraints.replace(hash, ""); - if (fromRegExp.test(constraint)) { - fromMatch = constraint.match(fromRegExp); - from = fromMatch[0]; - constraint = constraint.replace(fromMatch[0], ""); - } - if (constraint) { - rule.push(constraint.replace(/^\s*|\s*$/g, "")); - } - if (hash) { - rule.push(eval("(" + hash.replace(/(\$?\w+)\s*:\s*(\$?\w+)/g, '"$1" : "$2"') + ")")); - } - } else if (constraints && !isWhiteSpace(constraints)) { - if (fromRegExp.test(constraints)) { - fromMatch = constraints.match(fromRegExp); - from = fromMatch[0]; - constraints = constraints.replace(fromMatch[0], ""); - } - rule.push(constraints); - } - if (from) { - rule.push(from); - } - rules.push(rule); - } else { - throw new Error("Invalid constraint " + ruleLine); - } - } - } - return rules; - }; - return function (orig, context) { var src = orig.replace(/^when\s*/, "").replace(/^\s*|\s*$/g, ""); if (utils.findNextToken(src) === "{") { @@ -324,7 +338,7 @@ var topLevelTokens = { } }, - "rule": function (orig, context, parse) { + "rule": function (orig, context, parse) { var src = orig.replace(/^rule\s*/, ""); var name = src.match(/^([a-zA-Z_$][0-9a-zA-Z_$]*|"[^"]*"|'[^']*')/); if (name) { @@ -343,8 +357,7 @@ var topLevelTokens = { } else { throw new Error("missing name"); } - } }; -module.exports = topLevelTokens; +exports.topLevelTokens = topLevelTokens; diff --git a/lib/pattern.js b/lib/pattern.js index c776e2f..d18f238 100644 --- a/lib/pattern.js +++ b/lib/pattern.js @@ -99,6 +99,45 @@ ObjectPattern.extend().as(exports, "NotPattern"); ObjectPattern.extend().as(exports, "ExistsPattern"); FromPattern.extend().as(exports, "FromExistsPattern"); +var CollectPattern = FromPattern.extend({ + static: { + fromLiteral: function(pojo, prototype) { + var me = this._super([pojo, prototype || AccumulatePattern.prototype]); + return me; + } + }, + instance: { + type: "CollectPattern" + ,asLiteral: function(flow) { + var def = this._super(arguments); + def.$className = 'AccumulatePattern'; + return def; + }, + // + constructor: function (type, alias, conditions, store, options) { + //this._super([type, alias, conditions, store, options]); + ObjectPattern.prototype.constructor.call(this, type, alias, conditions, store, options); + // source expression is baked into CollectNode + this.from = { + asLiteral: function() { + return {} + } + ,assert: function() { + throw new Error('the collection is implicitly provided'); + } + }; + }, + // + getAccumulation: function() { + return this.from.accumulation; + } + // + ,block: function(b) { + this.from.blocked = b; + } + } +}).as(exports, "CollectPattern"); + Pattern.extend({ instance: { diff --git a/lib/rule.js b/lib/rule.js index 75a73d8..72dba38 100644 --- a/lib/rule.js +++ b/lib/rule.js @@ -8,14 +8,17 @@ var extd = require("./extended"), format = extd.format, parser = require("./parser"), pattern = require("./pattern"), + parseRules = require('./parser/nools/tokens.js').parseRules, ObjectPattern = pattern.ObjectPattern, FromPattern = pattern.FromPattern, NotPattern = pattern.NotPattern, ExistsPattern = pattern.ExistsPattern, FromNotPattern = pattern.FromNotPattern, FromExistsPattern = pattern.FromExistsPattern, - CompositePattern = pattern.CompositePattern; + CompositePattern = pattern.CompositePattern, + CollectPattern = pattern.CollectPattern; +// var parseConstraint = function (constraint) { if (typeof constraint === 'function') { // No parsing is needed for constraint functions @@ -130,7 +133,19 @@ var getParamType = function getParamType(type, scope) { return _getParamType(type); }; +// +var collectRegExp = /collect\s*\((.*)\)$/; +// +function mergePatterns(patterns, merged, setAliases) { + var flattened = extd(patterns).flatten().value(); + flattened.forEach(function (thePattern) { + setAliases.push(thePattern.alias); + merged.push([thePattern]); + }); + return merged; +} +// var parsePattern = extd .switcher() .containsAt("or", 0, function (condition) { @@ -192,22 +207,113 @@ var parsePattern = extd ]; } }) + .containsAt('collect', 0, function(condition) { + var scope = condition.scope, + type = condition[1], + alias = condition[2] || "__~PBV~__", + constraint = condition[3] || "true", + parsedCollect = condition[4], + validType = getParamType(type, scope), + parsedConstraint = parseConstraint(constraint), + collectHash = parsedCollect[3], + setAliases = [], merged = [], emptyHash = {}, + collectPattern, parsedConstraint, setConditions, patterns, merged, isRules, compPat; + // + collectPattern = new CollectPattern(validType, alias, parsedConstraint, emptyHash, {scope: scope, pattern: constraint}); + collectPattern.collectHash = collectHash; + // + isRules = extd.every(parsedCollect, function (cond) { + return isArray(cond); + }); + if (isRules && parsedCollect.length === 1) { + parsedCollect = parsedCollect[0]; + isRules = false; + } + // + if( isRules ) { + parsedCollect.map(function(cond) { + cond.scope = scope; + patterns = parsePattern(cond); + mergePatterns(patterns, merged, setAliases); + }); + // + collectPattern.setAliases = setAliases; + merged.push([collectPattern]); + return merged; + } + else { // there was a single condition inside of from collect( expr ); + parsedCollect.scope = scope; + patterns = parsePattern(parsedCollect); + mergePatterns(patterns, merged, setAliases); + collectPattern.setAliases = setAliases; + merged.push([collectPattern]); + return merged; + } + }) .def(function (condition) { if (typeof condition === 'function') { return [condition]; } condition = normailizeConstraint(condition); if (condition[4] && condition[4].from) { - return [ - new FromPattern( - getParamType(condition[0], condition.scope), - condition[1] || "m", - parseConstraint(condition[2] || "true"), - condition[3] || {}, - parseConstraint(condition[4].from), - {scope: condition.scope, pattern: condition[2]} - ) - ]; + var fromArg = condition[4].from + ,merged,setAliases,collectExpr, setConditions, patterns, merged, isRules, collectPattern,compPat; + // + if( 'string' === typeof fromArg && fromArg.match( collectRegExp ) ) { + collectExpr = fromArg.match(collectRegExp)[1]; + // + // every alias from these setConditions is an SOV: Set Oriented Variable + setConditions = parseRules(collectExpr); + setAliases = []; merged = []; + // + isRules = extd.every(setConditions, function (cond) { + return isArray(cond); + }); + if (isRules && setConditions.length === 1) { + setConditions = setConditions[0]; + isRules = false; + } + // + collectPattern = new CollectPattern( + getParamType(condition[0], condition.scope), // type; Array or Object + condition[1] || "__~PBV~__", // missing pattern binding must be detected + parseConstraint(condition[2] || "true"), // expression using from + condition[3] || {}, // store + {scope: condition.scope, pattern: condition[2]} // misc... + ); + // + if( isRules ) { + setConditions.map(function(cond) { + cond.scope = condition.scope; + patterns = parsePattern(cond); + mergePatterns(patterns, merged, setAliases); + }); + // + collectPattern.setAliases = setAliases; + merged.push([collectPattern]); + return merged; + } + else { // there was a single condition inside of from collect( expr ); + setConditions.scope = condition.scope; + patterns = parsePattern(setConditions); + mergePatterns(patterns, merged, setAliases); + collectPattern.setAliases = setAliases; + merged.push([collectPattern]); + return merged; + } + } + else { + return [ + new FromPattern( + getParamType(condition[0], condition.scope), + condition[1] || "m", + parseConstraint(condition[2] || "true"), + condition[3] || {}, + parseConstraint(condition[4].from), + {scope: condition.scope, pattern: condition[2]} + ) + ]; + } } else { return [ new ObjectPattern( @@ -218,15 +324,18 @@ var parsePattern = extd {scope: condition.scope, pattern: condition[2]} ) ]; - } + } }).switcher(); var Rule = declare({ + instance: { + constructor: function (name, options, pattern, cb) { this.name = name; this.pattern = pattern; this.cb = cb; + this.noLoop = options.noLoop; if (options.agendaGroup) { this.agendaGroup = options.agendaGroup; this.autoFocus = extd.isBoolean(options.autoFocus) ? options.autoFocus : false; @@ -236,7 +345,7 @@ var Rule = declare({ fire: function (flow, match) { var ret = new Promise(), cb = this.cb; - try { + try { if (cb.length === 3) { cb.call(flow, match.factHash, flow, ret.resolve); } else { @@ -249,67 +358,123 @@ var Rule = declare({ } } }); +exports.Rule = Rule; +// +function _mergePatterns (patterns) { + // + return function (patt, i) { + // [pattern], [pattern], ... in arrays of length 1 + // we wish to build a single array in order of lhs progression + if( isArray(patt) ) { + if( patt.length === 1 ) { + patt = patt[0]; + i = 0; + } + else { + throw new Error('invalid pattern structure'); + } + } + if (!patterns[i]) { + patterns[i] = i === 0 ? [] : patterns[i - 1].slice(); + //remove dup + if (i !== 0) { + patterns[i].pop(); + } + patterns[i].push(patt); + } else { + extd(patterns).forEach(function (p) { + p.push(patt); + }); + } + }; +} +// +// function createRule(name, options, conditions, cb) { - if (isArray(options)) { - cb = conditions; - conditions = options; - } else { - options = options || {}; - } - var isRules = extd.every(conditions, function (cond) { - return isArray(cond); - }); - if (isRules && conditions.length === 1) { - conditions = conditions[0]; - isRules = false; - } - var rules = []; - var scope = options.scope || {}; - conditions.scope = scope; - if (isRules) { - var _mergePatterns = function (patt, i) { - if (!patterns[i]) { - patterns[i] = i === 0 ? [] : patterns[i - 1].slice(); - //remove dup - if (i !== 0) { - patterns[i].pop(); - } - patterns[i].push(patt); - } else { - extd(patterns).forEach(function (p) { - p.push(patt); - }); - } - - }; - var l = conditions.length, patterns = [], condition; - for (var i = 0; i < l; i++) { - condition = conditions[i]; - condition.scope = scope; - extd.forEach(parsePattern(condition), _mergePatterns); - - } - rules = extd.map(patterns, function (patterns) { - var compPat = null; - for (var i = 0; i < patterns.length; i++) { - if (compPat === null) { - compPat = new CompositePattern(patterns[i++], patterns[i]); - } else { - compPat = new CompositePattern(compPat, patterns[i]); - } - } - return new Rule(name, options, compPat, cb); - }); - } else { - rules = extd.map(parsePattern(conditions), function (cond) { - return new Rule(name, options, cond, cb); - }); - } - return rules; + var rules = [], scope, patterns, isComposite; + function processConditions(conditions, scope) { + var l = conditions.length, + merged = [], + fnMerge = _mergePatterns(merged), + isRules = extd.every(conditions, function (cond) {return isArray(cond);}), + condition, rules, patterns; + // + if( isRules && conditions.length === 1 ) { + isRules = false; + conditions = conditions[0]; + } + // + function isSinglePattern(patterns) { + var ret = true; + if( patterns.length > 1 ) { + if( isArray(patterns[0]) ) { + ret = false; + } + // else it's OR [ p, p,...] which we treat as a single rule which results in multiple rules + } + return ret; + } + // + function patternFromCondition(condition, scope) { + var patterns; + condition.scope = scope; + patterns = parsePattern(condition); + return patterns; + } + // + function compositePattern(patterns) { + + return extd.map(merged, function (patterns) { + var compPat = null; + for (var i = 0; i < patterns.length; i++) { + if (compPat === null) { + compPat = new CompositePattern(patterns[i++], patterns[i]); + } else { + compPat = new CompositePattern(compPat, patterns[i]); + } + } + return new Rule(name, options, compPat, cb); + }); + } + // + function singlePattern(pattern) { + return extd.map(patterns, function (cond) { + return new Rule(name, options, cond, cb); + }); + } + // + if( isRules ) { + for (var i = 0; i < l; i++) { + condition = conditions[i]; + condition.scope = scope; + patterns = patternFromCondition(condition, scope); + extd.forEach( patterns, fnMerge ); + } + rules = compositePattern(merged); + } + else { + patterns = patternFromCondition(conditions, scope); + if( isSinglePattern(patterns) ) { + rules = singlePattern(patterns); + } + else { + extd.forEach( patterns, fnMerge ); + rules = compositePattern(merged); + } + } + return rules; + } + // + if (isArray(options)) { + cb = conditions; + conditions = options; + options = {}; + } else { + options = options || {}; + } + scope = options.scope || {}; + return processConditions(conditions, scope); } - exports.createRule = createRule; - - diff --git a/readme.md b/readme.md index e5bc0d9..f76c6f9 100644 --- a/readme.md +++ b/readme.md @@ -38,10 +38,11 @@ Or [download the source](https://raw.github.com/C2FO/nools/master/nools.js) ([mi * [Salience](#rule-salience) * [Scope](#rule-scope) * [Constraints](#constraints) - * [Custom](#custom-contraints) + * [Custom](#custom-contraints) * [Not](#not-constraint) * [Or](#or-constraint) * [From](#from-constraint) + * [Collect](#collect-modifer) * [Exists](#exists-constraint) * [Actions](#action) * [Async Actions](#action-async) @@ -80,7 +81,7 @@ var Message = function (message) { var flow = nools.flow("Hello World", function (flow) { //find any message that start with hello - flow.rule("Hello", [Message, "m", "m.text =~ /^hello\\sworld$/"], function (facts) { + flow.rule("Hello", [Message, "m", "m.text =~ /^hello(\\s*world)?$/"], function (facts) { facts.m.text = facts.m.text + " goodbye"; this.modify(facts.m); }); @@ -97,7 +98,7 @@ In the above flow definition 2 rules were defined * Hello * Requires a Message - * The messages's `text` must match the regular expression `/^hello\\sworld$/` + * The messages's `text` must match the regular expression `/^hello(\\s*world)?$/` * When matched the message's `text` is modified and then we let the engine know that we modified the message. * Goodbye * Requires a Message @@ -341,7 +342,6 @@ session.getFacts(Number); //[1, 2]; session.getFacts(String); //["A", "B"]; ``` - ## Firing the rules @@ -1003,8 +1003,6 @@ flow1 }); ``` - - ### Scope @@ -1038,7 +1036,7 @@ function matches(str, regex){ rule Hello { when { - m : Message matches(m.text, /^hello\s*world)?$/); + m : Message matches(m.text, /^hello(\\s*world)?$/); } then { modify(m, function(){ @@ -1061,7 +1059,7 @@ Or you can pass in a custom function using the scope option in compile. ``` rule Hello { when { - m : Message doesMatch(m.text, /^hello\sworld$/); + m : Message doesMatch(m.text, /^hello(\\s*world)?$/); } then { modify(m, function(){ @@ -1194,7 +1192,6 @@ session.match().then(function(){ console.log("DONE"); }); ``` - #### Not Constraint @@ -1406,6 +1403,29 @@ rule "my rule", { } ``` + + +###Collect Modifier +The 'collect' modifer results in a returned object, as such a pattern can specify collect as its 'from' source. +The 'collect' modifer returns an array which allows cardinality reasoning (when there are more than 7 red buses). + +This example chains two 'from's together. It finds customers who have bought items all of which are priced over 10, where the items are a field and not asserted into the working memory: + ```javascript +c : Customer +items : Array items.length == c.items.length ) from collect( item : Item item.price > 10 from c.items); + ``` + If the items where not a field, but instead asserted into the working memory, we could use a correlated 'collect' pattern: +```javascript +p : Person ; +list: Array from collect( item: Item item.owner === p ); +items : Array items.length === list.length from collect( item: Item item.price > 10 from list ); + ``` + This blog post from Marc Proctor the team lead on Drools explains collect in more detail. + http://blog.athico.com/2007/06/chained-from-accumulate-collect.html + + This paper was used to develop the collect node: + http://citeseer.ist.psu.edu/viewdoc/download?doi=10.1.1.25.1076&rep=rep1&type=pdf + ###Exists Constraint @@ -1690,7 +1710,7 @@ define Message { rule Hello { when { - m : Message m.text =~ /^hello\sworld$/ + m : Message m.text =~ /^hello(\\sworld)?$/ } then { modify(m, function(){ diff --git a/test/flow.dsl.test.js b/test/flow.dsl.test.js index 01e3855..8fdfe27 100644 --- a/test/flow.dsl.test.js +++ b/test/flow.dsl.test.js @@ -593,4 +593,5 @@ it.describe("Flow dsl", function (it) { }); }); }); + }); \ No newline at end of file diff --git a/test/flow/collect.nools b/test/flow/collect.nools new file mode 100644 index 0000000..53251f1 --- /dev/null +++ b/test/flow/collect.nools @@ -0,0 +1,83 @@ + +define System { + location : "", + status: 'normal', + constructor : function (loc) { + this.location = loc; + } +} + +define Alarm { + system: undefined + ,status: 'normal' + ,purpose: undefined + ,constructor: function(system, status, purpose) { + this.system = system; + this.status = status; + this.purpose = purpose; + } +} + +define Emergency { + system : undefined, + alarms: undefined, + constructor : function (system, alarms) { + this.system = system; + this.alarms = alarms; + } +} + + +/** + classic Drools alarm example +*/ +rule "Raise priority if system has more than 3 pending alarms" { + when { + $system : System $system.status === 'normal'; + $alarms : Array $alarms.length >= 3 from collect( $alarm : Alarm $alarm.system === $system and $alarm.status === 'pending' ); + } + then { + emit('system-alarms', $system, $alarms); + modify($system, function() {this.status = 'alarms-pending';}) + } +} +/** + if the system is alarmed respond by asserting as Emergency fact. +*/ +rule "If System is alarmed respond by asserting Emergency" { + when { + $system : System $system.status === 'alarms-pending'; + $alarms : Array from collect( $alarm : Alarm $alarm.system === $system and $alarm.status === 'pending' ); + } + then { + emit('system-emergency', $system, $alarms); + modify($system, function() {this.status = 'emergency-declared';}); + var e = new Emergency($system, $alarms); + assert(e); + } +} +/** + if there is an emergency turn of +*/ +rule "If System is alarmed respond by asserting Emergency" { + when { + $emergency : Emergency; + } + then { + emit('emergency-response', $emergency); + } +} + +/** + find customers who have bought items all of which are priced over 10 + where the items are a file and not in working memory. +*/ +rule TestCollect { + when { + c : Customer; + items: Array items.size === c.items.size from collect( item: Item item.price > 10 from c.items ); + } + then { + emit('test-collect', c, items); + } +} diff --git a/test/flow/collect.test.js b/test/flow/collect.test.js new file mode 100644 index 0000000..9fbe4a8 --- /dev/null +++ b/test/flow/collect.test.js @@ -0,0 +1,221 @@ +"use strict"; +var it = require("it"), + assert = require("assert"), + nools = require("../../"); + +// +function Customer(name) { + this.name = name; + this.items = []; +} +Customer.prototype = { + add: function(order) { + this.items.push(order); + } + ,getOrder: function(name) { + var item; + this.items.some(function(order) { if( order.type === name ) {item = order; return true;} }); + return item; + } +} +// +function Item(type, price) { + this.type = type; + this.price = price; +} +// +var rule1Called = 0; +var rule2Called = 0; +var rule3Called = 0; + +it.describe("collect condition", function (it) { + + it.describe("basic test of collection element", function (it) { +debugger; + var flow = nools.flow("collect test 1",function (flow) { + flow.addDefined('Customer', Customer); + flow.addDefined('Item', Item); + flow.rule("rule 1" , {salience: 10, scope: { Customer: Customer, Item: Item }}, [ + [Customer, 'c'] + ,[Array, 'list', 'list.size === c.items.size', 'from collect( item : Item item.price > 10 from c.items )'] + ], function(facts) { +debugger; + rule1Called++; + }); + }); + // + it.should("rhs for collection called a single time and set avaialable in lhs", function () { +debugger; + var session = flow.getSession(); + // + var customer = new Customer('John'); + var stroller = new Item('stroller', 50); + var bike = new Item('bike', 11); + var car = new Item('car', 2500); + session.assert(stroller);session.assert(bike);session.assert(car); + customer.add(stroller);customer.add(bike);customer.add(car); + session.assert(customer); + rule1Called = 0; +debugger; + return session.match().then(function () { +debugger; + assert.equal(rule1Called, 1); + }); + }); + + // + var flow = nools.flow("collect test 2",function (flow) { + flow.addDefined('Customer', Customer); + flow.addDefined('Item', Item); + flow.rule("rule 1" , {salience: 10, scope: { Customer: Customer, Item: Item }}, [ + [Customer, 'c'] + ,[Array, 'list', 'list.size === c.items.size', 'from collect( item : Item item.price > 10 from c.items)'] + ], function(facts) { + rule1Called++; + }); + flow.rule("rule 2", {salience: 5, noLoop: true, scope: { Customer: Customer, Item: Item }}, [ + [Item, 'item', "item.type == 'bike' && item.price !== 11"] + ], function (facts) { + rule2Called++; + facts.item.price = 11; + this.modify(facts.item); + }); + }); + // + it.should("use set in from expression with modified fact causing rhs to fire once", function () { + var session = flow.getSession(); + // + var customer = new Customer('John'); + var stroller = new Item('stroller', 50); + var bike = new Item('bike', 9); + var car = new Item('car', 2500); + session.assert(stroller);session.assert(bike);session.assert(car); + customer.add(stroller);customer.add(bike);customer.add(car); + session.assert(customer); + // + rule1Called = rule2Called = 0; + // + return session.match().then(function () { + assert.equal(rule1Called, 1); + assert.equal(rule2Called, 1); + }); + }); + + }); + + it.describe("basic test of collect using DSL", function (it) { + var defines = { Customer: Customer, Item: Item }, + flow = nools.compile(__dirname + '/collect.nools', { name: 'TestCollect', defines: defines} ), + rule1Called = 0; + it.should("alarm example initial states cause rule fire", function () { + // + var System = flow.getDefined('System'), + Alarm = flow.getDefined('Alarm'), + session = flow.getSession(), + system, alarmA, alarmB, alarmC; + // + session.on("system-alarms", function (system, alarms) { + rule1Called++; + assert.equal(alarms.length, 3); + assert.equal(system.location, 'kitchen'); + assert.deepEqual(alarms[0], alarmA); + assert.deepEqual(alarms[1], alarmB); + assert.deepEqual(alarms[2], alarmC); + }); + // + system = new System('kitchen'); + session.assert(system); + + alarmA = new Alarm(system, 'pending', 'door'); // create three alarms for the System + alarmB = new Alarm(system, 'pending', 'fire'); + alarmC = new Alarm(system, 'pending', 'window'); + // + session.assert(alarmA); + session.assert(alarmB); + session.assert(alarmC); + // + return session.match().then(function () { + assert.equal(rule1Called, 1); + }); + }); + it.should("modified alarm example alarm causes rule fire", function () { + // + var System = flow.getDefined('System'), + Alarm = flow.getDefined('Alarm'), + session = flow.getSession(), + system, alarmA, alarmB, alarmC; + + rule1Called = 0; + rule2Called = 0; + rule3Called = 0; + // + session.on("system-alarms", function (system, alarms) { + rule1Called++; + assert.equal(alarms.length, 3); + assert.equal(system.location, 'kitchen'); + assert.deepEqual(alarms[0], alarmA); + assert.deepEqual(alarms[1], alarmB); + assert.deepEqual(alarms[2], alarmC); + }); + // + session.on("system-emergency", function (system, alarms) { + rule2Called++; + assert.equal(alarms.length, 3); + assert.equal(system.status, 'alarms-pending'); + }); + // + session.on("emergency-response", function (emergency) { + rule3Called++; + assert.equal(emergency.alarms.length, 3); + assert.deepEqual(emergency.system, system); + }); + + + // + system = new System('kitchen'); + session.assert(system); + + alarmA = new Alarm(system, 'pending', 'door'); // create three alarms for the System + alarmB = new Alarm(system, 'pending', 'fire'); + alarmC = new Alarm(system, 'normal', 'window'); + // + session.assert(alarmA); + session.assert(alarmB); + session.assert(alarmC); + // + return session.match().then(function () { + assert.equal(rule1Called, 0); + alarmC.status = 'pending'; + session.modify(alarmC); + return session.match().then(function() { + assert.equal(rule1Called, 1); + }); + }); + }); + + // + it.should("rhs for collection called a single time and set avaialable in lhs", function () { + var session = flow.getSession(); + var rule1Called = 0; + // + session.on("test-collect", function (customer, items) { + rule1Called++; + assert.equal(items.length, 3); + assert(customer); + }); + + // + var customer = new Customer('John'); + var stroller = new Item('stroller', 50); + var bike = new Item('bike', 11); + var car = new Item('car', 2500); + session.assert(stroller);session.assert(bike);session.assert(car); + customer.add(stroller);customer.add(bike);customer.add(car); + session.assert(customer); + // + return session.match().then(function () { + assert.equal(rule1Called, 1); + }); + }); + }); +});