From 0fc2f4cb86d7a1d0533030b0869ddb1bee273298 Mon Sep 17 00:00:00 2001 From: Marcin Golebiowski Date: Sun, 19 Sep 2021 20:32:37 +0200 Subject: [PATCH 1/2] Improve Parse.Ref --- src/Sprache/Parse.cs | 1 + test/Sprache.Tests/RefTests.cs | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 test/Sprache.Tests/RefTests.cs diff --git a/src/Sprache/Parse.cs b/src/Sprache/Parse.cs index d5761fa..38a2990 100644 --- a/src/Sprache/Parse.cs +++ b/src/Sprache/Parse.cs @@ -434,6 +434,7 @@ public static Parser Or(this Parser first, Parser second) var fr = first(i); if (!fr.WasSuccessful) { + fr.Remainder.Memos.Clear(); return second(i).IfFailure(sf => DetermineBestError(fr, sf)); } diff --git a/test/Sprache.Tests/RefTests.cs b/test/Sprache.Tests/RefTests.cs new file mode 100644 index 0000000..324731f --- /dev/null +++ b/test/Sprache.Tests/RefTests.cs @@ -0,0 +1,25 @@ +using Xunit; + + +namespace Sprache.Tests +{ + public class RefTests + { + private static Parser ParserSomething = Parse.String("#").Text().Named("something"); + + private static Parser ParserIdentifier = (from a in Parse.String("a") select "a").Named("identifier"); + + private static Parser ParserUnderTest = + (from _0 in Parse.Ref(() => ParserIdentifier) + from _1 in Parse.Ref(() => ParserSomething) + from _2 in Parse.String("_") + select "assignment_1") + .Or(from _0 in Parse.Ref(() => ParserIdentifier) + from _1 in Parse.Ref(() => ParserSomething) + select "assignment_2") + .Or(from _0 in ParserIdentifier select _0); + + [Fact] + public void TestOr() => AssertParser.SucceedsWith(ParserUnderTest, "a=1", o => Assert.Equal("a", o)); + } +} From 0a7bd6f997b29aa1ab9e09b72fd590abd2ae5970 Mon Sep 17 00:00:00 2001 From: Marcin Golebiowski Date: Mon, 20 Sep 2021 21:35:24 +0200 Subject: [PATCH 2/2] Change behavior of Parse.Ref -> throw ParseException only if memo contains info about left recursion --- src/Sprache/Parse.cs | 16 ++-- test/Sprache.Tests/Parse.Ref.Tests.cs | 110 ++++++++++++++++++++++++++ test/Sprache.Tests/ParseTests.cs | 31 -------- test/Sprache.Tests/RefTests.cs | 25 ------ 4 files changed, 120 insertions(+), 62 deletions(-) create mode 100644 test/Sprache.Tests/Parse.Ref.Tests.cs delete mode 100644 test/Sprache.Tests/RefTests.cs diff --git a/src/Sprache/Parse.cs b/src/Sprache/Parse.cs index 38a2990..c36cfff 100644 --- a/src/Sprache/Parse.cs +++ b/src/Sprache/Parse.cs @@ -13,6 +13,11 @@ namespace Sprache /// public static partial class Parse { + /// + /// Message for a failure result when left recursion is detected. + /// + public const string LeftRecursionErrorMessage = "Left recursion in the grammar."; + /// /// TryParse a single character matching 'predicate' /// @@ -393,14 +398,14 @@ public static Parser Ref(Func> reference) if (i.Memos.ContainsKey(p)) { var pResult = (IResult)i.Memos[p]; - if (pResult.WasSuccessful) + if (pResult.WasSuccessful) return pResult; - throw new ParseException(pResult.ToString()); + + if (!pResult.WasSuccessful && pResult.Message == LeftRecursionErrorMessage) + throw new ParseException(pResult.ToString()); } - i.Memos[p] = Result.Failure(i, - "Left recursion in the grammar.", - new string[0]); + i.Memos[p] = Result.Failure(i, LeftRecursionErrorMessage, new string[0]); var result = p(i); i.Memos[p] = result; return result; @@ -434,7 +439,6 @@ public static Parser Or(this Parser first, Parser second) var fr = first(i); if (!fr.WasSuccessful) { - fr.Remainder.Memos.Clear(); return second(i).IfFailure(sf => DetermineBestError(fr, sf)); } diff --git a/test/Sprache.Tests/Parse.Ref.Tests.cs b/test/Sprache.Tests/Parse.Ref.Tests.cs new file mode 100644 index 0000000..533fbaa --- /dev/null +++ b/test/Sprache.Tests/Parse.Ref.Tests.cs @@ -0,0 +1,110 @@ +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Sprache.Tests +{ + public class ParseRefTests + { + private static Parser ParserHash = Parse.String("#").Text().Named("something"); + + private static Parser ParserIdentifier = (from id in Parse.String("id").Text() select id).Named("identifier"); + + private static Parser Parser1UnderTest = + (from _0 in Parse.Ref(() => ParserIdentifier) + from _1 in Parse.Ref(() => ParserHash) + from _2 in Parse.String("_") + select "alternative_1") + .Or(from _0 in Parse.Ref(() => ParserIdentifier) + from _1 in Parse.Ref(() => ParserHash) + select "alternative_2") + .Or(from _0 in ParserIdentifier select _0); + + private static Parser Parser2UnderTest = + (from _0 in Parse.String("a").Text() + from _1 in Parse.Ref(() => Parser2UnderTest) + select _0 + _1) + .Or(from _0 in Parse.String("b").Text() + from _1 in Parse.Ref(() => Parser2UnderTest) + select _0 + _1) + .Or(from _0 in Parse.String("0").Text() select _0); + + private static Parser Parser3UnderTest = + (from _0 in Parse.Ref(() => Parser3UnderTest) + from _1 in Parse.String("a").Text() + select _0 + _1) + .Or(from _0 in Parse.String("b").Text() + from _1 in Parse.Ref(() => Parser3UnderTest) + select _0 + _1) + .Or(from _0 in Parse.String("0").Text() select _0); + + private static Parser Parser4UnderTest = + from _0 in Parse.Ref(() => Parser4UnderTest) + select "simplest left recursion"; + + private static Parser Parser5UnderTest = + (from _0 in Parse.String("_").Text() + from _1 in Parse.Ref(() => Parser5UnderTest) + select _0 + _1) + .Or(from _0 in Parse.String("+").Text() + from _1 in Parse.Ref(() => Parser5UnderTest) + select _0 + _1) + .Or(Parse.Return("")); + + [Fact] + public void MultipleRefs() => AssertParser.SucceedsWith(Parser1UnderTest, "id=1", o => Assert.Equal("id", o)); + + [Fact] + public void RecursiveParserWithoutLeftRecursion() => AssertParser.SucceedsWith(Parser2UnderTest, "ababba0", o => Assert.Equal("ababba0", o)); + + [Fact] + public void RecursiveParserWithLeftRecursion() => Assert.Throws(() => Parser3UnderTest.TryParse("b0")); + + [Fact] + public void SimplestLeftRecursion() => Assert.Throws(() => Parser4UnderTest.TryParse("test")); + + [Fact] + public void EmptyAlternative1() => AssertParser.SucceedsWith(Parser5UnderTest, "_+_+a", o => Assert.Equal("_+_+", o)); + + [Fact] + public void Issue166() + { + var letterA = Parse.Char('a'); + var letterReferenced = Parse.Ref(() => letterA); + var someAlternative = letterReferenced.Or(letterReferenced); + + Assert.False(someAlternative.TryParse("b").WasSuccessful); + } + + static readonly Parser> ASeq = + (from first in Parse.Ref(() => ASeq) + from comma in Parse.Char(',') + from rest in Parse.Char('a').Once() + select first.Concat(rest)) + .Or(Parse.Char('a').Once()); + + [Fact] + public void DetectsLeftRecursion() + { + Assert.Throws(() => ASeq.TryParse("a,a,a")); + } + + static readonly Parser> ABSeq = + (from first in Parse.Ref(() => BASeq) + from rest in Parse.Char('a').Once() + select first.Concat(rest)) + .Or(Parse.Char('a').Once()); + + static readonly Parser> BASeq = + (from first in Parse.Ref(() => ABSeq) + from rest in Parse.Char('b').Once() + select first.Concat(rest)) + .Or(Parse.Char('b').Once()); + + [Fact] + public void DetectsMutualLeftRecursion() + { + Assert.Throws(() => ABSeq.End().TryParse("baba")); + } + } +} diff --git a/test/Sprache.Tests/ParseTests.cs b/test/Sprache.Tests/ParseTests.cs index 01108a1..5336146 100644 --- a/test/Sprache.Tests/ParseTests.cs +++ b/test/Sprache.Tests/ParseTests.cs @@ -144,37 +144,6 @@ public void ParsesString_AsSequenceOfChars() AssertParser.SucceedsWithAll(p, "abc"); } - static readonly Parser> ASeq = - (from first in Parse.Ref(() => ASeq) - from comma in Parse.Char(',') - from rest in Parse.Char('a').Once() - select first.Concat(rest)) - .Or(Parse.Char('a').Once()); - - [Fact] - public void DetectsLeftRecursion() - { - Assert.Throws(() => ASeq.TryParse("a,a,a")); - } - - static readonly Parser> ABSeq = - (from first in Parse.Ref(() => BASeq) - from rest in Parse.Char('a').Once() - select first.Concat(rest)) - .Or(Parse.Char('a').Once()); - - static readonly Parser> BASeq = - (from first in Parse.Ref(() => ABSeq) - from rest in Parse.Char('b').Once() - select first.Concat(rest)) - .Or(Parse.Char('b').Once()); - - [Fact] - public void DetectsMutualLeftRecursion() - { - Assert.Throws(() => ABSeq.End().TryParse("baba")); - } - [Fact] public void WithMany_WhenLastElementFails_FailureReportedAtLastElement() { diff --git a/test/Sprache.Tests/RefTests.cs b/test/Sprache.Tests/RefTests.cs deleted file mode 100644 index 324731f..0000000 --- a/test/Sprache.Tests/RefTests.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Xunit; - - -namespace Sprache.Tests -{ - public class RefTests - { - private static Parser ParserSomething = Parse.String("#").Text().Named("something"); - - private static Parser ParserIdentifier = (from a in Parse.String("a") select "a").Named("identifier"); - - private static Parser ParserUnderTest = - (from _0 in Parse.Ref(() => ParserIdentifier) - from _1 in Parse.Ref(() => ParserSomething) - from _2 in Parse.String("_") - select "assignment_1") - .Or(from _0 in Parse.Ref(() => ParserIdentifier) - from _1 in Parse.Ref(() => ParserSomething) - select "assignment_2") - .Or(from _0 in ParserIdentifier select _0); - - [Fact] - public void TestOr() => AssertParser.SucceedsWith(ParserUnderTest, "a=1", o => Assert.Equal("a", o)); - } -}