diff --git a/docs/parsers.md b/docs/parsers.md index ac46b42..ceb427c 100644 --- a/docs/parsers.md +++ b/docs/parsers.md @@ -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 Select(Func> selector) +Parser Select(Func> selector) where C : ParseContext +``` + +Usage: + +```c# +var parser = Select(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 NonWhiteSpace(bool includeNewLines = false) ``` @@ -857,7 +881,9 @@ Parser When(Func 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. @@ -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 Switch(Func> action) @@ -879,11 +904,15 @@ Parser Switch(Func> 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. @@ -937,6 +966,15 @@ Parser Always() Parser Always(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 Fail() +Parser Fail() +``` + ### OneOf Like [Or](#Or), with an unlimited list of parsers. diff --git a/src/Parlot/Fluent/Fail.cs b/src/Parlot/Fluent/Fail.cs new file mode 100644 index 0000000..fbca4ce --- /dev/null +++ b/src/Parlot/Fluent/Fail.cs @@ -0,0 +1,28 @@ +using Parlot.Compilation; +using System.Linq.Expressions; + +namespace Parlot.Fluent; + +/// +/// Doesn't parse anything and fails parsing. +/// +public sealed class Fail : Parser, ICompilable +{ + public Fail() + { + Name = "Fail"; + } + + public override bool Parse(ParseContext context, ref ParseResult result) + { + context.EnterParser(this); + + context.ExitParser(this); + return false; + } + + public CompilationResult Compile(CompilationContext context) + { + return context.CreateCompilationResult(false, Expression.Constant(default(T), typeof(T))); + } +} diff --git a/src/Parlot/Fluent/Parsers.cs b/src/Parlot/Fluent/Parsers.cs index f34b643..dfdc00f 100644 --- a/src/Parlot/Fluent/Parsers.cs +++ b/src/Parlot/Fluent/Parsers.cs @@ -60,23 +60,37 @@ public static partial class Parsers /// /// Builds a parser that invoked the next one if a condition is true. /// + [Obsolete("Use the Select parser instead.")] public static Parser If(Func predicate, S? state, Parser parser) where C : ParseContext => new If(parser, predicate, state); /// /// Builds a parser that invoked the next one if a condition is true. /// + [Obsolete("Use the Select parser instead.")] public static Parser If(Func predicate, S? state, Parser parser) => new If(parser, predicate, state); /// /// Builds a parser that invoked the next one if a condition is true. /// + [Obsolete("Use the Select parser instead.")] public static Parser If(Func predicate, Parser parser) where C : ParseContext => new If(parser, (c, s) => predicate(c), null); /// /// Builds a parser that invoked the next one if a condition is true. /// + [Obsolete("Use the Select parser instead.")] public static Parser If(Func predicate, Parser parser) => new If(parser, (c, s) => predicate(c), null); + /// + /// Builds a parser that selects another parser using custom logic. + /// + public static Parser Select(Func> selector) where C : ParseContext => new Select(selector); + + /// + /// Builds a parser that selects another parser using custom logic. + /// + public static Parser Select(Func> selector) => new Select(selector); + /// /// Builds a parser that can be defined later one. Use it when a parser need to be declared before its rule can be set. /// @@ -118,6 +132,15 @@ public static partial class Parsers /// public static Parser Always(T value) => new Always(value); + /// + /// Builds a parser that always fails. + /// + public static Parser Fail() => new Fail(); + + /// + /// Builds a parser that always fails. + /// + public static Parser Fail() => new Fail(); } public class LiteralBuilder diff --git a/src/Parlot/Fluent/Select.cs b/src/Parlot/Fluent/Select.cs new file mode 100644 index 0000000..d6d8e7a --- /dev/null +++ b/src/Parlot/Fluent/Select.cs @@ -0,0 +1,82 @@ +using Parlot.Compilation; +using System; +using System.Linq.Expressions; +using System.Reflection; + +namespace Parlot.Fluent; + +/// +/// Selects a parser instance at runtime and delegates parsing to it. +/// +/// The concrete type to use. +/// The output parser type. +public sealed class Select : Parser, ICompilable where C : ParseContext +{ + private static readonly MethodInfo _parse = typeof(Parser).GetMethod(nameof(Parse), [typeof(ParseContext), typeof(ParseResult).MakeByRefType()])!; + + private readonly Func> _selector; + + public Select(Func> selector) + { + _selector = selector ?? throw new ArgumentNullException(nameof(selector)); + } + + public override bool Parse(ParseContext context, ref ParseResult result) + { + context.EnterParser(this); + + var nextParser = _selector((C)context); + + if (nextParser == null) + { + context.ExitParser(this); + return false; + } + + var parsed = new ParseResult(); + + 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(); + var parserVariable = Expression.Variable(typeof(Parser), $"select{context.NextNumber}"); + var parseResult = Expression.Variable(typeof(ParseResult), $"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))), + 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.Value)))) + ))); + + result.Body.Add(body); + + return result; + } + + public override string ToString() => "(Select)"; +} diff --git a/test/Parlot.Tests/CompileTests.cs b/test/Parlot.Tests/CompileTests.cs index 0ac6d06..affbe57 100644 --- a/test/Parlot.Tests/CompileTests.cs +++ b/test/Parlot.Tests/CompileTests.cs @@ -347,6 +347,15 @@ public override bool Parse(ParseContext context, ref ParseResult result) } } + private sealed class CustomCompileParseContext : ParseContext + { + public CustomCompileParseContext(Scanner scanner) : base(scanner) + { + } + + public bool PreferYes { get; set; } + } + [Fact] public void ShouldCompileNonCompilableCharLiterals() { @@ -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); @@ -578,6 +589,45 @@ public void ShouldCompileSwitch() Assert.Equal("123", ((TextSpan)resultS).ToString()); } + [Fact] + public void SelectShouldCompilePickParserUsingRuntimeLogic() + { + var allowWhiteSpace = true; + var parser = Select(_ => 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(_ => null!).Compile(); + + Assert.False(parser.TryParse("123", out _)); + } + + [Fact] + public void SelectShouldCompileHonorConcreteParseContext() + { + var parser = Select(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() { diff --git a/test/Parlot.Tests/FluentTests.cs b/test/Parlot.Tests/FluentTests.cs index 33b71a6..fc1cfdb 100644 --- a/test/Parlot.Tests/FluentTests.cs +++ b/test/Parlot.Tests/FluentTests.cs @@ -30,8 +30,10 @@ public void IfShouldNotInvokeParserWhenFalse() { 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)); var oddState = If(predicate: (context, x) => x % 2 == 0, state: 1, parser: Literals.Integer().Then(x => invoked = true)); +#pragma warning restore CS0618 // Type or member is obsolete Assert.False(oddState.TryParse("1234", out var result1)); Assert.False(invoked); @@ -402,6 +404,54 @@ public void SwitchShouldReturnCommonType() Assert.Equal("123", resultS); } + [Fact] + public void SelectShouldPickParserUsingRuntimeLogic() + { + var allowWhiteSpace = true; + var parser = Select(_ => allowWhiteSpace ? Terms.Integer() : Literals.Integer()); + + 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 SelectShouldFailWhenSelectorReturnsNull() + { + var parser = Select(_ => null!); + + Assert.False(parser.TryParse("123", out _)); + } + + [Fact] + public void SelectShouldHonorConcreteParseContext() + { + var parser = Select(context => context.PreferYes ? Literals.Text("yes") : Literals.Text("no")); + + var yesContext = new CustomParseContext(new Scanner("yes")) { PreferYes = true }; + Assert.True(parser.TryParse(yesContext, out var yes, out _)); + Assert.Equal("yes", yes); + + var noContext = new CustomParseContext(new Scanner("no")) { PreferYes = false }; + Assert.True(parser.TryParse(noContext, out var no, out _)); + Assert.Equal("no", no); + } + + private sealed class CustomParseContext : ParseContext + { + public CustomParseContext(Scanner scanner) : base(scanner) + { + } + + public bool PreferYes { get; set; } + } + [Theory] [InlineData("a", "a")] [InlineData("foo", "foo")] @@ -573,6 +623,13 @@ public void EmptyShouldAlwaysSucceed() Assert.True(Always(1).TryParse("123", out var r2) && r2 == 1); } + + [Fact] + public void FailShouldFail() + { + Assert.False(Fail().TryParse("123", out var result)); + } + [Fact] public void NotShouldNegateParser() {