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
54 changes: 46 additions & 8 deletions docs/parsers.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,30 @@ Result:

Matches any non-blank spaces, optionally including new lines. Returns a `TextSpan` with the matched characters.

### Select

Selects the parser to execute at runtime. Use it when the next parser depends on mutable state or a custom `ParseContext` implementation.

```c#
Parser<T> Select<T>(Func<ParseContext, Parser<T>> selector)
Parser<T> Select<C, T>(Func<C, Parser<T>> selector) where C : ParseContext
```

Usage:

```c#
var parser = Select<CustomContext, string>(context =>
{
return context.PreferYes ? Literals.Text("yes") : Literals.Text("no");
});

var result = parser.Parse(new CustomContext(new Scanner("yes")) { PreferYes = true });
```
`CustomContext` is an application-defined type that derives from `ParseContext` and exposes additional configuration.


If the selector returns `null`, the `Select` parser fails without consuming any input. Capture additional state through closures or custom `ParseContext` properties when needed.

```c#
Parser<TextSpan> NonWhiteSpace(bool includeNewLines = false)
```
Expand Down Expand Up @@ -857,7 +881,9 @@ Parser<T> When(Func<ParseContext, T, bool> predicate)

To evaluate a condition before a parser is executed use the `If` parser instead.

### If
### If (Deprecated)

NB: This parser can be rewritten using `Select` (and `Fail`) which is more flexible and simpler to understand.

Executes a parser only if a condition is true.

Expand All @@ -869,8 +895,7 @@ To evaluate a condition before a parser is executed use the `If` parser instead.

### Switch

Returns the next parser based on some custom logic that can't be defined statically. It is typically used in conjunction with a `ParseContext` instance
which has custom options.
Returns a parser using custom logic based on previous results.

```c#
Parser<U> Switch<U>(Func<ParseContext, T, Parser<U>> action)
Expand All @@ -879,11 +904,15 @@ Parser<U> Switch<U>(Func<ParseContext, T, Parser<U>> action)
Usage:

```c#
var parser = Terms.Integer().And(Switch((context, x) =>
{
var customContext = context as CustomParseContext;
return Literals.Char(customContext.IntegerSeparator);
});
var parser = Terms.Integer().Switch((context, i) =>
{
// Valid entries: "1 is odd", "2 is even"
// Invalid: "7 is even"

return i % 2 == 0
? Terms.Text("is odd")
: Terms.Text("is even");
});
```

For performance reasons it is recommended to return a singleton (or static) Parser instance. Otherwise each `Parse` execution will allocate a new Parser instance.
Expand Down Expand Up @@ -937,6 +966,15 @@ Parser<object> Always()
Parser<T> Always<T>(T value)
```

### Fail

A parser that returns a failed attempt. Used when a Parser needs to be returned but one that should depict a failure.

```c#
Parser<T> Fail<T>()
Parser<object> Fail()
```

### OneOf

Like [Or](#Or), with an unlimited list of parsers.
Expand Down
28 changes: 28 additions & 0 deletions src/Parlot/Fluent/Fail.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Parlot.Compilation;
using System.Linq.Expressions;

namespace Parlot.Fluent;

/// <summary>
/// Doesn't parse anything and fails parsing.
/// </summary>
public sealed class Fail<T> : Parser<T>, ICompilable
{
public Fail()
{
Name = "Fail";
}

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

context.ExitParser(this);
return false;
}

public CompilationResult Compile(CompilationContext context)
{
return context.CreateCompilationResult<T>(false, Expression.Constant(default(T), typeof(T)));
}
}
23 changes: 23 additions & 0 deletions src/Parlot/Fluent/Parsers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,23 +60,37 @@ public static partial class Parsers
/// <summary>
/// Builds a parser that invoked the next one if a condition is true.
/// </summary>
[Obsolete("Use the Select parser instead.")]
public static Parser<T> If<C, S, T>(Func<C, S?, bool> predicate, S? state, Parser<T> parser) where C : ParseContext => new If<C, S, T>(parser, predicate, state);

/// <summary>
/// Builds a parser that invoked the next one if a condition is true.
/// </summary>
[Obsolete("Use the Select parser instead.")]
public static Parser<T> If<S, T>(Func<ParseContext, S?, bool> predicate, S? state, Parser<T> parser) => new If<ParseContext, S, T>(parser, predicate, state);

/// <summary>
/// Builds a parser that invoked the next one if a condition is true.
/// </summary>
[Obsolete("Use the Select parser instead.")]
public static Parser<T> If<C, T>(Func<C, bool> predicate, Parser<T> parser) where C : ParseContext => new If<C, object?, T>(parser, (c, s) => predicate(c), null);

/// <summary>
/// Builds a parser that invoked the next one if a condition is true.
/// </summary>
[Obsolete("Use the Select parser instead.")]
public static Parser<T> If<T>(Func<ParseContext, bool> predicate, Parser<T> parser) => new If<ParseContext, object?, T>(parser, (c, s) => predicate(c), null);

/// <summary>
/// Builds a parser that selects another parser using custom logic.
/// </summary>
public static Parser<T> Select<C, T>(Func<C, Parser<T>> selector) where C : ParseContext => new Select<C, T>(selector);

/// <summary>
/// Builds a parser that selects another parser using custom logic.
/// </summary>
public static Parser<T> Select<T>(Func<ParseContext, Parser<T>> selector) => new Select<ParseContext, T>(selector);

/// <summary>
/// Builds a parser that can be defined later one. Use it when a parser need to be declared before its rule can be set.
/// </summary>
Expand Down Expand Up @@ -118,6 +132,15 @@ public static partial class Parsers
/// </summary>
public static Parser<T> Always<T>(T value) => new Always<T>(value);

/// <summary>
/// Builds a parser that always fails.
/// </summary>
public static Parser<T> Fail<T>() => new Fail<T>();

/// <summary>
/// Builds a parser that always fails.
/// </summary>
public static Parser<object> Fail() => new Fail<object>();
}

public class LiteralBuilder
Expand Down
82 changes: 82 additions & 0 deletions src/Parlot/Fluent/Select.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using Parlot.Compilation;
using System;
using System.Linq.Expressions;
using System.Reflection;

namespace Parlot.Fluent;

/// <summary>
/// Selects a parser instance at runtime and delegates parsing to it.
/// </summary>
/// <typeparam name="C">The concrete <see cref="ParseContext" /> type to use.</typeparam>
/// <typeparam name="T">The output parser type.</typeparam>
public sealed class Select<C, T> : Parser<T>, ICompilable where C : ParseContext
{
private static readonly MethodInfo _parse = typeof(Parser<T>).GetMethod(nameof(Parse), [typeof(ParseContext), typeof(ParseResult<T>).MakeByRefType()])!;

private readonly Func<C, Parser<T>> _selector;

public Select(Func<C, Parser<T>> selector)
{
_selector = selector ?? throw new ArgumentNullException(nameof(selector));
}

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

var nextParser = _selector((C)context);

if (nextParser == null)
{
context.ExitParser(this);
return false;
}

var parsed = new ParseResult<T>();

if (nextParser.Parse(context, ref parsed))
{
result.Set(parsed.Start, parsed.End, parsed.Value);

context.ExitParser(this);
return true;
}

context.ExitParser(this);
return false;
}

public CompilationResult Compile(CompilationContext context)
{
var result = context.CreateCompilationResult<T>();
var parserVariable = Expression.Variable(typeof(Parser<T>), $"select{context.NextNumber}");
var parseResult = Expression.Variable(typeof(ParseResult<T>), $"value{context.NextNumber}");

var selectorInvoke = Expression.Invoke(
Expression.Constant(_selector),
Expression.Convert(context.ParseContext, typeof(C)));

var body = Expression.Block(
[parserVariable, parseResult],
Expression.Assign(parserVariable, selectorInvoke),
Expression.IfThen(
Expression.NotEqual(parserVariable, Expression.Constant(null, typeof(Parser<T>))),
Expression.Block(
Expression.Assign(
result.Success,
Expression.Call(parserVariable, _parse, context.ParseContext, parseResult)),
context.DiscardResult
? Expression.Empty()
: Expression.IfThen(
result.Success,
Expression.Assign(result.Value, Expression.Field(parseResult, nameof(ParseResult<T>.Value))))
)));

result.Body.Add(body);

return result;
}

public override string ToString() => "(Select)";
}
50 changes: 50 additions & 0 deletions test/Parlot.Tests/CompileTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,15 @@ public override bool Parse(ParseContext context, ref ParseResult<char> result)
}
}

private sealed class CustomCompileParseContext : ParseContext
{
public CustomCompileParseContext(Scanner scanner) : base(scanner)
{
}

public bool PreferYes { get; set; }
}

[Fact]
public void ShouldCompileNonCompilableCharLiterals()
{
Expand Down Expand Up @@ -499,8 +508,10 @@ public void CompiledIfShouldNotInvokeParserWhenFalse()
{
bool invoked = false;

#pragma warning disable CS0618 // Type or member is obsolete
var evenState = If(predicate: (context, x) => x % 2 == 0, state: 0, parser: Literals.Integer().Then(x => invoked = true)).Compile();
var oddState = If(predicate: (context, x) => x % 2 == 0, state: 1, parser: Literals.Integer().Then(x => invoked = true)).Compile();
#pragma warning restore CS0618 // Type or member is obsolete

Assert.False(oddState.TryParse("1234", out var result1));
Assert.False(invoked);
Expand Down Expand Up @@ -578,6 +589,45 @@ public void ShouldCompileSwitch()
Assert.Equal("123", ((TextSpan)resultS).ToString());
}

[Fact]
public void SelectShouldCompilePickParserUsingRuntimeLogic()
{
var allowWhiteSpace = true;
var parser = Select<long>(_ => allowWhiteSpace ? Terms.Integer() : Literals.Integer()).Compile();

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

allowWhiteSpace = false;

Assert.True(parser.TryParse("42", out var result2));
Assert.Equal(42, result2);

Assert.False(parser.TryParse(" 42", out _));
}

[Fact]
public void SelectShouldCompileFailWhenSelectorReturnsNull()
{
var parser = Select<long>(_ => null!).Compile();

Assert.False(parser.TryParse("123", out _));
}

[Fact]
public void SelectShouldCompileHonorConcreteParseContext()
{
var parser = Select<CustomCompileParseContext, string>(context => context.PreferYes ? Literals.Text("yes") : Literals.Text("no")).Compile();

var yesContext = new CustomCompileParseContext(new Scanner("yes")) { PreferYes = true };
Assert.True(parser.TryParse(yesContext, out var yes, out _));
Assert.Equal("yes", yes);

var noContext = new CustomCompileParseContext(new Scanner("no")) { PreferYes = false };
Assert.True(parser.TryParse(noContext, out var no, out _));
Assert.Equal("no", no);
}

[Fact]
public void ShouldCompileTextBefore()
{
Expand Down
Loading