From 3b41a66093e4142b1d0c5e7a299b0c6e5e462989 Mon Sep 17 00:00:00 2001 From: Mark Bjerke Date: Mon, 8 Feb 2016 16:05:25 -0800 Subject: [PATCH 1/2] checkin for rule attribute no-loop --- lib/agenda.js | 10 +++++- lib/parser/nools/tokens.js | 18 ++++++++++ lib/rule.js | 2 ++ readme.md | 27 +++++++++++++++ test/flow/noLoop.test.js | 67 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 test/flow/noLoop.test.js diff --git a/lib/agenda.js b/lib/agenda.js index 3028d40..38c38c0 100644 --- a/lib/agenda.js +++ b/lib/agenda.js @@ -84,7 +84,7 @@ module.exports = declare(EventEmitter, { register: function (node) { var agendaGroup = node.rule.agendaGroup; - this.rules[node.name] = {tree: new AVLTree({compare: this.comparator}), factTable: new FactHash()}; + this.rules[node.name] = { tree: new AVLTree({ compare: this.comparator }), factTable: new FactHash(), noLoop: {} }; if (agendaGroup) { this.addAgendaGroup(agendaGroup); } @@ -162,6 +162,14 @@ module.exports = declare(EventEmitter, { insert: function (node, insert) { var rule = this.rules[node.name], nodeRule = node.rule, agendaGroup = nodeRule.agendaGroup; + if (nodeRule.noLoop) { + if (rule.noLoop[insert.hashCode]) { + return; + } + else { + rule.noLoop[insert.hashCode] = true; + } + } rule.tree.insert(insert); this.getAgendaGroup(agendaGroup).insert(insert); if (nodeRule.autoFocus) { diff --git a/lib/parser/nools/tokens.js b/lib/parser/nools/tokens.js index 3ac7080..23391e2 100644 --- a/lib/parser/nools/tokens.js +++ b/lib/parser/nools/tokens.js @@ -94,6 +94,20 @@ var ruleTokens = { }; })(), + noLoop: (function () { + var noLoopRegexp = /^(noLoop|no-loop)\s*:\s*(-?true|false)\s*[,;]?/; + return function (src, context) { + if (noLoopRegexp.test(src)) { + var parts = src.match(noLoopRegexp); + // + context.options.noLoop = Boolean(parts[2]) + return src.replace(parts[0], ""); + } else { + throw new Error("invalid format"); + } + }; + })(), + "agenda-group": function () { return this.agendaGroup.apply(this, arguments); }, @@ -102,6 +116,10 @@ var ruleTokens = { return this.autoFocus.apply(this, arguments); }, + "no-loop": function () { + return this.noLoop.apply(this, arguments); + }, + priority: function () { return this.salience.apply(this, arguments); }, diff --git a/lib/rule.js b/lib/rule.js index 75a73d8..c473960 100644 --- a/lib/rule.js +++ b/lib/rule.js @@ -227,6 +227,8 @@ var Rule = declare({ 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; diff --git a/readme.md b/readme.md index aa8897c..5cc5c1b 100644 --- a/readme.md +++ b/readme.md @@ -43,6 +43,7 @@ Or [download the source](https://raw.github.com/C2FO/nools/master/nools.js) ([mi * [Structure](#rule-structure) * [Salience](#rule-salience) * [Scope](#rule-scope) + * [no-loop](#rule-no-loop) * [Constraints](#constraints) * [Custom](#custom-contraints) * [Not](#not-constraint) @@ -1008,6 +1009,32 @@ flow1 console.log(fired); //["Hello1", "Hello2", "Hello3", "Hello4"] }); ``` + +### No-Loop + +When a rule's action modifies a fact it may cause the rule to activate again, causing an infinite loop. Setting no-loop to true will skip the creation of another Activation for the rule with the current set of facts. + +```javascript +this.rule("Hello", {noLoop: true}, [Message, "m", "m.text like /hello/"], function (facts) { + var m = facts.m; + m.text = 'hello world'; + this.modify(m) +}); + +``` +Or using the DSL + +```javascript +rule Hello { + no-loop: true; + when { + m: Message m.name like /hello/; + } + then { modify(m, function() { + m.text = 'hello world' + }); +} +``` diff --git a/test/flow/noLoop.test.js b/test/flow/noLoop.test.js new file mode 100644 index 0000000..bcebe58 --- /dev/null +++ b/test/flow/noLoop.test.js @@ -0,0 +1,67 @@ +"use strict"; +var it = require("it"), + assert = require("assert"), + nools = require("../../"); + +it.describe("no-loop", function (it) { + /*jshint indent*/ + function Message(name) { + this.name = name; + } + var cnt = 0; + + var flow1 = nools.flow("noLoop1", function () { + + this.rule("Hello2", { noLoop: true }, [Message, "m", "m.name =~ /Hello/"], function (facts) { + var m = facts.m; + m.name = 'Hello World'; + this.modify(m); + }); + }), + + flow2 = nools.flow("noLoop2", function () { + + this.rule("Hello1", [Message, "m", "m.name =~ /Hello/"], function (facts) { + var m = facts.m; + if (cnt++ < 2) { + m.name = 'Hello World'; + this.modify(m); + } + }); + }); + + var noolsSource = "rule 'Hello3' { no-loop: true; when {m: Message m.name =~/Hello/;}then {modify(m, function () { this.name = 'Hello World'; });}}"; + + var flow3 = nools.compile(noolsSource, { + name: 'testDsl' + ,define: { + Message: Message + } + }); + + it.should("not loop with option on and loop otherwise", function () { + var fired1 = [], fired2 = [], fired3 = []; + var session1 = flow1.getSession(new Message("Hello")).on("fire", function (name) { + fired1.push(name); + }), + session2 = flow2.getSession(new Message("Hello")).on("fire", function (name) { + fired2.push(name); + }), + session3 = flow3.getSession(new Message("Hello")).on("fire", function (name) { + fired3.push(name); + }); + return session1.match() + .then(function () { + return session2.match().then(function () { + return session3.match().then(function () { + }) + }) + }) + .then(function () { + assert.deepEqual(fired1, ["Hello2"]); + assert.deepEqual(fired2, ["Hello1", "Hello1", "Hello1"]); + assert.deepEqual(fired3, ["Hello3"]); + }); + }); + +}); From 90542eafb60d0191ff38760a3ebef5662cd7843d Mon Sep 17 00:00:00 2001 From: Thomas TRIAU Date: Sun, 6 Nov 2016 12:27:45 +0100 Subject: [PATCH 2/2] fix --- lib/agenda.js | 4 +- lib/parser/nools/tokens.js | 5 +- test/flow/noLoop.test.js | 130 ++++++++++++++++++++++--------------- 3 files changed, 82 insertions(+), 57 deletions(-) diff --git a/lib/agenda.js b/lib/agenda.js index 38c38c0..39f4b3f 100644 --- a/lib/agenda.js +++ b/lib/agenda.js @@ -157,6 +157,7 @@ module.exports = declare(EventEmitter, { if (activation) { this.getAgendaGroup(node.rule.agendaGroup).remove(activation); rule.tree.remove(activation); + delete rule.noLoop[retract.hashCode]; } }, @@ -164,12 +165,13 @@ module.exports = declare(EventEmitter, { var rule = this.rules[node.name], nodeRule = node.rule, agendaGroup = nodeRule.agendaGroup; if (nodeRule.noLoop) { if (rule.noLoop[insert.hashCode]) { + rule.factTable.insert(insert); return; } else { rule.noLoop[insert.hashCode] = true; } - } + } rule.tree.insert(insert); this.getAgendaGroup(agendaGroup).insert(insert); if (nodeRule.autoFocus) { diff --git a/lib/parser/nools/tokens.js b/lib/parser/nools/tokens.js index 23391e2..3d9474c 100644 --- a/lib/parser/nools/tokens.js +++ b/lib/parser/nools/tokens.js @@ -95,12 +95,11 @@ var ruleTokens = { })(), noLoop: (function () { - var noLoopRegexp = /^(noLoop|no-loop)\s*:\s*(-?true|false)\s*[,;]?/; + var noLoopRegexp = /^(noLoop|no-loop)\s*:\s*(true|false)\s*[,;]?/; return function (src, context) { if (noLoopRegexp.test(src)) { var parts = src.match(noLoopRegexp); - // - context.options.noLoop = Boolean(parts[2]) + context.options.noLoop = Boolean(parts[2]); return src.replace(parts[0], ""); } else { throw new Error("invalid format"); diff --git a/test/flow/noLoop.test.js b/test/flow/noLoop.test.js index bcebe58..113744f 100644 --- a/test/flow/noLoop.test.js +++ b/test/flow/noLoop.test.js @@ -4,64 +4,88 @@ var it = require("it"), nools = require("../../"); it.describe("no-loop", function (it) { - /*jshint indent*/ - function Message(name) { - this.name = name; + var fired; + + function Wrapper(n) { + this.n = n; } - var cnt = 0; - - var flow1 = nools.flow("noLoop1", function () { - - this.rule("Hello2", { noLoop: true }, [Message, "m", "m.name =~ /Hello/"], function (facts) { - var m = facts.m; - m.name = 'Hello World'; - this.modify(m); - }); - }), - - flow2 = nools.flow("noLoop2", function () { - - this.rule("Hello1", [Message, "m", "m.name =~ /Hello/"], function (facts) { - var m = facts.m; - if (cnt++ < 2) { - m.name = 'Hello World'; - this.modify(m); - } - }); - }); - var noolsSource = "rule 'Hello3' { no-loop: true; when {m: Message m.name =~/Hello/;}then {modify(m, function () { this.name = 'Hello World'; });}}"; + it.should("not loop with option on", function () { + fired = []; - var flow3 = nools.compile(noolsSource, { - name: 'testDsl' - ,define: { - Message: Message - } + return nools + .flow("noLoop", function () { + this.rule("ping", { noLoop: true }, [Wrapper, "w", "w.n < 5"], function (facts) { + var w = facts.w; + w.n++; + this.modify(w); + }); + }) + .getSession(new Wrapper(0)) + .on("fire", function (name) { + fired.push(name); + }) + .match() + .then(function(){ + assert.deepEqual(fired, [ 'ping' ]) + }); }); - - it.should("not loop with option on and loop otherwise", function () { - var fired1 = [], fired2 = [], fired3 = []; - var session1 = flow1.getSession(new Message("Hello")).on("fire", function (name) { - fired1.push(name); - }), - session2 = flow2.getSession(new Message("Hello")).on("fire", function (name) { - fired2.push(name); - }), - session3 = flow3.getSession(new Message("Hello")).on("fire", function (name) { - fired3.push(name); - }); - return session1.match() - .then(function () { - return session2.match().then(function () { - return session3.match().then(function () { + + it.should("avoid only self-recursions", function () { + fired = []; + + return nools + .flow("noLoop2", function () { + this.rule("ping", { noLoop: true }, [Wrapper, "w", "w.n < 5"], function (facts) { + var w = facts.w; + w.n++; + this.modify(w); + }); + + this.rule("pong", { noLoop: true }, [Wrapper, "w", "w.n < 5"], function (facts) { + var w = facts.w; + w.n++; + this.modify(w); + }); + }) + .getSession(new Wrapper(0)) + .on("fire", function (name) { + fired.push(name); }) - }) - }) - .then(function () { - assert.deepEqual(fired1, ["Hello2"]); - assert.deepEqual(fired2, ["Hello1", "Hello1", "Hello1"]); - assert.deepEqual(fired3, ["Hello3"]); - }); + .match() + .then(function(){ + assert.deepEqual(fired, [ 'ping', 'pong', 'ping', 'pong', 'ping' ]) + }); }); + it.should("mix with noloop", function () { + fired = []; + + return nools + .flow("noLoop3", function () { + this.rule("a", { noLoop: true }, [Wrapper, "w", "w.n < 5"], function (facts) { + var w = facts.w; + w.n++; + this.modify(w); + }); + + this.rule("b", {}, [Wrapper, "w", "w.n < 5"], function (facts) { + + }); + + this.rule("c", { noLoop: true }, [Wrapper, "w", "w.n < 5"], function (facts) { + var w = facts.w; + w.n++; + this.modify(w); + }); + }) + .getSession(new Wrapper(0)) + .on("fire", function (name) { + fired.push(name); + }) + .match() + .then(function(){ + assert.deepEqual(fired, [ 'a', 'b', 'c', 'a', 'b', 'c', 'a']) + }); + }); });