From 0015c5e80d43efe307362357bb545c05e723ce4e Mon Sep 17 00:00:00 2001 From: rameel Date: Thu, 1 Oct 2026 20:51:43 +0500 Subject: [PATCH] parsers: add FoldL as the explicit left-associative fold --- src/Ramstack.Parsing/Parser.Fold.cs | 18 ++++-- src/Ramstack.Parsing/Parser`1.cs | 2 +- .../ParsersTests.Fold.cs | 57 +++++++++++++------ 3 files changed, 53 insertions(+), 24 deletions(-) diff --git a/src/Ramstack.Parsing/Parser.Fold.cs b/src/Ramstack.Parsing/Parser.Fold.cs index da8c7c1..4266260 100644 --- a/src/Ramstack.Parsing/Parser.Fold.cs +++ b/src/Ramstack.Parsing/Parser.Fold.cs @@ -2,6 +2,14 @@ namespace Ramstack.Parsing; partial class Parser { + /// + /// Creates a left-associative parser. This method is equivalent to . + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static Parser Fold(this Parser parser, Parser op, Func reduce) => + parser.FoldL(op, reduce); + /// /// Creates a left-associative parser. /// @@ -9,7 +17,7 @@ partial class Parser /// /// // Example: Number ([+-] Number)* /// // 1 + 2 + 3 + 4 => (((1 + 2) + 3) + 4) - /// var sum = number.Fold(OneOf("+-"), (l, r, op) => op == '+' ? l + r : l - r); + /// var sum = number.FoldL(OneOf("+-"), (l, r, op) => op == '+' ? l + r : l - r); /// /// If an operator-operand pair consumes no input, it is discarded and parsing stops /// without calling the reduction function for that pair. @@ -22,8 +30,8 @@ partial class Parser /// /// A parser that performs left-associative folding. /// - public static Parser Fold(this Parser parser, Parser op, Func reduce) => - new FoldParser(parser, op, reduce); + public static Parser FoldL(this Parser parser, Parser op, Func reduce) => + new FoldLParser(parser, op, reduce); /// /// Creates a right-associative parser. @@ -50,7 +58,7 @@ public static Parser Fold(this Parser parser, Parser FoldR(this Parser parser, Parser op, Func reduce) => new FoldRParser(parser, op, reduce); - #region Inner type: FoldParser + #region Inner type: FoldLParser /// /// Represents a left-associative parser. @@ -60,7 +68,7 @@ public static Parser FoldR(this Parser parser, ParserThe main parser that matches a value. /// The operator parser that matches an operator token. /// A reduction function. - private sealed class FoldParser(Parser parser, Parser op, Func reduce) : Parser + private sealed class FoldLParser(Parser parser, Parser op, Func reduce) : Parser { /// public override bool TryParse(ref ParseContext context, [NotNullWhen(true)] out T? value) diff --git a/src/Ramstack.Parsing/Parser`1.cs b/src/Ramstack.Parsing/Parser`1.cs index d078cd4..b2d14da 100644 --- a/src/Ramstack.Parsing/Parser`1.cs +++ b/src/Ramstack.Parsing/Parser`1.cs @@ -41,7 +41,7 @@ protected Parser(string? name) => /// /// /// Exceptions thrown by user-provided functions, such as the callbacks passed to - /// Map, Do, or Fold, are not suppressed and propagate to the caller. + /// Map, Do, or FoldL, are not suppressed and propagate to the caller. /// Use to capture them in /// instead. /// diff --git a/tests/Ramstack.Parsing.Tests/ParsersTests.Fold.cs b/tests/Ramstack.Parsing.Tests/ParsersTests.Fold.cs index bea7a1a..ea1bda7 100644 --- a/tests/Ramstack.Parsing.Tests/ParsersTests.Fold.cs +++ b/tests/Ramstack.Parsing.Tests/ParsersTests.Fold.cs @@ -12,16 +12,37 @@ partial class ParsersTests [TestCase("1+2-", 3, 3)] [TestCase("1+2+3+4-2", 8, 9)] [TestCase("1+2+3+4-2$", 8, 9)] - public void FoldTest(string expr, int result, int length) + public void FoldLTest(string expr, int result, int length) { var number = Literal.Number(); - var parser = number.Fold(OneOf("+-"), (l, r, o) => o == '+' ? l + r : l - r); + var parser = number.FoldL(OneOf("+-"), (l, r, o) => o == '+' ? l + r : l - r); Assert.That(parser.Parse(expr).Success, Is.True); Assert.That(parser.Parse(expr).Value, Is.EqualTo(result)); Assert.That(parser.Map(m => (m.Index, m.Length)).Parse(expr).Value, Is.EqualTo((0, length))); } + [TestCase("8", 8, 1)] + [TestCase("8-3-1", 4, 5)] + [TestCase("8-3-", 5, 3)] + public void Fold_EquivalentToFoldL(string expr, int expected, int length) + { + var number = Literal.Number(); + var op = L('-'); + var reduce = (int l, int r, char _) => l - r; + + var parsers = new[] { number.Fold(op, reduce), number.FoldL(op, reduce) }; + + foreach (var parser in parsers) + { + var result = parser.Parse(expr); + + Assert.That(result.Success, Is.True); + Assert.That(result.Value, Is.EqualTo(expected)); + Assert.That(result.Length, Is.EqualTo(length)); + } + } + [TestCase("2", "2", 1)] [TestCase("2$", "2", 1)] [TestCase("2**", "2", 1)] @@ -39,7 +60,7 @@ public void FoldRTest(string expr, string result, int length) } [Test] - public void Fold_NoInputConsumed_StopsWithoutReducing([Values] bool rightAssociative) + public void FoldL_NoInputConsumed_StopsWithoutReducing([Values] bool rightAssociative) { var operand = new BoundedParser(Return(2)); var op = Return('-'); @@ -53,7 +74,7 @@ public void Fold_NoInputConsumed_StopsWithoutReducing([Values] bool rightAssocia var parser = rightAssociative ? operand.FoldR(op, reduce) - : operand.Fold(op, reduce); + : operand.FoldL(op, reduce); var context = new ParseContext("!tail"); context.Advance(1); @@ -71,7 +92,7 @@ public void Fold_NoInputConsumed_StopsWithoutReducing([Values] bool rightAssocia } [Test] - public void FoldVoid_NoInputConsumed_StopsWithoutReducing([Values] bool rightAssociative) + public void FoldLVoid_NoInputConsumed_StopsWithoutReducing([Values] bool rightAssociative) { var operand = new BoundedParser(Return(2)); var op = Return('-'); @@ -85,7 +106,7 @@ public void FoldVoid_NoInputConsumed_StopsWithoutReducing([Values] bool rightAss var parser = rightAssociative ? operand.FoldR(op, reduce) - : operand.Fold(op, reduce); + : operand.FoldL(op, reduce); var context = new ParseContext("!tail"); context.Advance(1); @@ -103,7 +124,7 @@ public void FoldVoid_NoInputConsumed_StopsWithoutReducing([Values] bool rightAss [TestCase(false, 6)] [TestCase(true, 8)] - public void Fold_TrailingEmptyPair_StopsWithoutReducing(bool rightAssociative, int expected) + public void FoldL_TrailingEmptyPair_StopsWithoutReducing(bool rightAssociative, int expected) { var operand = new BoundedParser( Literal.Number().DefaultOnFail(2) @@ -120,7 +141,7 @@ public void Fold_TrailingEmptyPair_StopsWithoutReducing(bool rightAssociative, i var parser = rightAssociative ? operand.FoldR(op, reduce) - : operand.Fold(op, reduce); + : operand.FoldL(op, reduce); var result = parser.Parse("10-3-1!"); @@ -131,7 +152,7 @@ public void Fold_TrailingEmptyPair_StopsWithoutReducing(bool rightAssociative, i } [Test] - public void FoldVoid_TrailingEmptyPair_StopsWithoutReducing([Values] bool rightAssociative) + public void FoldLVoid_TrailingEmptyPair_StopsWithoutReducing([Values] bool rightAssociative) { var operand = new BoundedParser( Literal.Number().DefaultOnFail(2) @@ -148,7 +169,7 @@ public void FoldVoid_TrailingEmptyPair_StopsWithoutReducing([Values] bool rightA var parser = rightAssociative ? operand.FoldR(op, reduce) - : operand.Fold(op, reduce); + : operand.FoldL(op, reduce); var result = parser.Void().Parse("10-3-1!"); @@ -159,14 +180,14 @@ public void FoldVoid_TrailingEmptyPair_StopsWithoutReducing([Values] bool rightA [TestCase(false, -4)] [TestCase(true, 2)] - public void Fold_OnlyOperandConsumesInput_ContinuesParsing(bool rightAssociative, int expected) + public void FoldL_OnlyOperandConsumesInput_ContinuesParsing(bool rightAssociative, int expected) { var operand = Set('0', '9').Do(c => c - '0'); var op = Return('-'); var parser = rightAssociative ? operand.FoldR(op, (l, r, _) => l - r) - : operand.Fold(op, (l, r, _) => l - r); + : operand.FoldL(op, (l, r, _) => l - r); var result = parser.Parse("123!"); @@ -176,14 +197,14 @@ public void Fold_OnlyOperandConsumesInput_ContinuesParsing(bool rightAssociative } [Test] - public void FoldVoid_OnlyOperandConsumesInput_ContinuesParsing([Values] bool rightAssociative) + public void FoldLVoid_OnlyOperandConsumesInput_ContinuesParsing([Values] bool rightAssociative) { var operand = Set('0', '9').Do(c => c - '0'); var op = Return('-'); var parser = rightAssociative ? operand.FoldR(op, (l, r, _) => l - r) - : operand.Fold(op, (l, r, _) => l - r); + : operand.FoldL(op, (l, r, _) => l - r); var result = parser.Void().Parse("123!"); @@ -193,14 +214,14 @@ public void FoldVoid_OnlyOperandConsumesInput_ContinuesParsing([Values] bool rig [TestCase(false, -2)] [TestCase(true, 2)] - public void Fold_OnlyOperatorConsumesInput_ContinuesParsing(bool rightAssociative, int expected) + public void FoldL_OnlyOperatorConsumesInput_ContinuesParsing(bool rightAssociative, int expected) { var operand = Return(2); var op = L('-'); var parser = rightAssociative ? operand.FoldR(op, (l, r, _) => l - r) - : operand.Fold(op, (l, r, _) => l - r); + : operand.FoldL(op, (l, r, _) => l - r); var result = parser.Parse("--!"); @@ -210,14 +231,14 @@ public void Fold_OnlyOperatorConsumesInput_ContinuesParsing(bool rightAssociativ } [Test] - public void FoldVoid_OnlyOperatorConsumesInput_ContinuesParsing([Values] bool rightAssociative) + public void FoldLVoid_OnlyOperatorConsumesInput_ContinuesParsing([Values] bool rightAssociative) { var operand = Return(2); var op = L('-'); var parser = rightAssociative ? operand.FoldR(op, (l, r, _) => l - r) - : operand.Fold(op, (l, r, _) => l - r); + : operand.FoldL(op, (l, r, _) => l - r); var result = parser.Void().Parse("--!");