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
12 changes: 9 additions & 3 deletions Parlot.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.1.31903.286
# Visual Studio Version 18
VisualStudioVersion = 18.0.10912.84 main
MinimumVisualStudioVersion = 15.0.26124.0
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0D1E6480-3C81-4951-8F44-BF74398BA8D4}"
EndProject
Expand All @@ -17,15 +17,21 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.github\workflows\build.yml = .github\workflows\build.yml
Directory.Build.props = Directory.Build.props
Directory.Packages.props = Directory.Packages.props
NuGet.config = NuGet.config
.github\workflows\publish.yml = .github\workflows\publish.yml
README.md = README.md
Directory.Build.props = Directory.Build.props
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Samples", "src\Samples\Samples.csproj", "{B9A796FE-4BEB-499A-B506-25F20C749527}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
ProjectSection(SolutionItems) = preProject
docs\parsers.md = docs\parsers.md
docs\writing.md = docs\writing.md
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down
38 changes: 36 additions & 2 deletions docs/parsers.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ Usage:

```c#
var input = "hello world";
var parser = Terms.Char("h");
var parser = Terms.Char('h');
```

Result:
Expand Down Expand Up @@ -161,7 +161,7 @@ Result:

### String

Matches a quoted string literal, optionally use single or double enclosing quotes.
Matches a quoted string literal with escape sequences. Use this parser to parse strings from a programming language.

```c#
Parser<TextSpan> String(StringLiteralQuotes quotes = StringLiteralQuotes.SingleOrDouble)
Expand Down Expand Up @@ -227,6 +227,12 @@ abab

Matches any chars from a list of chars.

```c#
Parser<TextSpan> AnyOf(string values, int minSize = 1, int maxSize = 0)
```

The following overloads are available when targeting .NET 8 or later and use vectorized parsing for better performance.

```c#
Parser<TextSpan> AnyOf(ReadOnlySpan<char> values, int minSize = 1, int maxSize = 0)
Parser<TextSpan> AnyOf(SearchValue<char> searchValues, int minSize = 1, int maxSize = 0)
Expand All @@ -245,6 +251,34 @@ Result:
abab
```

### NoneOf

Matches any other chars than the ones specified.

```c#
Parser<TextSpan> NoneOf(string values, int minSize = 1, int maxSize = 0)
```

The following overloads are available when targeting .NET 8 or later and use vectorized parsing for better performance.

```c#
Parser<TextSpan> NoneOf(ReadOnlySpan<char> values, int minSize = 1, int maxSize = 0)
Parser<TextSpan> NoneOf(SearchValue<char> searchValues, int minSize = 1, int maxSize = 0)
```

Usage:

```c#
var input = "ababcad";
var parser = Terms.NoneOf("cd");
```

Result:

```
abab
```

## Combining parsers

### Or
Expand Down
6 changes: 4 additions & 2 deletions src/Parlot/Fluent/ListOfCharsLiteral.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ internal sealed class ListOfChars : Parser<TextSpan>, ISeekable
private readonly CharMap<object> _map = new();
private readonly int _minSize;
private readonly int _maxSize;
private readonly bool _negate;
private readonly bool _hasNewLine;

public bool CanSeek { get; }
Expand All @@ -17,7 +18,7 @@ internal sealed class ListOfChars : Parser<TextSpan>, ISeekable

public bool SkipWhitespace { get; }

public ListOfChars(string values, int minSize = 1, int maxSize = 0)
public ListOfChars(string values, int minSize = 1, int maxSize = 0, bool negate = false)
{
foreach (var c in values)
{
Expand All @@ -37,6 +38,7 @@ public ListOfChars(string values, int minSize = 1, int maxSize = 0)

_minSize = minSize;
_maxSize = maxSize;
_negate = negate;
}

public override bool Parse(ParseContext context, ref ParseResult<TextSpan> result)
Expand All @@ -52,7 +54,7 @@ public override bool Parse(ParseContext context, ref ParseResult<TextSpan> resul

for (var i = 0; i < maxLength; i++)
{
if (_map[span[i]] == null)
if (_map[span[i]] == null != _negate)
{
break;
}
Expand Down
32 changes: 28 additions & 4 deletions src/Parlot/Fluent/Parsers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -224,25 +224,49 @@ public Parser<TextSpan> Identifier(Func<char, bool>? extraStart = null, Func<cha
/// Builds a parser that matches a list of chars.
/// </summary>
/// <param name="searchValues">The <see cref="SearchValues{T}"/> instance to match against each char.</param>
/// <param name="minSize">The minimum number of matches required. Defaults to 1.</param>
/// <param name="minSize">The minimum number of chars required. Defaults to 1.</param>
/// <param name="maxSize">When the parser reaches the maximum number of matches it returns <see langword="True"/>. Defaults to 0, i.e. no maximum size.</param>
public Parser<TextSpan> AnyOf(SearchValues<char> searchValues, int minSize = 1, int maxSize = 0) => new SearchValuesCharLiteral(searchValues, minSize, maxSize);

/// <summary>
/// Builds a parser that matches a list of chars.
/// </summary>
/// <param name="values">The set of char to match.</param>
/// <param name="minSize">The minimum number of matches required. Defaults to 1.</param>
/// <param name="values">The set of chars to match.</param>
/// <param name="minSize">The minimum number of chars required. Defaults to 1.</param>
/// <param name="maxSize">When the parser reaches the maximum number of matches it returns <see langword="True"/>. Defaults to 0, i.e. no maximum size.</param>
public Parser<TextSpan> AnyOf(ReadOnlySpan<char> values, int minSize = 1, int maxSize = 0) => new SearchValuesCharLiteral(values, minSize, maxSize);

/// <summary>
/// Builds a parser that matches anything but a list of chars.
/// </summary>
/// <param name="searchValues">The <see cref="SearchValues{T}"/> instance to ignore against each char.</param>
/// <param name="minSize">The minimum number of chars required. Defaults to 1.</param>
/// <param name="maxSize">When the parser reaches the maximum number of chars it returns <see langword="True"/>. Defaults to 0, i.e. no maximum size.</param>
public Parser<TextSpan> NoneOf(SearchValues<char> searchValues, int minSize = 1, int maxSize = 0) => new SearchValuesCharLiteral(searchValues, minSize, maxSize, negate: true);

/// <summary>
/// Builds a parser that matches anything but a list of chars.
/// </summary>
/// <param name="values">The set of chars not to match.</param>
/// <param name="minSize">The minimum number of chars required. Defaults to 1.</param>
/// <param name="maxSize">When the parser reaches the maximum number of chars it returns <see langword="True"/>. Defaults to 0, i.e. no maximum size.</param>
public Parser<TextSpan> NoneOf(ReadOnlySpan<char> values, int minSize = 1, int maxSize = 0) => new SearchValuesCharLiteral(values, minSize, maxSize, negate: true);
#else
/// <summary>
/// Builds a parser that matches a list of chars.
/// </summary>
/// <param name="values">The set of char to match.</param>
/// <param name="values">The set of chars to match.</param>
/// <param name="minSize">The minimum number of matches required. Defaults to 1.</param>
/// <param name="maxSize">When the parser reaches the maximum number of matches it returns <see langword="True"/>. Defaults to 0, i.e. no maximum size.</param>
public Parser<TextSpan> AnyOf(string values, int minSize = 1, int maxSize = 0) => new ListOfChars(values, minSize, maxSize);

/// <summary>
/// Builds a parser that matches anything but a list of chars.
/// </summary>
/// <param name="values">The set of chars not to match.</param>
/// <param name="minSize">The minimum number of required chars. Defaults to 1.</param>
/// <param name="maxSize">When the parser reaches the maximum number of chars it returns <see langword="True"/>. Defaults to 0, i.e. no maximum size.</param>
public Parser<TextSpan> NoneOf(string values, int minSize = 1, int maxSize = 0) => new ListOfChars(values, minSize, maxSize, negate: true);
#endif
}

Expand Down
9 changes: 6 additions & 3 deletions src/Parlot/Fluent/SearchValuesCharLiteral.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,28 @@ internal sealed class SearchValuesCharLiteral : Parser<TextSpan>, ISeekable
private readonly SearchValues<char> _searchValues;
private readonly int _minSize;
private readonly int _maxSize;
private readonly bool _negate;

public bool CanSeek { get; }

public char[] ExpectedChars { get; } = [];

public bool SkipWhitespace { get; }

public SearchValuesCharLiteral(SearchValues<char> searchValues, int minSize = 1, int maxSize = 0)
public SearchValuesCharLiteral(SearchValues<char> searchValues, int minSize = 1, int maxSize = 0, bool negate = false)
{
_searchValues = searchValues ?? throw new ArgumentNullException(nameof(searchValues));
_minSize = minSize;
_maxSize = maxSize;
_negate = negate;
}

public SearchValuesCharLiteral(ReadOnlySpan<char> searchValues, int minSize = 1, int maxSize = 0)
public SearchValuesCharLiteral(ReadOnlySpan<char> searchValues, int minSize = 1, int maxSize = 0, bool negate = false)
{
_searchValues = SearchValues.Create(searchValues);
_minSize = minSize;
_maxSize = maxSize;
_negate = negate;

if (minSize > 0)
{
Expand All @@ -49,7 +52,7 @@ public override bool Parse(ParseContext context, ref ParseResult<TextSpan> resul
}

// First char not matching the searched values
var index = span.IndexOfAnyExcept(_searchValues);
var index = _negate ? span.IndexOfAny(_searchValues) : span.IndexOfAnyExcept(_searchValues);

var size = 0;

Expand Down
61 changes: 59 additions & 2 deletions test/Parlot.Tests/FluentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1143,7 +1143,7 @@ public void NumberShouldNotParseOverflow(string source)
[InlineData("ba", "ab", "ab")]
[InlineData("abc", "aaabbbccc", "aaabbbccc")]
[InlineData("a", "aaab", "aaa")]
[InlineData("aa", "aaaab", "aaaa")]
[InlineData("aa", "aaaaab", "aaaaa")]
public void AnyOfShouldMatch(string chars, string source, string expected)
{
Assert.Equal(expected, Literals.AnyOf(chars).Parse(source).ToString());
Expand All @@ -1153,7 +1153,7 @@ public void AnyOfShouldMatch(string chars, string source, string expected)
[InlineData("a", "b")]
[InlineData("a", "bbb")]
[InlineData("abc", "dabc")]
public void AnyOfShouldNotMAtch(string chars, string source)
public void AnyOfShouldNotMatch(string chars, string source)
{
Assert.False(Literals.AnyOf(chars).TryParse(source, out var _));
}
Expand Down Expand Up @@ -1192,6 +1192,63 @@ public void AnyOfShouldResetPositionWhenFalse()
.TryParse("aaaZZ", out _));
}

[Theory]
[InlineData("a", "b", "b")]
[InlineData("a", "bb", "bb")]
[InlineData("a", "bbbb", "bbbb")]
[InlineData("ab", "cd", "cd")]
[InlineData("ba", "cd", "cd")]
[InlineData("abc", "dddeeefff", "dddeeefff")]
[InlineData("a", "bbba", "bbb")]
[InlineData("aa", "bbbbba", "bbbbb")]
public void NoneOfShouldMatch(string chars, string source, string expected)
{
Assert.Equal(expected, Literals.NoneOf(chars).Parse(source).ToString());
}

[Theory]
[InlineData("a", "a")]
[InlineData("a", "aaa")]
[InlineData("abc", "beee")]
public void NoneOfShouldNotMatch(string chars, string source)
{
Assert.False(Literals.NoneOf(chars).TryParse(source, out var _));
}

[Fact]
public void NoneOfShouldRespectSizeConstraints()
{
Assert.True(Literals.NoneOf("a", minSize: 0).TryParse("bbb", out var r) && r.ToString() == "bbb");
Assert.True(Literals.NoneOf("a", minSize: 0).TryParse("aaa", out _));
Assert.False(Literals.NoneOf("a", minSize: 4).TryParse("bbb", out _));
Assert.False(Literals.NoneOf("a", minSize: 2).TryParse("ba", out _));
Assert.False(Literals.NoneOf("a", minSize: 3).TryParse("ba", out _));
Assert.Equal("bb", Literals.NoneOf("a", minSize: 2, maxSize: 2).Parse("bb"));
Assert.Equal("bb", Literals.NoneOf("a", minSize: 2, maxSize: 3).Parse("bb"));
Assert.Equal("b", Literals.NoneOf("a", maxSize: 1).Parse("bb"));
Assert.Equal("bbbb", Literals.NoneOf("a", minSize: 2, maxSize: 4).Parse("bbbbb"));
Assert.False(Literals.NoneOf("a", minSize: 2, maxSize: 2).TryParse("b", out _));
}

[Fact]
public void NoneOfShouldNotBeSeekableIfOptional()
{
var parser = Literals.NoneOf("a", minSize: 0) as ISeekable;
Assert.False(parser.CanSeek);
}

[Fact]
public void NoneOfShouldResetPositionWhenFalse()
{
Assert.False(Literals.NoneOf("Z", minSize: 3)
.And(Literals.NoneOf("a"))
.TryParse("aaZZ", out _));

Assert.True(Literals.NoneOf("Z", minSize: 3)
.And(Literals.NoneOf("a"))
.TryParse("aaaZZ", out _));
}

[Fact]
public void ElseErrorShouldNotBeSeekable()
{
Expand Down