diff --git a/docs/parsers.md b/docs/parsers.md index 981e5a3..010813a 100644 --- a/docs/parsers.md +++ b/docs/parsers.md @@ -629,6 +629,62 @@ var parser = OneOf( ); ``` +### Else + +Returns a value if the previous parser failed. + +Usage: + +```c# +var parser = Terms.Integer().Else(0).And(Terms.Text("years")); + +capture.Parse("years"); +capture.Parse("123 years"); +``` + +Result: + +``` +(0, "years") +(123, "years") +``` + +### ThenElse + +Converts the result of a parser, or returns a value if it didn't succeed. This parser always succeeds. + +NB: It is implemented using `Then()` and `Else()` parsers. + +```c# +Parser ThenElse(Func conversion, U elseValue) +Parser ThenElse(Func conversion, U elseValue) +Parser ThenElse(U value, U elseValue) +``` + +Usage: + +```c# +var parser = + Terms.Integer().ThenElse(x => x, null) + +parser.Parse("abc"); +``` + +Result: + +``` +(long?)null +``` + +When the previous results or the `ParseContext` are not used then the version without delegates can be used: + +```c# +var parser = OneOf( + Terms.Text("not").Then(UnaryOperator.Not), + Terms.Text("-").Then(UnaryOperator.Negate) +); +``` + ### ElseError Fails parsing with a custom error message when the inner parser didn't match. @@ -723,14 +779,14 @@ Returns any characters until the specified parser is matched. Parser AnyCharBefore(Parser parser, bool canBeEmpty = false, bool failOnEof = false, bool consumeDelimiter = false) ``` -### Empty +### Always Always returns successfully, with an optional return type or value. ```c# -Parser Empty() -Parser Empty() -Parser Empty(T value) +Parser Always() +Parser Always() +Parser Always(T value) ``` ### OneOf diff --git a/src/Parlot/Compilation/ExpressionHelper.cs b/src/Parlot/Compilation/ExpressionHelper.cs index d8cf12b..bf2243b 100644 --- a/src/Parlot/Compilation/ExpressionHelper.cs +++ b/src/Parlot/Compilation/ExpressionHelper.cs @@ -27,12 +27,12 @@ public static class ExpressionHelper internal static MethodInfo Cursor_AdvanceNoNewLines = typeof(Cursor).GetMethod(nameof(Parlot.Cursor.AdvanceNoNewLines), [typeof(int)]); internal static ConstructorInfo TextSpan_Constructor = typeof(TextSpan).GetConstructor([typeof(string), typeof(int), typeof(int)]); - internal static ConstructorInfo GetOptionalResult_Constructor() => typeof(OptionalResult).GetConstructor([typeof(bool), typeof(T)]); + //internal static ConstructorInfo GetOptionalResult_Constructor() => typeof(OptionalResult).GetConstructor([typeof(bool), typeof(T)]); public static Expression ArrayEmpty() => ((Expression>)(() => Array.Empty())).Body; public static Expression New() where T : new() => ((Expression>)(() => new T())).Body; - public static Expression NewOptionalResult(this CompilationContext _, Expression hasValue, Expression value) => Expression.New(GetOptionalResult_Constructor(), [hasValue, value]); + //public static Expression NewOptionalResult(this CompilationContext _, Expression hasValue, Expression value) => Expression.New(GetOptionalResult_Constructor(), [hasValue, value]); public static Expression NewTextSpan(this CompilationContext _, Expression buffer, Expression offset, Expression count) => Expression.New(TextSpan_Constructor, [buffer, offset, count]); public static MemberExpression Scanner(this CompilationContext context) => Expression.Field(context.ParseContext, "Scanner"); public static MemberExpression Cursor(this CompilationContext context) => Expression.Field(context.Scanner(), "Cursor"); diff --git a/src/Parlot/Fluent/Empty.cs b/src/Parlot/Fluent/Always.cs similarity index 88% rename from src/Parlot/Fluent/Empty.cs rename to src/Parlot/Fluent/Always.cs index 50cb836..0804873 100644 --- a/src/Parlot/Fluent/Empty.cs +++ b/src/Parlot/Fluent/Always.cs @@ -6,16 +6,16 @@ namespace Parlot.Fluent /// /// Doesn't parse anything and return the default value. /// - public sealed class Empty : Parser, ICompilable + public sealed class Always : Parser, ICompilable { private readonly T _value; - public Empty() + public Always() { _value = default; } - public Empty(T value) + public Always(T value) { _value = value; } diff --git a/src/Parlot/Fluent/Discard.cs b/src/Parlot/Fluent/Discard.cs index 7db3b93..286c0e7 100644 --- a/src/Parlot/Fluent/Discard.cs +++ b/src/Parlot/Fluent/Discard.cs @@ -13,7 +13,7 @@ public sealed class Discard : Parser, ICompilable public Discard(Parser parser) { - _value = default(U); + _value = default; _parser = parser; } diff --git a/src/Parlot/Fluent/Else.cs b/src/Parlot/Fluent/Else.cs new file mode 100644 index 0000000..3ee4777 --- /dev/null +++ b/src/Parlot/Fluent/Else.cs @@ -0,0 +1,71 @@ +using Parlot.Compilation; +using System.Linq.Expressions; + +namespace Parlot.Fluent +{ + /// + /// Returns a default value if the previous parser failed. + /// + public sealed class Else : Parser, ICompilable + { + private readonly Parser _parser; + private readonly T _value; + + public Else(Parser parser, T value) + { + _parser = parser; + _value = value; + } + + public override bool Parse(ParseContext context, ref ParseResult result) + { + context.EnterParser(this); + + if (!_parser.Parse(context, ref result)) + { + result.Set(result.Start, result.End, _value); + } + + return true; + } + + public CompilationResult Compile(CompilationContext context) + { + var result = new CompilationResult(); + + var success = context.DeclareSuccessVariable(result, true); + var value = context.DeclareValueVariable(result, Expression.Default(typeof(T)), typeof(T)); + + var parserCompileResult = _parser.Build(context); + + // success = true; + // + // parser instructions + // + // if (parser.success) + // { + // value = parser.Value + // } + // else + // { + // value = defaultValue + // } + + result.Body.Add( + Expression.Block( + parserCompileResult.Variables, + Expression.Block(parserCompileResult.Body), + context.DiscardResult + ? Expression.Empty() + : Expression.IfThenElse( + parserCompileResult.Success, + Expression.Assign(value, parserCompileResult.Value), + Expression.Assign(value, Expression.Constant(_value, typeof(T))) + ) + ) + ); + + return result; + } + } +} diff --git a/src/Parlot/Fluent/Optional.cs b/src/Parlot/Fluent/Optional.cs deleted file mode 100644 index bcce983..0000000 --- a/src/Parlot/Fluent/Optional.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Parlot.Fluent -{ - /// - /// Represents an optional result. - /// - /// The type of the result. - public readonly struct OptionalResult - { - public OptionalResult(bool hasValue, T value) - { - HasValue = hasValue; - Value = value; - } - - /// - /// Whether the result has a value or not. - /// - public bool HasValue { get; } - - /// - /// Gets the value of the result if any. - /// - public T Value { get; } - } -} diff --git a/src/Parlot/Fluent/Parser.cs b/src/Parlot/Fluent/Parser.cs index 3ff2948..6c02398 100644 --- a/src/Parlot/Fluent/Parser.cs +++ b/src/Parlot/Fluent/Parser.cs @@ -17,10 +17,25 @@ public abstract partial class Parser public Parser Then(Func conversion) => new Then(this, conversion); /// - /// Builds a parser that converts the previous result, and can alter the current . + /// Builds a parser that converts the previous result. /// public Parser Then(U value) => new Then(this, value); + /// + /// Builds a parser that converts the previous result when it succeeds or returns a default value if it fails. + /// + public Parser ThenElse(Func conversion, U elseValue) => new Then(this, conversion).Else(elseValue); + + /// + /// Builds a parser that converts the previous result or returns a default value if it fails, and can alter the current . + /// + public Parser ThenElse(Func conversion, U elseValue) => new Then(this, conversion).Else(elseValue); + + /// + /// Builds a parser that converts the previous result or returns a default value if it fails. + /// + public Parser ThenElse(U value, U elseValue) => new Then(this, value).Else(elseValue); + /// /// Builds a parser that emits an error when the previous parser failed. /// @@ -60,5 +75,10 @@ public abstract partial class Parser /// Builds a parser that discards the previous result and replaces it by the specified type or value. /// public Parser Discard(U value) => new Discard(this, value); + + /// + /// Builds a parser that returns a default value if the previous parser fails. + /// + public Parser Else(T value) => new Else(this, value); } } diff --git a/src/Parlot/Fluent/Parsers.cs b/src/Parlot/Fluent/Parsers.cs index 4be541d..9a168ce 100644 --- a/src/Parlot/Fluent/Parsers.cs +++ b/src/Parlot/Fluent/Parsers.cs @@ -29,7 +29,7 @@ public static partial class Parsers /// /// Builds a parser that looks for zero or one time the specified parser. /// - public static Parser> ZeroOrOne(Parser parser) => new ZeroOrOne(parser); + public static Parser ZeroOrOne(Parser parser, T defaultValue = default) => new ZeroOrOne(parser); /// /// Builds a parser that looks for zero or many times the specified parser. @@ -42,7 +42,7 @@ public static partial class Parsers public static Parser> OneOrMany(Parser parser) => new OneOrMany(parser); /// - /// Builds a parser that succeed when the specified parser fails to match. + /// Builds a parser that succeeds when the specified parser fails to match. /// public static Parser Not(Parser parser) => new Not(parser); @@ -75,17 +75,17 @@ public static partial class Parsers /// /// Builds a parser that always succeeds. /// - public static Parser Empty() => new Empty(); + public static Parser Always() => new Always(); /// /// Builds a parser that always succeeds. /// - public static Parser Empty() => new Empty(); + public static Parser Always() => new Always(); /// /// Builds a parser that always succeeds. /// - public static Parser Empty(T value) => new Empty(value); + public static Parser Always(T value) => new Always(value); } diff --git a/src/Parlot/Fluent/Then.cs b/src/Parlot/Fluent/Then.cs index 656e651..68ab91a 100644 --- a/src/Parlot/Fluent/Then.cs +++ b/src/Parlot/Fluent/Then.cs @@ -19,7 +19,7 @@ public sealed class Then : Parser, ICompilable, ISeekable private readonly U _value = default; private readonly Parser _parser; - public Then(Parser parser) + private Then(Parser parser) { _parser = parser ?? throw new ArgumentNullException(nameof(parser)); @@ -108,7 +108,7 @@ public CompilationResult Compile(CompilationContext context) } else { - transformation = Expression.Constant(_value); + transformation = Expression.Constant(_value, typeof(U)); } var block = Expression.Block( diff --git a/src/Parlot/Fluent/ZeroOrOne.cs b/src/Parlot/Fluent/ZeroOrOne.cs index fd1cf3d..5bff419 100644 --- a/src/Parlot/Fluent/ZeroOrOne.cs +++ b/src/Parlot/Fluent/ZeroOrOne.cs @@ -5,14 +5,15 @@ namespace Parlot.Fluent { - public sealed class ZeroOrOne : Parser>, ICompilable, ISeekable + public sealed class ZeroOrOne : Parser, ICompilable, ISeekable { private readonly Parser _parser; + private readonly T _defaultValue; - public ZeroOrOne(Parser parser) + public ZeroOrOne(Parser parser, T defaultValue = default) { _parser = parser ?? throw new ArgumentNullException(nameof(parser)); - + _defaultValue = defaultValue; if (_parser is ISeekable seekable) { CanSeek = seekable.CanSeek; @@ -27,7 +28,7 @@ public ZeroOrOne(Parser parser) public bool SkipWhitespace { get; } - public override bool Parse(ParseContext context, ref ParseResult> result) + public override bool Parse(ParseContext context, ref ParseResult result) { context.EnterParser(this); @@ -35,7 +36,7 @@ public override bool Parse(ParseContext context, ref ParseResult(success, parsed.Value)); + result.Set(parsed.Start, parsed.End, success ? parsed.Value : _defaultValue); // ZeroOrOne always succeeds return true; @@ -46,9 +47,9 @@ public CompilationResult Compile(CompilationContext context) var result = new CompilationResult(); var success = context.DeclareSuccessVariable(result, true); - var value = context.DeclareValueVariable(result, Expression.Default(typeof(OptionalResult))); + var value = context.DeclareValueVariable(result, Expression.Constant(_defaultValue, typeof(T))); - // T value; + // T value = _defaultValue; // // parse1 instructions // @@ -63,7 +64,11 @@ public CompilationResult Compile(CompilationContext context) Expression.Block(parserCompileResult.Body), context.DiscardResult ? Expression.Empty() - : Expression.Assign(value, context.NewOptionalResult(parserCompileResult.Success, parserCompileResult.Value)) + : Expression.IfThenElse( + parserCompileResult.Success, + Expression.Assign(value, parserCompileResult.Value), + Expression.Assign(value, Expression.Default(typeof(T))) + ) ) ); diff --git a/test/Parlot.Tests/CompileTests.cs b/test/Parlot.Tests/CompileTests.cs index 540e211..290177d 100644 --- a/test/Parlot.Tests/CompileTests.cs +++ b/test/Parlot.Tests/CompileTests.cs @@ -198,9 +198,8 @@ public void ShouldCompileZeroOrOne() { var parser = ZeroOrOne(Terms.Text("hello")).Compile(); - Assert.Equal("hello", parser.Parse(" hello world hello").Value); - Assert.False(parser.Parse(" foo").HasValue); - Assert.Null(parser.Parse(" foo").Value); + Assert.Equal("hello", parser.Parse(" hello world hello")); + Assert.Null(parser.Parse(" foo")); } [Fact] @@ -349,15 +348,15 @@ public void ShouldCompileSkipAnd() [Fact] public void ShouldCompileEmpty() { - Assert.True(Empty().Compile().TryParse("123", out var result) && result == null); - Assert.True(Empty(1).Compile().TryParse("123", out var r2) && r2 == 1); + Assert.True(Always().Compile().TryParse("123", out var result) && result == null); + Assert.True(Always(1).Compile().TryParse("123", out var r2) && r2 == 1); } [Fact] public void ShouldCompileEof() { - Assert.True(Empty().Eof().Compile().TryParse("", out _)); - Assert.False(Empty().Eof().Compile().TryParse(" ", out _)); + Assert.True(Always().Eof().Compile().TryParse("", out _)); + Assert.False(Always().Eof().Compile().TryParse(" ", out _)); Assert.True(Terms.Decimal().Eof().Compile().TryParse("123", out var result) && result == 123); Assert.False(Terms.Decimal().Eof().Compile().TryParse("123 ", out _)); } @@ -783,6 +782,31 @@ public void ShouldBuildCaseInsensitiveLookupTable() Assert.Null(parser.Parse("ABC")); } + + [Fact] + public void ShouldReturnElse() + { + var parser = Literals.Integer().Then(x => x).Else(null).Compile(); + + Assert.True(parser.TryParse("123", out var result1)); + Assert.Equal(123, result1); + + Assert.True(parser.TryParse(" 123", out var result2)); + Assert.Null(result2); + } + + [Fact] + public void ShouldThenElse() + { + var parser = Literals.Integer().ThenElse(x => x, null).Compile(); + + Assert.True(parser.TryParse("123", out var result1)); + Assert.Equal(123, result1); + + Assert.True(parser.TryParse(" 123", out var result2)); + Assert.Null(result2); + } + private class LogicalExpression { } private class ValueExpression(decimal Value) : LogicalExpression diff --git a/test/Parlot.Tests/FluentTests.cs b/test/Parlot.Tests/FluentTests.cs index b53e5be..6e807b7 100644 --- a/test/Parlot.Tests/FluentTests.cs +++ b/test/Parlot.Tests/FluentTests.cs @@ -31,15 +31,27 @@ public void WhenShouldResetPositionWhenFalse() } [Fact] - public void ZeroOrOneShouldFindOptionalParser() + public void ShouldReturnElse() { - var parser = ZeroOrOne(Literals.Integer()); + var parser = Literals.Integer().Then(x => x).Else(null); Assert.True(parser.TryParse("123", out var result1)); - Assert.Equal(123, result1.Value); + Assert.Equal(123, result1); Assert.True(parser.TryParse(" 123", out var result2)); - Assert.Equal(0, result2.Value); + Assert.Null(result2); + } + + [Fact] + public void ShouldThenElse() + { + var parser = Literals.Integer().ThenElse(x => x, null); + + Assert.True(parser.TryParse("123", out var result1)); + Assert.Equal(123, result1); + + Assert.True(parser.TryParse(" 123", out var result2)); + Assert.Null(result2); } [Fact] @@ -483,8 +495,8 @@ public void ShouldParseEmails() [Fact] public void ShouldParseEof() { - Assert.True(Empty().Eof().TryParse("", out _)); - Assert.False(Empty().Eof().TryParse(" ", out _)); + Assert.True(Always().Eof().TryParse("", out _)); + Assert.False(Always().Eof().TryParse(" ", out _)); Assert.True(Terms.Decimal().Eof().TryParse("123", out var result) && result == 123); Assert.False(Terms.Decimal().Eof().TryParse("123 ", out _)); } @@ -492,8 +504,8 @@ public void ShouldParseEof() [Fact] public void EmptyShouldAlwaysSucceed() { - Assert.True(Empty().TryParse("123", out var result) && result == null); - Assert.True(Empty(1).TryParse("123", out var r2) && r2 == 1); + Assert.True(Always().TryParse("123", out var result) && result == null); + Assert.True(Always(1).TryParse("123", out var r2) && r2 == 1); } [Fact]