diff --git a/Makefile b/Makefile
index 4f32b3d..09b55e0 100644
--- a/Makefile
+++ b/Makefile
@@ -18,6 +18,7 @@ MODULES = utils/arrays \
compiler/passes/generate-javascript \
compiler/passes/remove-proxy-rules \
compiler/passes/report-left-recursion \
+ compiler/passes/report-infinite-loops \
compiler/passes/report-missing-rules \
compiler \
peg
diff --git a/lib/compiler.js b/lib/compiler.js
index 84d3b50..253c81b 100644
--- a/lib/compiler.js
+++ b/lib/compiler.js
@@ -12,7 +12,8 @@ var compiler = {
passes: {
check: {
reportMissingRules: require("./compiler/passes/report-missing-rules"),
- reportLeftRecursion: require("./compiler/passes/report-left-recursion")
+ reportLeftRecursion: require("./compiler/passes/report-left-recursion"),
+ reportInfiniteLoops: require("./compiler/passes/report-infinite-loops")
},
transform: {
removeProxyRules: require("./compiler/passes/remove-proxy-rules")
diff --git a/lib/compiler/passes/report-infinite-loops.js b/lib/compiler/passes/report-infinite-loops.js
new file mode 100644
index 0000000..bfe87b6
--- /dev/null
+++ b/lib/compiler/passes/report-infinite-loops.js
@@ -0,0 +1,27 @@
+var GrammarError = require("../../grammar-error"),
+ asts = require("../asts"),
+ visitor = require("../visitor");
+
+/*
+ * Reports expressions that don't consume any input inside |*| or |+| in the
+ * grammar, which prevents infinite loops in the generated parser.
+ */
+function reportInfiniteLoops(ast) {
+ var check = visitor.build({
+ zero_or_more: function(node) {
+ if (asts.matchesEmpty(ast, node.expression)) {
+ throw new GrammarError("Infinite loop detected.");
+ }
+ },
+
+ one_or_more: function(node) {
+ if (asts.matchesEmpty(ast, node.expression)) {
+ throw new GrammarError("Infinite loop detected.");
+ }
+ }
+ });
+
+ check(ast);
+}
+
+module.exports = reportInfiniteLoops;
diff --git a/package.json b/package.json
index 517fd31..0aa440b 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
"lib/compiler/passes/generate-javascript.js",
"lib/compiler/passes/remove-proxy-rules.js",
"lib/compiler/passes/report-left-recursion.js",
+ "lib/compiler/passes/report-infinite-loops.js",
"lib/compiler/passes/report-missing-rules.js",
"lib/grammar-error.js",
"lib/parser.js",
diff --git a/spec/index.html b/spec/index.html
index a83dfd6..10a107c 100644
--- a/spec/index.html
+++ b/spec/index.html
@@ -11,6 +11,7 @@
+
diff --git a/spec/unit/compiler/passes/report-infinite-loops.spec.js b/spec/unit/compiler/passes/report-infinite-loops.spec.js
new file mode 100644
index 0000000..8e8c11a
--- /dev/null
+++ b/spec/unit/compiler/passes/report-infinite-loops.spec.js
@@ -0,0 +1,71 @@
+describe("compiler pass |reportLeftRecursion|", function() {
+ var pass = PEG.compiler.passes.check.reportInfiniteLoops;
+
+ it("reports infinite loops for zero_or_more", function() {
+ expect(pass).toReportError('start = ("")*', {
+ message: "Infinite loop detected."
+ });
+ });
+
+ it("reports infinite loops for one_or_more", function() {
+ expect(pass).toReportError('start = ("")+', {
+ message: "Infinite loop detected."
+ });
+ });
+
+ it("computes empty string matching correctly", function() {
+ expect(pass).toReportError('start = ("" / "a" / "b")*');
+ expect(pass).toReportError('start = ("a" / "" / "b")*');
+ expect(pass).toReportError('start = ("a" / "b" / "")*');
+ expect(pass).not.toReportError('start = ("a" / "b" / "c")*');
+
+ expect(pass).toReportError('start = ("" { })*');
+ expect(pass).not.toReportError('start = ("a" { })*');
+
+ expect(pass).toReportError('start = ("" "" "")*');
+ expect(pass).not.toReportError('start = ("a" "" "")*');
+ expect(pass).not.toReportError('start = ("" "a" "")*');
+ expect(pass).not.toReportError('start = ("" "" "a")*');
+
+ expect(pass).toReportError('start = (a:"")*');
+ expect(pass).not.toReportError('start = (a:"a")*');
+
+ expect(pass).toReportError('start = ($"")*');
+ expect(pass).not.toReportError('start = ($"a")*');
+
+ expect(pass).toReportError('start = (&"")*');
+ expect(pass).toReportError('start = (&"a")*');
+
+ expect(pass).toReportError('start = (!"")*');
+ expect(pass).toReportError('start = (!"a")*');
+
+ expect(pass).toReportError('start = (""?)*');
+ expect(pass).toReportError('start = ("a"?)*');
+
+ expect(pass).toReportError('start = (""*)*');
+ expect(pass).toReportError('start = ("a"*)*');
+
+ expect(pass).toReportError('start = (""+)*');
+ expect(pass).not.toReportError('start = ("a"+)*');
+
+ expect(pass).toReportError('start = (&{ })*');
+
+ expect(pass).toReportError('start = (!{ })*');
+
+ expect(pass).toReportError([
+ 'start = a*',
+ 'a = ""'
+ ].join('\n'));
+ expect(pass).not.toReportError([
+ 'start = a*',
+ 'a = "a"'
+ ].join('\n'));
+
+ expect(pass).toReportError('start = ""*');
+ expect(pass).not.toReportError('start = "a"*');
+
+ expect(pass).not.toReportError('start = [a-d]*');
+
+ expect(pass).not.toReportError('start = "."*');
+ });
+});