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 = "."*'); + }); +});