InterpolatedParser is a nuget library enabling string interpolation, but in reverse.
Example code:
using InterpolatedParsing;
int x = 0;
string input = "x is 69";
InterpolatedParser.Parse($"x is {x}", input);
Console.WriteLine(x); // Prints 69.
InterpolatedParser supports anything that implements IParseable<T>
and ISpanParseable<T>
, which includes many common types in .NET. This also means you can use your own types by having them implement either of the two interfaces.
The parser supports Lists and Arrays. A separator is provided as a format string. Format strings don't allow trailing whitespace, so if you need that enclose the format string in single quotes.
using InterpolatedParsing;
List<int> numbers = null!;
InterpolatedParser.Parse(
$"Winning numbers are: {numbers:,}",
"Winning numbers are: 5,10,15,25);
List<string> beans = null!;
InterpolatedParser.Parse(
$"Bean list: {beans:', '}", // Add single quotes to support whitespace
"Bean list: black, coffee, green");
C# 10 added support for writing custom interpolated string handlers. At compile time they translate interpolated strings into a series of calls to magic methods: AppendLiteral
for literal strings, and AppendFormatted
to the parameters of the string.
var str = $"Hello {123}!";
// Becomes this code on compile time:
var handler = new DefaultInterpolatedStringHandler(7, 4);
handler.AppendLiteral("Hello ");
handler.AppendFormatted(123);
handler.AppendLiteral("!");
var str = handler.ToStringAndClear();
That's all good and normally doesn't enable the shenanigans we need. However we can abuse the in parameter modifier. The in parameter can be implicit, so the generated calls to AppendFormatted
allow it. This means we're now passing down a read only reference to the value when we call AppendFormatted
. This is where things become really cursed, using Unsafe.AsRef
it's possible to cast it into a ref parameter, allowing the parser to change the parameters value.
public readonly void AppendFormatted(in int value) {
Unsafe.AsRef(in value) = 123;
This is the main hack that makes this work, but when AppendFormatted
is called, we don't yet have the information to extract what part of the input string we should parse. (Previous versions of this parser stored the ref as a pointer which was giga unsafe as it could not be pinned.) The information we need for that is getting the next part of the string, which is added with an AppendLiteral
after the AppendFormatted
. To get the upcoming literal string this library uses a C# Source Generator to make a list of every single call to the Parse
method and what string components each Parse
call will have. Since generated code lives in the user project, to make it accessible to the Parser the entire Parser is also code generated. This has some other benefits, like allowing code generation to support both ISpanParsable
and IParsable
types.
Of course even with a list of all calls we still need a way to pick the right call out of this list. Somewhat surprisingly there's a set of slightly obscure attributes that will inject the file path and the line number of the calling method at compile time.
Even more surprisingly these attributes still work on the auto generated constructor of the custom string interpolater. One side effect of this is that the accuracy is limited to line number, so placing two calls to Parse
on the same line will break the parser.
The input string also gets passed to the interpolater's constructor using another attribute and so we have all the information we need before any calls to AppendFormatted
.