Superpower, a C# parser toolkit where the error message is the product
A C# parser construction toolkit with high-quality error reporting
At a glance
- What is it?
- Superpower is a C# parser combinator library based on Sprache, with two layers: text parsers that consume characters and token parsers that consume a tagged token list. The token layer is the point, because it turns a failure message from unexpected character m into unexpected identifier frm, expected keyword from.
- Who is it for?
- Superpower suits C# teams that need to parse a small or medium language, a configuration format or a filter expression, and that care about what the error message looks like when a user types something wrong, since the token layer is where the diagnostics come from.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 156 days ago.
- What is it written in?
- Mainly C#, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Parsers as functions, and combinators as the only API to learn
The README states the job of a parser plainly: to take a sequence of characters as input and produce a data structure that is easier for a program to analyze, manipulate or transform. From that point of view a parser is just a function from string to T, where T might be a number, a list of fields in a data format, or the abstract syntax tree of a programming language.
It also notes that parsers can be built by hand from scratch, and calls that a lot of fun depending on the complexity of the parser you need and how you plan to spend the next few dozen nights and weekends. Superpower's answer is a declarative style that mirrors the structure of the target grammar, with no additional tools and no build-time code generation, embedded directly into your C# program.
The API has exactly two ideas in it. A simple parser consumes one thing, and a combinator builds a more complex parser out of simpler ones. The smallest example in the README parses a run of capital A characters:
// Parse any number of capital 'A's in a row
var parseA = Character.EqualTo('A').AtLeastOnce();Character.EqualTo is the built-in parser and AtLeastOnce is the combinator, and that vocabulary then scales up. An identifier is built with a query expression that reads almost like the grammar it accepts:
TextParser<string> identifier =
from first in Character.Letter
from rest in Character.LetterOrDigit.Or(Character.EqualTo('_')).Many()
select first + new string(rest);
var id = identifier.Parse("abc123");
Assert.Equal("abc123", id);The modularity claim follows from that structure: smaller parsers can be built and tested independently of the larger parsers that use them, which is the property that makes a combinator library practical for a grammar of any size.
Token parsers are what make the error messages worth reading
Alongside text parsers that consume input character by character, Superpower supports token parsers, which consume elements from a list of tokens. A token is a fragment of the input text tagged with the kind of item that fragment represents, usually specified with an enum:
public enum ArithmeticExpressionToken
{
None,
Number,
Plus,The benefit the README states is specific and worth repeating, because it is the library's whole argument. Driving parsing from tokens instead of individual characters means errors can be reported in terms of tokens, unexpected identifier frm, expected keyword from, instead of the cryptic unexpected m.
That is a bigger difference than it sounds. A user who mistypes a keyword in a configuration file or a filter expression sees a message that names the thing they typed and the thing the grammar wanted. A user of a character level parser sees a character offset and a guess. The first is a message you can put in a CLI without a manual, the second is a message that sends someone to the source code to work out what went wrong.
The mechanics of token driven parsing take place in two distinct steps, and separating them is what makes the token kind an enum rather than a string. First comes tokenization, using a class derived from Tokenizer<TKind>. Then comes parsing, using a function of type TokenListParser<TKind>.
var expression = "1 * (2 + 3)";
// 1.
var tokenizer = new ArithmeticExpressionTokenizer();
var tokenList = tokenizer.Tokenize(expression);
// 2.
var parser = ArithmeticExpressionParser.Lambda; // parser built with combinators
var expressionTree = parser.Parse(tokenList);
// Use the result
var eval = expressionTree.Compile();
Console.WriteLine(eval()); // -> 5Note that the token parsers are written with the same combinators as the text parsers. The difference is the input type, and with it the vocabulary of the error messages.
TokenizerBuilder assembles a tokenizer from recognizers
The job of a tokenizer is to split the input into a list of tokens, numbers, keywords, identifiers and operators, while discarding irrelevant trivia such as whitespace or comments. Superpower provides TokenizerBuilder<TKind> to assemble a tokenizer quickly from recognizers, which are themselves text parsers matching the kinds of tokens the grammar requires.
The builder reads as a list of match rules, each pairing a text parser with the token kind it produces:
var tokenizer = new TokenizerBuilder<ArithmeticExpressionToken>()
.Ignore(Span.WhiteSpace)
.Match(Character.EqualTo('+'), ArithmeticExpressionToken.Plus)
.Match(Character.EqualTo('-'), ArithmeticExpressionToken.Minus)
.Match(Character.EqualTo('*'), ArithmeticExpressionToken.Times)
.Match(Character.EqualTo('/'), ArithmeticExpressionToken.Divide)
.Match(Character.EqualTo('('), ArithmeticExpressionToken.LParen)
.Match(Character.EqualTo(')'), ArithmeticExpressionToken.RParen)
.Match(Numerics.Natural, ArithmeticExpressionToken.Number)
.Build();The Ignore call is how trivia is dropped, and the ordering matters: tokenizers built this way produce a list of tokens by repeatedly attempting to match recognizers against the input in top to bottom order. The first match wins, so the recognizers are effectively an ordered rule list, and a longer operator placed after a shorter one would never be reached.
The builder is the fast path, not the only path. Tokenizers can alternatively be written by hand, which the README says can provide the most flexibility, performance and control at the expense of more complicated code. A handwritten arithmetic expression tokenizer is included in the test suite, and a more complete example is linked in the Serilog filters expressions repository, which is a good thing to read if you are weighing the two approaches, since it shows the hand written form in a real project rather than in a tutorial.
Token list parsers use the same combinators over a different input
Token parsers are defined in the same manner as text parsers, using combinators to build up more sophisticated parsers out of simpler ones. The difference is the type of the combinators, TokenListParser<TKind, T> rather than TextParser<T>, and the vocabulary of the primitives.
The arithmetic expression example shows the pattern clearly. Each operator is a parser that matches a token and maps it to a value:
class ArithmeticExpressionParser
{
static readonly TokenListParser<ArithmeticExpressionToken, ExpressionType> Add =
Token.EqualTo(ArithmeticExpressionToken.Plus).Value(ExpressionType.AddChecked);
static readonly TokenListParser<ArithmeticExpressionToken, ExpressionType> Subtract =
Token.EqualTo(ArithmeticExpressionToken.Minus).Value(ExpressionType.SubtractChecked);The .Apply and .Select combinators then shape the result, so a number token becomes a constant expression. Recursion is handled with Parse.Ref, which defers to a parser defined later, so a factor can refer to the expression parser that in turn refers back to the factor:
static readonly TokenListParser<ArithmeticExpressionToken, Expression> Factor =
(from lparen in Token.EqualTo(ArithmeticExpressionToken.LParen)
from expr in Parse.Ref(() => Expr)
from rparen in Token.EqualTo(ArithmeticExpressionToken.RParen)
select expr)
.Or(Constant);The .Or at the end is the choice combinator, and the deferred reference is what lets left recursive or mutually recursive grammar rules be expressed without an infinite loop at construction time.
The result is an expression tree, and the example compiles it and evaluates it, printing 5 for the input 1 * (2 + 3). That last step is not part of the library, it is what the README does with the parsed structure, and it is a good demonstration of the general shape: parse into a tree, then do whatever you want with the tree.
What the repository contains, and how the project is versioned
The repository layout is conventional for a .NET library with a real test suite. There is Superpower.sln at the root, a src/ directory for the library, a test/ directory, a sample/ directory, and build tooling in the form of Build.ps1 and Directory.Build.props, with global.json pinning the SDK. Benchmark.ps1 and a results/ directory are also present, which tells you that performance is measured rather than asserted, and a .vscode/ directory and a .sln.DotSettings file hold the editor configuration for the maintainers.
The sample/ directory contains three projects, and they are a good map of what the library is for: DateTimeTextParser, IntCalc and JsonParser. The third is the interesting one, because a JSON parser is the classic case where people reach for a parser combinator library and then discover they wanted a library that already exists. It is included as a sample, which suggests the library is happy to be used for that too.
Installation is a single package reference:
dotnet add package SuperpowerThe library is published on NuGet under the name Superpower, with a pre-release version badge in the README header, and the licence is Apache-2.0 with the LICENSE file at the root. The default branch is dev, and the recent release tags are v3.2.1 from 2026-04-29, v3.2.0 from 2026-04-27 and v3.1.0 from 2025-06-18, with the last push on 2026-05-20. The project is based on Sprache, the parser combinator library for .NET, so the design lineage is clear and the borrowings are deliberate rather than accidental.
Editorial conclusion
Superpower suits C# teams that need to parse a small or medium language, a configuration format or a filter expression, and that care about what the error message looks like when a user types something wrong, since the token layer is where the diagnostics come from. It is the wrong choice if you need a parser for a language with substantial ambiguity, since this is a combinator toolkit and you write the grammar yourself, and it is the wrong choice if you want speed above all else, since a hand written tokenizer is the option the README points to for that and it means giving up the builder. Before adopting it, check the target framework against your project, decide early whether you will use the TokenizerBuilder or write the tokenizer by hand, and look at the sample/ directory, which holds DateTimeTextParser, IntCalc and JsonParser examples, alongside the results/ and Benchmark.ps1 files for the performance side.
Frequently asked questions
How do I install Superpower?
Add the package to your C# project with dotnet add package Superpower. The library embeds directly into your program with no extra tools and no build-time code generation step, and it is published on NuGet under the name Superpower.
What is the difference between a text parser and a token parser in Superpower?
A text parser consumes characters directly from the source text, while a token parser consumes elements from a list of tokens, where each token is a fragment tagged with a kind, usually from an enum. Token driven parsing is what lets errors be reported in terms of tokens, such as unexpected identifier frm, expected keyword from.
How do I build a tokenizer in Superpower?
Use TokenizerBuilder<TKind> and chain Ignore and Match calls, where Match pairs a text parser with the token kind it produces, then call Build. Recognizers are tried in top to bottom order, so the first match wins. You can also write a tokenizer by hand for more control and performance.
How do I write a recursive token parser?
Use Parse.Ref with a lambda that defers to the parser being defined, for example from expr in Parse.Ref(() => Expr). The deferred reference lets mutually recursive grammar rules be expressed without an infinite loop when the parsers are constructed.
What samples and performance tooling does the Superpower repository include?
The sample/ directory holds three projects, DateTimeTextParser, IntCalc and JsonParser, and the repository root carries Benchmark.ps1 together with a results/ directory, so performance is measured rather than asserted.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/datalust-superpower)Was this useful?