Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/Ramstack.Parsing/Parser.Fold.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ partial class Parser
/// // 1 + 2 + 3 + 4 => (((1 + 2) + 3) + 4)
/// var sum = number.Fold(OneOf("+-"), (l, r, op) => op == '+' ? l + r : l - r);
/// </code>
/// If an operator-operand pair consumes no input, it is discarded and parsing stops
/// without calling the reduction function for that pair.
/// </remarks>
/// <typeparam name="T">The type of the value produced by the main parser.</typeparam>
/// <typeparam name="TOperator">The type of the operator token produced by the parser.</typeparam>
Expand All @@ -34,6 +36,8 @@ public static Parser<T> Fold<T, TOperator>(this Parser<T> parser, Parser<TOperat
/// // Number ("^" Number)*
/// var power = number.FoldR(L('^'), (l, r, op) => Math.Pow(l, r));
/// </code>
/// If an operator-operand pair consumes no input, it is discarded and parsing stops
/// without calling the reduction function for that pair.
/// </remarks>
/// <typeparam name="T">The type of value produced by the main parser.</typeparam>
/// <typeparam name="TOperator">The type of the operator token produced by the parser.</typeparam>
Expand Down Expand Up @@ -71,7 +75,9 @@ public override bool TryParse(ref ParseContext context, [NotNullWhen(true)] out
{
var rollback = context.BookmarkPosition();

if (op.TryParse(ref context, out var o) && parser.TryParse(ref context, out v))
if (op.TryParse(ref context, out var o)
&& parser.TryParse(ref context, out v)
&& context.Position != rollback.Position)
{
result = reduce(result, v, o);
continue;
Expand Down Expand Up @@ -126,7 +132,9 @@ public override bool TryParse(ref ParseContext context, [NotNullWhen(true)] out
{
var rollback = context.BookmarkPosition();

if (op.TryParse(ref context, out var o) && parser.TryParse(ref context, out v))
if (op.TryParse(ref context, out var o)
&& parser.TryParse(ref context, out v)
&& context.Position != rollback.Position)
{
list.Add((o, v));
continue;
Expand Down
30 changes: 26 additions & 4 deletions src/Ramstack.Parsing/Parser.Separated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,28 @@ partial class Parser
/// <summary>
/// Creates a parser that repeatedly applies the main parser, interleaved with a separator specified by another parser.
/// </summary>
/// <remarks>
/// If an item and its following separator both succeed without consuming input, repetition stops
/// once the minimum number of items has been matched. The successfully parsed item is included in the result.
/// </remarks>
/// <typeparam name="T">The type of the value produced by the main parser.</typeparam>
/// <typeparam name="TSeparator">The type of the value produced by the separator parser.</typeparam>
/// <param name="parser">The main parser.</param>
/// <param name="separator">The parser that identifies the separators placed between the elements parsed by the main parser.</param>
/// <param name="allowTrailing"><see langword="true" /> if a trailing separator is allowed; otherwise, <see langword="false" />.</param>
/// <param name="min">The minimum number of repetitions.</param>
/// <param name="max">The maximum number of repetitions.</param>
/// <param name="min">The minimum number of repetitions. Must be non-negative and no greater than <paramref name="max"/>.</param>
/// <param name="max">The maximum number of repetitions. Must be greater than zero.</param>
/// <returns>
/// A parser that repeatedly applies the main parser, interleaved with the specified separator.
/// </returns>
public static Parser<List<T>> Separated<T, TSeparator>(this Parser<T> parser, Parser<TSeparator> separator, bool allowTrailing = false, int min = 0, int max = int.MaxValue) =>
new SeparatedParser<T>(parser, separator.Void(), allowTrailing, min, max);
public static Parser<List<T>> Separated<T, TSeparator>(this Parser<T> parser, Parser<TSeparator> separator, bool allowTrailing = false, int min = 0, int max = int.MaxValue)
{
Argument.ThrowIfNegative(min);
Argument.ThrowIfNegativeOrZero(max);
Argument.ThrowIfGreaterThan(min, max);

return new SeparatedParser<T>(parser, separator.Void(), allowTrailing, min, max);
}

#region Inner type: SeparatedParser

Expand All @@ -40,6 +50,8 @@ public override bool TryParse(ref ParseContext context, [NotNullWhen(true)] out

do
{
var position = context.Position;

if (!parser.TryParse(ref context, out var result))
break;

Expand All @@ -48,6 +60,10 @@ public override bool TryParse(ref ParseContext context, [NotNullWhen(true)] out
separatorBookmark = context.BookmarkPosition();
if (!separator.TryParse(ref context, out _))
break;

// Stop empty matches once the minimum count has been reached.
if (list.Count >= min && context.Position == position)
break;
}
while (list.Count < max);

Expand Down Expand Up @@ -113,6 +129,8 @@ public override bool TryParse(ref ParseContext context, out Unit value)

do
{
var position = context.Position;

if (!_parser.TryParse(ref context, out value))
break;

Expand All @@ -121,6 +139,10 @@ public override bool TryParse(ref ParseContext context, out Unit value)
separatorBookmark = context.BookmarkPosition();
if (!_separator.TryParse(ref context, out value))
break;

// Stop empty matches once the minimum count has been reached.
if (count >= _min && context.Position == position)
break;
}
while (count < _max);

Expand Down
187 changes: 187 additions & 0 deletions tests/Ramstack.Parsing.Tests/ParsersTests.Fold.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,191 @@ public void FoldRTest(string expr, string result, int length)
Assert.That(parser.Parse(expr).Value, Is.EqualTo(BigInteger.Parse(result)));
Assert.That(parser.Map(m => (m.Index, m.Length)).Parse(expr).Value, Is.EqualTo((0, length)));
}

[Test]
public void Fold_NoInputConsumed_StopsWithoutReducing([Values] bool rightAssociative)
{
var operand = new BoundedParser<int>(Return(2));
var op = Return('-');

var reductions = 0;
var reduce = (int l, int r, char _) =>
{
reductions++;
return l - r;
};

var parser = rightAssociative
? operand.FoldR(op, reduce)
: operand.Fold(op, reduce);

var context = new ParseContext("!tail");
context.Advance(1);

var success = parser.TryParse(ref context, out var value);

Assert.That(success, Is.True);
Assert.That(value, Is.EqualTo(2));
Assert.That(reductions, Is.Zero);

Assert.That(context.Position, Is.EqualTo(1));
Assert.That(context.MatchedSegment.Index, Is.EqualTo(1));
Assert.That(context.MatchedSegment.Length, Is.Zero);
Assert.That(context.Remaining.ToString(), Is.EqualTo("tail"));
}

[Test]
public void FoldVoid_NoInputConsumed_StopsWithoutReducing([Values] bool rightAssociative)
{
var operand = new BoundedParser<int>(Return(2));
var op = Return('-');

var reductions = 0;
var reduce = (int l, int r, char _) =>
{
reductions++;
return l - r;
};

var parser = rightAssociative
? operand.FoldR(op, reduce)
: operand.Fold(op, reduce);

var context = new ParseContext("!tail");
context.Advance(1);

var success = parser.Void().TryParse(ref context, out _);

Assert.That(success, Is.True);
Assert.That(reductions, Is.Zero);

Assert.That(context.Position, Is.EqualTo(1));
Assert.That(context.MatchedSegment.Index, Is.EqualTo(1));
Assert.That(context.MatchedSegment.Length, Is.Zero);
Assert.That(context.Remaining.ToString(), Is.EqualTo("tail"));
}

[TestCase(false, 6)]
[TestCase(true, 8)]
public void Fold_TrailingEmptyPair_StopsWithoutReducing(bool rightAssociative, int expected)
{
var operand = new BoundedParser<int>(
Literal.Number<int>().DefaultOnFail(2)
);

var op = L('-').DefaultOnFail('-');

var reductions = 0;
var reduce = (int l, int r, char _) =>
{
reductions++;
return l - r;
};

var parser = rightAssociative
? operand.FoldR(op, reduce)
: operand.Fold(op, reduce);

var result = parser.Parse("10-3-1!");

Assert.That(result.Success, Is.True);
Assert.That(result.Value, Is.EqualTo(expected));
Assert.That(result.Length, Is.EqualTo(6));
Assert.That(reductions, Is.EqualTo(2));
}

[Test]
public void FoldVoid_TrailingEmptyPair_StopsWithoutReducing([Values] bool rightAssociative)
{
var operand = new BoundedParser<int>(
Literal.Number<int>().DefaultOnFail(2)
);

var op = L('-').DefaultOnFail('-');

var reductions = 0;
var reduce = (int l, int r, char _) =>
{
reductions++;
return l - r;
};

var parser = rightAssociative
? operand.FoldR(op, reduce)
: operand.Fold(op, reduce);

var result = parser.Void().Parse("10-3-1!");

Assert.That(result.Success, Is.True);
Assert.That(result.Length, Is.EqualTo(6));
Assert.That(reductions, Is.Zero);
}

[TestCase(false, -4)]
[TestCase(true, 2)]
public void Fold_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);

var result = parser.Parse("123!");

Assert.That(result.Success, Is.True);
Assert.That(result.Value, Is.EqualTo(expected));
Assert.That(result.Length, Is.EqualTo(3));
}

[Test]
public void FoldVoid_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);

var result = parser.Void().Parse("123!");

Assert.That(result.Success, Is.True);
Assert.That(result.Length, Is.EqualTo(3));
}

[TestCase(false, -2)]
[TestCase(true, 2)]
public void Fold_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);

var result = parser.Parse("--!");

Assert.That(result.Success, Is.True);
Assert.That(result.Value, Is.EqualTo(expected));
Assert.That(result.Length, Is.EqualTo(2));
}

[Test]
public void FoldVoid_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);

var result = parser.Void().Parse("--!");

Assert.That(result.Success, Is.True);
Assert.That(result.Length, Is.EqualTo(2));
}
}
Loading
Loading