The key items we need to accomplish is
- Find the builder type
- Pass user arguments (Span, culture, etc ...) to builder ctor
- Pass compiler arguments (hole count, fixed length) to the builder ctor
- Allow for non-string results
- Binary compatibility: let existing code evolve without breaking existing callers.
Consider if we allowed developers to define methods like the following
[InterpolatedBuilder(typeof(ValueStringBuilder))]
static extern string Format(Span<byte> span, string format, params object[] args);Now at the call site the way we handle this is if we see the
[InterpolatedBuilder] attribute we do the following. Take all of
the arguments before format (first N-2 arguments) and pass them
along with the compiler arguments to the constructor of
ValueStringBuilder
return Format(mySpan, $"Hello {name}");
// becomes
var builder = new ValueStringBuilder(mySpan, baseLength: 9, holeCount: 1);
builder.TryFormat(name);
return builder.GetResult();This makes passing arguments natural and visible at the call site. It also doesn't required name based mapping it's just simply positional based mapping like we've done in say LINQ.
This also extends to an aribtary number of arguments. Essentially the pattern is to
pass all of the arguments up until format to the constructor of the builder.
[InterpolatedBuilder(typeof(ValueStringBuilder))]
static extern string Format2(Type1 param2, Type2 param2, ... TypeN paramN, string format, object[] args);
// At the callsite
Format2(arg1, arg2, ... argN, $"Hello {name}");
// Becomes
var builder = new ValueStringBuilder(arg1, arg2, ... argN, baseLength: 9, holeCount: 1);
builder.TryFormat(name);
return builder.GetResult();For APIs that want to move to new builders but maintain binary compatibility could do so by defining the method as non-extern and defering to their existing manner of formatting strings
// V1 version of API
static string Log(string format, params object[] args) => string.Format(foramt, args);
// V2 new callers use ValueStringBuilder but existing callers go through old path
[InterpolatedBuilder(typeof(ValueStringBuilder))]
static string Log(string format, params object[] args) => string.Format(foramt, args); This allows allows for us to have non-string return types. The compiler would enforce
that the return type of GetResult() on the builder matched the return type of
the method where the attribute was placed.