(items);
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/test/Superpower.Tests/Combinators/TextCombinatorTests.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Parsers;
2 | using Superpower.Tests.Support;
3 | using Xunit;
4 |
5 | namespace Superpower.Tests.Combinators;
6 |
7 | public class TextCombinatorTests
8 | {
9 | [Fact]
10 | public void TextSucceedsWithAnyCharArrayInput()
11 | {
12 | AssertParser.SucceedsWith(Character.AnyChar.Many().Text(), "ab", "ab");
13 | }
14 |
15 | [Fact]
16 | public void TextSucceedsWithTextSpanInput()
17 | {
18 | AssertParser.SucceedsWith(Span.Length(2).Text(), "ab", "ab");
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | version: '{build}'
2 | skip_tags: true
3 | image: Visual Studio 2022
4 | build_script:
5 | - ps: ./Build.ps1
6 | test: off
7 | artifacts:
8 | - path: artifacts/Superpower.*.nupkg
9 | deploy:
10 | - provider: NuGet
11 | api_key:
12 | secure: wW4TJY85P9BSN53XNfB0IE2WqskPu0RdsWelB1tQco1wql3g8ov5yhmk5v8dXy81
13 | skip_symbols: true
14 | on:
15 | branch: /^(main|dev)$/
16 | - provider: GitHub
17 | auth_token:
18 | secure: hX+cZmW+9BCXy7vyH8myWsYdtQHyzzil9K5yvjJv7dK9XmyrGYYDj/DPzMqsXSjo
19 | artifact: /Superpower.*\.nupkg/
20 | tag: v$(appveyor_build_version)
21 | on:
22 | branch: main
23 |
--------------------------------------------------------------------------------
/test/Superpower.Tests/Combinators/ManyDelimitedByCombinatorTests.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Parsers;
2 | using Superpower.Tests.Support;
3 | using Xunit;
4 |
5 | namespace Superpower.Tests.Combinators
6 | {
7 | public class ManyDelimitedByCombinatorTests
8 | {
9 | [Fact]
10 | public void AnEndDelimiterCanBeSpecified()
11 | {
12 | AssertParser.SucceedsWith(
13 | Token.EqualTo('a').Value('a')
14 | .ManyDelimitedBy(Token.EqualTo('b'), end: Token.EqualTo('c')),
15 | "ababac",
16 | new[] {'a', 'a', 'a'});
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/test/Superpower.Tests/ComplexTokenScenario/SExpressionXToken.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Display;
2 |
3 | namespace Superpower.Tests.ComplexTokenScenario
4 | {
5 | [Token(Category = "S-Expression Token")]
6 | class SExpressionXToken
7 | {
8 | public SExpressionXToken(SExpressionType type)
9 | {
10 | Type = type;
11 | }
12 | public SExpressionXToken(int number)
13 | {
14 | Type = SExpressionType.Number;
15 | Number = number;
16 | }
17 | public SExpressionType Type { get; set; }
18 | public int Number { get; set; }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/sample/JsonParser/test.json:
--------------------------------------------------------------------------------
1 | {
2 | "glossary": {
3 | "title": "example glossary",
4 | "GlossDiv": {
5 | "title": "S",
6 | "GlossList": {
7 | "GlossEntry": {
8 | "ID": "SGML",
9 | "SortAs": "SGML",
10 | "GlossTerm": "Standard Generalized Markup Language",
11 | "Acronym": "SGML",
12 | "Abbrev": "ISO 8879:1986",
13 | "GlossDef": {
14 | "para": "A meta-markup language, used to create markup languages such as DocBook.",
15 | "GlossSeeAlso": [ "GML", "XML" ]
16 | },
17 | "GlossSee": "markup"
18 | }
19 | }
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/sample/IntCalc/ArithmeticExpressionToken.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Display;
2 | using Superpower.Model;
3 |
4 | namespace IntCalc
5 | {
6 | enum ArithmeticExpressionToken
7 | {
8 | None,
9 |
10 | Number,
11 |
12 | [Token(Category="operator", Example = "+")]
13 | Plus,
14 |
15 | [Token(Category = "operator", Example = "-")]
16 | Minus,
17 |
18 | [Token(Category = "operator", Example = "*")]
19 | Times,
20 |
21 | [Token(Category = "operator", Example = "/")]
22 | Divide,
23 |
24 | [Token(Example = "(")]
25 | LParen,
26 |
27 | [Token(Example = ")")]
28 | RParen
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/results/ArithmeticExpressionBenchmark-report-github.md:
--------------------------------------------------------------------------------
1 | ```ini
2 |
3 | Host Process Environment Information:
4 | BenchmarkDotNet.Core=v0.9.9.0
5 | OS=Windows
6 | Processor=?, ProcessorCount=8
7 | Frequency=2533306 ticks, Resolution=394.7411 ns, Timer=TSC
8 | CLR=CORE, Arch=64-bit ? [RyuJIT]
9 | GC=Concurrent Workstation
10 | dotnet cli version: 1.0.0-preview2-003121
11 |
12 | Type=ArithmeticExpressionBenchmark Mode=Throughput
13 |
14 | ```
15 | Method | Median | StdDev | Scaled | Scaled-SD |
16 | ---------------- |------------ |---------- |------- |---------- |
17 | Sprache | 256.6565 us | 3.3398 us | 1.00 | 0.00 |
18 | SuperpowerToken | 79.7273 us | 1.3012 us | 0.31 | 0.01 |
19 |
--------------------------------------------------------------------------------
/test/Superpower.Tests/Combinators/MessageCombinatorTests.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Parsers;
2 | using Superpower.Tests.Support;
3 | using Xunit;
4 |
5 | namespace Superpower.Tests.Combinators
6 | {
7 | public class MessageCombinatorTests
8 | {
9 | [Fact]
10 | public void FailedParsingProducesMessage()
11 | {
12 | AssertParser.FailsWithMessage(Character.EqualTo('a').Message("hello"), "b", "Syntax error (line 1, column 1): hello.");
13 | }
14 |
15 | [Fact]
16 | public void TokenFailedParsingProducesMessage()
17 | {
18 | AssertParser.FailsWithMessage(Token.EqualTo('a').Message("hello"), "b", "Syntax error (line 1, column 1): hello.");
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/test/Superpower.Benchmarks/ArithmeticExpressionScenario/ArithmeticExpressionToken.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Display;
2 |
3 | namespace Superpower.Benchmarks.ArithmeticExpressionScenario
4 | {
5 | public enum ArithmeticExpressionToken
6 | {
7 | None,
8 |
9 | Number,
10 | [Token(Category = "operator", Example = "+")]
11 | Plus,
12 |
13 | [Token(Category = "operator", Example = "-")]
14 | Minus,
15 |
16 | [Token(Category = "operator", Example = "*")]
17 | Times,
18 |
19 | [Token(Category = "operator", Example = "-")]
20 | Divide,
21 |
22 | [Token(Example = "(")]
23 | LParen,
24 |
25 | [Token(Example = ")")]
26 | RParen
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/results/ArithmeticExpressionBenchmark-report.html:
--------------------------------------------------------------------------------
1 |
2 | Host Process Environment Information:
3 | BenchmarkDotNet.Core=v0.9.9.0
4 | OS=Windows
5 | Processor=?, ProcessorCount=8
6 | Frequency=2533306 ticks, Resolution=394.7411 ns, Timer=TSC
7 | CLR=CORE, Arch=64-bit ? [RyuJIT]
8 | GC=Concurrent Workstation
9 | dotnet cli version: 1.0.0-preview2-003121
10 |
11 | Type=ArithmeticExpressionBenchmark Mode=Throughput
12 |
13 |
14 |
15 | | Method | Median | StdDev | Scaled | Scaled-SD |
16 |
|---|
| Sprache | 256.6565 us | 3.3398 us | 1.00 | 0.00 |
17 |
| SuperpowerToken | 79.7273 us | 1.3012 us | 0.31 | 0.01 |
18 |
19 |
--------------------------------------------------------------------------------
/test/Superpower.Tests/Combinators/NamedCombinatorTests.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Parsers;
2 | using Superpower.Tests.Support;
3 | using Xunit;
4 |
5 | namespace Superpower.Tests.Combinators
6 | {
7 | public class NamedCombinatorTests
8 | {
9 | [Fact]
10 | public void FailedParsingProducesMessage()
11 | {
12 | AssertParser.FailsWithMessage(Character.EqualTo('a').Named("hello"), "b", "Syntax error (line 1, column 1): unexpected `b`, expected hello.");
13 | }
14 |
15 | [Fact]
16 | public void TokenFailedParsingProducesMessage()
17 | {
18 | AssertParser.FailsWithMessage(Token.EqualTo('a').Named("hello"), "b", "Syntax error (line 1, column 1): unexpected b `b`, expected hello.");
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/results/NumberListBenchmark-report.csv:
--------------------------------------------------------------------------------
1 | Type;Method;Mode;Platform;Jit;Toolchain;Runtime;GcMode;LaunchCount;WarmupCount;TargetCount;Affinity;Median;StdDev;Scaled;Scaled-SD
2 | NumberListBenchmark;StringSplitAndInt32Parse;Throughput;Host;Host;Host;Host;Host;Auto;Auto;Auto;Auto;138.7217 us;5.7115 us;1.00;0.00
3 | NumberListBenchmark;SpracheSimple;Throughput;Host;Host;Host;Host;Host;Auto;Auto;Auto;Auto;2,740.3251 us;11.9340 us;19.40;0.72
4 | NumberListBenchmark;SuperpowerSimple;Throughput;Host;Host;Host;Host;Host;Auto;Auto;Auto;Auto;937.1593 us;43.9867 us;6.64;0.39
5 | NumberListBenchmark;SuperpowerChar;Throughput;Host;Host;Host;Host;Host;Auto;Auto;Auto;Auto;691.5509 us;9.9246 us;4.89;0.19
6 | NumberListBenchmark;SuperpowerToken;Throughput;Host;Host;Host;Host;Host;Auto;Auto;Auto;Auto;1,189.8622 us;15.9538 us;8.46;0.33
7 |
--------------------------------------------------------------------------------
/src/Superpower/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 |
4 | // This number will remain fixed at 1.0 permanently.
5 | [assembly: AssemblyVersion("1.0.0.0")]
6 |
7 | [assembly: InternalsVisibleTo("Superpower.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1849e9a81844f" +
8 | "189acadef7aebbea576c0f3a8017fca0ff035cdd56b8a0ddff7d1be650709ba3f0a581fbe2874a" +
9 | "68eb420251507e46abc97d6a5f309ff66c8f06d78b50e1273c851b3f98d2cb6e26ab3e6bedee34" +
10 | "a3725c8ead2ad01803d53594755fe8bbd85810270f4346e68057ffdfd7fe48e190c71e1c9d2000" +
11 | "8449eae0")]
12 |
--------------------------------------------------------------------------------
/test/Superpower.Tests/Parsers/QuotedStringTests.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Model;
2 | using Superpower.Parsers;
3 | using Xunit;
4 |
5 | namespace Superpower.Tests.Parsers
6 | {
7 | public class QuotedStringTests
8 | {
9 | [Fact]
10 | public void SqlStyleStringsAreParsed()
11 | {
12 | var input = new TextSpan("'Hello, ''world''!'x");
13 | var parser = QuotedString.SqlStyle;
14 | var r = parser(input);
15 | Assert.Equal("Hello, 'world'!", r.Value);
16 | }
17 |
18 | [Fact]
19 | public void CStyleStringsAreParsed()
20 | {
21 | var input = new TextSpan("\"Hello, \\\"world\\\"!\"x");
22 | var parser = QuotedString.CStyle;
23 | var r = parser(input);
24 | Assert.Equal("Hello, \"world\"!", r.Value);
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/test/Superpower.Tests/Parsers/InstantTests.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Parsers;
2 | using Superpower.Tests.Support;
3 | using Xunit;
4 |
5 | namespace Superpower.Tests.Parsers
6 | {
7 | public class InstantTests
8 | {
9 | [Theory]
10 | [InlineData("0", false)]
11 | [InlineData("1910-10-28T03:04:05", true)]
12 | [InlineData("2020-10-28T03:04:05", true)]
13 | [InlineData("1910-10-28T03:04:05.6789", true)]
14 | [InlineData("1910-10-28T03:04:05Z", true)]
15 | [InlineData("1910-10-28T03:04:05+10:00", true)]
16 | [InlineData("1910-10-28T03:04:05-07:30", true)]
17 | // A number of cases allowed by the spec aren't yet covered, here.
18 | public void IsoDateTimesAreRecognized(string input, bool isMatch)
19 | {
20 | AssertParser.FitsTheory(Instant.Iso8601DateTime, input, isMatch);
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/test/Superpower.Tests/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyConfiguration("")]
9 | [assembly: AssemblyCompany("")]
10 | [assembly: AssemblyProduct("Superpower.Tests")]
11 | [assembly: AssemblyTrademark("")]
12 |
13 | // Setting ComVisible to false makes the types in this assembly not visible
14 | // to COM components. If you need to access a type in this assembly from
15 | // COM, set the ComVisible attribute to true on that type.
16 | [assembly: ComVisible(false)]
17 |
18 | // The following GUID is for the ID of the typelib if this project is exposed to COM
19 | [assembly: Guid("cd473266-4aed-4207-89fd-0b185239f1c7")]
20 |
--------------------------------------------------------------------------------
/test/Superpower.Tests/Model/TextSpanTest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Superpower.Model;
3 | using Xunit;
4 |
5 | namespace Superpower.Tests.Model
6 | {
7 | public class TextSpanTest
8 | {
9 | [Theory]
10 | [InlineData("hello", 0, 5, "hello")]
11 | [InlineData("hello", 1, 3, "ell")]
12 | [InlineData("hello", 2, 0, "")]
13 | [InlineData("The quick brown fox jumps over the lazy dog", 9, 7, " brown ")]
14 | public void AsReadOnlySpanWorks(string input, int start, int length, string expected)
15 | {
16 | var span = new TextSpan(input).Skip(start).First(length);
17 | var readOnlySpan = span.AsReadOnlySpan();
18 | Assert.Equal(expected, readOnlySpan.ToString());
19 | }
20 |
21 | [Fact]
22 | public void AsReadOnlySpanEnsureHasValue()
23 | {
24 | Assert.Throws(() => TextSpan.None.AsReadOnlySpan());
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/results/NumberListBenchmark-report-github.md:
--------------------------------------------------------------------------------
1 | ```ini
2 |
3 | Host Process Environment Information:
4 | BenchmarkDotNet.Core=v0.9.9.0
5 | OS=Windows
6 | Processor=?, ProcessorCount=8
7 | Frequency=2533306 ticks, Resolution=394.7411 ns, Timer=TSC
8 | CLR=CORE, Arch=64-bit ? [RyuJIT]
9 | GC=Concurrent Workstation
10 | dotnet cli version: 1.0.0-preview2-003121
11 |
12 | Type=NumberListBenchmark Mode=Throughput
13 |
14 | ```
15 | Method | Median | StdDev | Scaled | Scaled-SD |
16 | ------------------------- |-------------- |----------- |------- |---------- |
17 | StringSplitAndInt32Parse | 138.7217 us | 5.7115 us | 1.00 | 0.00 |
18 | SpracheSimple | 2,740.3251 us | 11.9340 us | 19.40 | 0.72 |
19 | SuperpowerSimple | 937.1593 us | 43.9867 us | 6.64 | 0.39 |
20 | SuperpowerChar | 691.5509 us | 9.9246 us | 4.89 | 0.19 |
21 | SuperpowerToken | 1,189.8622 us | 15.9538 us | 8.46 | 0.33 |
22 |
--------------------------------------------------------------------------------
/test/Superpower.Tests/Support/PreviousCheckingTokenizer.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using Superpower.Model;
3 | using Xunit;
4 |
5 | namespace Superpower.Tests.Support
6 | {
7 | public class PreviousCheckingTokenizer : Tokenizer
8 | {
9 | protected override IEnumerable> Tokenize(TextSpan span, TokenizationState state)
10 | {
11 | Assert.NotNull(state);
12 | Assert.Null(state.Previous);
13 | var next = span.ConsumeChar();
14 | yield return Result.Value(0, next.Location, next.Remainder);
15 |
16 | for (var i = 1; i < span.Length; ++i)
17 | {
18 | Assert.NotNull(state.Previous);
19 | Assert.Equal(i - 1, state.Previous!.Value.Kind);
20 | next = next.Remainder.ConsumeChar();
21 | yield return Result.Value(i, next.Location, next.Remainder);
22 | }
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "build",
6 | "command": "dotnet",
7 | "type": "process",
8 | "group": {
9 | "kind": "build",
10 | "isDefault": true
11 | },
12 | "args": [
13 | "build",
14 | "/p:GenerateFullPaths=true"
15 | ],
16 | "problemMatcher": "$msCompile"
17 | },
18 | {
19 | "label": "test",
20 | "command": "dotnet",
21 | "type": "process",
22 | "group": {
23 | "kind": "test",
24 | "isDefault": true
25 | },
26 | "args": [
27 | "test",
28 | "${workspaceFolder}/test/Superpower.Tests/Superpower.Tests.csproj",
29 | "/p:GenerateFullPaths=true"
30 | ],
31 | "problemMatcher": "$msCompile"
32 | }
33 | ]
34 | }
--------------------------------------------------------------------------------
/test/Superpower.Tests/Combinators/WhereCombinatorTests.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Parsers;
2 | using Superpower.Tests.Support;
3 | using Xunit;
4 |
5 | namespace Superpower.Tests.Combinators
6 | {
7 | public class WhereCombinatorTests
8 | {
9 | [Fact]
10 | public void WhereFailsIfPrecedingParserFails()
11 | {
12 | AssertParser.Fails(Character.EqualTo('a').Where(_ => true), "b");
13 | }
14 |
15 | [Fact]
16 | public void WhereSucceedsWhenPredicateMatches()
17 | {
18 | AssertParser.SucceedsWith(Character.EqualTo('a').Where(a => a == 'a'), "a", 'a');
19 | }
20 |
21 | [Fact]
22 | public void WhereFailsWhenPredicateDoesNotMatch()
23 | {
24 | AssertParser.FailsWithMessage(
25 | Character.EqualTo('a').Where(a => a != 'a', "character should be an A"),
26 | "a",
27 | "Syntax error (line 1, column 1): character should be an A.");
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/test/Superpower.Tests/ArithmeticExpressionScenario/ArithmeticExpressionToken.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Display;
2 | using Superpower.Model;
3 |
4 | namespace Superpower.Tests.ArithmeticExpressionScenario
5 | {
6 | public enum ArithmeticExpressionToken
7 | {
8 | None,
9 |
10 | Number,
11 |
12 | [Token(Category = "operator", Example = "+")]
13 | Plus,
14 |
15 | [Token(Category = "operator", Example = "-")]
16 | Minus,
17 |
18 | [Token(Category = "operator", Example = "*")]
19 | Times,
20 |
21 | [Token(Category = "operator", Example = "-")]
22 | Divide,
23 |
24 | [Token(Example = "(")]
25 | LParen,
26 |
27 | [Token(Example = ")")]
28 | RParen,
29 |
30 | [Token(Category = "keyword", Example = "zero")]
31 | Zero,
32 |
33 | [Token(Category = "keyword", Description = "literal one")]
34 | One,
35 |
36 | [Token(Description = "literal two")]
37 | Two
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/test/Superpower.Benchmarks/Superpower.Benchmarks.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0;net8.0
5 | true
6 | enable
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | all
17 | runtime; build; native; contentfiles; analyzers; buildtransitive
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/test/Superpower.Tests/Combinators/ValueCombinatorTests.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Parsers;
2 | using Superpower.Tests.Support;
3 | using Xunit;
4 |
5 | namespace Superpower.Tests.Combinators
6 | {
7 | public class ValueCombinatorTests
8 | {
9 | [Fact]
10 | public void ValueFailsIfPrecedingParserFails()
11 | {
12 | AssertParser.Fails(Character.EqualTo('a').Value(42), "b");
13 | }
14 |
15 | [Fact]
16 | public void ValueTransformsPrecedingResult()
17 | {
18 | AssertParser.SucceedsWith(Character.EqualTo('a').Value(42), "a", 42);
19 | }
20 |
21 | [Fact]
22 | public void TokenValueFailsIfPrecedingParserFails()
23 | {
24 | AssertParser.Fails(Character.EqualTo('a').Value(42), "b");
25 | }
26 |
27 | [Fact]
28 | public void TokenValueTransformsPrecedingResult()
29 | {
30 | AssertParser.SucceedsWith(Token.EqualTo('a').Value(42), "a", 42);
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/test/Superpower.Tests/Combinators/SelectCombinatorTests.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Parsers;
2 | using Superpower.Tests.Support;
3 | using Xunit;
4 |
5 | namespace Superpower.Tests.Combinators
6 | {
7 | public class SelectCombinatorTests
8 | {
9 | [Fact]
10 | public void SelectFailsIfPrecedingParserFails()
11 | {
12 | AssertParser.Fails(Character.EqualTo('a').Select(_ => 42), "b");
13 | }
14 |
15 | [Fact]
16 | public void SelectTransformsPrecedingResult()
17 | {
18 | AssertParser.SucceedsWith(Character.EqualTo('a').Select(_ => 42), "a", 42);
19 | }
20 |
21 | [Fact]
22 | public void TokenSelectFailsIfPrecedingParserFails()
23 | {
24 | AssertParser.Fails(Character.EqualTo('a').Select(_ => 42), "b");
25 | }
26 |
27 | [Fact]
28 | public void TokenSelectTransformsPrecedingResult()
29 | {
30 | AssertParser.SucceedsWith(Token.EqualTo('a').Select(_ => 42), "a", 42);
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Superpower/Model/Unit.cs:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | namespace Superpower.Model
16 | {
17 | ///
18 | /// A structure with no information.
19 | ///
20 | public struct Unit
21 | {
22 | ///
23 | /// The singleton value of the struct, with no value.
24 | ///
25 | public static Unit Value { get; } = default;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/results/NumberListBenchmark-report.html:
--------------------------------------------------------------------------------
1 |
2 | Host Process Environment Information:
3 | BenchmarkDotNet.Core=v0.9.9.0
4 | OS=Windows
5 | Processor=?, ProcessorCount=8
6 | Frequency=2533306 ticks, Resolution=394.7411 ns, Timer=TSC
7 | CLR=CORE, Arch=64-bit ? [RyuJIT]
8 | GC=Concurrent Workstation
9 | dotnet cli version: 1.0.0-preview2-003121
10 |
11 | Type=NumberListBenchmark Mode=Throughput
12 |
13 |
14 |
15 | | Method | Median | StdDev | Scaled | Scaled-SD |
16 |
|---|
| StringSplitAndInt32Parse | 138.7217 us | 5.7115 us | 1.00 | 0.00 |
17 |
| SpracheSimple | 2,740.3251 us | 11.9340 us | 19.40 | 0.72 |
18 |
| SuperpowerSimple | 937.1593 us | 43.9867 us | 6.64 | 0.39 |
19 |
| SuperpowerChar | 691.5509 us | 9.9246 us | 4.89 | 0.19 |
20 |
| SuperpowerToken | 1,189.8622 us | 15.9538 us | 8.46 | 0.33 |
21 |
22 |
--------------------------------------------------------------------------------
/sample/DateTimeTextParser/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace DateTimeParser
4 | {
5 | static class Program
6 | {
7 | static void ParseAndPrint(string input)
8 | {
9 | try
10 | {
11 | var dt = DateTimeTextParser.Parse(input);
12 | Console.WriteLine("Input: \"{0}\", Parsed value: \"{1:o}\"", input, dt);
13 | }
14 | catch (Exception ex)
15 | {
16 | Console.ForegroundColor = ConsoleColor.Red;
17 | Console.WriteLine("Input: \"{0}\"", input);
18 | Console.WriteLine(ex.ToString());
19 | Console.ForegroundColor = ConsoleColor.White;
20 | }
21 | }
22 |
23 | static void Main()
24 | {
25 | ParseAndPrint("2017-01-01");
26 | ParseAndPrint("2017-01-01 05:28:10");
27 | ParseAndPrint("2017-01-01 05:28");
28 | ParseAndPrint("2017-01-01T05:28:10");
29 | ParseAndPrint("2017-01-01T05:28");
30 | ParseAndPrint("2017-01-01 05:x8:10");
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Superpower/Model/TokenizationState.cs:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Datalust, Superpower Contributors, Sprache Contributors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | namespace Superpower.Model
16 | {
17 | ///
18 | /// Represents the progress of a single tokenization operation.
19 | ///
20 | /// The kind of token being produced.
21 | public class TokenizationState
22 | {
23 | ///
24 | /// The last produced token.
25 | ///
26 | public Token? Previous { get; set; }
27 | }
28 | }
--------------------------------------------------------------------------------
/src/Superpower/TextParser`1.cs:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | using Superpower.Model;
16 |
17 | namespace Superpower
18 | {
19 | ///
20 | /// A parser that consumes text from a string span.
21 | ///
22 | /// The type of values produced by the parser.
23 | /// The span of text to parse.
24 | /// A result with a parsed value, or an empty result indicating error.
25 | public delegate Result TextParser(TextSpan input);
26 | }
--------------------------------------------------------------------------------
/sample/IntCalc/Program.cs:
--------------------------------------------------------------------------------
1 | using Superpower;
2 | using System;
3 |
4 | namespace IntCalc
5 | {
6 | public class Program
7 | {
8 | public static int Main(string[] args)
9 | {
10 | var cmdline = string.Join(" ", args);
11 | if (string.IsNullOrWhiteSpace(cmdline))
12 | {
13 | Console.Error.WriteLine("Usage: intcalc.exe ");
14 | return 1;
15 | }
16 |
17 | try
18 | {
19 | var tok = new ArithmeticExpressionTokenizer();
20 | var tokens = tok.Tokenize(cmdline);
21 | var expr = ArithmeticExpressionParser.Lambda.Parse(tokens);
22 | var compiled = expr.Compile();
23 | var result = compiled();
24 | Console.WriteLine(result);
25 | return 0;
26 | }
27 | catch (ParseException ex)
28 | {
29 | Console.Error.WriteLine(ex.Message);
30 | }
31 | catch (Exception ex)
32 | {
33 | Console.Error.WriteLine(ex);
34 | }
35 | return 1;
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/Superpower/Parsers/Instant.cs:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Datalust, Superpower Contributors, Sprache Contributors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | using Superpower.Model;
16 |
17 | namespace Superpower.Parsers
18 | {
19 | ///
20 | /// Parsers for matching date and time formats.
21 | ///
22 | public static class Instant
23 | {
24 | ///
25 | /// Matches ISO-8601 datetimes.
26 | ///
27 | public static TextParser Iso8601DateTime { get; } =
28 | Span.Regex("\\d{4}-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d(\\.\\d+)?(([+-]\\d\\d:\\d\\d)|Z)?");
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/test/Superpower.Tests/Parsers/IdentifierTests.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Model;
2 | using Superpower.Parsers;
3 | using Superpower.Tests.Support;
4 | using Xunit;
5 |
6 | namespace Superpower.Tests.Parsers
7 | {
8 | public class IdentifierTests
9 | {
10 | [Fact]
11 | public void CStyleIdentifiersAreMatched()
12 | {
13 | var input = new TextSpan("C_Style!");
14 | var r = Identifier.CStyle(input);
15 | Assert.Equal("C_Style", r.Value.ToStringValue());
16 | }
17 |
18 | [Fact]
19 | public void CStyleIdentifiersMayStartWithLeadingUnderscore()
20 | {
21 | var input = new TextSpan("_cStyle1!");
22 | var r = Identifier.CStyle(input);
23 | Assert.Equal("_cStyle1", r.Value.ToStringValue());
24 | }
25 |
26 | [Theory]
27 | [InlineData("0", false)]
28 | [InlineData("__", true)]
29 | [InlineData("A0", true)]
30 | [InlineData("ab", true)]
31 | [InlineData("a_b", true)]
32 | [InlineData("_b", true)]
33 | [InlineData("1CStyle", false)]
34 | public void CStyleIdentifiersAreRecognized(string input, bool isMatch)
35 | {
36 | AssertParser.FitsTheory(Identifier.CStyle, input, isMatch);
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Superpower/Parsers/Identifier.cs:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Datalust, Superpower Contributors, Sprache Contributors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | using Superpower.Model;
16 |
17 | namespace Superpower.Parsers
18 | {
19 | ///
20 | /// Parsers for matching identifiers in various styles.
21 | ///
22 | public static class Identifier
23 | {
24 | ///
25 | /// Parse a C_Style identifier.
26 | ///
27 | public static TextParser CStyle { get; } =
28 | Span.MatchedBy(
29 | Character.Letter.Or(Character.EqualTo('_'))
30 | .IgnoreThen(Character.LetterOrDigit.Or(Character.EqualTo('_')).Many()));
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Superpower/TokenListParser`2.cs:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | using Superpower.Model;
16 |
17 | namespace Superpower
18 | {
19 | ///
20 | /// A parser that consumes elements from a list of tokens.
21 | ///
22 | /// The type of values produced by the parser.
23 | /// The type of tokens being parsed.
24 | /// The list of tokens to parse.
25 | /// A result with a parsed value, or an empty result indicating error.
26 | public delegate TokenListParserResult TokenListParser(TokenList input);
27 | }
--------------------------------------------------------------------------------
/test/Superpower.Tests/Combinators/NotCombinatorTests.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Parsers;
2 | using Superpower.Tests.Support;
3 | using Xunit;
4 |
5 | namespace Superpower.Tests.Combinators
6 | {
7 | public class NotCombinatorTests
8 | {
9 | [Fact]
10 | public void NotSucceedsIfLookaheadFails()
11 | {
12 | AssertParser.SucceedsWith(Parse.Not(Span.EqualTo("ab")).Then(_ => Character.EqualTo('a')), "ac", 'a');
13 | }
14 |
15 | [Fact]
16 | public void NotFailsIfLookaheadSucceeds()
17 | {
18 | AssertParser.FailsWithMessage(Parse.Not(Span.EqualTo("ab")).Then(_ => Character.EqualTo('a')), "ab",
19 | "Syntax error (line 1, column 1): unexpected successful parsing of `ab`.");
20 | }
21 |
22 | [Fact]
23 | public void TokenNotSucceedsIfLookaheadFails()
24 | {
25 | AssertParser.SucceedsWith(Parse.Not(Token.EqualTo('a').Then(_ => Token.EqualTo('b'))).Then(_ => Token.EqualTo('a')), "ac", 'a');
26 | }
27 |
28 | [Fact]
29 | public void TokenNotFailsIfLookaheadSucceeds()
30 | {
31 | AssertParser.FailsWithMessage(Parse.Not(Token.Sequence('a', 'b')).Then(_ => Token.EqualTo('a')), "ab",
32 | "Syntax error (line 1, column 1): unexpected successful parsing of `ab`.");
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Superpower/Util/CharInfo.cs:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Datalust, Superpower Contributors, Sprache Contributors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | namespace Superpower.Util
16 | {
17 | static class CharInfo
18 | {
19 | public static bool IsLatinDigit(char ch)
20 | {
21 | return ch >= '0' && ch <= '9';
22 | }
23 |
24 | public static bool IsHexDigit(char ch)
25 | {
26 | return IsLatinDigit(ch) || ch >= 'a' && ch <= 'f' || ch >= 'A' && ch <= 'F';
27 | }
28 |
29 | public static int HexValue(char ch)
30 | {
31 | if (IsLatinDigit(ch))
32 | return ch - '0';
33 |
34 | if (ch >= 'a' && ch <= 'f')
35 | return 15 + ch - 'f';
36 |
37 | return 15 + ch - 'F';
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/test/Superpower.Tests/Combinators/AtLeastOnceCombinatorTests.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Parsers;
2 | using Superpower.Tests.Support;
3 | using Xunit;
4 |
5 | namespace Superpower.Tests.Combinators
6 | {
7 | public class AtLeastOnceCombinatorTests
8 | {
9 | [Fact]
10 | public void AtLeastOnceSucceedsWithOne()
11 | {
12 | AssertParser.SucceedsWithAll(Character.EqualTo('a').AtLeastOnce(), "a");
13 | }
14 |
15 | [Fact]
16 | public void AtLeastOnceSucceedsWithTwo()
17 | {
18 | AssertParser.SucceedsWithAll(Character.EqualTo('a').AtLeastOnce(), "aa");
19 | }
20 |
21 | [Fact]
22 | public void AtLeastOnceFailsWithNone()
23 | {
24 | AssertParser.Fails(Character.EqualTo('a').AtLeastOnce(), "");
25 | }
26 |
27 | [Fact]
28 | public void TokenAtLeastOnceSucceedsWithOne()
29 | {
30 | AssertParser.SucceedsWithAll(Token.EqualTo('a').AtLeastOnce(), "a");
31 | }
32 |
33 | [Fact]
34 | public void TokenAtLeastOnceSucceedsWithTwo()
35 | {
36 | AssertParser.SucceedsWithAll(Token.EqualTo('a').AtLeastOnce(), "aa");
37 | }
38 |
39 | [Fact]
40 | public void TokenAtLeastOnceFailsWithNone()
41 | {
42 | AssertParser.Fails(Token.EqualTo('a').AtLeastOnce(), "");
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/test/Superpower.Tests/Superpower.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net6.0;net8.0
4 | Superpower.Tests
5 | ../../asset/Superpower.snk
6 | true
7 | true
8 | Superpower.Tests
9 | true
10 | false
11 | false
12 | false
13 | enable
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | all
22 | runtime; build; native; contentfiles; analyzers; buildtransitive
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/test/Superpower.Tests/Display/PresentationTests.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Display;
2 | using Superpower.Tests.SExpressionScenario;
3 | using Xunit;
4 | using Superpower.Parsers;
5 |
6 | namespace Superpower.Tests.Display
7 | {
8 | public class PresentationTests
9 | {
10 | [Fact]
11 | public void AnUnadornedEnumMemberIsLowercasedForDisplay()
12 | {
13 | var display = Presentation.FormatExpectation(SExpressionToken.Number);
14 | Assert.Equal("number", display);
15 | }
16 |
17 | [Fact]
18 | public void DescriptionAttributeIsInterrogatedForDisplay()
19 | {
20 | var display = Presentation.FormatExpectation(SExpressionToken.LParen);
21 | Assert.Equal("open parenthesis", display);
22 | }
23 | [Fact]
24 | public void ProperNameIsDisplayedWhenNonGraphicalCausesFailure()
25 | {
26 | var result=Character.EqualTo('a').TryParse("\x2007");
27 |
28 | Assert.Equal("Syntax error (line 1, column 1): unexpected U+2007 figure space, expected `a`.", result.ToString());
29 | }
30 | [Fact]
31 | public void ProperNameIsDisplayedWhenNonGraphicalIsFailed()
32 | {
33 | var result=Character.EqualTo('\x2007').TryParse("a");
34 | Assert.Equal("Syntax error (line 1, column 1): unexpected `a`, expected U+2007 figure space.", result.ToString());
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/test/Superpower.Benchmarks/NumberListScenario/NumberListTokenizer.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using Superpower.Model;
3 |
4 | namespace Superpower.Benchmarks.NumberListScenario
5 | {
6 | public class NumberListTokenizer : Tokenizer
7 | {
8 | public static NumberListTokenizer Instance { get; } = new NumberListTokenizer();
9 |
10 | protected override IEnumerable> Tokenize(TextSpan span)
11 | {
12 | var next = SkipWhiteSpace(span);
13 | if (!next.HasValue)
14 | yield break;
15 |
16 | do
17 | {
18 | var ch = next.Value;
19 | if (ch >= '0' && ch <= '9')
20 | {
21 | var start = next;
22 | next = next.Remainder.ConsumeChar();
23 | while (next.HasValue && next.Value >= '0' && next.Value <= '9')
24 | {
25 | next = next.Remainder.ConsumeChar();
26 | }
27 | yield return Result.Value(NumberListToken.Number, start.Location, next.Location);
28 | }
29 | else
30 | {
31 | yield return Result.Empty(next.Location, new[] { "digit" });
32 | }
33 |
34 | next = SkipWhiteSpace(next.Location);
35 | } while (next.HasValue);
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/test/Superpower.Tests/Combinators/OneOfCombinatorTests.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Model;
2 | using Superpower.Parsers;
3 | using Superpower.Tests.Support;
4 | using System.Linq;
5 | using Xunit;
6 |
7 | namespace Superpower.Tests.Combinators
8 | {
9 | public class OneOfCombinatorTests
10 | {
11 | [Fact]
12 | public void OneOf2Succeeds()
13 | {
14 | var p = Parse.OneOf(Character.Digit, Character.AnyChar);
15 | AssertParser.SucceedsWithAll(Span.MatchedBy(p), "1");
16 | AssertParser.SucceedsWithAll(Span.MatchedBy(p), "w");
17 | }
18 |
19 | [Fact]
20 | public void OneOf2TokenSucceeds()
21 | {
22 | var p = Parse.OneOf(Token.EqualTo('1'), Token.EqualTo('w'));
23 | AssertParser.SucceedsWith(p, "1", '1');
24 | AssertParser.SucceedsWith(p, "w", 'w');
25 | }
26 |
27 | [Fact]
28 | public void OneOfReportsCorrectError()
29 | {
30 | var names = new[] { "one", "two" };
31 | TextParser p = Parse.OneOf(names.Select(Span.EqualTo).ToArray());
32 | AssertParser.SucceedsWith(p.Select(t => t.ToStringValue()), "one", "one");
33 | AssertParser.SucceedsWith(p.Select(t => t.ToStringValue()), "two", "two");
34 | AssertParser.FailsWithMessage(p.Select(t => t.ToStringValue()), "four", "Syntax error (line 1, column 1): unexpected `f`, expected `one` or `two`.");
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/test/Superpower.Tests/Combinators/ThenCombinatorTests.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Parsers;
2 | using Superpower.Tests.Support;
3 | using Xunit;
4 |
5 | namespace Superpower.Tests.Combinators
6 | {
7 | public class ThenCombinatorTests
8 | {
9 | [Fact]
10 | public void ThenFailsIfFirstParserFails()
11 | {
12 | AssertParser.Fails(Character.EqualTo('a').Then(_ => Character.EqualTo('b')), "cb");
13 | }
14 |
15 | [Fact]
16 | public void ThenFailsIfSecondParserFails()
17 | {
18 | AssertParser.Fails(Character.EqualTo('a').Then(_ => Character.EqualTo('b')), "ac");
19 | }
20 |
21 | [Fact]
22 | public void ThenSucceedsIfBothSucceedInSequence()
23 | {
24 | AssertParser.SucceedsWith(Character.EqualTo('a').Then(_ => Character.EqualTo('b')), "ab", 'b');
25 | }
26 |
27 | [Fact]
28 | public void TokenThenFailsIfFirstParserFails()
29 | {
30 | AssertParser.Fails(Token.EqualTo('a').Then(_ => Token.EqualTo('b')), "cb");
31 | }
32 |
33 | [Fact]
34 | public void TokenThenFailsIfSecondParserFails()
35 | {
36 | AssertParser.Fails(Token.EqualTo('a').Then(_ => Token.EqualTo('b')), "ac");
37 | }
38 |
39 | [Fact]
40 | public void TokenThenSucceedsIfBothSucceedInSequence()
41 | {
42 | AssertParser.SucceedsWith(Token.EqualTo('a').Then(_ => Token.EqualTo('b')), "ab", 'b');
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Superpower/Superpower.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | netstandard2.0;net6.0;net8.0
4 | A parser combinator library for C#
5 | 3.1.1
6 | Datalust;Superpower Contributors;Sprache Contributors
7 | true
8 | true
9 | ../../asset/Superpower.snk
10 | true
11 | true
12 | superpower;parser
13 | https://github.com/datalust/superpower
14 | false
15 | Superpower-White-400px.png
16 | Apache-2.0
17 | https://github.com/datalust/superpower
18 | git
19 | enable
20 | latest
21 |
22 |
23 | $(DefineConstants);CHECKED
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/test/Superpower.Tests/Combinators/RepeatCombinatorTests.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Parsers;
2 | using Superpower.Tests.Support;
3 | using Xunit;
4 |
5 | namespace Superpower.Tests.Combinators
6 | {
7 | public class RepeatCombinatorTests
8 | {
9 | [Fact]
10 | public void RepeatSucceedsWithNone()
11 | {
12 | AssertParser.SucceedsWithAll(Character.EqualTo('a').Repeat(0), "");
13 | }
14 |
15 | [Fact]
16 | public void RepeatSucceedsWithOne()
17 | {
18 | AssertParser.SucceedsWithAll(Character.EqualTo('a').Repeat(1), "a");
19 | }
20 |
21 | [Fact]
22 | public void RepeatSucceedsWithTwo()
23 | {
24 | AssertParser.SucceedsWithAll(Character.EqualTo('a').Repeat(2), "aa");
25 | }
26 |
27 | [Fact]
28 | public void RepeatFailsWithTooFew()
29 | {
30 | AssertParser.Fails(Character.EqualTo('a').Repeat(3), "aa");
31 | }
32 |
33 | [Fact]
34 | public void TokenRepeatSucceedsWithNone()
35 | {
36 | AssertParser.SucceedsWithAll(Token.EqualTo('a').Repeat(0), "");
37 | }
38 |
39 | [Fact]
40 | public void TokenRepeatSucceedsWithOne()
41 | {
42 | AssertParser.SucceedsWithAll(Token.EqualTo('a').Repeat(1), "a");
43 | }
44 |
45 | [Fact]
46 | public void TokenRepeatSucceedsWithTwo()
47 | {
48 | AssertParser.SucceedsWithAll(Token.EqualTo('a').Repeat(2), "aa");
49 | }
50 |
51 | [Fact]
52 | public void TokenRepeatFailsWithTooFew()
53 | {
54 | AssertParser.Fails(Token.EqualTo('a').Repeat(3), "aa");
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Superpower.sln.DotSettings:
--------------------------------------------------------------------------------
1 |
2 | <Policy Inspect="True" Prefix="T" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="AA_BB" /></Policy>
3 | True
4 | True
5 | True
6 | True
7 | True
8 | True
9 | True
10 | True
11 | True
12 | True
13 | True
14 | True
--------------------------------------------------------------------------------
/test/Superpower.Tests/NumberListScenario/NumberListTokenizer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using Superpower.Model;
4 | using Superpower.Parsers;
5 |
6 | namespace Superpower.Tests.NumberListScenario
7 | {
8 | class NumberListTokenizer : Tokenizer
9 | {
10 | readonly bool _useCustomErrors;
11 |
12 | public NumberListTokenizer(bool useCustomErrors = false)
13 | {
14 | _useCustomErrors = useCustomErrors;
15 | }
16 |
17 | protected override IEnumerable> Tokenize(TextSpan span)
18 | {
19 | var next = SkipWhiteSpace(span);
20 | if (!next.HasValue)
21 | yield break;
22 |
23 | do
24 | {
25 | var ch = next.Value;
26 | if (ch >= '0' && ch <= '9')
27 | {
28 | var integer = Numerics.Integer(next.Location);
29 | next = integer.Remainder.ConsumeChar();
30 | yield return Result.Value(NumberListToken.Number, integer.Location, integer.Remainder);
31 | }
32 | else
33 | {
34 | if (_useCustomErrors)
35 | {
36 | yield return Result.Empty(next.Location, "list must contain only numbers");
37 | }
38 | else
39 | {
40 | yield return Result.Empty(next.Location, new[] { "digit" });
41 | }
42 | }
43 |
44 | next = SkipWhiteSpace(next.Location);
45 | } while (next.HasValue);
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Superpower/Display/TokenAttribute.cs:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | using System;
16 |
17 | // ReSharper disable UnusedAutoPropertyAccessor.Global, ClassNeverInstantiated.Global
18 |
19 | namespace Superpower.Display
20 | {
21 | ///
22 | /// Applied to enum members representing tokens to control how they are rendered.
23 | ///
24 | [AttributeUsage(AttributeTargets.Field|AttributeTargets.Class)]
25 | public class TokenAttribute : Attribute
26 | {
27 | ///
28 | /// The category of the token, e.g. "keyword" or "identifier".
29 | ///
30 | public string? Category { get; set; }
31 |
32 | ///
33 | /// For tokens that correspond to exact text, e.g. punctuation, the canonical
34 | /// example of how the token looks.
35 | ///
36 | public string? Example { get; set; }
37 |
38 | ///
39 | /// A description of the token, for example "regular expression".
40 | ///
41 | public string? Description { get; set; }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Build.ps1:
--------------------------------------------------------------------------------
1 | # This script originally (c) 2016 Serilog Contributors - license Apache 2.0
2 |
3 | echo "build: Build started"
4 |
5 | Push-Location $PSScriptRoot
6 |
7 | if(Test-Path .\artifacts) {
8 | echo "build: Cleaning .\artifacts"
9 | Remove-Item .\artifacts -Force -Recurse
10 | }
11 |
12 | & dotnet restore --no-cache
13 |
14 | $branch = @{ $true = $env:APPVEYOR_REPO_BRANCH; $false = $(git symbolic-ref --short -q HEAD) }[$env:APPVEYOR_REPO_BRANCH -ne $NULL];
15 | $revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10); $false = "local" }[$env:APPVEYOR_BUILD_NUMBER -ne $NULL];
16 | $suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)).Replace("/", "-"))-$revision"}[$branch -eq "main" -and $revision -ne "local"]
17 |
18 | echo "build: Version suffix is $suffix"
19 |
20 | foreach ($src in ls src/*) {
21 | Push-Location $src
22 |
23 | echo "build: Packaging project in $src"
24 |
25 | if ($suffix) {
26 | & dotnet pack -c Release -o ..\..\artifacts --version-suffix=$suffix --include-source
27 | } else {
28 | & dotnet pack -c Release -o ..\..\artifacts --include-source
29 | }
30 | if($LASTEXITCODE -ne 0) { exit 1 }
31 |
32 | Pop-Location
33 | }
34 |
35 | foreach ($test in ls test/*.Benchmarks) {
36 | Push-Location $test
37 |
38 | echo "build: Building performance test project in $test"
39 |
40 | & dotnet build -c Release
41 | if($LASTEXITCODE -ne 0) { exit 2 }
42 |
43 | Pop-Location
44 | }
45 |
46 | foreach ($test in ls test/*.Tests) {
47 | Push-Location $test
48 |
49 | echo "build: Testing project in $test"
50 |
51 | & dotnet test -c Release
52 | if($LASTEXITCODE -ne 0) { exit 3 }
53 |
54 | Pop-Location
55 | }
56 |
57 | Pop-Location
58 |
--------------------------------------------------------------------------------
/test/Superpower.Tests/Combinators/AtEndCombinatorTests.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Tests.Support;
2 | using Superpower.Model;
3 | using Xunit;
4 | using Superpower.Parsers;
5 |
6 | namespace Superpower.Tests.Combinators
7 | {
8 | public class AtEndCombinatorTests
9 | {
10 | [Fact]
11 | public void AtEndSucceedsAtTheEnd()
12 | {
13 | AssertParser.SucceedsWith(Character.EqualTo('a').AtEnd(), "a", 'a');
14 | }
15 |
16 | [Fact]
17 | public void AtEndFailsIfThereIsARemainder()
18 | {
19 | AssertParser.Fails(Character.EqualTo('a').AtEnd(), "ab");
20 | }
21 |
22 | [Fact]
23 | public void AtEndFailsIfThePrecedingParserFails()
24 | {
25 | AssertParser.Fails(Character.EqualTo('b').AtEnd(), "a");
26 | }
27 |
28 | [Fact]
29 | public void AtEndSucceedsIfThereIsNoInput()
30 | {
31 | AssertParser.SucceedsWith(Parse.Return('a').AtEnd(), "", 'a');
32 | }
33 |
34 | [Fact]
35 | public void TokenAtEndSucceedsAtTheEnd()
36 | {
37 | AssertParser.SucceedsWith(Token.EqualTo('a').AtEnd(), "a", 'a');
38 | }
39 |
40 | [Fact]
41 | public void TokenAtEndFailsIfThereIsARemainder()
42 | {
43 | AssertParser.Fails(Token.EqualTo('a').AtEnd(), "ab");
44 | }
45 |
46 | [Fact]
47 | public void TokenAtEndFailsIfThePrecedingParserFails()
48 | {
49 | AssertParser.Fails(Token.EqualTo('b').AtEnd(), "a");
50 | }
51 |
52 | [Fact]
53 | public void TokenAtEndSucceedsIfThereIsNoInput()
54 | {
55 | AssertParser.SucceedsWith(Parse.Return>(new Token('a', TextSpan.Empty)).AtEnd(), "", 'a');
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "IntCalc",
6 | "type": "coreclr",
7 | "request": "launch",
8 | "preLaunchTask": "build",
9 | "program": "${workspaceFolder}/sample/IntCalc/bin/Debug/netcoreapp2.0/IntCalc.dll",
10 | "args": [ "IntCalc" ],
11 | "cwd": "${workspaceFolder}/sample/IntCalc",
12 | "console": "integratedTerminal",
13 | "stopAtEntry": false,
14 | "internalConsoleOptions": "openOnSessionStart"
15 | },
16 | {
17 | "name": "JsonParser",
18 | "type": "coreclr",
19 | "request": "launch",
20 | "preLaunchTask": "build",
21 | "program": "${workspaceFolder}/sample/JsonParser/bin/Debug/netcoreapp2.0/JsonParser.dll",
22 | "args": [ "IntCalc" ],
23 | "cwd": "${workspaceFolder}/sample/JsonParser",
24 | "console": "integratedTerminal",
25 | "stopAtEntry": false,
26 | "internalConsoleOptions": "openOnSessionStart"
27 | },
28 | {
29 | "name": "DateTimeTextParser",
30 | "type": "coreclr",
31 | "request": "launch",
32 | "preLaunchTask": "build",
33 | "program": "${workspaceFolder}/sample/DateTimeTextParser/bin/Debug/netcoreapp2.0/DateTimeParser.dll",
34 | "args": [ "IntCalc" ],
35 | "cwd": "${workspaceFolder}/sample/DateTimeTextParser",
36 | "console": "integratedTerminal",
37 | "stopAtEntry": false,
38 | "internalConsoleOptions": "openOnSessionStart"
39 | },
40 | {
41 | "name": ".NET Core Attach",
42 | "type": "coreclr",
43 | "request": "attach",
44 | "processId": "${command:pickProcess}"
45 | }
46 | ]
47 | }
--------------------------------------------------------------------------------
/test/Superpower.Benchmarks/TokenizerBuilderBenchmark.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using BenchmarkDotNet.Attributes;
3 | using BenchmarkDotNet.Running;
4 | using Superpower.Benchmarks.NumberListScenario;
5 | using Superpower.Model;
6 | using Superpower.Parsers;
7 | using Superpower.Tokenizers;
8 | using Xunit;
9 |
10 | namespace Superpower.Benchmarks
11 | {
12 | [MemoryDiagnoser]
13 | public class TokenizerBuilderBenchmark
14 | {
15 | const int NumbersLength = 1000;
16 | static readonly string Numbers = string.Join(" ", Enumerable.Range(0, NumbersLength));
17 |
18 | static readonly Tokenizer BuilderTokenizer = new TokenizerBuilder()
19 | .Match(Numerics.Integer, NumberListToken.Number)
20 | .Ignore(Span.WhiteSpace)
21 | .Build();
22 |
23 | static void AssertComplete(TokenList numbers)
24 | {
25 | var tokens = numbers.ToArray();
26 | Assert.Equal(NumbersLength, tokens.Length);
27 | for (var i = 0; i < NumbersLength; ++i)
28 | {
29 | Assert.Equal(NumberListToken.Number, tokens[i].Kind);
30 | Assert.Equal(i.ToString(), tokens[i].ToStringValue());
31 | }
32 | }
33 |
34 | [Fact]
35 | public void Verify()
36 | {
37 | AssertComplete(HandCoded());
38 | AssertComplete(Builder());
39 | }
40 |
41 | [Fact]
42 | public void Benchmark()
43 | {
44 | BenchmarkRunner.Run();
45 | }
46 |
47 | [Benchmark(Baseline = true)]
48 | public TokenList HandCoded()
49 | {
50 | return NumberListTokenizer.Instance.Tokenize(Numbers);
51 | }
52 |
53 | [Benchmark]
54 | public TokenList Builder()
55 | {
56 | return BuilderTokenizer.Tokenize(Numbers);
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/sample/DateTimeTextParser/DateTimeTextParser.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using Superpower;
4 | using Superpower.Model;
5 | using Superpower.Parsers;
6 |
7 | namespace DateTimeParser
8 | {
9 | public static class DateTimeTextParser
10 | {
11 | static TextParser IntDigits(int count) =>
12 | Character.Digit
13 | .Repeat(count)
14 | .Select(chars => int.Parse(new string(chars)));
15 |
16 | static TextParser TwoDigits { get; } = IntDigits(2);
17 | static TextParser FourDigits { get; } = IntDigits(4);
18 |
19 | static TextParser Dash { get; } = Character.EqualTo('-');
20 | static TextParser Colon { get; } = Character.EqualTo(':');
21 | static TextParser TimeSeparator { get; } = Character.In('T', ' ');
22 |
23 | static TextParser Date { get; } =
24 | from year in FourDigits
25 | from _ in Dash
26 | from month in TwoDigits
27 | from __ in Dash
28 | from day in TwoDigits
29 | select new DateTime(year, month, day);
30 |
31 | static TextParser Time { get; } =
32 | from hour in TwoDigits
33 | from _ in Colon
34 | from minute in TwoDigits
35 | from second in Colon
36 | .IgnoreThen(TwoDigits)
37 | .OptionalOrDefault()
38 | select new TimeSpan(hour, minute, second);
39 |
40 | static TextParser DateTime { get; } =
41 | from date in Date
42 | from time in TimeSeparator
43 | .IgnoreThen(Time)
44 | .OptionalOrDefault()
45 | select date + time;
46 |
47 | static TextParser DateTimeOnly { get; } = DateTime.AtEnd();
48 |
49 | public static DateTime Parse(string input)
50 | {
51 | return DateTimeOnly.Parse(input);
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/test/Superpower.Benchmarks/SequencingBenchmark.cs:
--------------------------------------------------------------------------------
1 | using BenchmarkDotNet.Attributes;
2 | using BenchmarkDotNet.Running;
3 | using Superpower.Parsers;
4 | using Superpower.Model;
5 | using Xunit;
6 |
7 | // ReSharper disable ParameterOnlyUsedForPreconditionCheck.Local
8 |
9 | namespace Superpower.Benchmarks
10 | {
11 | [MemoryDiagnoser]
12 | public class SequencingBenchmark
13 | {
14 | static readonly string Numbers = "123";
15 | static readonly TextSpan Input = new TextSpan(Numbers);
16 |
17 | static void AssertValues((char First, char Second, char Third) numbers)
18 | {
19 | Assert.Equal('1', numbers.First);
20 | Assert.Equal('2', numbers.Second);
21 | Assert.Equal('3', numbers.Third);
22 | }
23 |
24 | [Fact]
25 | public void Verify()
26 | {
27 | AssertValues(ApplyThen().Value);
28 | AssertValues(ApplySequence().Value);
29 | }
30 |
31 | [Fact]
32 | public void Benchmark()
33 | {
34 | BenchmarkRunner.Run();
35 | }
36 |
37 | static readonly TextParser<(char, char, char)> ThenParser =
38 | Character.Digit.Then(first =>
39 | Character.Digit.Then(second =>
40 | Character.Digit.Then(third => Parse.Return((first, second, third)))));
41 |
42 | [Benchmark(Baseline = true)]
43 | public Result<(char, char, char)> ApplyThen()
44 | {
45 | return ThenParser(Input);
46 | }
47 |
48 | static readonly TextParser<(char, char, char)> SequenceParser =
49 | Parse.Sequence(
50 | Character.Digit,
51 | Character.Digit,
52 | Character.Digit)
53 | .Select(t => t); // Even up the work done
54 |
55 | [Benchmark]
56 | public Result<(char, char, char)> ApplySequence()
57 | {
58 | return SequenceParser(Input);
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/Superpower/Util/ArrayEnumerable.cs:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | using System.Runtime.CompilerServices;
16 |
17 | namespace Superpower.Util
18 | {
19 | static class ArrayEnumerable
20 | {
21 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
22 | public static T[] Cons(T first, T[] rest)
23 | {
24 | var all = new T[rest.Length + 1];
25 | all[0] = first;
26 | for (var i = 0; i < rest.Length; ++i)
27 | all[i + 1] = rest[i];
28 | return all;
29 | }
30 |
31 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
32 | public static T[] Concat(T[] first, T[] rest)
33 | {
34 | var all = new T[first.Length + rest.Length];
35 | var i = 0;
36 | for (; i < first.Length; ++i)
37 | all[i] = first[i];
38 | for (var j = 0; j < rest.Length; ++i, ++j)
39 | all[i] = rest[j];
40 | return all;
41 | }
42 |
43 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
44 | public static T[] Append(T[] first, T last)
45 | {
46 | var all = new T[first.Length + 1];
47 | for (var i = 0; i < first.Length; ++i)
48 | all[i] = first[i];
49 | all[first.Length] = last;
50 | return all;
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/test/Superpower.Tests/ArithmeticExpressionScenario/ArithmeticExpressionTokenizer.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using Superpower.Model;
3 | using Superpower.Parsers;
4 |
5 | namespace Superpower.Tests.ArithmeticExpressionScenario
6 | {
7 | class ArithmeticExpressionTokenizer : Tokenizer
8 | {
9 | readonly Dictionary _operators = new Dictionary
10 | {
11 | ['+'] = ArithmeticExpressionToken.Plus,
12 | ['-'] = ArithmeticExpressionToken.Minus,
13 | ['*'] = ArithmeticExpressionToken.Times,
14 | ['/'] = ArithmeticExpressionToken.Divide,
15 | ['('] = ArithmeticExpressionToken.LParen,
16 | [')'] = ArithmeticExpressionToken.RParen,
17 | };
18 |
19 | protected override IEnumerable> Tokenize(TextSpan span)
20 | {
21 | var next = SkipWhiteSpace(span);
22 | if (!next.HasValue)
23 | yield break;
24 |
25 | do
26 | {
27 | var ch = next.Value;
28 | if (ch >= '0' && ch <= '9')
29 | {
30 | var natural = Numerics.Natural(next.Location);
31 | next = natural.Remainder.ConsumeChar();
32 | yield return Result.Value(ArithmeticExpressionToken.Number, natural.Location, natural.Remainder);
33 | }
34 | else if (_operators.TryGetValue(ch, out var charToken))
35 | {
36 | yield return Result.Value(charToken, next.Location, next.Remainder);
37 | next = next.Remainder.ConsumeChar();
38 | }
39 | else
40 | {
41 | yield return Result.Empty(next.Location, new[] { "number", "operator" });
42 | }
43 |
44 | next = SkipWhiteSpace(next.Location);
45 | } while (next.HasValue);
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/sample/IntCalc/ArithmeticExpressionTokenizer.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using Superpower.Model;
3 | using Superpower.Parsers;
4 | using Superpower;
5 |
6 | namespace IntCalc
7 | {
8 | class ArithmeticExpressionTokenizer : Tokenizer
9 | {
10 | readonly Dictionary _operators = new Dictionary
11 | {
12 | ['+'] = ArithmeticExpressionToken.Plus,
13 | ['-'] = ArithmeticExpressionToken.Minus,
14 | ['*'] = ArithmeticExpressionToken.Times,
15 | ['/'] = ArithmeticExpressionToken.Divide,
16 | ['('] = ArithmeticExpressionToken.LParen,
17 | [')'] = ArithmeticExpressionToken.RParen,
18 | };
19 |
20 | protected override IEnumerable> Tokenize(TextSpan span)
21 | {
22 | var next = SkipWhiteSpace(span);
23 | if (!next.HasValue)
24 | yield break;
25 |
26 | do
27 | {
28 | ArithmeticExpressionToken charToken;
29 |
30 | var ch = next.Value;
31 | if (ch >= '0' && ch <= '9')
32 | {
33 | var integer = Numerics.Integer(next.Location);
34 | next = integer.Remainder.ConsumeChar();
35 | yield return Result.Value(ArithmeticExpressionToken.Number, integer.Location, integer.Remainder);
36 | }
37 | else if (_operators.TryGetValue(ch, out charToken))
38 | {
39 | yield return Result.Value(charToken, next.Location, next.Remainder);
40 | next = next.Remainder.ConsumeChar();
41 | }
42 | else
43 | {
44 | yield return Result.Empty(next.Location, new[] { "number", "operator" });
45 | }
46 |
47 | next = SkipWhiteSpace(next.Location);
48 | } while (next.HasValue);
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/test/Superpower.Tests/Combinators/BetweenCombinatorTests.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Parsers;
2 | using Superpower.Tests.Support;
3 | using Xunit;
4 |
5 | namespace Superpower.Tests.Combinators
6 | {
7 | public class BetweenCombinatorTests
8 | {
9 | [Fact]
10 | public void BetweenFailsIfLeftParserFails()
11 | {
12 | AssertParser.Fails(Character.EqualTo('a').Between(Character.EqualTo('('), Character.EqualTo(')')), "{a)");
13 | }
14 |
15 | [Fact]
16 | public void BetweenFailsIfRightParserFails()
17 | {
18 | AssertParser.Fails(Character.EqualTo('a').Between(Character.EqualTo('('), Character.EqualTo(')')), "(a}");
19 | }
20 |
21 | [Fact]
22 | public void BetweenFailsIfMiddleParserFails()
23 | {
24 | AssertParser.Fails(Character.EqualTo('a').Between(Character.EqualTo('('), Character.EqualTo(')')), "(b)");
25 | }
26 |
27 | [Fact]
28 | public void BetweenSucceedsIfAllParsersSucceed()
29 | {
30 | AssertParser.SucceedsWith(Character.EqualTo('a').Between(Character.EqualTo('('), Character.EqualTo(')')), "(a)", 'a');
31 | }
32 |
33 | [Fact]
34 | public void TokenBetweenFailsIfLeftParserFails()
35 | {
36 | AssertParser.Fails(Token.EqualTo('a').Between(Token.EqualTo('('), Token.EqualTo(')')), "{a)");
37 | }
38 |
39 | [Fact]
40 | public void TokenBetweenFailsIfRightParserFails()
41 | {
42 | AssertParser.Fails(Token.EqualTo('a').Between(Token.EqualTo('('), Token.EqualTo(')')), "(a}");
43 | }
44 |
45 | [Fact]
46 | public void TokenBetweenFailsIfMiddleParserFails()
47 | {
48 | AssertParser.Fails(Token.EqualTo('a').Between(Token.EqualTo('('), Token.EqualTo(')')), "(b)");
49 | }
50 |
51 | [Fact]
52 | public void TokenBetweenSucceedsIfAllParsersSucceed()
53 | {
54 | AssertParser.SucceedsWith(Token.EqualTo('a').Between(Token.EqualTo('('), Token.EqualTo(')')), "(a)", 'a');
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/test/Superpower.Benchmarks/ArithmeticExpressionScenario/SpracheArithmeticExpressionParser.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using Sprache;
4 | using System.Linq.Expressions;
5 |
6 | namespace Superpower.Benchmarks.ArithmeticExpressionScenario
7 | {
8 | static class SpracheArithmeticExpressionParser
9 | {
10 | static Parser Operator(string op, ExpressionType opType)
11 | {
12 | return Sprache.Parse.String(op).Token().Return(opType);
13 | }
14 |
15 | static readonly Parser Add = Operator("+", ExpressionType.AddChecked);
16 | static readonly Parser Subtract = Operator("-", ExpressionType.SubtractChecked);
17 | static readonly Parser Multiply = Operator("*", ExpressionType.MultiplyChecked);
18 | static readonly Parser Divide = Operator("/", ExpressionType.Divide);
19 |
20 | static readonly Parser Constant =
21 | Sprache.Parse.Decimal
22 | .Select(x => Expression.Constant(int.Parse(x)))
23 | .Named("number");
24 |
25 | static readonly Parser Factor =
26 | (from lparen in Sprache.Parse.Char('(')
27 | from expr in Sprache.Parse.Ref(() => Expr)
28 | from rparen in Sprache.Parse.Char(')')
29 | select expr)
30 | .XOr(Constant);
31 |
32 | static readonly Parser Operand =
33 | ((from sign in Sprache.Parse.Char('-')
34 | from factor in Factor
35 | select Expression.Negate(factor)
36 | ).XOr(Factor)).Named("expression").Token();
37 |
38 | static readonly Parser Term = Sprache.Parse.XChainOperator(Multiply.XOr(Divide), Operand, Expression.MakeBinary);
39 |
40 | static readonly Parser Expr = Sprache.Parse.XChainOperator(Add.XOr(Subtract), Term, Expression.MakeBinary);
41 |
42 | public static readonly Parser>> Lambda =
43 | Expr.End().Select(body => Expression.Lambda>(body));
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Superpower/Parsers/QuotedString.cs:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Datalust, Superpower Contributors, Sprache Contributors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | namespace Superpower.Parsers
16 | {
17 | ///
18 | /// Parsers for matching strings in various styles.
19 | ///
20 | public static class QuotedString
21 | {
22 | static readonly TextParser SqlStringContentChar =
23 | Span.EqualTo("''").Value('\'').Try().Or(Character.ExceptIn('\'', '\r', '\n'));
24 |
25 | static readonly TextParser CStringContentChar =
26 | Span.EqualTo("\\\"").Value('"').Try().Or(Character.ExceptIn('"', '\\', '\r', '\n'));
27 |
28 | ///
29 | /// A 'SQL-style' string. Single quote delimiters, with embedded single quotes
30 | /// escaped by '' doubling.
31 | ///
32 | public static TextParser SqlStyle { get; } =
33 | Character.EqualTo('\'')
34 | .IgnoreThen(SqlStringContentChar.Many())
35 | .Then(s => Character.EqualTo('\'').Value(new string(s)));
36 |
37 | ///
38 | /// A "C-style" string. Double quote delimiters, with ability to escape
39 | /// characters by using \".
40 | ///
41 | public static TextParser CStyle { get; } =
42 | Character.EqualTo('"')
43 | .IgnoreThen(CStringContentChar.Many())
44 | .Then(s => Character.EqualTo('"').Value(new string(s)));
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/test/Superpower.Benchmarks/ArithmeticExpressionScenario/ArithmeticExpressionTokenizer.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using Superpower.Model;
3 | using Superpower.Parsers;
4 |
5 | namespace Superpower.Benchmarks.ArithmeticExpressionScenario
6 | {
7 | class ArithmeticExpressionTokenizer : Tokenizer
8 | {
9 | readonly Dictionary _operators = new Dictionary
10 | {
11 | ['+'] = ArithmeticExpressionToken.Plus,
12 | ['-'] = ArithmeticExpressionToken.Minus,
13 | ['*'] = ArithmeticExpressionToken.Times,
14 | ['/'] = ArithmeticExpressionToken.Divide,
15 | ['('] = ArithmeticExpressionToken.LParen,
16 | [')'] = ArithmeticExpressionToken.RParen,
17 | };
18 |
19 | protected override IEnumerable> Tokenize(TextSpan span)
20 | {
21 | var next = SkipWhiteSpace(span);
22 | if (!next.HasValue)
23 | yield break;
24 |
25 | do
26 | {
27 | ArithmeticExpressionToken charToken;
28 |
29 | var ch = next.Value;
30 | if (ch >= '0' && ch <= '9')
31 | {
32 | var integer = Numerics.Integer(next.Location);
33 | next = integer.Remainder.ConsumeChar();
34 | yield return Result.Value(ArithmeticExpressionToken.Number, integer.Location, integer.Remainder);
35 | }
36 | else if (_operators.TryGetValue(ch, out charToken))
37 | {
38 | yield return Result.Value(charToken, next.Location, next.Remainder);
39 | next = next.Remainder.ConsumeChar();
40 | }
41 | else
42 | {
43 | yield return Result.Empty(next.Location, new[] { "number", "operator" });
44 | }
45 |
46 | next = SkipWhiteSpace(next.Location);
47 | } while (next.HasValue);
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/test/Superpower.Tests/Tokenizer`1Tests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.CodeDom;
3 | using System.Linq;
4 | using Superpower.Tests.NumberListScenario;
5 | using Superpower.Tests.Support;
6 | using Xunit;
7 |
8 | namespace Superpower.Tests
9 | {
10 | public class TokenizerTests
11 | {
12 | [Fact]
13 | public void TryTokenizeReportsFailures()
14 | {
15 | var tokenizer = new NumberListTokenizer();
16 | var result = tokenizer.TryTokenize("1 a");
17 | Assert.False(result.HasValue);
18 | Assert.Equal("unexpected `a`, expected digit", result.FormatErrorMessageFragment());
19 | }
20 |
21 | [Fact]
22 | public void TryTokenizeReportsCustomErrors()
23 | {
24 | var tokenizer = new NumberListTokenizer(useCustomErrors: true);
25 | var result = tokenizer.TryTokenize("1 a");
26 | Assert.False(result.HasValue);
27 | Assert.Equal("list must contain only numbers", result.FormatErrorMessageFragment());
28 | }
29 |
30 | [Fact]
31 | public void TokenizeThrowsOnFailure()
32 | {
33 | var tokenizer = new NumberListTokenizer();
34 | Assert.Throws(() => tokenizer.Tokenize("1 a"));
35 | }
36 |
37 | [Fact]
38 | public void TryTokenizeSucceedsIfTokenizationSucceeds()
39 | {
40 | var tokenizer = new NumberListTokenizer();
41 | var result = tokenizer.TryTokenize("1 23 456");
42 | Assert.True(result.HasValue);
43 | }
44 |
45 | [Fact]
46 | public void TokenizeReturnsAllProducedTokens()
47 | {
48 | var tokenizer = new NumberListTokenizer();
49 | var result = tokenizer.Tokenize("1 23 456");
50 | Assert.Equal(3, result.Count());
51 | }
52 |
53 | [Fact]
54 | public void TokenizationStateTracksTheLastProducedToken()
55 | {
56 | var tokenizer = new PreviousCheckingTokenizer();
57 | var input = new string('_', 6);
58 | var result = tokenizer.Tokenize(input);
59 | Assert.Equal(input.Length, result.Count());
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/Superpower/Util/Friendly.cs:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | using System;
16 | using System.Collections.Generic;
17 | using System.Linq;
18 |
19 | namespace Superpower.Util
20 | {
21 | static class Friendly
22 | {
23 | public static string Pluralize(string noun, int count)
24 | {
25 | if (noun == null) throw new ArgumentNullException(nameof(noun));
26 |
27 | if (count == 1)
28 | return noun;
29 |
30 | return noun + "s";
31 | }
32 |
33 | public static string List(IEnumerable items)
34 | {
35 | if (items == null) throw new ArgumentNullException(nameof(items));
36 |
37 | // Keep the order stable
38 | var seen = new HashSet();
39 | var unique = new List();
40 | foreach (var item in items)
41 | {
42 | if (seen.Contains(item)) continue;
43 | seen.Add(item);
44 | unique.Add(item);
45 | }
46 |
47 | if (unique.Count == 0)
48 | throw new ArgumentException("Friendly list formatting requires at least one element.", nameof(items));
49 |
50 | if (unique.Count == 1)
51 | return unique.Single();
52 |
53 | return $"{string.Join(", ", unique.Take(unique.Count - 1))} or {unique.Last()}";
54 | }
55 |
56 | public static string Clip(string value, int maxLength)
57 | {
58 | if (value.Length > maxLength)
59 | return value.Substring(0, maxLength - 3) + "...";
60 | return value;
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/test/Superpower.Tests/SExpressionScenario/SExpressionTokenizer.cs:
--------------------------------------------------------------------------------
1 | using Superpower;
2 | using Superpower.Parsers;
3 | using Superpower.Model;
4 | using System.Collections.Generic;
5 |
6 | namespace Superpower.Tests.SExpressionScenario
7 | {
8 | class SExpressionTokenizer : Tokenizer
9 | {
10 | protected override IEnumerable> Tokenize(TextSpan span)
11 | {
12 | var next = SkipWhiteSpace(span);
13 | if (!next.HasValue)
14 | yield break;
15 |
16 | do
17 | {
18 | if (next.Value == '(')
19 | {
20 | yield return Result.Value(SExpressionToken.LParen, next.Location, next.Remainder);
21 | next = next.Remainder.ConsumeChar();
22 | }
23 | else if (next.Value == ')')
24 | {
25 | yield return Result.Value(SExpressionToken.RParen, next.Location, next.Remainder);
26 | next = next.Remainder.ConsumeChar();
27 | }
28 | else if (next.Value >= '0' && next.Value <= '9')
29 | {
30 | var integer = Numerics.Integer(next.Location);
31 | next = integer.Remainder.ConsumeChar();
32 |
33 | yield return Result.Value(SExpressionToken.Number, integer.Location, integer.Remainder);
34 |
35 | if (next.HasValue && !char.IsPunctuation(next.Value) && !char.IsWhiteSpace(next.Value))
36 | {
37 | yield return Result.Empty(next.Location, new[] {"whitespace", "punctuation"});
38 | }
39 | }
40 | else
41 | {
42 | var beginIdentifier = next.Location;
43 | while (next.HasValue && char.IsLetterOrDigit(next.Value))
44 | {
45 | next = next.Remainder.ConsumeChar();
46 | }
47 |
48 | yield return Result.Value(SExpressionToken.Atom, beginIdentifier, next.Location);
49 | }
50 |
51 | next = SkipWhiteSpace(next.Location);
52 | } while (next.HasValue);
53 | }
54 | }
55 | }
--------------------------------------------------------------------------------
/test/Superpower.Benchmarks/ArithmeticExpressionBenchmark.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq.Expressions;
3 | using BenchmarkDotNet.Attributes;
4 | using BenchmarkDotNet.Running;
5 | using Sprache;
6 | using Superpower.Benchmarks.ArithmeticExpressionScenario;
7 | using Superpower.Model;
8 | using Xunit;
9 |
10 | namespace Superpower.Benchmarks
11 | {
12 | [MemoryDiagnoser]
13 | public class ArithmeticExpressionBenchmark
14 | {
15 | // This benchmark includes construction of the input, and unwrapping of results, while
16 | // NumberListBenchmark does not.
17 |
18 | static readonly ArithmeticExpressionTokenizer Tokenizer = new ArithmeticExpressionTokenizer();
19 | const string Expression = "123 + 456 * 123 - 456 / 123 + 456 * 123 - 456 / 123 + 456 * 123 - 456 / 123 + 456 * 123 - 456 / 123 + 456 * 123 - 456";
20 | static readonly TokenList Tokens = Tokenizer.Tokenize(Expression);
21 | const int ExpectedValue = 280095;
22 |
23 | [Fact]
24 | public void Verify()
25 | {
26 | Assert.Equal(ExpectedValue, SpracheText().Compile()());
27 | Assert.Equal(ExpectedValue, SuperpowerTokenListParser().Compile()());
28 | Assert.Equal(ExpectedValue, SuperpowerComplete().Compile()());
29 | }
30 |
31 | [Fact]
32 | public void Benchmark()
33 | {
34 | BenchmarkRunner.Run();
35 | }
36 |
37 | [Benchmark(Baseline=true)]
38 | public Expression> SpracheText()
39 | {
40 | return SpracheArithmeticExpressionParser.Lambda.Parse(Expression);
41 | }
42 |
43 | [Benchmark]
44 | public TokenList SuperpowerTokenizer()
45 | {
46 | return Tokenizer.Tokenize(Expression);
47 | }
48 |
49 | [Benchmark]
50 | public Expression> SuperpowerTokenListParser()
51 | {
52 | return ArithmeticExpressionParser.Lambda.Parse(Tokens);
53 | }
54 |
55 | [Benchmark]
56 | public Expression> SuperpowerComplete()
57 | {
58 | return ArithmeticExpressionParser.Lambda.Parse(Tokenizer.Tokenize(Expression));
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/test/Superpower.Tests/ComplexTokenScenario/SExpressionXTokenizer.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Parsers;
2 | using Superpower.Model;
3 | using System.Collections.Generic;
4 |
5 | namespace Superpower.Tests.ComplexTokenScenario
6 | {
7 | class SExpressionXTokenizer : Tokenizer
8 | {
9 | protected override IEnumerable> Tokenize(TextSpan span)
10 | {
11 | var next = SkipWhiteSpace(span);
12 | if (!next.HasValue)
13 | yield break;
14 |
15 | do
16 | {
17 | if (next.Value == '(')
18 | {
19 | yield return Result.Value(new SExpressionXToken(SExpressionType.LParen), next.Location, next.Remainder);
20 | next = next.Remainder.ConsumeChar();
21 | }
22 | else if (next.Value == ')')
23 | {
24 | yield return Result.Value(new SExpressionXToken(SExpressionType.RParen), next.Location, next.Remainder);
25 | next = next.Remainder.ConsumeChar();
26 | }
27 | else if (next.Value >= '0' && next.Value <= '9')
28 | {
29 | var integer = Numerics.IntegerInt32(next.Location);
30 | next = integer.Remainder.ConsumeChar();
31 |
32 | yield return Result.Value(new SExpressionXToken(integer.Value), integer.Location, integer.Remainder);
33 |
34 | if (next.HasValue && !char.IsPunctuation(next.Value) && !char.IsWhiteSpace(next.Value))
35 | {
36 | yield return Result.Empty(next.Location, new[] {"whitespace", "punctuation"});
37 | }
38 | }
39 | else
40 | {
41 | var beginIdentifier = next.Location;
42 | while (next.HasValue && char.IsLetterOrDigit(next.Value))
43 | {
44 | next = next.Remainder.ConsumeChar();
45 | }
46 |
47 | yield return Result.Value(new SExpressionXToken(SExpressionType.Atom), beginIdentifier, next.Location);
48 | }
49 |
50 | next = SkipWhiteSpace(next.Location);
51 | } while (next.HasValue);
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/test/Superpower.Tests/Combinators/OrCombinatorTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Superpower.Tests.Support;
6 | using Xunit;
7 | using Superpower.Parsers;
8 |
9 | namespace Superpower.Tests.Combinators
10 | {
11 | public class OrCombinatorTests
12 | {
13 | [Fact]
14 | public void OrFailsWithNone()
15 | {
16 | AssertParser.Fails(Character.EqualTo('a').Or(Character.EqualTo('b')), "");
17 | }
18 |
19 | [Fact]
20 | public void OrFailsWithUnmatched()
21 | {
22 | AssertParser.Fails(Character.EqualTo('a').Or(Character.EqualTo('b')), "c");
23 | }
24 |
25 | [Fact]
26 | public void OrSucceedsWithFirstMatch()
27 | {
28 | AssertParser.SucceedsWith(Character.EqualTo('a').Or(Character.EqualTo('b')), "a", 'a');
29 | }
30 |
31 | [Fact]
32 | public void OrSucceedsWithSecondMatch()
33 | {
34 | AssertParser.SucceedsWith(Character.EqualTo('a').Or(Character.EqualTo('b')), "b", 'b');
35 | }
36 |
37 | [Fact]
38 | public void OrFailsWithPartialFirstMatch()
39 | {
40 | AssertParser.Fails(Character.EqualTo('a').Then(_ => Character.EqualTo('b')).Or(Character.EqualTo('a')), "a");
41 | }
42 |
43 | [Fact]
44 | public void TokenOrFailsWithNone()
45 | {
46 | AssertParser.Fails(Token.EqualTo('a').Or(Token.EqualTo('b')), "");
47 | }
48 |
49 | [Fact]
50 | public void TokenOrFailsWithUnmatched()
51 | {
52 | AssertParser.Fails(Token.EqualTo('a').Or(Token.EqualTo('b')), "c");
53 | }
54 |
55 | [Fact]
56 | public void TokenOrSucceedsWithFirstMatch()
57 | {
58 | AssertParser.SucceedsWith(Token.EqualTo('a').Or(Token.EqualTo('b')), "a", 'a');
59 | }
60 |
61 | [Fact]
62 | public void TokenOrSucceedsWithSecondMatch()
63 | {
64 | AssertParser.SucceedsWith(Token.EqualTo('a').Or(Token.EqualTo('b')), "b", 'b');
65 | }
66 |
67 | [Fact]
68 | public void TokenOrFailsWithPartialFirstMatch()
69 | {
70 | AssertParser.Fails(Token.EqualTo('a').Then(_ => Token.EqualTo('b')).Or(Token.EqualTo('a')), "a");
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/Superpower/Model/Token`1.cs:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | namespace Superpower.Model
16 | {
17 | ///
18 | /// A token.
19 | ///
20 | /// The type of the token's kind.
21 | public struct Token
22 | {
23 | ///
24 | /// The kind of the token.
25 | ///
26 | public TKind Kind { get; }
27 |
28 | ///
29 | /// The string span containing the value of the token.
30 | ///
31 | public TextSpan Span { get; }
32 |
33 | ///
34 | /// Get the string value of the token.
35 | ///
36 | /// The token as a string.
37 | public string ToStringValue() => Span.ToStringValue();
38 |
39 | ///
40 | /// The position of the token within the source string.
41 | ///
42 | public Position Position => Span.Position;
43 |
44 | ///
45 | /// True if the token has a value.
46 | ///
47 | public bool HasValue => Span != TextSpan.None;
48 |
49 | ///
50 | /// Construct a token.
51 | ///
52 | /// The kind of the token.
53 | /// The span holding the token's value.
54 | public Token(TKind kind, TextSpan span)
55 | {
56 | Kind = kind;
57 | Span = span;
58 | }
59 |
60 | ///
61 | /// A token with no value.
62 | ///
63 | public static Token Empty { get; } = default;
64 |
65 | ///
66 | public override string ToString()
67 | {
68 | if (!HasValue)
69 | return "(empty token)";
70 |
71 | return $"{Kind}@{Position}: {Span}";
72 | }
73 | }
74 | }
--------------------------------------------------------------------------------
/sample/IntCalc/ArithmeticExpressionParser.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Parsers;
2 | using Superpower;
3 | using System;
4 | using System.Linq.Expressions;
5 |
6 | namespace IntCalc
7 | {
8 | class ArithmeticExpressionParser
9 | {
10 | static TokenListParser Operator(ArithmeticExpressionToken op, ExpressionType opType)
11 | {
12 | return Token.EqualTo(op).Value(opType);
13 | }
14 |
15 | static readonly TokenListParser Add = Operator(ArithmeticExpressionToken.Plus, ExpressionType.AddChecked);
16 | static readonly TokenListParser Subtract = Operator(ArithmeticExpressionToken.Minus, ExpressionType.SubtractChecked);
17 | static readonly TokenListParser Multiply = Operator(ArithmeticExpressionToken.Times, ExpressionType.MultiplyChecked);
18 | static readonly TokenListParser Divide = Operator(ArithmeticExpressionToken.Divide, ExpressionType.Divide);
19 |
20 | static readonly TokenListParser Constant =
21 | Token.EqualTo(ArithmeticExpressionToken.Number)
22 | .Apply(Numerics.IntegerInt32)
23 | .Select(n => (Expression)Expression.Constant(n));
24 |
25 | static readonly TokenListParser Factor =
26 | (from lparen in Token.EqualTo(ArithmeticExpressionToken.LParen)
27 | from expr in Parse.Ref(() => Expr!)
28 | from rparen in Token.EqualTo(ArithmeticExpressionToken.RParen)
29 | select expr)
30 | .Or(Constant);
31 |
32 | static readonly TokenListParser Operand =
33 | (from sign in Token.EqualTo(ArithmeticExpressionToken.Minus)
34 | from factor in Factor
35 | select (Expression)Expression.Negate(factor))
36 | .Or(Factor).Named("expression");
37 |
38 | static readonly TokenListParser Term = Parse.Chain(Multiply.Or(Divide), Operand, Expression.MakeBinary);
39 |
40 | static readonly TokenListParser Expr = Parse.Chain(Add.Or(Subtract), Term, Expression.MakeBinary);
41 |
42 | public static readonly TokenListParser>> Lambda =
43 | Expr
44 | .AtEnd()
45 | .Select(body => Expression.Lambda>(body));
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/test/Superpower.Tests/ArithmeticExpressionScenario/ArithmeticExpressionParser.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Parsers;
2 | using System;
3 | using System.Linq.Expressions;
4 |
5 | namespace Superpower.Tests.ArithmeticExpressionScenario
6 | {
7 | class ArithmeticExpressionParser
8 | {
9 | static TokenListParser Operator(ArithmeticExpressionToken op, ExpressionType opType)
10 | {
11 | return Token.EqualTo(op).Value(opType);
12 | }
13 |
14 | static readonly TokenListParser Add = Operator(ArithmeticExpressionToken.Plus, ExpressionType.AddChecked);
15 | static readonly TokenListParser Subtract = Operator(ArithmeticExpressionToken.Minus, ExpressionType.SubtractChecked);
16 | static readonly TokenListParser Multiply = Operator(ArithmeticExpressionToken.Times, ExpressionType.MultiplyChecked);
17 | static readonly TokenListParser Divide = Operator(ArithmeticExpressionToken.Divide, ExpressionType.Divide);
18 |
19 | static readonly TokenListParser Constant =
20 | Token.EqualTo(ArithmeticExpressionToken.Number)
21 | .Apply(Numerics.IntegerInt32)
22 | .Select(n => (Expression)Expression.Constant(n));
23 |
24 | static readonly TokenListParser Factor =
25 | (from lparen in Token.EqualTo(ArithmeticExpressionToken.LParen)
26 | from expr in Parse.Ref(() => Expr!)
27 | from rparen in Token.EqualTo(ArithmeticExpressionToken.RParen)
28 | select expr)
29 | .Or(Constant);
30 |
31 | static readonly TokenListParser Operand =
32 | (from sign in Token.EqualTo(ArithmeticExpressionToken.Minus)
33 | from factor in Factor
34 | select (Expression)Expression.Negate(factor))
35 | .Or(Factor).Named("expression");
36 |
37 | static readonly TokenListParser Term = Parse.Chain(Multiply.Or(Divide), Operand, Expression.MakeBinary);
38 |
39 | static readonly TokenListParser Expr = Parse.Chain(Add.Or(Subtract), Term, Expression.MakeBinary);
40 |
41 | public static readonly TokenListParser>> Lambda =
42 | Expr
43 | .AtEnd()
44 | .Select(body => Expression.Lambda>(body));
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/test/Superpower.Benchmarks/ArithmeticExpressionScenario/ArithmeticExpressionParser.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Parsers;
2 | using System;
3 | using System.Linq.Expressions;
4 |
5 | namespace Superpower.Benchmarks.ArithmeticExpressionScenario
6 | {
7 | static class ArithmeticExpressionParser
8 | {
9 | static TokenListParser Operator(ArithmeticExpressionToken op, ExpressionType opType)
10 | {
11 | return Token.EqualTo(op).Value(opType);
12 | }
13 |
14 | static readonly TokenListParser Add = Operator(ArithmeticExpressionToken.Plus, ExpressionType.AddChecked);
15 | static readonly TokenListParser Subtract = Operator(ArithmeticExpressionToken.Minus, ExpressionType.SubtractChecked);
16 | static readonly TokenListParser Multiply = Operator(ArithmeticExpressionToken.Times, ExpressionType.MultiplyChecked);
17 | static readonly TokenListParser Divide = Operator(ArithmeticExpressionToken.Divide, ExpressionType.Divide);
18 |
19 | static readonly TokenListParser Constant =
20 | Token.EqualTo(ArithmeticExpressionToken.Number)
21 | .Apply(Numerics.IntegerInt32)
22 | .Select(n => (Expression)Expression.Constant(n));
23 |
24 | static readonly TokenListParser Factor =
25 | (from lparen in Token.EqualTo(ArithmeticExpressionToken.LParen)
26 | from expr in Parse.Ref(() => Expr!)
27 | from rparen in Token.EqualTo(ArithmeticExpressionToken.RParen)
28 | select expr)
29 | .Or(Constant);
30 |
31 | static readonly TokenListParser Operand =
32 | (from sign in Token.EqualTo(ArithmeticExpressionToken.Minus)
33 | from factor in Factor
34 | select (Expression)Expression.Negate(factor))
35 | .Or(Factor).Named("expression");
36 |
37 | static readonly TokenListParser Term = Parse.Chain(Multiply.Or(Divide), Operand, Expression.MakeBinary);
38 |
39 | static readonly TokenListParser Expr = Parse.Chain(Add.Or(Subtract), Term, Expression.MakeBinary);
40 |
41 | public static readonly TokenListParser>> Lambda =
42 | Expr
43 | .AtEnd()
44 | .Select(body => Expression.Lambda>(body));
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/test/Superpower.Tests/Combinators/TryCombinatorTests.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Parsers;
2 | using Superpower.Tests.Support;
3 | using Xunit;
4 |
5 | namespace Superpower.Tests.Combinators
6 | {
7 | public class TryCombinatorTests
8 | {
9 | [Fact]
10 | public void TryFailureConsumesNoInput()
11 | {
12 | var tryAb = Character.EqualTo('a').Then(_ => Character.EqualTo('b')).Try();
13 | var result = tryAb.TryParse("ac");
14 | Assert.False(result.HasValue);
15 | Assert.True(result.Backtrack);
16 | }
17 |
18 | [Fact]
19 | public void TrySuccessIsTransparent()
20 | {
21 | var tryAb = Character.EqualTo('a').Then(_ => Character.EqualTo('b')).Try();
22 | var result = tryAb.TryParse("ab");
23 | Assert.True(result.HasValue);
24 | Assert.True(result.Remainder.IsAtEnd);
25 | }
26 |
27 | [Fact]
28 | public void TryItemMakesManyBacktrack()
29 | {
30 | var ab = Character.EqualTo('a').Then(_ => Character.EqualTo('b'));
31 | var list = ab.Try().Many();
32 | AssertParser.SucceedsWithMany(list, "ababa", "bb".ToCharArray());
33 | }
34 |
35 | [Fact]
36 | public void TryAlternativeMakesOrBacktrack()
37 | {
38 | var tryAOrAB = Character.EqualTo('a').Then(_ => Character.EqualTo('b')).Try().Or(Character.EqualTo('a'));
39 | AssertParser.SucceedsWith(tryAOrAB, "a", 'a');
40 | }
41 |
42 | [Fact]
43 | public void TokenTryFailureBacktracks()
44 | {
45 | var tryAb = Token.EqualTo('a').Then(_ => Token.EqualTo('b')).Try();
46 | var result = tryAb.TryParse(StringAsCharTokenList.Tokenize("ac"));
47 | Assert.False(result.HasValue);
48 | Assert.True(result.Backtrack);
49 | }
50 |
51 | [Fact]
52 | public void TokenTrySuccessIsTransparent()
53 | {
54 | var tryAb = Token.EqualTo('a').Then(_ => Token.EqualTo('b')).Try();
55 | var result = tryAb.TryParse(StringAsCharTokenList.Tokenize("ab"));
56 | Assert.True(result.HasValue);
57 | Assert.True(result.Remainder.IsAtEnd);
58 | }
59 |
60 | [Fact]
61 | public void TokenTryItemMakesManyBacktrack()
62 | {
63 | var ab = Token.EqualTo('a').Then(_ => Token.EqualTo('b'));
64 | var list = ab.Try().Many();
65 | AssertParser.SucceedsWithMany(list, "ababa", "bb".ToCharArray());
66 | }
67 |
68 | [Fact]
69 | public void TokenTryAlternativeMakesOrBacktrack()
70 | {
71 | var tryAOrAB = Token.EqualTo('a').Then(_ => Token.EqualTo('b')).Try().Or(Token.EqualTo('a'));
72 | AssertParser.SucceedsWith(tryAOrAB, "a", 'a');
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/test/Superpower.Tests/Combinators/ChainCombinatorTests.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using Superpower.Parsers;
3 | using Superpower.Tests.Support;
4 | using Xunit;
5 |
6 | namespace Superpower.Tests.Combinators
7 | {
8 | public class ChainCombinatorTests
9 | {
10 | [Fact]
11 | public void SuccessWithLongChains()
12 | {
13 | const int chainLength = 5000;
14 | string input = string.Join("+", Enumerable.Repeat("1", chainLength));
15 | var chainParser = Parse.Chain(
16 | Character.EqualTo('+'),
17 | Numerics.IntegerInt32,
18 | (opr, val1, val2) => val1 + val2);
19 |
20 | AssertParser.SucceedsWith(chainParser, input, chainLength);
21 | }
22 |
23 | [Fact]
24 | public void TokenSuccessWithLongChains()
25 | {
26 | const int chainLength = 5000;
27 | string input = string.Join("+", Enumerable.Repeat("1", chainLength));
28 |
29 | var chainParser = Parse.Chain(
30 | Token.EqualTo('+'),
31 | Token.EqualTo('1').Value(1),
32 | (opr, val1, val2) => val1 + val2);
33 |
34 | AssertParser.SucceedsWith(chainParser, input, chainLength);
35 | }
36 |
37 | [Fact]
38 | public void ChainFailWithMultiTokenOperator()
39 | {
40 | // Addition is represented with operator '++'
41 | // If we only have one '+', ensure we get error
42 | var nPlusPlusN = Parse.Chain(
43 | Character.EqualTo('+').IgnoreThen(Character.EqualTo('+')),
44 | Numerics.IntegerInt32,
45 | (opr, val1, val2) => val1 + val2);
46 |
47 | AssertParser.FailsAt(nPlusPlusN, "1+1", 2);
48 | }
49 |
50 | [Fact]
51 | public void TokenChainFailWithMultiTokenOperator()
52 | {
53 | // Addition is represented with operator '++'
54 | // If we only have one '+', ensure we get error
55 | var nPlusPlusN = Parse.Chain(
56 | Token.EqualTo('+').IgnoreThen(Token.EqualTo('+')),
57 | Token.EqualTo('1').Value(1),
58 | (opr, val1, val2) => val1 + val2);
59 |
60 | AssertParser.FailsAt(nPlusPlusN, "1+1", 2);
61 | }
62 |
63 | [Fact]
64 | public void SuccessLeftAssociativeChain()
65 | {
66 | const string input = "A.1.2.3";
67 | var seed = Character.EqualTo('A').Select(i => System.Collections.Immutable.ImmutableList.Create());
68 | var chainParser = seed.Chain(
69 | Character.EqualTo('.'),
70 | Numerics.IntegerInt32,
71 | (o, r, i) => r.Add(i));
72 |
73 | AssertParser.SucceedsWith(chainParser, input, System.Collections.Immutable.ImmutableList.Create(1, 2, 3));
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/test/Superpower.Tests/Combinators/ManyCombinatorTests.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Parsers;
2 | using Superpower.Tests.Support;
3 | using Xunit;
4 |
5 | namespace Superpower.Tests.Combinators
6 | {
7 | public class ManyCombinatorTests
8 | {
9 | [Fact]
10 | public void ManySucceedsWithNone()
11 | {
12 | AssertParser.SucceedsWithAll(Character.EqualTo('a').Many(), "");
13 | }
14 |
15 | [Fact]
16 | public void ManySucceedsWithOne()
17 | {
18 | AssertParser.SucceedsWithAll(Character.EqualTo('a').Many(), "a");
19 | }
20 |
21 | [Fact]
22 | public void ManySucceedsWithTwo()
23 | {
24 | AssertParser.SucceedsWithAll(Character.EqualTo('a').Many(), "aa");
25 | }
26 |
27 | [Fact]
28 | public void ManyFailsWithPartialItemMatch()
29 | {
30 | var ab = Character.EqualTo('a').Then(_ => Character.EqualTo('b'));
31 | var list = ab.Many();
32 | AssertParser.Fails(list, "ababa");
33 | }
34 |
35 | [Fact]
36 | public void TokenManySucceedsWithNone()
37 | {
38 | AssertParser.SucceedsWithAll(Token.EqualTo('a').Many(), "");
39 | }
40 |
41 | [Fact]
42 | public void TokenManySucceedsWithOne()
43 | {
44 | AssertParser.SucceedsWithAll(Token.EqualTo('a').Many(), "a");
45 | }
46 |
47 | [Fact]
48 | public void TokenManySucceedsWithTwo()
49 | {
50 | AssertParser.SucceedsWithAll(Token.EqualTo('a').Many(), "aa");
51 | }
52 |
53 | [Fact]
54 | public void TokenManyFailsWithPartialItemMatch()
55 | {
56 | var ab = Token.EqualTo('a').Then(_ => Token.EqualTo('b'));
57 | var list = ab.Many();
58 | AssertParser.Fails(list, "ababa");
59 | }
60 |
61 | [Fact]
62 | public void ManySucceedsWithBacktrackedPartialItemMatch()
63 | {
64 | var ab = Character.EqualTo('a').Then(_ => Character.EqualTo('b'));
65 | var ac = Character.EqualTo('a').Then(_ => Character.EqualTo('c'));
66 | var list = Span.MatchedBy(ab.Try().Many().Then(_ => ac));
67 | AssertParser.SucceedsWithAll(list, "ababac");
68 | }
69 |
70 | [Fact]
71 | public void ManyReportsCorrectErrorPositionForNonBacktrackingPartialItemMatch()
72 | {
73 | var ab = Character.EqualTo('a').Then(_ => Character.EqualTo('b'));
74 | var list = ab.Many();
75 | AssertParser.FailsWithMessage(list, "ababac", "Syntax error (line 1, column 6): unexpected `c`, expected `b`.");
76 | }
77 |
78 | [Fact]
79 | public void ManyReportsCorrectRemainderForBacktrackingPartialItemMatch()
80 | {
81 | var ab = Character.EqualTo('a').Then(_ => Character.EqualTo('b'));
82 | var list = Span.MatchedBy(ab.Try().Many()).Select(s => s.ToStringValue());
83 | AssertParser.SucceedsWith(list, "ababac", "abab");
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/test/Superpower.Benchmarks/NumberListBenchmark.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using BenchmarkDotNet.Attributes;
3 | using BenchmarkDotNet.Running;
4 | using Sprache;
5 | using Superpower.Parsers;
6 | using Superpower.Model;
7 | using Superpower.Benchmarks.NumberListScenario;
8 | using Xunit;
9 |
10 | namespace Superpower.Benchmarks
11 | {
12 | [MemoryDiagnoser]
13 | public class NumberListBenchmark
14 | {
15 | public const int NumbersLength = 1000;
16 | static readonly string Numbers = string.Join(" ", Enumerable.Range(0, NumbersLength));
17 | static readonly Input SpracheInput = new Input(Numbers);
18 | static readonly TextSpan SuperpowerTextSpan = new TextSpan(Numbers);
19 |
20 | static void AssertComplete(int[] numbers)
21 | {
22 | Assert.Equal(NumbersLength, numbers.Length);
23 | for (var i = 0; i < NumbersLength; ++i)
24 | Assert.Equal(i, numbers[i]);
25 | }
26 |
27 | [Fact]
28 | public void Verify()
29 | {
30 | AssertComplete(StringSplitAndInt32Parse());
31 | AssertComplete(SpracheText().Value);
32 | AssertComplete(SuperpowerText().Value);
33 | AssertComplete(SuperpowerToken().Value);
34 | }
35 |
36 | [Fact]
37 | public void Benchmark()
38 | {
39 | BenchmarkRunner.Run();
40 | }
41 |
42 | [Benchmark(Baseline = true)]
43 | public int[] StringSplitAndInt32Parse()
44 | {
45 | var tokens = Numbers.Split(' ');
46 | var numbers = new int[tokens.Length];
47 | for(var i = 0; i < tokens.Length; ++i)
48 | {
49 | numbers[i] = int.Parse(tokens[i]);
50 | }
51 |
52 | return numbers;
53 | }
54 |
55 | static readonly Parser SpracheParser =
56 | Sprache.Parse.Number.Token()
57 | .Select(int.Parse)
58 | .Many()
59 | .Select(n => n.ToArray());
60 |
61 | [Benchmark]
62 | public IResult SpracheText()
63 | {
64 | return SpracheParser(SpracheInput);
65 | }
66 |
67 | static readonly TextParser SuperpowerTextParser =
68 | Span.WhiteSpace.Optional()
69 | .IgnoreThen(Numerics.IntegerInt32)
70 | .Many()
71 | .AtEnd();
72 |
73 | [Benchmark]
74 | public Result SuperpowerText()
75 | {
76 | return SuperpowerTextParser(SuperpowerTextSpan);
77 | }
78 |
79 | static readonly TokenListParser SuperpowerTokenListParser =
80 | Token.EqualTo(NumberListToken.Number)
81 | .Apply(Numerics.IntegerInt32) // Slower that int.Parse(), but worth benchmarking
82 | .Many()
83 | .AtEnd();
84 |
85 | [Benchmark]
86 | public TokenListParserResult SuperpowerToken()
87 | {
88 | return SuperpowerTokenListParser(NumberListTokenizer.Instance.Tokenize(Numbers));
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/Superpower/Model/Position.cs:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | using System;
16 |
17 | namespace Superpower.Model
18 | {
19 | ///
20 | /// A position within a stream of character input.
21 | ///
22 | public readonly struct Position
23 | {
24 | ///
25 | /// The zero-based absolute index of the position.
26 | ///
27 | public int Absolute { get; }
28 |
29 | ///
30 | /// The one-based line number.
31 | ///
32 | public int Line { get; }
33 |
34 | ///
35 | /// The one-based column number.
36 | ///
37 | public int Column { get; }
38 |
39 | ///
40 | /// Construct a position.
41 | ///
42 | /// The absolute position.
43 | /// The line number.
44 | /// The column number.
45 | public Position(int absolute, int line, int column)
46 | {
47 | #if CHECKED
48 | if (absolute < 0) throw new ArgumentOutOfRangeException(nameof(line), "Absolute positions start at 0.");
49 | if (line < 1) throw new ArgumentOutOfRangeException(nameof(line), "Line numbering starts at 1.");
50 | if (column < 1) throw new ArgumentOutOfRangeException(nameof(column), "Column numbering starts at 1.");
51 | #endif
52 | Absolute = absolute;
53 | Line = line;
54 | Column = column;
55 | }
56 |
57 | ///
58 | /// The position corresponding to the zero index.
59 | ///
60 | public static Position Zero { get; } = new Position(0, 1, 1);
61 |
62 | ///
63 | /// A position with no value.
64 | ///
65 | public static Position Empty { get; } = default;
66 |
67 | ///
68 | /// True if the position has a value.
69 | ///
70 | public bool HasValue => Line > 0;
71 |
72 | ///
73 | /// Advance over , advancing line and column numbers
74 | /// as appropriate.
75 | ///
76 | /// The character being advanced over.
77 | /// The updated position.
78 | public Position Advance(char overChar)
79 | {
80 | if (overChar == '\n')
81 | return new Position(Absolute + 1, Line + 1, 1);
82 |
83 | return new Position(Absolute + 1, Line, Column + 1);
84 | }
85 |
86 | ///
87 | public override string ToString()
88 | {
89 | return $"{Absolute} (line {Line}, column {Column})";
90 | }
91 | }
92 | }
--------------------------------------------------------------------------------
/src/Superpower/ParseException.cs:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | using System;
16 | using Superpower.Model;
17 |
18 | // ReSharper disable IntroduceOptionalParameters.Global, MemberCanBePrivate.Global, UnusedAutoPropertyAccessor.Global
19 |
20 | namespace Superpower
21 | {
22 | ///
23 | /// Represents an error that occurs during parsing.
24 | ///
25 | public class ParseException : Exception
26 | {
27 | ///
28 | /// Initializes a new instance of the class with a default error message.
29 | ///
30 | public ParseException() : this("Parsing failed.", Position.Empty, null) { }
31 |
32 | ///
33 | /// Initializes a new instance of the class with a specified error message.
34 | ///
35 | /// The message that describes the error.
36 | public ParseException(string message) : this(message, Position.Empty, null)
37 | {
38 | }
39 |
40 | ///
41 | /// Initializes a new instance of the class with a specified error message.
42 | ///
43 | /// The message that describes the error.
44 | /// The exception that is the cause of the current exception.
45 | public ParseException(string message, Exception? innerException) : this(message, Position.Empty, innerException)
46 | {
47 | }
48 |
49 | ///
50 | /// Initializes a new instance of the class with a specified error message.
51 | ///
52 | /// The message that describes the error.
53 | /// The position of the error in the input text.
54 | public ParseException(string message, Position errorPosition) : this(message, errorPosition, null) { }
55 |
56 | ///
57 | /// Initializes a new instance of the class with a specified error message.
58 | ///
59 | /// The message that describes the error.
60 | /// The position of the error in the input text.
61 | /// The exception that is the cause of the current exception.
62 | public ParseException(string message, Position errorPosition, Exception? innerException) : base(message, innerException)
63 | {
64 | ErrorPosition = errorPosition;
65 | }
66 |
67 | ///
68 | /// The position of the error in the input text, or if no position is specified.
69 | ///
70 | public Position ErrorPosition { get; }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/test/Superpower.Tests/Combinators/SequenceCombinatorTests.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Model;
2 | using Superpower.Parsers;
3 | using Superpower.Tests.Support;
4 | using Xunit;
5 |
6 | namespace Superpower.Tests.Combinators
7 | {
8 | public class SequenceCombinatorTests
9 | {
10 | [Fact]
11 | public void Sequence2Succeeds()
12 | {
13 | var p = Parse.Sequence(Character.Digit, Character.AnyChar);
14 | AssertParser.SucceedsWithAll(Span.MatchedBy(p), "1w");
15 | }
16 |
17 | [Fact]
18 | public void Sequence3Succeeds()
19 | {
20 | var p = Parse.Sequence(Character.Digit, Character.AnyChar, Character.Upper);
21 | AssertParser.SucceedsWithAll(Span.MatchedBy(p), "1wU");
22 | }
23 |
24 | [Fact]
25 | public void Sequence4Succeeds()
26 | {
27 | var p = Parse.Sequence(Character.Digit, Character.AnyChar, Character.Upper, Character.Letter);
28 | AssertParser.SucceedsWithAll(Span.MatchedBy(p), "1wUh");
29 | }
30 |
31 | [Fact]
32 | public void Sequence5Succeeds()
33 | {
34 | var p = Parse.Sequence(Character.Digit, Character.AnyChar, Character.Upper, Character.Letter, Character.Lower);
35 | AssertParser.SucceedsWithAll(Span.MatchedBy(p), "1wUhh");
36 | }
37 |
38 | [Fact]
39 | public void Sequence3ReportsCorrectErrorPosition()
40 | {
41 | var p = Parse.Sequence(Character.Digit, Character.AnyChar, Character.Upper);
42 | AssertParser.FailsAt(Span.MatchedBy(p), "1w1g", 2);
43 | }
44 |
45 | [Fact]
46 | public void Sequence2TokenSucceeds()
47 | {
48 | // Issue - explicit tuple argument types are needed here; see:
49 | // https://github.com/dotnet/csharplang/issues/258
50 | // Keeping this instance as an example, but using the "cleaner" .Item1, .Item2 syntax below.
51 | var p = Parse.Sequence(Token.EqualTo('1'), Token.EqualTo('w'))
52 | .Select(((Token a, Token b) t) => new []{t.a, t.b});
53 |
54 | AssertParser.SucceedsWithAll(p, "1w");
55 | }
56 |
57 | [Fact]
58 | public void Sequence3TokenSucceeds()
59 | {
60 | var p = Parse.Sequence(Token.EqualTo('1'), Token.EqualTo('w'), Token.EqualTo('U'))
61 | .Select(t => new []{t.Item1, t.Item2, t.Item3});
62 | AssertParser.SucceedsWithAll(p, "1wU");
63 | }
64 |
65 | [Fact]
66 | public void Sequence4TokenSucceeds()
67 | {
68 | var p = Parse.Sequence(Token.EqualTo('1'), Token.EqualTo('w'), Token.EqualTo('U'), Token.EqualTo('h'))
69 | .Select(t => new []{t.Item1, t.Item2, t.Item3, t.Item4});
70 | AssertParser.SucceedsWithAll(p, "1wUh");
71 | }
72 |
73 | [Fact]
74 | public void Sequence5TokenSucceeds()
75 | {
76 | var p = Parse.Sequence(Token.EqualTo('1'), Token.EqualTo('w'), Token.EqualTo('U'), Token.EqualTo('h'), Token.EqualTo('h'))
77 | .Select(t => new []{t.Item1, t.Item2, t.Item3, t.Item4, t.Item5});
78 | AssertParser.SucceedsWithAll(p, "1wUhh");
79 | }
80 |
81 | [Fact]
82 | public void Sequence3TokenReportsCorrectErrorPosition()
83 | {
84 | var p = Parse.Sequence(Token.EqualTo('1'), Token.EqualTo('w'), Token.EqualTo('U'))
85 | .Select(t => new []{t.Item1, t.Item2, t.Item3});
86 | AssertParser.FailsAt(p, "1w1g", 2);
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/test/Superpower.Tests/Tokenizers/TokenizerBuilderTests.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using Superpower.Parsers;
3 | using Superpower.Tests.SExpressionScenario;
4 | using Superpower.Tokenizers;
5 | using Xunit;
6 |
7 | namespace Superpower.Tests.Tokenizers
8 | {
9 | public class TokenizerBuilderTests
10 | {
11 | [Fact]
12 | public void SExpressionsCanBeTokenized()
13 | {
14 | var tokenizer = new TokenizerBuilder()
15 | .Ignore(Span.WhiteSpace)
16 | .Match(Character.EqualTo('('), SExpressionToken.LParen)
17 | .Match(Character.EqualTo(')'), SExpressionToken.RParen)
18 | .Match(Numerics.Integer, SExpressionToken.Number, requireDelimiters: true)
19 | .Match(Character.Letter.IgnoreThen(Character.LetterOrDigit.AtLeastOnce()), SExpressionToken.Atom, requireDelimiters: true)
20 | .Ignore(Comment.ShellStyle)
21 | .Build();
22 |
23 | var tokens = tokenizer.TryTokenize("abc (123 def) # this is a comment");
24 | Assert.True(tokens.HasValue);
25 | Assert.Equal(5, tokens.Value.Count());
26 | }
27 |
28 | [Fact]
29 | public void KeywordsRequireDelimiters()
30 | {
31 | var tokenizer = new TokenizerBuilder()
32 | .Ignore(Span.WhiteSpace)
33 | .Match(Span.EqualTo("is"), true, requireDelimiters: true)
34 | .Match(Character.Letter.AtLeastOnce(), false, requireDelimiters: true)
35 | .Build();
36 |
37 | var tokens = tokenizer.TryTokenize("is isnot is notis ins not is");
38 | Assert.True(tokens.HasValue);
39 | Assert.Equal(7, tokens.Value.Count());
40 | Assert.Equal(3, tokens.Value.Count(v => v.Kind));
41 | }
42 |
43 | [Fact]
44 | public void PartiallyFailedTokenizationIsReported()
45 | {
46 | var tokenizer = new TokenizerBuilder()
47 | .Ignore(Span.WhiteSpace)
48 | .Match(Span.EqualTo("abc"), "abc")
49 | .Match(Span.EqualTo("def"), "def")
50 | .Build();
51 |
52 | var tokens = tokenizer.TryTokenize(" abd");
53 | Assert.False(tokens.HasValue);
54 | var msg = tokens.ToString();
55 | Assert.Equal("Syntax error (line 1, column 2): invalid abc, unexpected `d`, expected `c` at line 1, column 4.", msg);
56 | }
57 |
58 | [Fact]
59 | public void ShortTokenizationIsReported()
60 | {
61 | var tokenizer = new TokenizerBuilder()
62 | .Ignore(Span.WhiteSpace)
63 | .Match(Span.EqualTo("abc"), "abc")
64 | .Match(Span.EqualTo("def"), "def")
65 | .Build();
66 |
67 | var tokens = tokenizer.TryTokenize(" ab");
68 | Assert.False(tokens.HasValue);
69 | var msg = tokens.ToString();
70 | Assert.Equal("Syntax error (line 1, column 2): incomplete abc, unexpected end of input, expected `c`.", msg);
71 | }
72 |
73 | [Fact]
74 | public void InvalidDelimitedTokenAtEndIsReported()
75 | {
76 | var tokenizer = new TokenizerBuilder()
77 | .Match(Span.EqualTo("abc"), "abc", requireDelimiters: true)
78 | .Match(Character.LetterOrDigit.AtLeastOnce(), "lod", requireDelimiters: true)
79 | .Build();
80 |
81 | var tokens = tokenizer.TryTokenize("abc_");
82 | Assert.False(tokens.HasValue);
83 | }
84 | }
85 | }
--------------------------------------------------------------------------------
/test/Superpower.Tests/Parsers/NumericsTests.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Parsers;
2 | using Superpower.Tests.Support;
3 | using Xunit;
4 |
5 | namespace Superpower.Tests.Parsers
6 | {
7 | public class NumericsTests
8 | {
9 | [Theory]
10 | [InlineData("0", true)]
11 | [InlineData("01", true)]
12 | [InlineData("910", true)]
13 | [InlineData("-1", true)]
14 | [InlineData("+1", true)]
15 | [InlineData("1.1", false)]
16 | [InlineData("a", false)]
17 | [InlineData("", false)]
18 | [InlineData("\u0669\u0661\u0660", false)] // 910 in Arabic
19 | [InlineData("9\u0661\u0660", false)] // 9 in Latin then 10 in Arabic
20 | public void IntegersAreRecognized(string input, bool isMatch)
21 | {
22 | AssertParser.FitsTheory(Numerics.Integer, input, isMatch);
23 | }
24 |
25 | [Theory]
26 | [InlineData("0", true)]
27 | [InlineData("01", true)]
28 | [InlineData("910", true)]
29 | [InlineData("-1", false)]
30 | [InlineData("+1", false)]
31 | [InlineData("1.1", false)]
32 | [InlineData("a", false)]
33 | [InlineData("", false)]
34 | [InlineData("\u0669\u0661\u0660", false)] // 910 in Arabic
35 | [InlineData("9\u0661\u0660", false)] // 9 in Latin then 10 in Arabic
36 | public void NaturalNumbersAreRecognized(string input, bool isMatch)
37 | {
38 | AssertParser.FitsTheory(Numerics.Natural, input, isMatch);
39 | }
40 |
41 | [Theory]
42 | [InlineData("0", true)]
43 | [InlineData("-1", false)]
44 | [InlineData("910", true)]
45 | [InlineData("0x123", false)]
46 | [InlineData("a", true)]
47 | [InlineData("A", true)]
48 | [InlineData("0123456789abcdef", true)]
49 | [InlineData("g", false)]
50 | [InlineData("", false)]
51 | [InlineData("\u0669\u0661\u0660", false)] // 910 in Arabic
52 | [InlineData("9\u0661\u0660", false)] // 9 in Latin then 10 in Arabic
53 | public void HexDigitsAreRecognized(string input, bool isMatch)
54 | {
55 | AssertParser.FitsTheory(Numerics.HexDigits, input, isMatch);
56 | }
57 |
58 | [Theory]
59 | [InlineData("0", 0)]
60 | [InlineData("a", 0xa)]
61 | [InlineData("910", 0x910)]
62 | [InlineData("A", 0xA)]
63 | [InlineData("012345678", 0x12345678)]
64 | [InlineData("9abcdef", 0x9abcdef)]
65 | public void HexDigitsAreParsed(string input, uint value)
66 | {
67 | var parsed = Numerics.HexDigitsUInt32.Parse(input);
68 | Assert.Equal(value, parsed);
69 | }
70 |
71 | [Theory]
72 | [InlineData("0", true)]
73 | [InlineData("01", true)]
74 | [InlineData("910", true)]
75 | [InlineData("-1", true)]
76 | [InlineData("+1", true)]
77 | [InlineData("1.1", true)]
78 | [InlineData("-1.1", true)]
79 | [InlineData("a", false)]
80 | [InlineData("", false)]
81 | [InlineData("123.456", true)]
82 | [InlineData("123.+456", false)]
83 | [InlineData("123.", false)]
84 | [InlineData(".456", false)]
85 | [InlineData("-.456", false)]
86 | public void DecimalNumbersAreRecognized(string input, bool isMatch)
87 | {
88 | AssertParser.FitsTheory(Numerics.Decimal, input, isMatch);
89 | }
90 |
91 | [Fact]
92 | public void DecimalNumbersAreParsed()
93 | {
94 | var parsed = Numerics.DecimalDecimal.Parse("-123.456");
95 | Assert.Equal(-123.456m, parsed);
96 | }
97 |
98 | [Fact]
99 | public void DecimalDoublesAreParsed()
100 | {
101 | var parsed = Numerics.DecimalDouble.Parse("-123.456");
102 | Assert.Equal(-123.456, parsed);
103 | }
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/Superpower/Parsers/Comment.cs:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | using Superpower.Model;
16 |
17 | namespace Superpower.Parsers
18 | {
19 | ///
20 | /// Parsers for matching comments in various styles.
21 | ///
22 | public static class Comment
23 | {
24 | ///
25 | /// Parses a comment that begins with a specified pattern and continues to the end of the line.
26 | ///
27 | ///
28 | /// The comment span does not include the end-of-line characters that terminate it.
29 | ///
30 | /// Recognizes the beginning of the comment.
31 | /// The span covered by the comment.
32 | public static TextParser ToEndOfLine(TextParser beginComment)
33 | {
34 | return i =>
35 | {
36 | var begin = beginComment(i);
37 | if (!begin.HasValue)
38 | return begin;
39 |
40 | var remainder = begin.Remainder;
41 | while (!remainder.IsAtEnd)
42 | {
43 | var ch = remainder.ConsumeChar();
44 | if (ch.Value == '\r' || ch.Value == '\n')
45 | break;
46 |
47 | remainder = ch.Remainder;
48 | }
49 |
50 | return Result.Value(i.Until(remainder), i, remainder);
51 | };
52 | }
53 |
54 | ///
55 | /// Parses a C++ style comment, beginning with a double forward slash `//`
56 | /// and continuing to the end of the line.
57 | ///
58 | public static TextParser CPlusPlusStyle { get; } = ToEndOfLine(Span.EqualTo("//"));
59 |
60 | ///
61 | /// Parses a SQL style comment, beginning with a double dash `--`
62 | /// and continuing to the end of the line.
63 | ///
64 | public static TextParser SqlStyle { get; } = ToEndOfLine(Span.EqualTo("--"));
65 |
66 | ///
67 | /// Parses a shell style comment, beginning with a pound/hash `#` sign
68 | /// and continuing to the end of the line.
69 | ///
70 | public static TextParser ShellStyle { get; } = ToEndOfLine(Span.EqualTo("#"));
71 |
72 | ///
73 | /// Parses a C-style multiline comment beginning with `/*` and ending with `*/`.
74 | ///
75 | public static TextParser CStyle
76 | {
77 | get
78 | {
79 | var beginComment = Span.EqualTo("/*");
80 | var endComment = Span.EqualTo("*/");
81 | return i =>
82 | {
83 | var begin = beginComment(i);
84 | if (!begin.HasValue)
85 | return begin;
86 |
87 | var content = begin.Remainder;
88 | while (!content.IsAtEnd)
89 | {
90 | var end = endComment(content);
91 | if (end.HasValue)
92 | return Result.Value(i.Until(end.Remainder), i, end.Remainder);
93 |
94 | content = content.ConsumeChar().Remainder;
95 | }
96 |
97 | return endComment(content); // Will fail, because we're at the end-of-input.
98 | };
99 |
100 | }
101 | }
102 | }
103 | }
--------------------------------------------------------------------------------
/test/Superpower.Tests/Combinators/ApplyCombinatorTests.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Model;
2 | using Superpower.Parsers;
3 | using Superpower.Tests.Support;
4 | using Xunit;
5 |
6 | namespace Superpower.Tests.Combinators
7 | {
8 | public class ApplyCombinatorTests
9 | {
10 | [Fact]
11 | public void ApplyOnParsedSpanCallsAppliedParser()
12 | {
13 | var input = new TextSpan("1234");
14 | var twodigits = Span.Length(2).Apply(Numerics.IntegerInt32);
15 | var result = twodigits(input);
16 | Assert.Equal(12, result.Value);
17 | }
18 |
19 | [Fact]
20 | public void AnAppliedParserMustConsumeAllInput()
21 | {
22 | var input = new TextSpan("1234");
23 | var twodigits = Character.AnyChar.IgnoreThen(Span.Length(2).Apply(Character.Digit));
24 | var result = twodigits(input);
25 | Assert.False(result.HasValue);
26 | Assert.Equal("Syntax error (line 1, column 3): unexpected `3`.", result.ToString());
27 | }
28 |
29 | [Fact]
30 | public void AnAppliedParserIsNotCalledIfThePrecedingParseFails()
31 | {
32 | var input = new TextSpan("1234");
33 | var twodigits = Span.EqualTo("aa").Apply(Character.Digit);
34 | var result = twodigits(input);
35 | Assert.False(result.HasValue);
36 | Assert.Equal("Syntax error (line 1, column 1): unexpected `1`, expected `aa`.", result.ToString());
37 | }
38 |
39 | [Fact]
40 | public void ApplyOnParsedTokenCallsAppliedParser()
41 | {
42 | var input = StringAsCharTokenList.Tokenize("abcd");
43 | var aAs42 = Token.EqualTo('a').Apply(Character.AnyChar.Value(42));
44 | var result = aAs42(input);
45 | Assert.Equal(42, result.Value);
46 | }
47 |
48 | [Fact]
49 | public void AnAppliedParserMustConsumeTheWholeTokenSpan()
50 | {
51 | var input = StringAsCharTokenList.Tokenize("abcd");
52 | var just42 = Token.EqualTo('a').Apply(Parse.Return(42));
53 | var result = just42(input);
54 | Assert.False(result.HasValue);
55 | // The "invalid a" here is the token name, since we're using characters as tokens - in normal use
56 | // this would read more like "invalid URI: unexpected `:`".
57 | Assert.Equal("Syntax error (line 1, column 1): invalid a, unexpected `a`.", result.ToString());
58 | }
59 |
60 | [Fact]
61 | public void AFailingAppliedParserDoesNotCauseBacktracking()
62 | {
63 | var input = new TextSpan("aa");
64 | var twodigits = Span.EqualTo("aa")
65 | .Apply(Character.Digit.Value(TextSpan.Empty))
66 | .Or(Span.EqualTo("bb"));
67 | var result = twodigits(input);
68 | Assert.False(result.HasValue);
69 | Assert.Equal("Syntax error (line 1, column 1): unexpected `a`, expected digit.", result.ToString());
70 | }
71 |
72 | [Fact]
73 | public void AFailingAppliedParserDoesNotCauseTokenBacktracking1()
74 | {
75 | var input = StringAsCharTokenList.Tokenize("abcd");
76 | var just42 = Token.EqualTo('a').Apply(Span.EqualTo("ab"))
77 | .Or(Token.EqualTo('x').Value(TextSpan.Empty));
78 | var result = just42(input);
79 | Assert.False(result.HasValue);
80 | // The "invalid a" here is the token name, since we're using characters as tokens - in normal use
81 | // this would read more like "invalid URI: expected `:`".
82 | Assert.Equal("Syntax error (line 1, column 2): incomplete a, expected `b`.", result.ToString());
83 | }
84 |
85 | [Fact]
86 | public void AFailingAppliedParserDoesNotCauseTokenBacktracking2()
87 | {
88 | var input = StringAsCharTokenList.Tokenize("abcd");
89 | var just42 = Token.EqualTo('a').Apply(Span.EqualTo("b"))
90 | .Or(Token.EqualTo('x').Value(TextSpan.Empty));
91 | var result = just42(input);
92 | Assert.False(result.HasValue);
93 | // The "invalid a" here is the token name, since we're using characters as tokens - in normal use
94 | // this would read more like "invalid URI: expected `:`".
95 | Assert.Equal("Syntax error (line 1, column 1): invalid a, unexpected `a`, expected `b`.", result.ToString());
96 | }
97 | }
98 | }
--------------------------------------------------------------------------------
/src/Superpower/Model/Result.cs:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | using Superpower.Util;
16 |
17 | namespace Superpower.Model
18 | {
19 | ///
20 | /// Helper methods for working with .
21 | ///
22 | public static class Result
23 | {
24 | ///
25 | /// An empty result indicating no value could be parsed.
26 | ///
27 | /// The result type.
28 | /// The start of un-parsed input.
29 | /// A result.
30 | public static Result Empty(TextSpan remainder)
31 | {
32 | return new Result(remainder, null, null, false);
33 | }
34 |
35 | ///
36 | /// An empty result indicating no value could be parsed.
37 | ///
38 | /// The result type.
39 | /// The start of un-parsed input.
40 | /// Literal descriptions of expectations not met.
41 | /// A result.
42 | public static Result Empty(TextSpan remainder, string[] expectations)
43 | {
44 | return new Result(remainder, null, expectations, false);
45 | }
46 |
47 | ///
48 | /// An empty result indicating no value could be parsed.
49 | ///
50 | /// The result type.
51 | /// The start of un-parsed input.
52 | /// Error message to present.
53 | /// A result.
54 | public static Result Empty(TextSpan remainder, string errorMessage)
55 | {
56 | return new Result(remainder, errorMessage, null, false);
57 | }
58 |
59 | ///
60 | /// A result carrying a successfully-parsed value.
61 | ///
62 | /// The result type.
63 | /// The value.
64 | /// The location corresponding to the beginning of the parsed span.
65 | /// The start of un-parsed input.
66 | /// A result.
67 | public static Result Value(T value, TextSpan location, TextSpan remainder)
68 | {
69 | return new Result(value, location, remainder, false);
70 | }
71 |
72 | ///
73 | /// Convert an empty result of one type into another.
74 | ///
75 | /// The source type.
76 | /// The target type.
77 | /// The value to convert.
78 | /// A result of type carrying the same information as .
79 | public static Result CastEmpty(Result result)
80 | {
81 | return new Result(result.Remainder, result.ErrorMessage, result.Expectations, result.Backtrack);
82 | }
83 |
84 | ///
85 | /// Combine two empty results.
86 | ///
87 | /// The source type.
88 | /// The first value to combine.
89 | /// The second value to combine.
90 | /// A result of type carrying information from both results.
91 | public static Result CombineEmpty(Result first, Result second)
92 | {
93 | if (first.Remainder != second.Remainder)
94 | return second;
95 |
96 | var expectations = first.Expectations;
97 | if (expectations == null)
98 | expectations = second.Expectations;
99 | else if (second.Expectations != null)
100 | expectations = ArrayEnumerable.Concat(first.Expectations!, second.Expectations);
101 |
102 | return new Result(second.Remainder, second.ErrorMessage, expectations, second.Backtrack);
103 | }
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/test/Superpower.Tests/StringSpanTests.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Model;
2 | using System;
3 | using Xunit;
4 |
5 | namespace Superpower.Tests
6 | {
7 | public class StringSpanTests
8 | {
9 | [Fact]
10 | public void ADefaultSpanHasNoValue()
11 | {
12 | var span = default(TextSpan);
13 | Assert.Throws(() => span.ToStringValue());
14 | }
15 |
16 | [Fact]
17 | public void IdenticalSpansAreEqual()
18 | {
19 | var source = "123";
20 | var t1 = new TextSpan(source, Position.Zero, 1);
21 | var t2 = new TextSpan(source, Position.Zero, 1);
22 | Assert.Equal(t1, t2);
23 | }
24 |
25 | [Fact]
26 | public void SpansFromDifferentSourcesAreNotEqual()
27 | {
28 | string source1 = "123", source2 = "1234".Substring(0, 3);
29 | var t1 = new TextSpan(source1, Position.Zero, 1);
30 | var t2 = new TextSpan(source2, Position.Zero, 1);
31 | Assert.NotEqual(t1, t2);
32 | }
33 |
34 | [Fact]
35 | public void DifferentLengthSpansAreNotEqual()
36 | {
37 | var source = "123";
38 | var t1 = new TextSpan(source, Position.Zero, 1);
39 | var t2 = new TextSpan(source, Position.Zero, 2);
40 | Assert.NotEqual(t1, t2);
41 | }
42 |
43 | [Fact]
44 | public void EqualSpansAreEqualCase65()
45 | {
46 | var source = "123";
47 | var one = Position.Zero.Advance(source[0]);
48 | var t1 = new TextSpan(source);
49 | var t2 = new TextSpan(source, one, 1);
50 | Assert.Equal("1", t1.Until(t2).ToStringValue());
51 | }
52 |
53 | [Fact]
54 | public void SpansAtDifferentPositionsAreNotEqual()
55 | {
56 | var source = "111";
57 | var t1 = new TextSpan(source, Position.Zero, 1);
58 | var t2 = new TextSpan(source, new Position(1, 1, 1), 1);
59 | Assert.NotEqual(t1, t2);
60 | }
61 |
62 | [Theory]
63 | [InlineData("Hello", 0, 5, "Hello")]
64 | [InlineData("Hello", 1, 4, "ello")]
65 | [InlineData("Hello", 1, 3, "ell")]
66 | [InlineData("Hello", 0, 0, "")]
67 | public void ASpanIsEqualInValueToAMatchingString(string str, int offset, int length, string value)
68 | {
69 | var span = new TextSpan(str, new Position(offset, 1, offset + 1), length);
70 | Assert.True(span.EqualsValue(value));
71 | }
72 |
73 | [Theory]
74 | [InlineData("Hello", 0, 5, "HELLO")]
75 | [InlineData("Hello", 1, 4, "ELLO")]
76 | [InlineData("Hello", 1, 3, "ELL")]
77 | [InlineData("Hello", 0, 0, "")]
78 | public void ASpanIsEqualInValueIgnoringCaseToAMatchingUppsercaseString(string str, int offset, int length, string value)
79 | {
80 | var span = new TextSpan(str, new Position(offset, 1, offset + 1), length);
81 | Assert.True(span.EqualsValueIgnoreCase(value));
82 | }
83 |
84 | [Theory]
85 | [InlineData("Hello", 0, 5, "HELLO")]
86 | [InlineData("Hello", 1, 4, "Hell")]
87 | [InlineData("Hello", 1, 3, "fll")]
88 | public void ASpanIsNotEqualToADifferentString(string str, int offset, int length, string value)
89 | {
90 | var span = new TextSpan(str, new Position(offset, 1, offset + 1), length);
91 | Assert.False(span.EqualsValue(value));
92 | }
93 |
94 | [Theory]
95 | [InlineData("Hello", 0, 5)]
96 | [InlineData("Hello", 0, 3)]
97 | [InlineData("Hello", 1, 3)]
98 | public void SliceWithLengthExtractsCorrectCharacters(string input, int index, int end)
99 | {
100 | var inputSpan = new TextSpan(input, new Position(0, 1, 1), input.Length);
101 | var slice = inputSpan[index..end];
102 | Assert.Equal(expected: input[index..end], actual: slice.ToStringValue());
103 | }
104 |
105 | [Theory]
106 | [InlineData("Hello", 0)]
107 | [InlineData("Hello", 2)]
108 | [InlineData("Hello", 5)]
109 | public void SliceWithoutLengthExtractsCorrectCharacters(string input, int index)
110 | {
111 | var inputSpan = new TextSpan(input, new Position(0, 1, 1), input.Length);
112 | var slice = inputSpan[index..];
113 | Assert.Equal(expected: input[index..], actual: slice.ToStringValue());
114 | }
115 |
116 | [Theory]
117 | [InlineData("Hello", 0)]
118 | [InlineData("Hello", 2)]
119 | [InlineData("Hello", 4)]
120 | public void IndexerExtractsCorrectCharacter(string input, int index)
121 | {
122 | var inputSpan = new TextSpan(input, new Position(0, 1, 1), input.Length);
123 | var ch = inputSpan[index];
124 | Assert.Equal(expected: input[index], actual: ch);
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/src/Superpower/Tokenizer`1.cs:
--------------------------------------------------------------------------------
1 | // Copyright 2016-2018 Datalust, Superpower Contributors, Sprache Contributors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | using System;
16 | using System.Collections.Generic;
17 | using Superpower.Display;
18 | using Superpower.Model;
19 |
20 | namespace Superpower
21 | {
22 | ///
23 | /// Base class for tokenizers, types whose instances convert strings into lists of tokens.
24 | ///
25 | /// The kind of tokens produced.
26 | public abstract class Tokenizer
27 | {
28 | ///
29 | /// Tokenize .
30 | ///
31 | /// The source to tokenize.
32 | /// The list of tokens or an error.
33 | /// Tokenization failed.
34 | public TokenList Tokenize(string source)
35 | {
36 | var result = TryTokenize(source);
37 | if (result.HasValue)
38 | return result.Value;
39 |
40 | throw new ParseException(result.ToString(), result.ErrorPosition);
41 | }
42 |
43 | ///
44 | /// Tokenize .
45 | ///
46 | /// The source to tokenize.
47 | /// A result with the list of tokens or an error.
48 | /// is null.
49 | /// The tokenizer could not correctly perform tokenization.
50 | public Result> TryTokenize(string source)
51 | {
52 | if (source == null) throw new ArgumentNullException(nameof(source));
53 |
54 | var state = new TokenizationState();
55 |
56 | var sourceSpan = new TextSpan(source);
57 | var remainder = sourceSpan;
58 | var results = new List>();
59 | foreach (var result in Tokenize(sourceSpan, state))
60 | {
61 | if (!result.HasValue)
62 | return Result.CastEmpty>(result);
63 |
64 | if (result.Remainder == remainder) // Broken parser, not a failed parsing.
65 | throw new ParseException($"Zero-width tokens are not supported; token {Presentation.FormatExpectation(result.Value)} at position {result.Location.Position}.", result.Location.Position);
66 |
67 | remainder = result.Remainder;
68 | var token = new Token(result.Value, result.Location.Until(result.Remainder));
69 | state.Previous = token;
70 | results.Add(token);
71 | }
72 |
73 | var value = new TokenList(results.ToArray());
74 | return Result.Value(value, sourceSpan, remainder);
75 | }
76 |
77 | ///
78 | /// Subclasses should override to perform tokenization.
79 | ///
80 | /// The input span to tokenize.
81 | /// A list of parsed tokens.
82 | protected virtual IEnumerable> Tokenize(TextSpan span)
83 | {
84 | throw new NotImplementedException("Either `Tokenize(TextSpan)` or `Tokenize(TextSpan, TokenizationState)` must be implemented.");
85 | }
86 |
87 | ///
88 | /// Subclasses should override to perform tokenization when the
89 | /// last-produced-token needs to be tracked.
90 | ///
91 | /// The input span to tokenize.
92 | /// The tokenization state maintained during the operation.
93 | /// A list of parsed tokens.
94 | protected virtual IEnumerable> Tokenize(TextSpan span, TokenizationState state)
95 | {
96 | return Tokenize(span);
97 | }
98 |
99 | ///
100 | /// Advance until the first non-whitespace character is encountered.
101 | ///
102 | /// The span to advance from.
103 | /// A result with the first non-whitespace character.
104 | protected static Result SkipWhiteSpace(TextSpan span)
105 | {
106 | var next = span.ConsumeChar();
107 | while (next.HasValue && char.IsWhiteSpace(next.Value))
108 | {
109 | next = next.Remainder.ConsumeChar();
110 | }
111 | return next;
112 | }
113 | }
114 | }
--------------------------------------------------------------------------------
/test/Superpower.Tests/Parsers/SpanTests.cs:
--------------------------------------------------------------------------------
1 | using Superpower.Model;
2 | using Superpower.Parsers;
3 | using Xunit;
4 |
5 | namespace Superpower.Tests.Parsers
6 | {
7 | using System;
8 | using System.Text.RegularExpressions;
9 |
10 | public class SpanTests
11 | {
12 | [Theory]
13 | [InlineData("aaa", "aa", "aa")]
14 | [InlineData("aaa", "a+", "aaa")]
15 | [InlineData("aaa", "b", null)]
16 | [InlineData("abcd", "bc", "bc", 1)]
17 | [InlineData("abcd", "bc", null, 1, 1)]
18 | public void RegularExpressionParsersAreApplied(
19 | string input,
20 | string regex,
21 | string match,
22 | int start = 0,
23 | int length = -1)
24 | {
25 | var parser = Span.Regex(regex);
26 | var i = new TextSpan(input).Skip(start).First(length == -1 ? input.Length - start : length);
27 | var r = parser(i);
28 | if (match == null && !r.HasValue)
29 | return; // Success, shouldn't have matched
30 |
31 | Assert.Equal(match, i.Until(r.Remainder).ToStringValue());
32 | }
33 |
34 | [Fact]
35 | public void WhiteSpaceMatches()
36 | {
37 | var parser = Span.WhiteSpace;
38 | var input = new TextSpan(" a");
39 | var r = parser(input);
40 | Assert.True(r.Value.ToStringValue() == " ");
41 | }
42 |
43 | [Fact]
44 | public void WhiteSpaceDoesNotMatchZeroLength()
45 | {
46 | var parser = Span.WhiteSpace;
47 | var input = new TextSpan("a");
48 | var r = parser(input);
49 | Assert.False(r.HasValue);
50 | }
51 |
52 | [Fact]
53 | public void NonWhiteSpaceMatches()
54 | {
55 | var parser = Span.NonWhiteSpace;
56 | var input = new TextSpan("ab ");
57 | var r = parser(input);
58 | Assert.True(r.Value.ToStringValue() == "ab");
59 | }
60 |
61 | [Fact]
62 | public void NonWhiteSpaceDoesNotMatchZeroLength()
63 | {
64 | var parser = Span.NonWhiteSpace;
65 | var input = new TextSpan(" ");
66 | var r = parser(input);
67 | Assert.False(r.HasValue);
68 | }
69 |
70 | [Fact]
71 | public void MatchedByReturnsTheSpanMatchedByAParser()
72 | {
73 | var parser = Span.MatchedBy(Numerics.IntegerInt32);
74 | var input = new TextSpan("123abc");
75 | var r = parser(input);
76 | Assert.Equal("123", r.Value.ToStringValue());
77 | }
78 |
79 | [Fact]
80 | public void RegexMatches()
81 | {
82 | var parser = Span.Regex("foo", RegexOptions.IgnoreCase);
83 | var input = new TextSpan("Foo");
84 | var r = parser(input);
85 | Assert.Equal("Foo", r.Value.ToStringValue());
86 | }
87 |
88 | [Theory]
89 | [InlineData("123STOP", "123")]
90 | [InlineData("123stopSTOP", "123stop")]
91 | [InlineData("123STSTOP", "123ST")]
92 | [InlineData("123STOP456STOP789", "123")]
93 | [InlineData("123", "123")]
94 | public void ExceptMatchesUntilStopwordIsPresent(string text, string expected)
95 | {
96 | var result = Span.Except("STOP").Parse(text);
97 | Assert.Equal(expected, result.ToStringValue());
98 | }
99 |
100 | [Theory]
101 | [InlineData("123STOP", "123")]
102 | [InlineData("123stopSTOP", "123")]
103 | [InlineData("123STSToP", "123ST")]
104 | [InlineData("123stop456STOP789", "123")]
105 | [InlineData("123", "123")]
106 | public void ExceptIgnoreCaseMatchesUntilStopwordIsPresent(string text, string expected)
107 | {
108 | var result = Span.ExceptIgnoreCase("STOP").Parse(text);
109 | Assert.Equal(expected, result.ToStringValue());
110 | }
111 |
112 | [Theory]
113 | [InlineData("")]
114 | [InlineData("STOP")]
115 | [InlineData("STOP123")]
116 | public void ExceptDoesNotProduceZeroLengthMatches(string text)
117 | {
118 | Assert.False(Span.Except("STOP").TryParse(text).HasValue);
119 | }
120 |
121 | [Fact]
122 | public void ExceptFailsWhenArgumentIsNull()
123 | {
124 | Assert.Throws(() => Span.Except(null!).Parse("foo"));
125 | }
126 |
127 | [Fact]
128 | public void ExceptFailsWhenArgumentIsEmpty()
129 | {
130 | Assert.Throws(() => Span.Except("").Parse("foo"));
131 | }
132 |
133 | [Theory]
134 | [InlineData("Begin123STOPEnd")]
135 | public void ExceptMatchesWhenTheInputAbsolutePositionIsNonZero(string text)
136 | {
137 | var test =
138 | from begin in Span.EqualTo("Begin")
139 | from value in Span.Except("STOP")
140 | from stop in Span.EqualTo("STOP")
141 | from end in Span.EqualTo("End")
142 | select value;
143 | var result = test.Parse(text).ToStringValue();
144 |
145 | Assert.Equal("123", result);
146 | }
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.userosscache
8 | *.sln.docstates
9 |
10 | # User-specific files (MonoDevelop/Xamarin Studio)
11 | *.userprefs
12 |
13 | # Build results
14 | [Dd]ebug/
15 | [Dd]ebugPublic/
16 | [Rr]elease/
17 | [Rr]eleases/
18 | x64/
19 | x86/
20 | bld/
21 | [Bb]in/
22 | [Oo]bj/
23 | [Ll]og/
24 |
25 | # Visual Studio 2015 cache/options directory
26 | .vs/
27 | # Uncomment if you have tasks that create the project's static files in wwwroot
28 | #wwwroot/
29 |
30 | # MSTest test Results
31 | [Tt]est[Rr]esult*/
32 | [Bb]uild[Ll]og.*
33 |
34 | # NUNIT
35 | *.VisualState.xml
36 | TestResult.xml
37 |
38 | # Build Results of an ATL Project
39 | [Dd]ebugPS/
40 | [Rr]eleasePS/
41 | dlldata.c
42 |
43 | # DNX
44 | project.lock.json
45 | artifacts/
46 |
47 | *_i.c
48 | *_p.c
49 | *_i.h
50 | *.ilk
51 | *.meta
52 | *.obj
53 | *.pch
54 | *.pdb
55 | *.pgc
56 | *.pgd
57 | *.rsp
58 | *.sbr
59 | *.tlb
60 | *.tli
61 | *.tlh
62 | *.tmp
63 | *.tmp_proj
64 | *.log
65 | *.vspscc
66 | *.vssscc
67 | .builds
68 | *.pidb
69 | *.svclog
70 | *.scc
71 |
72 | # Chutzpah Test files
73 | _Chutzpah*
74 |
75 | # Visual C++ cache files
76 | ipch/
77 | *.aps
78 | *.ncb
79 | *.opendb
80 | *.opensdf
81 | *.sdf
82 | *.cachefile
83 | *.VC.db
84 | *.VC.VC.opendb
85 |
86 | # Visual Studio profiler
87 | *.psess
88 | *.vsp
89 | *.vspx
90 | *.sap
91 |
92 | # TFS 2012 Local Workspace
93 | $tf/
94 |
95 | # Guidance Automation Toolkit
96 | *.gpState
97 |
98 | # ReSharper is a .NET coding add-in
99 | _ReSharper*/
100 | *.[Rr]e[Ss]harper
101 | *.DotSettings.user
102 |
103 | # JustCode is a .NET coding add-in
104 | .JustCode
105 |
106 | # TeamCity is a build add-in
107 | _TeamCity*
108 |
109 | # DotCover is a Code Coverage Tool
110 | *.dotCover
111 |
112 | # NCrunch
113 | _NCrunch_*
114 | .*crunch*.local.xml
115 | nCrunchTemp_*
116 |
117 | # MightyMoose
118 | *.mm.*
119 | AutoTest.Net/
120 |
121 | # Web workbench (sass)
122 | .sass-cache/
123 |
124 | # Installshield output folder
125 | [Ee]xpress/
126 |
127 | # DocProject is a documentation generator add-in
128 | DocProject/buildhelp/
129 | DocProject/Help/*.HxT
130 | DocProject/Help/*.HxC
131 | DocProject/Help/*.hhc
132 | DocProject/Help/*.hhk
133 | DocProject/Help/*.hhp
134 | DocProject/Help/Html2
135 | DocProject/Help/html
136 |
137 | # Click-Once directory
138 | publish/
139 |
140 | # Publish Web Output
141 | *.[Pp]ublish.xml
142 | *.azurePubxml
143 | # TODO: Comment the next line if you want to checkin your web deploy settings
144 | # but database connection strings (with potential passwords) will be unencrypted
145 | *.pubxml
146 | *.publishproj
147 |
148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
149 | # checkin your Azure Web App publish settings, but sensitive information contained
150 | # in these scripts will be unencrypted
151 | PublishScripts/
152 |
153 | # NuGet Packages
154 | *.nupkg
155 | # The packages folder can be ignored because of Package Restore
156 | **/packages/*
157 | # except build/, which is used as an MSBuild target.
158 | !**/packages/build/
159 | # Uncomment if necessary however generally it will be regenerated when needed
160 | #!**/packages/repositories.config
161 | # NuGet v3's project.json files produces more ignoreable files
162 | *.nuget.props
163 | *.nuget.targets
164 |
165 | # Microsoft Azure Build Output
166 | csx/
167 | *.build.csdef
168 |
169 | # Microsoft Azure Emulator
170 | ecf/
171 | rcf/
172 |
173 | # Windows Store app package directories and files
174 | AppPackages/
175 | BundleArtifacts/
176 | Package.StoreAssociation.xml
177 | _pkginfo.txt
178 |
179 | # Visual Studio cache files
180 | # files ending in .cache can be ignored
181 | *.[Cc]ache
182 | # but keep track of directories ending in .cache
183 | !*.[Cc]ache/
184 |
185 | # Others
186 | ClientBin/
187 | ~$*
188 | *~
189 | *.dbmdl
190 | *.dbproj.schemaview
191 | *.pfx
192 | *.publishsettings
193 | node_modules/
194 | orleans.codegen.cs
195 |
196 | # Since there are multiple workflows, uncomment next line to ignore bower_components
197 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
198 | #bower_components/
199 |
200 | # RIA/Silverlight projects
201 | Generated_Code/
202 |
203 | # Backup & report files from converting an old project file
204 | # to a newer Visual Studio version. Backup files are not needed,
205 | # because we have git ;-)
206 | _UpgradeReport_Files/
207 | Backup*/
208 | UpgradeLog*.XML
209 | UpgradeLog*.htm
210 |
211 | # SQL Server files
212 | *.mdf
213 | *.ldf
214 |
215 | # Business Intelligence projects
216 | *.rdl.data
217 | *.bim.layout
218 | *.bim_*.settings
219 |
220 | # Microsoft Fakes
221 | FakesAssemblies/
222 |
223 | # GhostDoc plugin setting file
224 | *.GhostDoc.xml
225 |
226 | # Node.js Tools for Visual Studio
227 | .ntvs_analysis.dat
228 |
229 | # Visual Studio 6 build log
230 | *.plg
231 |
232 | # Visual Studio 6 workspace options file
233 | *.opt
234 |
235 | # Visual Studio LightSwitch build output
236 | **/*.HTMLClient/GeneratedArtifacts
237 | **/*.DesktopClient/GeneratedArtifacts
238 | **/*.DesktopClient/ModelManifest.xml
239 | **/*.Server/GeneratedArtifacts
240 | **/*.Server/ModelManifest.xml
241 | _Pvt_Extensions
242 |
243 | # Paket dependency manager
244 | .paket/paket.exe
245 | paket-files/
246 |
247 | # FAKE - F# Make
248 | .fake/
249 |
250 | # JetBrains Rider
251 | .idea/
252 | *.sln.iml
253 |
--------------------------------------------------------------------------------
/src/Superpower/Model/Result`1.cs:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | using Superpower.Util;
16 | using System;
17 |
18 | namespace Superpower.Model
19 | {
20 | ///
21 | /// The result of parsing from a text span.
22 | ///
23 | /// The type of the value being parsed.
24 | public struct Result
25 | {
26 | readonly T _value;
27 |
28 | ///
29 | /// If the result is a value, the location in the input corresponding to the
30 | /// value. If the result is an error, it's the location of the error.
31 | ///
32 | public TextSpan Location { get; }
33 |
34 | ///
35 | /// The first un-parsed location in the input.
36 | ///
37 | public TextSpan Remainder { get; }
38 |
39 | ///
40 | /// True if the result carries a successfully-parsed value; otherwise, false.
41 | ///
42 | public bool HasValue { get; }
43 |
44 | ///
45 | /// If the result is an error, the source-level position of the error; otherwise, .
46 | ///
47 | public Position ErrorPosition => HasValue ? Position.Empty : Location.Position;
48 |
49 | ///
50 | /// A provided error message, or null.
51 | ///
52 | public string? ErrorMessage { get; }
53 |
54 | ///
55 | /// A list of expectations that were unmet, or null.
56 | ///
57 | public string[]? Expectations { get; }
58 |
59 | internal bool IsPartial(TextSpan from) => from != Remainder;
60 |
61 | internal bool Backtrack { get; set; }
62 |
63 | ///
64 | /// The parsed value.
65 | ///
66 | public T Value
67 | {
68 | get
69 | {
70 | if (!HasValue)
71 | throw new InvalidOperationException($"{nameof(Result)} has no value.");
72 | return _value;
73 | }
74 | }
75 |
76 | internal Result(T value, TextSpan location, TextSpan remainder, bool backtrack)
77 | {
78 | Location = location;
79 | Remainder = remainder;
80 | _value = value;
81 | HasValue = true;
82 | ErrorMessage = null;
83 | Expectations = null;
84 | Backtrack = backtrack;
85 | }
86 |
87 | internal Result(TextSpan location, TextSpan remainder, string? errorMessage, string[]? expectations, bool backtrack)
88 | {
89 | Location = location;
90 | Remainder = remainder;
91 | _value = default!; // Default value is not observable.
92 | HasValue = false;
93 | Expectations = expectations;
94 | ErrorMessage = errorMessage;
95 | Backtrack = backtrack;
96 | }
97 |
98 | internal Result(TextSpan remainder, string? errorMessage, string[]? expectations, bool backtrack)
99 | {
100 | Location = Remainder = remainder;
101 | _value = default!; // Default value is not observable.
102 | HasValue = false;
103 | Expectations = expectations;
104 | ErrorMessage = errorMessage;
105 | Backtrack = backtrack;
106 | }
107 |
108 | ///
109 | public override string ToString()
110 | {
111 | if (Remainder == TextSpan.None)
112 | return "(Empty result.)";
113 |
114 | if (HasValue)
115 | return $"Successful parsing of {Value}.";
116 |
117 | var message = FormatErrorMessageFragment();
118 | var location = "";
119 | if (!Location.IsAtEnd)
120 | {
121 | location = $" (line {Location.Position.Line}, column {Location.Position.Column})";
122 | }
123 |
124 | return $"Syntax error{location}: {message}.";
125 | }
126 |
127 | ///
128 | /// If the result is empty, format the fragment of text describing the error.
129 | ///
130 | /// The error fragment.
131 | public string FormatErrorMessageFragment()
132 | {
133 | if (ErrorMessage != null)
134 | return ErrorMessage;
135 |
136 | string message;
137 | if (Location.IsAtEnd)
138 | {
139 | message = "unexpected end of input";
140 | }
141 | else
142 | {
143 | var next = Location.ConsumeChar().Value;
144 | message = $"unexpected {Display.Presentation.FormatLiteral(next)}";
145 | }
146 |
147 | if (Expectations != null)
148 | {
149 | var expected = Friendly.List(Expectations);
150 | message += $", expected {expected}";
151 | }
152 |
153 | return message;
154 | }
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/src/Superpower/ParserExtensions.cs:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | using System;
16 | using Superpower.Model;
17 |
18 | namespace Superpower
19 | {
20 | ///
21 | /// Helper methods for working with parsers.
22 | ///
23 | public static class ParserExtensions
24 | {
25 | ///
26 | /// Tries to parse the input without throwing an exception upon failure.
27 | ///
28 | /// The type of the result.
29 | /// The parser.
30 | /// The input.
31 | /// The result of the parser
32 | /// The parser or input is null.
33 | public static Result TryParse(this TextParser parser, string input)
34 | {
35 | if (parser == null) throw new ArgumentNullException(nameof(parser));
36 | if (input == null) throw new ArgumentNullException(nameof(input));
37 |
38 | return parser(new TextSpan(input));
39 | }
40 |
41 | ///
42 | /// Tries to parse the input without throwing an exception upon failure.
43 | ///
44 | /// The type of tokens consumed by the parser.
45 | /// The type of the result.
46 | /// The parser.
47 | /// The input.
48 | /// The result of the parser
49 | /// The parser or input is null.
50 | public static TokenListParserResult TryParse(this TokenListParser parser, TokenList input)
51 | {
52 | if (parser == null) throw new ArgumentNullException(nameof(parser));
53 |
54 | return parser(input);
55 | }
56 |
57 | ///
58 | /// Parses the specified input string.
59 | ///
60 | /// The type of the result.
61 | /// The parser.
62 | /// The input.
63 | /// The result of the parser.
64 | /// The parser or input is null.
65 | /// It contains the details of the parsing error.
66 | public static T Parse(this TextParser parser, string input)
67 | {
68 | if (parser == null) throw new ArgumentNullException(nameof(parser));
69 | if (input == null) throw new ArgumentNullException(nameof(input));
70 |
71 | var result = parser.TryParse(input);
72 |
73 | if (result.HasValue)
74 | return result.Value;
75 |
76 | throw new ParseException(result.ToString(), result.ErrorPosition);
77 | }
78 |
79 | ///
80 | /// Parses the specified input.
81 | ///
82 | /// The type of tokens consumed by the parser.
83 | /// The type of the result.
84 | /// The parser.
85 | /// The input.
86 | /// The result of the parser.
87 | /// The parser or input is null.
88 | /// It contains the details of the parsing error.
89 | public static T Parse(this TokenListParser parser, TokenList input)
90 | {
91 | if (parser == null) throw new ArgumentNullException(nameof(parser));
92 |
93 | var result = parser.TryParse(input);
94 |
95 | if (result.HasValue)
96 | return result.Value;
97 |
98 | throw new ParseException(result.ToString(), result.ErrorPosition);
99 | }
100 |
101 | ///
102 | /// Tests whether the parser matches the entire provided .
103 | ///
104 | /// The type of the parser's result.
105 | /// The parser.
106 | /// The input.
107 | /// True if the parser is a complete match for the input; otherwise, false.
108 | /// The parser is null.
109 | /// The input is .
110 | public static bool IsMatch(this TextParser parser, TextSpan input)
111 | {
112 | if (parser == null) throw new ArgumentNullException(nameof(parser));
113 | if (input == TextSpan.Empty) throw new ArgumentException("Input text span is empty.", nameof(input));
114 |
115 | var result = parser(input);
116 | return result.HasValue && result.Remainder.IsAtEnd;
117 | }
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/Superpower.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.27004.2009
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{AC295EEA-D319-4146-97E0-B978DF6F2557}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{66B005E9-A083-41E8-BD89-4D6E753CD8BF}"
9 | ProjectSection(SolutionItems) = preProject
10 | appveyor.yml = appveyor.yml
11 | Benchmark.ps1 = Benchmark.ps1
12 | Build.ps1 = Build.ps1
13 | LICENSE = LICENSE
14 | README.md = README.md
15 | global.json = global.json
16 | EndProjectSection
17 | EndProject
18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{2ED926D3-7AC8-4BFD-A16B-74D942602968}"
19 | EndProject
20 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "asset", "asset", "{69238E6E-26AB-494B-8CBD-65F8C1F0696A}"
21 | ProjectSection(SolutionItems) = preProject
22 | asset\Superpower.snk = asset\Superpower.snk
23 | asset\Superpower.svg = asset\Superpower.svg
24 | asset\Superpower-Transparent-400px.png = asset\Superpower-Transparent-400px.png
25 | asset\Superpower-White-200px.png = asset\Superpower-White-200px.png
26 | asset\Superpower-White-400px.png = asset\Superpower-White-400px.png
27 | EndProjectSection
28 | EndProject
29 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sample", "sample", "{7533E145-1C93-4348-A70D-E68746C5438C}"
30 | EndProject
31 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "results", "results", "{D8D9BA69-4FD8-4F31-9ECC-227D85733D15}"
32 | ProjectSection(SolutionItems) = preProject
33 | results\ArithmeticExpressionBenchmark-report-github.md = results\ArithmeticExpressionBenchmark-report-github.md
34 | results\ArithmeticExpressionBenchmark-report.csv = results\ArithmeticExpressionBenchmark-report.csv
35 | results\ArithmeticExpressionBenchmark-report.html = results\ArithmeticExpressionBenchmark-report.html
36 | results\NumberListBenchmark-report-github.md = results\NumberListBenchmark-report-github.md
37 | results\NumberListBenchmark-report.csv = results\NumberListBenchmark-report.csv
38 | results\NumberListBenchmark-report.html = results\NumberListBenchmark-report.html
39 | EndProjectSection
40 | EndProject
41 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Superpower", "src\Superpower\Superpower.csproj", "{D4E037DE-9778-4E48-A4A7-E8C1751E637C}"
42 | EndProject
43 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Superpower.Tests", "test\Superpower.Tests\Superpower.Tests.csproj", "{CD473266-4AED-4207-89FD-0B185239F1C7}"
44 | EndProject
45 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Superpower.Benchmarks", "test\Superpower.Benchmarks\Superpower.Benchmarks.csproj", "{1A9C8D7E-4DFC-48CD-99B0-63612197E95F}"
46 | EndProject
47 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntCalc", "sample\IntCalc\IntCalc.csproj", "{34BBD428-8297-484E-B771-0B72C172C264}"
48 | EndProject
49 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DateTimeParser", "sample\DateTimeTextParser\DateTimeParser.csproj", "{A842DA99-4EAB-423D-B532-7902FED0D8F1}"
50 | EndProject
51 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonParser", "sample\JsonParser\JsonParser.csproj", "{5C9AB721-559A-4617-B990-2D9EE85BEB7C}"
52 | EndProject
53 | Global
54 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
55 | Debug|Any CPU = Debug|Any CPU
56 | Release|Any CPU = Release|Any CPU
57 | EndGlobalSection
58 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
59 | {D4E037DE-9778-4E48-A4A7-E8C1751E637C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
60 | {D4E037DE-9778-4E48-A4A7-E8C1751E637C}.Debug|Any CPU.Build.0 = Debug|Any CPU
61 | {D4E037DE-9778-4E48-A4A7-E8C1751E637C}.Release|Any CPU.ActiveCfg = Release|Any CPU
62 | {D4E037DE-9778-4E48-A4A7-E8C1751E637C}.Release|Any CPU.Build.0 = Release|Any CPU
63 | {CD473266-4AED-4207-89FD-0B185239F1C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
64 | {CD473266-4AED-4207-89FD-0B185239F1C7}.Debug|Any CPU.Build.0 = Debug|Any CPU
65 | {CD473266-4AED-4207-89FD-0B185239F1C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
66 | {CD473266-4AED-4207-89FD-0B185239F1C7}.Release|Any CPU.Build.0 = Release|Any CPU
67 | {1A9C8D7E-4DFC-48CD-99B0-63612197E95F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
68 | {1A9C8D7E-4DFC-48CD-99B0-63612197E95F}.Debug|Any CPU.Build.0 = Debug|Any CPU
69 | {1A9C8D7E-4DFC-48CD-99B0-63612197E95F}.Release|Any CPU.ActiveCfg = Release|Any CPU
70 | {1A9C8D7E-4DFC-48CD-99B0-63612197E95F}.Release|Any CPU.Build.0 = Release|Any CPU
71 | {34BBD428-8297-484E-B771-0B72C172C264}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
72 | {34BBD428-8297-484E-B771-0B72C172C264}.Debug|Any CPU.Build.0 = Debug|Any CPU
73 | {34BBD428-8297-484E-B771-0B72C172C264}.Release|Any CPU.ActiveCfg = Release|Any CPU
74 | {34BBD428-8297-484E-B771-0B72C172C264}.Release|Any CPU.Build.0 = Release|Any CPU
75 | {A842DA99-4EAB-423D-B532-7902FED0D8F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
76 | {A842DA99-4EAB-423D-B532-7902FED0D8F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
77 | {A842DA99-4EAB-423D-B532-7902FED0D8F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
78 | {A842DA99-4EAB-423D-B532-7902FED0D8F1}.Release|Any CPU.Build.0 = Release|Any CPU
79 | {5C9AB721-559A-4617-B990-2D9EE85BEB7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
80 | {5C9AB721-559A-4617-B990-2D9EE85BEB7C}.Debug|Any CPU.Build.0 = Debug|Any CPU
81 | {5C9AB721-559A-4617-B990-2D9EE85BEB7C}.Release|Any CPU.ActiveCfg = Release|Any CPU
82 | {5C9AB721-559A-4617-B990-2D9EE85BEB7C}.Release|Any CPU.Build.0 = Release|Any CPU
83 | EndGlobalSection
84 | GlobalSection(SolutionProperties) = preSolution
85 | HideSolutionNode = FALSE
86 | EndGlobalSection
87 | GlobalSection(NestedProjects) = preSolution
88 | {D4E037DE-9778-4E48-A4A7-E8C1751E637C} = {AC295EEA-D319-4146-97E0-B978DF6F2557}
89 | {CD473266-4AED-4207-89FD-0B185239F1C7} = {2ED926D3-7AC8-4BFD-A16B-74D942602968}
90 | {1A9C8D7E-4DFC-48CD-99B0-63612197E95F} = {2ED926D3-7AC8-4BFD-A16B-74D942602968}
91 | {34BBD428-8297-484E-B771-0B72C172C264} = {7533E145-1C93-4348-A70D-E68746C5438C}
92 | {A842DA99-4EAB-423D-B532-7902FED0D8F1} = {7533E145-1C93-4348-A70D-E68746C5438C}
93 | {5C9AB721-559A-4617-B990-2D9EE85BEB7C} = {7533E145-1C93-4348-A70D-E68746C5438C}
94 | EndGlobalSection
95 | GlobalSection(ExtensibilityGlobals) = postSolution
96 | SolutionGuid = {F3941419-6499-4871-BEAA-861F4FE5D2D4}
97 | EndGlobalSection
98 | EndGlobal
99 |
--------------------------------------------------------------------------------
/src/Superpower/Parsers/Token.cs:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | using System;
16 | using Superpower.Display;
17 | using Superpower.Model;
18 |
19 | namespace Superpower.Parsers
20 | {
21 | ///
22 | /// Parsers for matching individual tokens.
23 | ///
24 | public static class Token
25 | {
26 | ///
27 | /// Parse a token of the kind .
28 | ///
29 | /// The type of the token being matched.
30 | /// The kind of token to match.
31 | /// The matched token.
32 | // ReSharper disable once MemberCanBePrivate.Global
33 | public static TokenListParser> EqualTo(TKind kind)
34 | {
35 | var expectations = new[] { Presentation.FormatExpectation(kind) };
36 |
37 | return input =>
38 | {
39 | var next = input.ConsumeToken();
40 | if (!next.HasValue || !next.Value.Kind!.Equals(kind))
41 | return TokenListParserResult.Empty>(input, expectations);
42 |
43 | return next;
44 | };
45 | }
46 |
47 | ///
48 | /// Parse a sequence of tokens of the kind .
49 | ///
50 | /// The type of the tokens being matched.
51 | /// The kinds of token to match, once each in order.
52 | /// The matched tokens.
53 | public static TokenListParser[]> Sequence(params TKind[] kinds)
54 | {
55 | if (kinds == null) throw new ArgumentNullException(nameof(kinds));
56 |
57 | TokenListParser[]> result = input => TokenListParserResult.Value(new Token[kinds.Length], input, input);
58 | for (var i = 0; i < kinds.Length; ++i)
59 | {
60 | var token = EqualTo(kinds[i]);
61 | var index = i;
62 | result = result.Then(arr => token.Select(t => { arr[index] = t; return arr; }));
63 | }
64 | return result;
65 | }
66 |
67 | ///
68 | /// Parse a token where the span of text matches a particular value.
69 | ///
70 | /// The kind of token to match.
71 | /// The string value to compare against the token's underlying span.
72 | /// The type of the token being matched.
73 | /// A parser that will match tokens with the specified kind and value.
74 | public static TokenListParser> EqualToValue(TKind kind, string value)
75 | {
76 | if (value == null) throw new ArgumentNullException(nameof(value));
77 |
78 | return EqualTo(kind).Where(t => t.Span.EqualsValue(value)).Named(Presentation.FormatLiteral(value));
79 | }
80 |
81 | ///
82 | /// Parse a token where the span of text matches a particular value, ignoring invariant character case.
83 | ///
84 | /// The kind of token to match.
85 | /// The string value to compare against the token's underlying span.
86 | /// The type of the token being matched.
87 | /// A parser that will match tokens with the specified kind and value.
88 | public static TokenListParser> EqualToValueIgnoreCase(TKind kind, string value)
89 | {
90 | if (value == null) throw new ArgumentNullException(nameof(value));
91 |
92 | return EqualTo(kind).Where(t => t.Span.EqualsValueIgnoreCase(value)).Named(Presentation.FormatLiteral(value));
93 | }
94 |
95 | ///
96 | /// Parse a token of the kind similar to EqualTo, matching not on the type, but on an arbitrary .
97 | ///
98 | /// The type of the token being matched.
99 | /// The predicate to apply.
100 | /// Textual parser description for error reporting.
101 | /// The matched token.
102 | // ReSharper disable once MemberCanBePrivate.Global
103 | public static TokenListParser> Matching(Func predicate, string name)
104 | {
105 | if (predicate == null) throw new ArgumentNullException(nameof(predicate));
106 | if (name == null) throw new ArgumentNullException(nameof(name));
107 |
108 | return Matching(predicate, new[] { name });
109 | }
110 |
111 | private static TokenListParser> Matching(Func predicate, string[] expectations)
112 | {
113 | if (predicate == null) throw new ArgumentNullException(nameof(predicate));
114 | if (expectations == null) throw new ArgumentNullException(nameof(expectations));
115 |
116 | return input =>
117 | {
118 | var next = input.ConsumeToken();
119 | if (!next.HasValue || !predicate(next.Value.Kind))
120 | return TokenListParserResult.Empty>(input , expectations);
121 |
122 | return next;
123 | };
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/Superpower/Parsers/Character.cs:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | using Superpower.Model;
16 | using Superpower.Util;
17 | using System;
18 | using System.Linq;
19 | using Superpower.Display;
20 |
21 | namespace Superpower.Parsers
22 | {
23 | ///
24 | /// Parsers for matching individual characters.
25 | ///
26 | public static class Character
27 | {
28 | static TextParser Matching(Func predicate, string[] expectations)
29 | {
30 | if (predicate == null) throw new ArgumentNullException(nameof(predicate));
31 | if (expectations == null) throw new ArgumentNullException(nameof(expectations));
32 |
33 | return input =>
34 | {
35 | var next = input.ConsumeChar();
36 | if (!next.HasValue || !predicate(next.Value))
37 | return Result.Empty(input, expectations);
38 |
39 | return next;
40 | };
41 | }
42 |
43 | ///
44 | /// Parse a single character matching .
45 | ///
46 | public static TextParser Matching(Func predicate, string name)
47 | {
48 | if (predicate == null) throw new ArgumentNullException(nameof(predicate));
49 | if (name == null) throw new ArgumentNullException(nameof(name));
50 |
51 | return Matching(predicate, new[] { name });
52 | }
53 |
54 | ///
55 | /// Parse a single character except those matching .
56 | ///
57 | /// Characters not to match.
58 | /// Description of characters that don't match.
59 | /// A parser for characters except those matching .
60 | public static TextParser Except(Func predicate, string description)
61 | {
62 | if (predicate == null) throw new ArgumentNullException(nameof(predicate));
63 | if (description == null) throw new ArgumentNullException(nameof(description));
64 |
65 | return Matching(c => !predicate(c), "any character except " + description);
66 | }
67 |
68 | ///
69 | /// Parse a single specified character.
70 | ///
71 | public static TextParser EqualTo(char ch)
72 | {
73 | return Matching(parsed => parsed == ch, Presentation.FormatLiteral(ch));
74 | }
75 |
76 | ///
77 | /// Parse a single specified character, ignoring case differences.
78 | ///
79 | public static TextParser EqualToIgnoreCase(char ch)
80 | {
81 | return Matching(parsed => char.ToUpper(parsed) == char.ToUpperInvariant(ch), Presentation.FormatLiteral(ch));
82 | }
83 |
84 | ///
85 | /// Parse any single character in .
86 | ///
87 | public static TextParser In(params char[] chars)
88 | {
89 | return Matching(chars.Contains, chars.Select(Presentation.FormatLiteral).ToArray());
90 | }
91 |
92 | ///
93 | /// Parse a single character except .
94 | ///
95 | public static TextParser Except(char ch)
96 | {
97 | return Except(parsed => parsed == ch, Presentation.FormatLiteral(ch));
98 | }
99 |
100 | ///
101 | /// Parse any single character except those in .
102 | ///
103 | public static TextParser ExceptIn(params char[] chars)
104 | {
105 | return Matching(c => !chars.Contains(c), "any character except " + Friendly.List(chars.Select(Presentation.FormatLiteral)));
106 | }
107 |
108 | ///
109 | /// Parse any character.
110 | ///
111 | public static TextParser AnyChar { get; } = Matching(c => true, "any character");
112 |
113 | ///
114 | /// Parse a whitespace character.
115 | ///
116 | public static TextParser WhiteSpace { get; } = Matching(char.IsWhiteSpace, "whitespace");
117 |
118 | ///
119 | /// Parse a digit.
120 | ///
121 | public static TextParser Digit { get; } = Matching(char.IsDigit, "digit");
122 |
123 | ///
124 | /// Parse a letter.
125 | ///
126 | public static TextParser Letter { get; } = Matching(char.IsLetter, "letter");
127 |
128 | ///
129 | /// Parse a letter or digit.
130 | ///
131 | public static TextParser LetterOrDigit { get; } = Matching(char.IsLetterOrDigit, new[] { "letter", "digit" });
132 |
133 | ///
134 | /// Parse a lowercase letter.
135 | ///
136 | public static TextParser Lower { get; } = Matching(char.IsLower, "lowercase letter");
137 |
138 | ///
139 | /// Parse an uppercase letter.
140 | ///
141 | public static TextParser Upper { get; } = Matching(char.IsUpper, "uppercase letter");
142 |
143 | ///
144 | /// Parse a numeric character.
145 | ///
146 | public static TextParser Numeric { get; } = Matching(char.IsNumber, "numeric character");
147 |
148 | ///
149 | /// Parse a hexadecimal digit (0-9, a-f, A-F).
150 | ///
151 | public static TextParser HexDigit { get; } = Matching(CharInfo.IsHexDigit, "hex digit");
152 | }
153 | }
154 |
155 |
--------------------------------------------------------------------------------
/src/Superpower/Model/TokenList`1.cs:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | using System;
16 | using System.Collections;
17 | using System.Collections.Generic;
18 |
19 | namespace Superpower.Model
20 | {
21 | ///
22 | /// A list of
23 | ///
24 | /// The kind of tokens held in the list.
25 | public readonly struct TokenList : IEquatable>, IEnumerable>
26 | {
27 | readonly Token[]? _tokens;
28 |
29 | ///
30 | /// The position of the token list in the token stream.
31 | ///
32 | public int Position { get; }
33 |
34 | ///
35 | /// Construct a token list containing .
36 | ///
37 | /// The tokens in the list.
38 | public TokenList(Token[] tokens)
39 | : this(tokens, 0)
40 | {
41 | if (tokens == null) throw new ArgumentNullException(nameof(tokens));
42 | }
43 |
44 | TokenList(Token[] tokens, int position)
45 | {
46 | #if CHECKED // Called on every advance or backtrack
47 | if (tokens == null) throw new ArgumentNullException(nameof(tokens));
48 | if (position > tokens.Length) throw new ArgumentOutOfRangeException(nameof(position), "Position is past end + 1.");
49 | #endif
50 |
51 | Position = position;
52 | _tokens = tokens;
53 | }
54 |
55 | ///
56 | /// A token list with no value.
57 | ///
58 | public static TokenList Empty { get; } = default;
59 |
60 | ///
61 | /// True if the token list contains no tokens.
62 | ///
63 | public bool IsAtEnd
64 | {
65 | get
66 | {
67 | EnsureHasValue();
68 | return Position == _tokens!.Length;
69 | }
70 | }
71 |
72 | void EnsureHasValue()
73 | {
74 | if (_tokens == null)
75 | throw new InvalidOperationException("Token list has no value.");
76 | }
77 |
78 | ///
79 | /// Consume a token from the start of the list, returning a result with the token and remainder.
80 | ///
81 | ///
82 | public TokenListParserResult> ConsumeToken()
83 | {
84 | EnsureHasValue();
85 |
86 | if (IsAtEnd)
87 | return TokenListParserResult.Empty>(this);
88 |
89 | var token = _tokens![Position];
90 | return TokenListParserResult.Value(token, this, new TokenList(_tokens, Position + 1));
91 | }
92 |
93 | ///
94 | public IEnumerator> GetEnumerator()
95 | {
96 | EnsureHasValue();
97 |
98 | for (var position = Position; position < _tokens!.Length; ++position)
99 | yield return _tokens[position];
100 | }
101 |
102 | IEnumerator IEnumerable.GetEnumerator()
103 | {
104 | return GetEnumerator();
105 | }
106 |
107 | ///