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
46 changes: 39 additions & 7 deletions docs/parsers.md
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ Assert.Equal(12, result.Item2);

### ZeroOrOne

Makes an existing parser optional.
Makes an existing parser optional. The method can also be be post-fixed.

```c#
Parser<T> ZeroOrOne<T>(Parser<T> parser)
Expand All @@ -397,6 +397,7 @@ Usage:

```c#
var parser = ZeroOrOne(Terms.Text("hello"));
// or Terms.Text("hello").ZeroOrOne()
parser.Parse("hello");
parser.Parse(""); // returns null but with a successful state
```
Expand All @@ -408,18 +409,48 @@ Result:
null
```

### Optional

Makes an existing parser optional. Contrary to `ZeroOrOne` the result is always a list, with
either zero or one element. It is then easy to know if the parser was successful or not by
using the Linq operators `Any()` and `FirstOrDefault()`.

```c#
static Parser<IReadOnlyList<T>> Optional<T>(this Parser<T> parser)
```

Usage:

```c#
var parser = Terms.Text("hello").Optional();
parser.Parse("hello");
parser.Parse(""); // returns an empty list
parser.Parse("hello").FirstOrDefault();
parser.Parse("").FirstOrDefault(); // returns null
```

Result:

```
["hello"]
[]
"hello"
null
```

### ZeroOrMany

Executes a parser as long as it's successful. The result is a list of all individual results.
Executes a parser as long as it's successful. The result is a list of all individual results. The method can also be post-fixed.

```c#
Parser<List<T>> ZeroOrMany<T>(Parser<T> parser)
Parser<IReadOnlyList<T> ZeroOrMany<T>(Parser<T> parser)
```

Usage:

```c#
var parser = ZeroOrMany(Terms.Text("hello"));
// or Terms.Text("hello").ZeroOrMany()
parser.Parse("hello hello");
parser.Parse("");
```
Expand All @@ -433,16 +464,17 @@ Result:

### OneOrMany

Executes a parser as long as it's successful, and is successful if at least one occurrence is found. The result is a list of all individual results.
Executes a parser as long as it's successful, and is successful if at least one occurrence is found. The result is a list of all individual results. The method can also be post-fixed.

```c#
Parser<List<T>> OneOrMany<T>(Parser<T> parser)
Parser<IReadOnlyList<T> OneOrMany<T>(Parser<T> parser)
```

Usage:

```c#
var parser = OneOrMany(Terms.Text("hello"));
// or Terms.Text("hello").OneOrMany()
parser.Parse("hello hello");
parser.Parse("");
```
Expand Down Expand Up @@ -484,7 +516,7 @@ null // success
Matches all occurrences of a parser that are separated by another one. If a separator is not followed by a value, it is not consumed.

```
Parser<List<T>> Separated<U, T>(Parser<U> separator, Parser<T> parser)
Parser<IReadOnlyList<T> Separated<U, T>(Parser<U> separator, Parser<T> parser)
```

Usage:
Expand Down Expand Up @@ -641,7 +673,7 @@ Convert the result of a parser. This is usually used to create custom data struc
Parser<U> Then<U>(Func<T, U> conversion)
Parser<U> Then<U>(Func<ParseContext, T, U> conversion)
Parser<U> Then<U>(U value)
Parser<U?> Then<U>() // returns default(U)
Parser<U?> Then<U>() // Converts the result to `U`
```

Usage:
Expand Down
70 changes: 70 additions & 0 deletions src/Parlot/Fluent/Optional.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using Parlot.Compilation;
using System;
using System.Collections.Generic;
using System.Linq.Expressions;

namespace Parlot.Fluent;

/// <summary>
/// Returns a list containing zero or one element.
/// </summary>
/// <remarks>
/// This parser will always succeed. If the previous parser fails, it will return an empty list.
/// </remarks>
public sealed class Optional<T> : Parser<IReadOnlyList<T>>, ICompilable
{
private readonly Parser<T> _parser;
public Optional(Parser<T> parser)
{
_parser = parser ?? throw new ArgumentNullException(nameof(parser));
}

public override bool Parse(ParseContext context, ref ParseResult<IReadOnlyList<T>> result)
{
context.EnterParser(this);

var parsed = new ParseResult<T>();

var success = _parser.Parse(context, ref parsed);

result.Set(parsed.Start, parsed.End, success ? [parsed.Value] : []);

// Optional always succeeds
context.ExitParser(this);
return true;
}

public CompilationResult Compile(CompilationContext context)
{
var result = context.CreateCompilationResult<IReadOnlyList<T>>(true, ExpressionHelper.ArrayEmpty<T>());

// T value = _defaultValue;
//
// parse1 instructions
//
// value = new OptionalResult<T>(parser1.Success, success ? [parsed.Value] : []);
//

var parserCompileResult = _parser.Build(context);

var block = Expression.Block(
parserCompileResult.Variables,
Expression.Block(
Expression.Block(parserCompileResult.Body),
context.DiscardResult
? Expression.Empty()
: Expression.IfThenElse(
parserCompileResult.Success,
Expression.Assign(result.Value, Expression.NewArrayInit(typeof(T), parserCompileResult.Value)),
Expression.Assign(result.Value, Expression.Constant(Array.Empty<T>(), typeof(T[])))
)
)
);

result.Body.Add(block);

return result;
}

public override string ToString() => $"{_parser}?";
}
5 changes: 3 additions & 2 deletions src/Parlot/Fluent/Parser.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Parlot.Rewriting;
using System;
using System.Globalization;
using System.Linq;

namespace Parlot.Fluent;
Expand All @@ -26,7 +27,7 @@ public abstract partial class Parser<T>
/// <summary>
/// Builds a parser that converts the previous result.
/// </summary>
public Parser<U?> Then<U>() => new Then<T, U?>(this, default(U));
public Parser<U?> Then<U>() => new Then<T, U?>(this, x => (U?)Convert.ChangeType(x, typeof(U?), CultureInfo.CurrentCulture));

/// <summary>
/// Builds a parser that converts the previous result when it succeeds or returns a default value if it fails.
Expand Down Expand Up @@ -97,7 +98,7 @@ public Parser<T> Named(string name)
/// <summary>
/// Builds a parser that discards the previous result and replaces it by the specified type or value.
/// </summary>
[Obsolete("Use Then<U>() instead.")]
[Obsolete("Use Then<U>(value) instead.")]
public Parser<U> Discard<U>(U value) => new Discard<T, U>(this, value);

/// <summary>
Expand Down
21 changes: 21 additions & 0 deletions src/Parlot/Fluent/ParserExtensions.Cardinality.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Collections.Generic;

namespace Parlot.Fluent;

public static partial class ParserExtensions
{
public static Parser<IReadOnlyList<T>> OneOrMany<T>(this Parser<T> parser)
=> new OneOrMany<T>(parser);

public static Parser<IReadOnlyList<T>> ZeroOrMany<T>(this Parser<T> parser)
=> new ZeroOrMany<T>(parser);

public static Parser<T> ZeroOrOne<T>(this Parser<T> parser, T defaultValue)
=> new ZeroOrOne<T>(parser, defaultValue);

public static Parser<T> ZeroOrOne<T>(this Parser<T> parser)
=> new ZeroOrOne<T>(parser, default!);

public static Parser<IReadOnlyList<T>> Optional<T>(this Parser<T> parser)
=> new Optional<T>(parser);
}
20 changes: 15 additions & 5 deletions test/Parlot.Tests/CompileTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Parlot.Fluent;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Xunit;
using static Parlot.Fluent.Parsers;
Expand Down Expand Up @@ -222,6 +223,15 @@ public void ShouldZeroOrOne()
Assert.Null(parser.Parse(" foo"));
}

[Fact]
public void OptionalShouldSucceed()
{
var parser = Terms.Text("hello").Optional().Compile();

Assert.Equal("hello", parser.Parse(" hello world hello").FirstOrDefault());
Assert.Null(parser.Parse(" foo").FirstOrDefault());
}

[Fact]
public void ShouldZeroOrOneWithDefault()
{
Expand Down Expand Up @@ -287,10 +297,10 @@ public void ShouldCompileCapture()
Parser<char> Plus = Literals.Char('+');
Parser<char> Minus = Literals.Char('-');
Parser<char> At = Literals.Char('@');
Parser<TextSpan> WordChar = Literals.Pattern(char.IsLetterOrDigit);
Parser<IReadOnlyList<char>> WordDotPlusMinus = OneOrMany(OneOf(WordChar.Then(x => 'w'), Dot, Plus, Minus));
Parser<IReadOnlyList<char>> WordDotMinus = OneOrMany(OneOf(WordChar.Then(x => 'w'), Dot, Minus));
Parser<IReadOnlyList<char>> WordMinus = OneOrMany(OneOf(WordChar.Then(x => 'w'), Minus));
Parser<char> WordChar = Literals.Pattern(char.IsLetterOrDigit).Then<char>(x => x.Span[0]);
Parser<IReadOnlyList<char>> WordDotPlusMinus = OneOrMany(OneOf(WordChar, Dot, Plus, Minus));
Parser<IReadOnlyList<char>> WordDotMinus = OneOrMany(OneOf(WordChar, Dot, Minus));
Parser<IReadOnlyList<char>> WordMinus = OneOrMany(OneOf(WordChar, Minus));
Parser<TextSpan> Email = Capture(WordDotPlusMinus.And(At).And(WordMinus).And(Dot).And(WordDotMinus));

string _email = "[email protected]";
Expand Down Expand Up @@ -409,7 +419,7 @@ public void ShouldCompileDiscard()
Assert.False(Terms.Decimal().Discard<bool>(true).Compile().TryParse("abc", out _));
#pragma warning restore CS0618 // Type or member is obsolete

Assert.True(Terms.Decimal().Then<bool>().Compile().TryParse("123", out var t1) && t1 == false);
Assert.True(Terms.Decimal().Then<int>().Compile().TryParse("123", out var t1) && t1 == 123);
Assert.True(Terms.Decimal().Then(true).Compile().TryParse("123", out var t2) && t2 == true);
Assert.False(Terms.Decimal().Then(true).Compile().TryParse("abc", out _));
}
Expand Down
33 changes: 26 additions & 7 deletions test/Parlot.Tests/FluentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Numerics;
using Xunit;

Expand Down Expand Up @@ -48,16 +49,25 @@ public void WhenShouldResetPositionWhenFalse()
Assert.Equal(1235, result1.Item2);
}

[Fact]
public void ShouldCast()
{
var parser = Literals.Integer().Then<decimal>();

Assert.True(parser.TryParse("123", out var result1));
Assert.Equal(123, result1);
}

[Fact]
public void ShouldReturnElse()
{
var parser = Literals.Integer().Then<long?>(x => x).Else(null);
var parser = Literals.Integer().Then<decimal>().Else(0);

Assert.True(parser.TryParse("123", out var result1));
Assert.Equal(123, result1);

Assert.True(parser.TryParse(" 123", out var result2));
Assert.Null(result2);
Assert.Equal(0, result2);
}

[Fact]
Expand Down Expand Up @@ -515,10 +525,10 @@ public void ShouldParseEmails()
Parser<char> Plus = Literals.Char('+');
Parser<char> Minus = Literals.Char('-');
Parser<char> At = Literals.Char('@');
Parser<TextSpan> WordChar = Literals.Pattern(char.IsLetterOrDigit);
Parser<IReadOnlyList<char>> WordDotPlusMinus = OneOrMany(OneOf(WordChar.Then<char>(), Dot, Plus, Minus));
Parser<IReadOnlyList<char>> WordDotMinus = OneOrMany(OneOf(WordChar.Then<char>(), Dot, Minus));
Parser<IReadOnlyList<char>> WordMinus = OneOrMany(OneOf(WordChar.Then<char>(), Minus));
Parser<char> WordChar = Literals.Pattern(char.IsLetterOrDigit).Then<char>(x => x.Span[0]);
Parser<IReadOnlyList<char>> WordDotPlusMinus = OneOrMany(OneOf(WordChar, Dot, Plus, Minus));
Parser<IReadOnlyList<char>> WordDotMinus = OneOrMany(OneOf(WordChar, Dot, Minus));
Parser<IReadOnlyList<char>> WordMinus = OneOrMany(OneOf(WordChar, Minus));
Parser<TextSpan> Email = Capture(WordDotPlusMinus.And(At).And(WordMinus).And(Dot).And(WordDotMinus));

string _email = "[email protected]";
Expand Down Expand Up @@ -579,7 +589,7 @@ public void DiscardShouldReplaceValue()
Assert.False(Terms.Decimal().Discard<bool>(true).TryParse("abc", out _));
#pragma warning restore CS0618 // Type or member is obsolete

Assert.True(Terms.Decimal().Then<bool>().TryParse("123", out var t1) && t1 == false);
Assert.True(Terms.Decimal().Then<int>().TryParse("123", out var t1) && t1 == 123);
Assert.True(Terms.Decimal().Then(true).TryParse("123", out var t2) && t2 == true);
Assert.False(Terms.Decimal().Then(true).TryParse("abc", out _));
}
Expand Down Expand Up @@ -976,6 +986,15 @@ public void ShouldZeroOrOne()
Assert.Null(parser.Parse(" foo"));
}

[Fact]
public void OptionalShouldSucceed()
{
var parser = Terms.Text("hello").Optional();

Assert.Equal("hello", parser.Parse(" hello world hello").FirstOrDefault());
Assert.Null(parser.Parse(" foo").FirstOrDefault());
}

[Fact]
public void ZeroOrOneShouldNotBeSeekable()
{
Expand Down