├── .gitattributes ├── asset ├── Superpower.snk ├── Superpower-White-200px.png ├── Superpower-White-400px.png └── Superpower-Transparent-400px.png ├── global.json ├── test ├── Superpower.Tests │ ├── NumberListScenario │ │ ├── NumberListToken.cs │ │ └── NumberListTokenizer.cs │ ├── SExpressionScenario │ │ ├── SExpressionToken.cs │ │ └── SExpressionTokenizer.cs │ ├── ComplexTokenScenario │ │ ├── SExpressionType.cs │ │ ├── SExpressionXToken.cs │ │ └── SExpressionXTokenizer.cs │ ├── Util │ │ └── FriendlyTests.cs │ ├── Support │ │ ├── StringAsCharTokenList.cs │ │ └── PreviousCheckingTokenizer.cs │ ├── Combinators │ │ ├── TextCombinatorTests.cs │ │ ├── ManyDelimitedByCombinatorTests.cs │ │ ├── MessageCombinatorTests.cs │ │ ├── NamedCombinatorTests.cs │ │ ├── WhereCombinatorTests.cs │ │ ├── ValueCombinatorTests.cs │ │ ├── SelectCombinatorTests.cs │ │ ├── NotCombinatorTests.cs │ │ ├── AtLeastOnceCombinatorTests.cs │ │ ├── OneOfCombinatorTests.cs │ │ ├── ThenCombinatorTests.cs │ │ ├── RepeatCombinatorTests.cs │ │ ├── AtEndCombinatorTests.cs │ │ ├── BetweenCombinatorTests.cs │ │ ├── OrCombinatorTests.cs │ │ ├── TryCombinatorTests.cs │ │ ├── ChainCombinatorTests.cs │ │ ├── ManyCombinatorTests.cs │ │ ├── SequenceCombinatorTests.cs │ │ └── ApplyCombinatorTests.cs │ ├── Parsers │ │ ├── QuotedStringTests.cs │ │ ├── InstantTests.cs │ │ ├── IdentifierTests.cs │ │ ├── NumericsTests.cs │ │ └── SpanTests.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── Model │ │ └── TextSpanTest.cs │ ├── ArithmeticExpressionScenario │ │ ├── ArithmeticExpressionToken.cs │ │ ├── ArithmeticExpressionTokenizer.cs │ │ └── ArithmeticExpressionParser.cs │ ├── Superpower.Tests.csproj │ ├── Display │ │ └── PresentationTests.cs │ ├── Tokenizer`1Tests.cs │ ├── Tokenizers │ │ └── TokenizerBuilderTests.cs │ └── StringSpanTests.cs └── Superpower.Benchmarks │ ├── NumberListScenario │ ├── NumberListToken.cs │ └── NumberListTokenizer.cs │ ├── ArithmeticExpressionScenario │ ├── ArithmeticExpressionToken.cs │ ├── SpracheArithmeticExpressionParser.cs │ ├── ArithmeticExpressionTokenizer.cs │ └── ArithmeticExpressionParser.cs │ ├── Superpower.Benchmarks.csproj │ ├── TokenizerBuilderBenchmark.cs │ ├── SequencingBenchmark.cs │ ├── ArithmeticExpressionBenchmark.cs │ └── NumberListBenchmark.cs ├── sample ├── IntCalc │ ├── IntCalc.csproj │ ├── ArithmeticExpressionToken.cs │ ├── Program.cs │ ├── ArithmeticExpressionTokenizer.cs │ └── ArithmeticExpressionParser.cs ├── JsonParser │ ├── JsonParser.csproj │ └── test.json └── DateTimeTextParser │ ├── DateTimeParser.csproj │ ├── README.md │ ├── Program.cs │ └── DateTimeTextParser.cs ├── results ├── ArithmeticExpressionBenchmark-report.csv ├── ArithmeticExpressionBenchmark-report-github.md ├── ArithmeticExpressionBenchmark-report.html ├── NumberListBenchmark-report.csv ├── NumberListBenchmark-report-github.md └── NumberListBenchmark-report.html ├── Benchmark.ps1 ├── appveyor.yml ├── src └── Superpower │ ├── Properties │ └── AssemblyInfo.cs │ ├── Model │ ├── Unit.cs │ ├── TokenizationState.cs │ ├── Token`1.cs │ ├── Position.cs │ ├── Result.cs │ ├── Result`1.cs │ └── TokenList`1.cs │ ├── TextParser`1.cs │ ├── Parsers │ ├── Instant.cs │ ├── Identifier.cs │ ├── QuotedString.cs │ ├── Comment.cs │ ├── Token.cs │ └── Character.cs │ ├── TokenListParser`2.cs │ ├── Util │ ├── CharInfo.cs │ ├── ArrayEnumerable.cs │ └── Friendly.cs │ ├── Superpower.csproj │ ├── Display │ ├── TokenAttribute.cs │ └── Presentation.cs │ ├── ParseException.cs │ ├── Tokenizer`1.cs │ └── ParserExtensions.cs ├── .vscode ├── tasks.json └── launch.json ├── Superpower.sln.DotSettings ├── Build.ps1 ├── .gitignore └── Superpower.sln /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | 3 | * text=auto 4 | -------------------------------------------------------------------------------- /asset/Superpower.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datalust/superpower/HEAD/asset/Superpower.snk -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "8.0.300", 4 | "rollForward": "latestFeature" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /asset/Superpower-White-200px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datalust/superpower/HEAD/asset/Superpower-White-200px.png -------------------------------------------------------------------------------- /asset/Superpower-White-400px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datalust/superpower/HEAD/asset/Superpower-White-400px.png -------------------------------------------------------------------------------- /asset/Superpower-Transparent-400px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datalust/superpower/HEAD/asset/Superpower-Transparent-400px.png -------------------------------------------------------------------------------- /test/Superpower.Tests/NumberListScenario/NumberListToken.cs: -------------------------------------------------------------------------------- 1 | namespace Superpower.Tests.NumberListScenario 2 | { 3 | enum NumberListToken 4 | { 5 | None, 6 | Number 7 | } 8 | } -------------------------------------------------------------------------------- /test/Superpower.Benchmarks/NumberListScenario/NumberListToken.cs: -------------------------------------------------------------------------------- 1 | namespace Superpower.Benchmarks.NumberListScenario 2 | { 3 | public enum NumberListToken 4 | { 5 | None, 6 | Number 7 | } 8 | } -------------------------------------------------------------------------------- /sample/IntCalc/IntCalc.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /sample/JsonParser/JsonParser.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /sample/DateTimeTextParser/DateTimeParser.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /results/ArithmeticExpressionBenchmark-report.csv: -------------------------------------------------------------------------------- 1 | Type;Method;Mode;Platform;Jit;Toolchain;Runtime;GcMode;LaunchCount;WarmupCount;TargetCount;Affinity;Median;StdDev;Scaled;Scaled-SD 2 | ArithmeticExpressionBenchmark;Sprache;Throughput;Host;Host;Host;Host;Host;Auto;Auto;Auto;Auto;256.6565 us;3.3398 us;1.00;0.00 3 | ArithmeticExpressionBenchmark;SuperpowerToken;Throughput;Host;Host;Host;Host;Host;Auto;Auto;Auto;Auto;79.7273 us;1.3012 us;0.31;0.01 4 | -------------------------------------------------------------------------------- /sample/DateTimeTextParser/README.md: -------------------------------------------------------------------------------- 1 | # Superpower sample / `DateTimeTextParser` 2 | 3 | This example shows how to build a simple text parser with Superpower. 4 | It uses a simple and well known requirement: parsing date and time values 5 | according to ISO-8601 format. (Time zones and fractional seconds are not 6 | covered by the example. 7 | 8 | For example: 9 | 10 | - `2017-01-01` 11 | - `2017-01-01 12:10` 12 | - `2017-01-01 12:10:30` 13 | -------------------------------------------------------------------------------- /test/Superpower.Tests/SExpressionScenario/SExpressionToken.cs: -------------------------------------------------------------------------------- 1 | using Superpower.Display; 2 | using Superpower.Model; 3 | 4 | namespace Superpower.Tests.SExpressionScenario 5 | { 6 | enum SExpressionToken 7 | { 8 | None, 9 | Atom, 10 | Number, 11 | 12 | [Token(Description = "open parenthesis")] 13 | LParen, 14 | 15 | [Token(Description = "closing parenthesis")] 16 | RParen 17 | } 18 | } -------------------------------------------------------------------------------- /test/Superpower.Tests/ComplexTokenScenario/SExpressionType.cs: -------------------------------------------------------------------------------- 1 | using Superpower.Display; 2 | using Superpower.Model; 3 | 4 | namespace Superpower.Tests.ComplexTokenScenario 5 | { 6 | enum SExpressionType 7 | { 8 | None, 9 | Atom, 10 | Number, 11 | 12 | [Token(Description = "open parenthesis")] 13 | LParen, 14 | 15 | [Token(Description = "closing parenthesis")] 16 | RParen 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Benchmark.ps1: -------------------------------------------------------------------------------- 1 | Push-Location $PSScriptRoot 2 | 3 | ./Build.ps1 4 | 5 | foreach ($test in ls test/*.Benchmarks) { 6 | Push-Location $test 7 | 8 | echo "perf: Running benchmark project in $test" 9 | 10 | & dotnet test -c Release 11 | if($LASTEXITCODE -ne 0) { exit 2 } 12 | 13 | rm -Recurse $PSScriptRoot/results 14 | mv ./BenchmarkDotNet.Artifacts/results $PSScriptRoot 15 | rm -Recurse ./BenchmarkDotNet.Artifacts 16 | 17 | Pop-Location 18 | } 19 | 20 | Pop-Location 21 | -------------------------------------------------------------------------------- /test/Superpower.Tests/Util/FriendlyTests.cs: -------------------------------------------------------------------------------- 1 | using Superpower.Util; 2 | using Xunit; 3 | 4 | namespace Superpower.Tests.Util 5 | { 6 | public class FriendlyTests 7 | { 8 | [Fact] 9 | public void FriendlyListsPreserveOrderButRemoveDuplicates() 10 | { 11 | var actual = Friendly.List(new[] {"one", "two", "two", "one", "three"}); 12 | const string expected = "one, two or three"; 13 | Assert.Equal(expected, actual); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/Superpower.Tests/Support/StringAsCharTokenList.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Superpower.Model; 3 | 4 | namespace Superpower.Tests.Support 5 | { 6 | static class StringAsCharTokenList 7 | { 8 | public static TokenList Tokenize(string tokens) 9 | { 10 | var items = tokens.ToCharArray() 11 | .Select((ch, i) => new Token(ch, new TextSpan(tokens, new Position(i, 1, 1), 1))) 12 | .ToArray(); 13 | 14 | return new TokenList(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 | 16 | 17 | 18 |
MethodMedianStdDevScaledScaled-SD
Sprache256.6565 us3.3398 us1.000.00
SuperpowerToken79.7273 us1.3012 us0.310.01
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 | 16 | 17 | 18 | 19 | 20 | 21 |
Method MedianStdDevScaledScaled-SD
StringSplitAndInt32Parse138.7217 us5.7115 us1.000.00
SpracheSimple2,740.3251 us11.9340 us19.400.72
SuperpowerSimple937.1593 us43.9867 us6.640.39
SuperpowerChar691.5509 us9.9246 us4.890.19
SuperpowerToken1,189.8622 us15.9538 us8.460.33
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 | /// 108 | public override bool Equals(object? obj) 109 | { 110 | if (!(obj is TokenList other)) 111 | return false; 112 | 113 | return Equals(other); 114 | } 115 | 116 | /// 117 | public override int GetHashCode() 118 | { 119 | unchecked 120 | { 121 | return ((_tokens?.GetHashCode() ?? 0) * 397) ^ Position; 122 | } 123 | } 124 | 125 | /// 126 | /// Compare two token lists using identity semantics - same list, same position. 127 | /// 128 | /// The other token list. 129 | /// True if the token lists are the same. 130 | public bool Equals(TokenList other) 131 | { 132 | return Equals(_tokens, other._tokens) && Position == other.Position; 133 | } 134 | 135 | /// 136 | /// Compare two token lists using identity semantics. 137 | /// 138 | /// The first token list. 139 | /// The second token list. 140 | /// True if the token lists are the same. 141 | public static bool operator ==(TokenList lhs, TokenList rhs) 142 | { 143 | return lhs.Equals(rhs); 144 | } 145 | 146 | /// 147 | /// Compare two token lists using identity semantics. 148 | /// 149 | /// The first token list. 150 | /// The second token list. 151 | /// True if the token lists are the different. 152 | public static bool operator !=(TokenList lhs, TokenList rhs) 153 | { 154 | return !(lhs == rhs); 155 | } 156 | 157 | /// 158 | public override string ToString() 159 | { 160 | if (_tokens == null) 161 | return "Token list (empty)"; 162 | 163 | return "Token list"; 164 | } 165 | 166 | // A mildly expensive way to find the "end of input" position for error reporting. 167 | internal Position ComputeEndOfInputPosition() 168 | { 169 | EnsureHasValue(); 170 | 171 | if (_tokens!.Length == 0) 172 | return Model.Position.Zero; 173 | 174 | var lastSpan = _tokens[_tokens.Length - 1].Span; 175 | var source = lastSpan.Source; 176 | var position = lastSpan.Position; 177 | for (var i = position.Absolute; i < source!.Length; ++i) 178 | position = position.Advance(source[i]); 179 | return position; 180 | } 181 | } 182 | } -------------------------------------------------------------------------------- /src/Superpower/Display/Presentation.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.Reflection; 17 | using Superpower.Util; 18 | 19 | namespace Superpower.Display 20 | { 21 | static class Presentation 22 | { 23 | static string FormatKind(object kind) 24 | { 25 | return kind.ToString()!.ToLower(); 26 | } 27 | 28 | static TokenAttribute? TryGetTokenAttribute(Type type) 29 | { 30 | return type.GetTypeInfo().GetCustomAttribute(); 31 | } 32 | 33 | static TokenAttribute? TryGetTokenAttribute(TKind kind) 34 | { 35 | var kindTypeInfo = typeof(TKind).GetTypeInfo(); 36 | if (kindTypeInfo.IsEnum) 37 | { 38 | var field = kindTypeInfo.GetDeclaredField(kind!.ToString()!); 39 | if (field != null) 40 | { 41 | return field.GetCustomAttribute() ?? TryGetTokenAttribute(typeof(TKind)); 42 | } 43 | } 44 | 45 | return TryGetTokenAttribute(typeof(TKind)); 46 | } 47 | 48 | public static string FormatExpectation(TKind kind) 49 | { 50 | var description = TryGetTokenAttribute(kind); 51 | if (description != null) 52 | { 53 | if (description.Description != null) 54 | return description.Description; 55 | if (description.Example != null) 56 | return FormatLiteral(description.Example); 57 | } 58 | 59 | return FormatKind(kind!); 60 | } 61 | 62 | public static string FormatAppearance(TKind kind, string value) 63 | { 64 | var clipped = FormatLiteral(Friendly.Clip(value, 12)); 65 | 66 | var description = TryGetTokenAttribute(kind); 67 | if (description != null) 68 | { 69 | if (description.Category != null) 70 | return $"{description.Category} {clipped}"; 71 | 72 | if (description.Example != null) 73 | return clipped; 74 | } 75 | 76 | return $"{FormatKind(kind!)} {clipped}"; 77 | } 78 | public static string FormatLiteral(char literal) 79 | { 80 | switch (literal) 81 | { 82 | //Unicode Category: Space Separators 83 | case '\x00A0': return "U+00A0 no-break space"; 84 | case '\x1680': return "U+1680 ogham space mark"; 85 | case '\x2000': return "U+2000 en quad"; 86 | case '\x2001': return "U+2001 em quad"; 87 | case '\x2002': return "U+2002 en space"; 88 | case '\x2003': return "U+2003 em space"; 89 | case '\x2004': return "U+2004 three-per-em space"; 90 | case '\x2005': return "U+2005 four-per-em space"; 91 | case '\x2006': return "U+2006 six-per-em space"; 92 | case '\x2007': return "U+2007 figure space"; 93 | case '\x2008': return "U+2008 punctuation space"; 94 | case '\x2009': return "U+2009 thin space"; 95 | case '\x200A': return "U+200A hair space"; 96 | case '\x202F': return "U+202F narrow no-break space"; 97 | case '\x205F': return "U+205F medium mathematical space"; 98 | case '\x3000': return "U+3000 ideographic space"; 99 | 100 | //Line Separator 101 | case '\x2028': return "U+2028 line separator"; 102 | 103 | //Paragraph Separator 104 | case '\x2029': return "U+2029 paragraph separator"; 105 | 106 | //Unicode C0 Control Codes (ASCII equivalent) 107 | case '\x0000': return "NUL"; //\0 108 | case '\x0001': return "U+0001 start of heading"; 109 | case '\x0002': return "U+0002 start of text"; 110 | case '\x0003': return "U+0003 end of text"; 111 | case '\x0004': return "U+0004 end of transmission"; 112 | case '\x0005': return "U+0005 enquiry"; 113 | case '\x0006': return "U+0006 acknowledge"; 114 | case '\x0007': return "U+0007 bell"; 115 | case '\x0008': return "U+0008 backspace"; 116 | case '\x0009': return "tab"; //\t 117 | case '\x000A': return "line feed"; //\n 118 | case '\x000B': return "U+000B vertical tab"; 119 | case '\x000C': return "U+000C form feed"; 120 | case '\x000D': return "carriage return"; //\r 121 | case '\x000E': return "U+000E shift in"; 122 | case '\x000F': return "U+000F shift out"; 123 | case '\x0010': return "U+0010 data link escape"; 124 | case '\x0011': return "U+0011 device ctrl 1"; 125 | case '\x0012': return "U+0012 device ctrl 2"; 126 | case '\x0013': return "U+0013 device ctrl 3"; 127 | case '\x0014': return "U+0014 device ctrl 4"; 128 | case '\x0015': return "U+0015 not acknowledge"; 129 | case '\x0016': return "U+0016 synchronous idle"; 130 | case '\x0017': return "U+0017 end transmission block"; 131 | case '\x0018': return "U+0018 cancel"; 132 | case '\x0019': return "U+0019 end of medium"; 133 | case '\x0020': return "space"; 134 | case '\x001A': return "U+001A substitute"; 135 | case '\x001B': return "U+001B escape"; 136 | case '\x001C': return "U+001C file separator"; 137 | case '\x001D': return "U+001D group separator"; 138 | case '\x001E': return "U+001E record separator"; 139 | case '\x001F': return "U+001F unit separator"; 140 | case '\x007F': return "U+007F delete"; 141 | 142 | default: return "`" + literal + "`"; 143 | } 144 | } 145 | 146 | public static string FormatLiteral(string literal) 147 | { 148 | return "`" + literal + "`"; 149 | } 150 | } 151 | } 152 | --------------------------------------------------------------------------------