From 04de1d4c709a539ed5649b38ea7badf052169feb Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Sun, 11 May 2025 21:12:04 -0700 Subject: [PATCH 1/2] Create suffixed cardinality methods --- docs/parsers.md | 6 +-- src/Parlot/Fluent/Optional.cs | 40 +++++++++++++++++++ src/Parlot/Fluent/Parser.cs | 12 +++++- .../Fluent/ParserExtensions.Cardinality.cs | 23 +++++++++++ test/Parlot.Tests/CompileTests.cs | 8 ++-- test/Parlot.Tests/FluentTests.cs | 32 +++++++++++---- 6 files changed, 105 insertions(+), 16 deletions(-) create mode 100644 src/Parlot/Fluent/Optional.cs create mode 100644 src/Parlot/Fluent/ParserExtensions.Cardinality.cs diff --git a/docs/parsers.md b/docs/parsers.md index 6a34347..7b5d918 100644 --- a/docs/parsers.md +++ b/docs/parsers.md @@ -413,7 +413,7 @@ null Executes a parser as long as it's successful. The result is a list of all individual results. ```c# -Parser> ZeroOrMany(Parser parser) +Parser ZeroOrMany(Parser parser) ``` Usage: @@ -436,7 +436,7 @@ Result: 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. ```c# -Parser> OneOrMany(Parser parser) +Parser OneOrMany(Parser parser) ``` Usage: @@ -484,7 +484,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> Separated(Parser separator, Parser parser) +Parser Separated(Parser separator, Parser parser) ``` Usage: diff --git a/src/Parlot/Fluent/Optional.cs b/src/Parlot/Fluent/Optional.cs new file mode 100644 index 0000000..395a453 --- /dev/null +++ b/src/Parlot/Fluent/Optional.cs @@ -0,0 +1,40 @@ +using System; + +namespace Parlot.Fluent +{ + public readonly struct Optional + { + private readonly T _value; + private readonly bool _hasValue; + + public Optional(T value) + { + _value = value; + _hasValue = true; + } + + public bool HasValue => _hasValue; + + public T Value + { + get + { + if (!_hasValue) + { + throw new InvalidOperationException("No value present."); + } + return _value; + } + } + + public T GetValueOrDefault(T defaultValue = default) + { + return _hasValue ? _value : defaultValue; + } + + public override string ToString() + { + return _hasValue ? _value?.ToString() ?? "null" : "No value"; + } + } +} \ No newline at end of file diff --git a/src/Parlot/Fluent/Parser.cs b/src/Parlot/Fluent/Parser.cs index 5ea821f..99c6562 100644 --- a/src/Parlot/Fluent/Parser.cs +++ b/src/Parlot/Fluent/Parser.cs @@ -1,5 +1,6 @@ using Parlot.Rewriting; using System; +using System.Globalization; using System.Linq; namespace Parlot.Fluent; @@ -26,7 +27,7 @@ public abstract partial class Parser /// /// Builds a parser that converts the previous result. /// - public Parser Then() => new Then(this, default(U)); + public Parser Then() => new Then(this, x => (U?)Convert.ChangeType(x, typeof(U?), CultureInfo.CurrentCulture)); /// /// Builds a parser that converts the previous result when it succeeds or returns a default value if it fails. @@ -97,7 +98,7 @@ public Parser Named(string name) /// /// Builds a parser that discards the previous result and replaces it by the specified type or value. /// - [Obsolete("Use Then() instead.")] + [Obsolete("Use Then(value) instead.")] public Parser Discard(U value) => new Discard(this, value); /// @@ -114,4 +115,11 @@ public Parser Named(string name) /// Builds a parser that lists all possible matches to improve performance. /// public Parser Lookup(params ISeekable[] parsers) => new Seekable(this, parsers.All(x => x.SkipWhitespace), parsers.SelectMany(x => x.ExpectedChars).ToArray()); + + public Parser Optional(T defaultValue) + => new ZeroOrOne(this, defaultValue); + + public Parser Optional() + => new ZeroOrOne(this, default); + } diff --git a/src/Parlot/Fluent/ParserExtensions.Cardinality.cs b/src/Parlot/Fluent/ParserExtensions.Cardinality.cs new file mode 100644 index 0000000..6902d39 --- /dev/null +++ b/src/Parlot/Fluent/ParserExtensions.Cardinality.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; + +namespace Parlot.Fluent; + +public static partial class ParserExtensions +{ + public static Parser Optional(this Parser parser, T defaultValue) + => new ZeroOrOne(parser, defaultValue); + + public static Parser> OneOrMany(this Parser parser) + => new OneOrMany(parser); + + public static Parser> ZeroOrMany(this Parser parser) + => new ZeroOrMany(parser); + +// TODO: Create Optional + public static Parser ZeroOrOne(this Parser parser, T defaultValue) + => new ZeroOrOne(parser, defaultValue); + + public static Parser ZeroOrOne(this Parser parser) + => new ZeroOrOne(parser, default!); + +} diff --git a/test/Parlot.Tests/CompileTests.cs b/test/Parlot.Tests/CompileTests.cs index 8b94a7d..d9f939e 100644 --- a/test/Parlot.Tests/CompileTests.cs +++ b/test/Parlot.Tests/CompileTests.cs @@ -287,10 +287,10 @@ public void ShouldCompileCapture() Parser Plus = Literals.Char('+'); Parser Minus = Literals.Char('-'); Parser At = Literals.Char('@'); - Parser WordChar = Literals.Pattern(char.IsLetterOrDigit); - Parser> WordDotPlusMinus = OneOrMany(OneOf(WordChar.Then(x => 'w'), Dot, Plus, Minus)); - Parser> WordDotMinus = OneOrMany(OneOf(WordChar.Then(x => 'w'), Dot, Minus)); - Parser> WordMinus = OneOrMany(OneOf(WordChar.Then(x => 'w'), Minus)); + Parser WordChar = Literals.Pattern(char.IsLetterOrDigit).Then(x => x.Span[0]); + Parser> WordDotPlusMinus = OneOrMany(OneOf(WordChar, Dot, Plus, Minus)); + Parser> WordDotMinus = OneOrMany(OneOf(WordChar, Dot, Minus)); + Parser> WordMinus = OneOrMany(OneOf(WordChar, Minus)); Parser Email = Capture(WordDotPlusMinus.And(At).And(WordMinus).And(Dot).And(WordDotMinus)); string _email = "sebastien.ros@gmail.com"; diff --git a/test/Parlot.Tests/FluentTests.cs b/test/Parlot.Tests/FluentTests.cs index 24bb680..63dcd2a 100644 --- a/test/Parlot.Tests/FluentTests.cs +++ b/test/Parlot.Tests/FluentTests.cs @@ -48,16 +48,25 @@ public void WhenShouldResetPositionWhenFalse() Assert.Equal(1235, result1.Item2); } + [Fact] + public void ShouldCast() + { + var parser = Literals.Integer().Then(); + + Assert.True(parser.TryParse("123", out var result1)); + Assert.Equal(123, result1); + } + [Fact] public void ShouldReturnElse() { - var parser = Literals.Integer().Then(x => x).Else(null); + var parser = Literals.Integer().Then().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] @@ -515,10 +524,10 @@ public void ShouldParseEmails() Parser Plus = Literals.Char('+'); Parser Minus = Literals.Char('-'); Parser At = Literals.Char('@'); - Parser WordChar = Literals.Pattern(char.IsLetterOrDigit); - Parser> WordDotPlusMinus = OneOrMany(OneOf(WordChar.Then(), Dot, Plus, Minus)); - Parser> WordDotMinus = OneOrMany(OneOf(WordChar.Then(), Dot, Minus)); - Parser> WordMinus = OneOrMany(OneOf(WordChar.Then(), Minus)); + Parser WordChar = Literals.Pattern(char.IsLetterOrDigit).Then(x => x.Span[0]); + Parser> WordDotPlusMinus = OneOrMany(OneOf(WordChar, Dot, Plus, Minus)); + Parser> WordDotMinus = OneOrMany(OneOf(WordChar, Dot, Minus)); + Parser> WordMinus = OneOrMany(OneOf(WordChar, Minus)); Parser Email = Capture(WordDotPlusMinus.And(At).And(WordMinus).And(Dot).And(WordDotMinus)); string _email = "sebastien.ros@gmail.com"; @@ -579,7 +588,7 @@ public void DiscardShouldReplaceValue() Assert.False(Terms.Decimal().Discard(true).TryParse("abc", out _)); #pragma warning restore CS0618 // Type or member is obsolete - Assert.True(Terms.Decimal().Then().TryParse("123", out var t1) && t1 == false); + Assert.True(Terms.Decimal().Then().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 _)); } @@ -976,6 +985,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")); + Assert.Null(parser.Parse(" foo")); + } + [Fact] public void ZeroOrOneShouldNotBeSeekable() { From 30d38624da582d96e38bd0d86bfed22f8b6ffd4f Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Mon, 12 May 2025 22:00:11 -0700 Subject: [PATCH 2/2] Complete --- docs/parsers.md | 40 ++++++- src/Parlot/Fluent/Optional.cs | 100 ++++++++++++------ src/Parlot/Fluent/Parser.cs | 7 -- .../Fluent/ParserExtensions.Cardinality.cs | 6 +- test/Parlot.Tests/CompileTests.cs | 12 ++- test/Parlot.Tests/FluentTests.cs | 5 +- 6 files changed, 117 insertions(+), 53 deletions(-) diff --git a/docs/parsers.md b/docs/parsers.md index 7b5d918..03bc43e 100644 --- a/docs/parsers.md +++ b/docs/parsers.md @@ -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 ZeroOrOne(Parser parser) @@ -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 ``` @@ -408,9 +409,38 @@ 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> Optional(this Parser 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 ZeroOrMany(Parser parser) @@ -420,6 +450,7 @@ Usage: ```c# var parser = ZeroOrMany(Terms.Text("hello")); +// or Terms.Text("hello").ZeroOrMany() parser.Parse("hello hello"); parser.Parse(""); ``` @@ -433,7 +464,7 @@ 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 OneOrMany(Parser parser) @@ -443,6 +474,7 @@ Usage: ```c# var parser = OneOrMany(Terms.Text("hello")); +// or Terms.Text("hello").OneOrMany() parser.Parse("hello hello"); parser.Parse(""); ``` @@ -641,7 +673,7 @@ Convert the result of a parser. This is usually used to create custom data struc Parser Then(Func conversion) Parser Then(Func conversion) Parser Then(U value) -Parser Then() // returns default(U) +Parser Then() // Converts the result to `U` ``` Usage: diff --git a/src/Parlot/Fluent/Optional.cs b/src/Parlot/Fluent/Optional.cs index 395a453..d8e1b0f 100644 --- a/src/Parlot/Fluent/Optional.cs +++ b/src/Parlot/Fluent/Optional.cs @@ -1,40 +1,70 @@ +using Parlot.Compilation; using System; +using System.Collections.Generic; +using System.Linq.Expressions; -namespace Parlot.Fluent +namespace Parlot.Fluent; + +/// +/// Returns a list containing zero or one element. +/// +/// +/// This parser will always succeed. If the previous parser fails, it will return an empty list. +/// +public sealed class Optional : Parser>, ICompilable { - public readonly struct Optional + private readonly Parser _parser; + public Optional(Parser parser) { - private readonly T _value; - private readonly bool _hasValue; - - public Optional(T value) - { - _value = value; - _hasValue = true; - } - - public bool HasValue => _hasValue; - - public T Value - { - get - { - if (!_hasValue) - { - throw new InvalidOperationException("No value present."); - } - return _value; - } - } - - public T GetValueOrDefault(T defaultValue = default) - { - return _hasValue ? _value : defaultValue; - } - - public override string ToString() - { - return _hasValue ? _value?.ToString() ?? "null" : "No value"; - } + _parser = parser ?? throw new ArgumentNullException(nameof(parser)); } -} \ No newline at end of file + + public override bool Parse(ParseContext context, ref ParseResult> result) + { + context.EnterParser(this); + + var parsed = new ParseResult(); + + 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>(true, ExpressionHelper.ArrayEmpty()); + + // T value = _defaultValue; + // + // parse1 instructions + // + // value = new OptionalResult(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(), typeof(T[]))) + ) + ) + ); + + result.Body.Add(block); + + return result; + } + + public override string ToString() => $"{_parser}?"; +} diff --git a/src/Parlot/Fluent/Parser.cs b/src/Parlot/Fluent/Parser.cs index 99c6562..b8facea 100644 --- a/src/Parlot/Fluent/Parser.cs +++ b/src/Parlot/Fluent/Parser.cs @@ -115,11 +115,4 @@ public Parser Named(string name) /// Builds a parser that lists all possible matches to improve performance. /// public Parser Lookup(params ISeekable[] parsers) => new Seekable(this, parsers.All(x => x.SkipWhitespace), parsers.SelectMany(x => x.ExpectedChars).ToArray()); - - public Parser Optional(T defaultValue) - => new ZeroOrOne(this, defaultValue); - - public Parser Optional() - => new ZeroOrOne(this, default); - } diff --git a/src/Parlot/Fluent/ParserExtensions.Cardinality.cs b/src/Parlot/Fluent/ParserExtensions.Cardinality.cs index 6902d39..b1f8d74 100644 --- a/src/Parlot/Fluent/ParserExtensions.Cardinality.cs +++ b/src/Parlot/Fluent/ParserExtensions.Cardinality.cs @@ -4,20 +4,18 @@ namespace Parlot.Fluent; public static partial class ParserExtensions { - public static Parser Optional(this Parser parser, T defaultValue) - => new ZeroOrOne(parser, defaultValue); - public static Parser> OneOrMany(this Parser parser) => new OneOrMany(parser); public static Parser> ZeroOrMany(this Parser parser) => new ZeroOrMany(parser); -// TODO: Create Optional public static Parser ZeroOrOne(this Parser parser, T defaultValue) => new ZeroOrOne(parser, defaultValue); public static Parser ZeroOrOne(this Parser parser) => new ZeroOrOne(parser, default!); + public static Parser> Optional(this Parser parser) + => new Optional(parser); } diff --git a/test/Parlot.Tests/CompileTests.cs b/test/Parlot.Tests/CompileTests.cs index d9f939e..86a89fc 100644 --- a/test/Parlot.Tests/CompileTests.cs +++ b/test/Parlot.Tests/CompileTests.cs @@ -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; @@ -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() { @@ -409,7 +419,7 @@ public void ShouldCompileDiscard() Assert.False(Terms.Decimal().Discard(true).Compile().TryParse("abc", out _)); #pragma warning restore CS0618 // Type or member is obsolete - Assert.True(Terms.Decimal().Then().Compile().TryParse("123", out var t1) && t1 == false); + Assert.True(Terms.Decimal().Then().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 _)); } diff --git a/test/Parlot.Tests/FluentTests.cs b/test/Parlot.Tests/FluentTests.cs index 63dcd2a..5990d53 100644 --- a/test/Parlot.Tests/FluentTests.cs +++ b/test/Parlot.Tests/FluentTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Numerics; using Xunit; @@ -990,8 +991,8 @@ public void OptionalShouldSucceed() { var parser = Terms.Text("hello").Optional(); - Assert.Equal("hello", parser.Parse(" hello world hello")); - Assert.Null(parser.Parse(" foo")); + Assert.Equal("hello", parser.Parse(" hello world hello").FirstOrDefault()); + Assert.Null(parser.Parse(" foo").FirstOrDefault()); } [Fact]