diff --git a/lib/compiler.js b/lib/compiler.js index e1be6ee..d987b11 100644 --- a/lib/compiler.js +++ b/lib/compiler.js @@ -44,6 +44,9 @@ PEG.buildParser = function(grammar, startRule) { if (ast[startRule] === undefined) { throw new PEG.Grammar.GrammarError("Missing \"" + startRule + "\" rule."); } + for (var key in ast) { + ast[key].checkNoLeftRecursion(ast, []); + } return PEG.Compiler.compileParser(ast, startRule); }; @@ -140,6 +143,47 @@ PEG.Grammar.Action.prototype.checkReferencedRulesExist = function(grammar) { this._expression.checkReferencedRulesExist(grammar); }; +/* ===== Left Recursion Checks ===== */ + +PEG.Grammar.Rule.prototype.checkNoLeftRecursion = function(grammar, appliedRules) { + this._expression.checkNoLeftRecursion(grammar, appliedRules.concat(this._name)); +}; + +PEG.Grammar.Literal.prototype.checkNoLeftRecursion = function(grammar, appliedRules) {}; + +PEG.Grammar.Any.prototype.checkNoLeftRecursion = function(grammar, appliedRules) {}; + +PEG.Grammar.Sequence.prototype.checkNoLeftRecursion = function(grammar, appliedRules) { + if (this._elements.length > 0) { + this._elements[0].checkNoLeftRecursion(grammar, appliedRules); + } +}; + +PEG.Grammar.Choice.prototype.checkNoLeftRecursion = function(grammar, appliedRules) { + PEG.ArrayUtils.each(this._alternatives, function(alternative) { + alternative.checkNoLeftRecursion(grammar, appliedRules); + }); +}; + +PEG.Grammar.ZeroOrMore.prototype.checkNoLeftRecursion = function(grammar, appliedRules) { + this._element.checkNoLeftRecursion(grammar, appliedRules); +}; + +PEG.Grammar.NotPredicate.prototype.checkNoLeftRecursion = function(grammar, appliedRules) { + this._expression.checkNoLeftRecursion(grammar, appliedRules); +}; + +PEG.Grammar.RuleRef.prototype.checkNoLeftRecursion = function(grammar, appliedRules) { + if (appliedRules.indexOf(this._name) !== -1) { + throw new PEG.Grammar.GrammarError("Left recursion detected for rule \"" + this._name + "\"."); + } + grammar[this._name].checkNoLeftRecursion(grammar, appliedRules); +}; + +PEG.Grammar.Action.prototype.checkNoLeftRecursion = function(grammar, appliedRules) { + this._expression.checkNoLeftRecursion(grammar, appliedRules); +}; + /* ===== PEG.Compiler ===== */ PEG.Compiler = { diff --git a/test/compiler-test.js b/test/compiler-test.js index 49c0404..990443a 100644 --- a/test/compiler-test.js +++ b/test/compiler-test.js @@ -131,6 +131,27 @@ test("buildParser reports missing referenced rules", function() { }); }); +test("buildParser reports left recursion", function() { + var grammars = [ + /* Direct */ + 'start: start', + 'start: start "a" "b"', + 'start: start / "a" / "b"', + 'start: "a" / "b" / start', + 'start: start*', + 'start: !start', + 'start: &start', + 'start: start { }', + + /* Indirect */ + 'start: stop\nstop: start' + ]; + + PEG.ArrayUtils.each(grammars, function(grammar) { + throws(function() { PEG.buildParser(grammar); }, PEG.Grammar.GrammarError); + }); +}); + test("buildParser allows custom start rule", function() { var parser = PEG.buildParser('s: "abcd"', "s"); parses(parser, "abcd", "abcd");