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
4 changes: 2 additions & 2 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,11 @@ It is not necessary to wrap `Terms` parsers with `SkipWhitespace()` as they alre

### `Optional()` usage

Use the `Optional()` combinator to make a parser optional. If the parser doesn't match it will still succeed, the result will be an empty list.
Use the `Optional()` combinator to make a parser optional. If will always return an instance of `Option<T>` regardless of whether the parser matches or not. Use `HasValue` to check if the parser was successful. Or use `OrSome()` to provide a default value when the parser does not match.

```c#
// Parse an integer or return -1 if not present
var optionalParser = Terms.Integer().Optional().Then(x => x.Count > 0 ? x[0] : -1);
var optionalParser = Terms.Integer().Optional().Then(x => x.HasValue ? x.Value : -1);
```

## File Organization
Expand Down
23 changes: 7 additions & 16 deletions docs/parsers.md
Original file line number Diff line number Diff line change
Expand Up @@ -480,32 +480,21 @@ 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()`.
Makes an existing parser optional by always returning an `Option<T>` result. It is then easy to know if the parser was successful or not by using the `HasValue` property.

```c#
static Parser<IReadOnlyList<T>> Optional<T>(this Parser<T> parser)
static Parser<Option<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
parser.Parse("hello"); // HasValue -> true
parser.Parse(""); // HasValue -> false
```

Result:

```
["hello"]
[]
"hello"
null
```
Use the `OrSome<T>()` method to provide a default value if the `Option<T>` instance has no value.

### ZeroOrMany

Expand Down Expand Up @@ -792,6 +781,8 @@ Result:
(123, "years")
```

NB: This is similar to using `Optional()` since the result is always successful, but `Else()` returns a value. Use `Optional()` if you need to know if the parser was successful or not, and `Else()` if you only care about having a value as a result.

### ThenElse

Converts the result of a parser, or returns a value if it didn't succeed. This parser always succeeds.
Expand Down
16 changes: 9 additions & 7 deletions src/Parlot/Fluent/Optional.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using Parlot.Compilation;
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;

namespace Parlot.Fluent;

Expand All @@ -11,23 +11,25 @@ namespace Parlot.Fluent;
/// <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
public sealed class Optional<T> : Parser<Option<T>>, ICompilable
{
private static readonly ConstructorInfo _optionConstructor = typeof(Option<T>).GetConstructor([typeof(T)])!;

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)
public override bool Parse(ParseContext context, ref ParseResult<Option<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] : []);
result.Set(parsed.Start, parsed.End, success ? new Option<T>(parsed.Value) : new Option<T>());

// Optional always succeeds
context.ExitParser(this);
Expand All @@ -36,7 +38,7 @@ public override bool Parse(ParseContext context, ref ParseResult<IReadOnlyList<T

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

// T value = _defaultValue;
//
Expand All @@ -55,8 +57,8 @@ public CompilationResult Compile(CompilationContext context)
? 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[])))
Expression.Assign(result.Value, Expression.New(_optionConstructor, parserCompileResult.Value)),
Expression.Assign(result.Value, Expression.Constant(new Option<T>(), typeof(Option<T>)))
)
)
);
Expand Down
4 changes: 3 additions & 1 deletion src/Parlot/Fluent/ParserExtensions.Cardinality.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.Collections.Generic;

using Parlot;

namespace Parlot.Fluent;

public static partial class ParserExtensions
Expand All @@ -16,6 +18,6 @@ public static Parser<T> ZeroOrOne<T>(this Parser<T> parser, T 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)
public static Parser<Option<T>> Optional<T>(this Parser<T> parser)
=> new Optional<T>(parser);
}
55 changes: 55 additions & 0 deletions src/Parlot/Option.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
namespace Parlot;

/// <summary>
/// Represents the result of an optional parser invocation.
/// </summary>
/// <typeparam name="T">The type of the wrapped value.</typeparam>
public readonly struct Option<T>
{
private readonly bool _hasValue;
private readonly T _value;

private Option(bool hasValue, T value)
{
_hasValue = hasValue;
_value = value;
}

/// <summary>
/// Creates an <see cref="Option{T}"/> that wraps a value.
/// </summary>
public Option(T value)
{
_hasValue = true;
_value = value;
}

/// <summary>
/// Gets a value indicating whether the optional value has been set.
/// </summary>
public bool HasValue => _hasValue;

/// <summary>
/// Gets the wrapped value. When <see cref="HasValue"/> is <see langword="false"/>, the default value of <typeparamref name="T"/> is returned.
/// </summary>
public T Value => _value;

/// <summary>
/// Tries to get the wrapped value.
/// </summary>
/// <param name="value">The wrapped value when set; otherwise the default value of <typeparamref name="T"/>.</param>
/// <returns><see langword="true"/> when the value is set; otherwise <see langword="false"/>.</returns>
public bool TryGetValue(out T value)
{
value = _value;
return _hasValue;
}

/// <summary>
/// Gets the wrapped value or the specified default value.
/// </summary>
/// <param name="defaultValue"></param>
/// <returns></returns>
public T OrSome(T? defaultValue)
=> _hasValue ? _value : defaultValue!;
}
4 changes: 2 additions & 2 deletions src/Parlot/TextSpan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ public TextSpan(string? buffer, int offset, int count)

public ReadOnlySpan<char> Span => Buffer == null ? [] : Buffer.AsSpan(Offset, Length);

public override string? ToString()
public override string ToString()
{
return Buffer?.Substring(Offset, Length);
return Buffer?.Substring(Offset, Length) ?? "";
}

public bool Equals(string? other)
Expand Down
10 changes: 6 additions & 4 deletions src/Samples/Sql/SqlAst.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public CommonTableExpression(string name, IReadOnlyList<UnionStatement> query, I

public class SelectStatement : ISqlNode
{
public SelectRestriction? Restriction { get; }
public SelectRestriction Restriction { get; }
public IReadOnlyList<ColumnItem> ColumnItemList { get; }
public FromClause? FromClause { get; }
public WhereClause? WhereClause { get; }
Expand All @@ -113,7 +113,7 @@ public SelectStatement(
OffsetClause? offsetClause = null)
{
ColumnItemList = columnItemList;
Restriction = restriction;
Restriction = restriction ?? SelectRestriction.NotSpecified;
FromClause = fromClause;
WhereClause = whereClause;
GroupByClause = groupByClause;
Expand All @@ -126,6 +126,7 @@ public SelectStatement(

public enum SelectRestriction
{
NotSpecified,
All,
Distinct
}
Expand Down Expand Up @@ -274,9 +275,9 @@ public OrderByClause(IReadOnlyList<OrderByItem> items)
public class OrderByItem : ISqlNode
{
public Identifier Identifier { get; }
public OrderDirection? Direction { get; }
public OrderDirection Direction { get; }

public OrderByItem(Identifier identifier, OrderDirection? direction = null)
public OrderByItem(Identifier identifier, OrderDirection direction)
{
Identifier = identifier;
Direction = direction;
Expand All @@ -285,6 +286,7 @@ public OrderByItem(Identifier identifier, OrderDirection? direction = null)

public enum OrderDirection
{
NotSpecified,
Asc,
Desc
}
Expand Down
Loading