-
Notifications
You must be signed in to change notification settings - Fork 4.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Bridging generic constraints doc #97084
Conversation
* Stop doing the verification-level constraint validation when compiling `call` (i.e. the check on uninstantiated forms). | ||
* Keep constraint validation of the instantiated form. | ||
* If the instantiated form of the call target doesn't satisfy constraint, replace the call with a call to a throw helper (we can likely reuse infrastructure around `CORINFO_ACCESS_ILLEGAL`). | ||
* If the constraint validation requires a runtime check due to shared code, delay validation to run time (this can potentially be optimized out if the potential constraint violation is already in a guarded block). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the extra constrained check going to impact the code quality? Or is it going to happen naturally "for free" as part of the dictionary cell initialization?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know details on timing of that - if it happens at suitable times (right before it's needed and never earlier), it can be part of initialization. A codegen check otherwise.
* `struct` constraint: add `bool RuntimeHelpers.IsValueType<T>()`, or reuse `typeof(T).IsValueType` | ||
* `class` constraint: can be `!struct` as long as we don't allow using pointers as generic parameters, but probably better to add API | ||
* `unmanaged` constraint: does `RuntimeHelpers.IsReferenceOrContainsReferences` fit the bill? | ||
* class/interface constraint: add new RuntimeHelpers API or reuse `Type.IsAssignableFrom`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feels a bit off if such constraint-checking APIs are spread to multiple places. I'd say to add forwarding methods to RuntimeHelpers
(or some other dedicated place) regardless, primarily so that they're neatly beside each other and easy to find. It could potentially have some advantages for the language implementation too, but that could also be a stretch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
primarily so that they're neatly beside each other and easy to find
The expectations is that these would be called from compiler-generated code (there would be a syntax to do the bridging, since the language compiler needs to be aware of this no matter what).
If we have existing API that fits the bill (that part is called out as "it's questionable if we're okay with the perf characteristics or whether we want new APIs" in the doc), the compiler doesn't care what namespace or type it is on. New API has a cost.
We might need to introduce new APIs, but not because we want them to be nicely grouped.
} | ||
``` | ||
|
||
The requirement of never having an invalid type instantiation within the runtime type system means that if we're compiling `Do<int>` (where we cannot allow `Constrained<int>` to exist), the code generator cannot ask questions about all locals (IL locals are scoped to the method, not to a basic block), exception handling regions, or any IL within the `IsAssignableFrom` check. While it may be reasonable to expect the `IsAssignableFrom` check gets optimized out, it would likely not happen for unoptimized code. Locals and EH regions pose additional programs. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the code generator cannot ask questions about all locals
This is not limited to code generator. It extends to diagnostic tools or IL inspection APIs like https://learn.microsoft.com/dotnet/api/system.reflection.localvariableinfo.localtype
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Diagnostics tools are an important consideration here as this would introduce novel failure modes. Most modern debuggers have the "move IP to" gesture. Moving the IP within an invalid branch for the generic would require serious thought on the correct way to fail.
} | ||
``` | ||
|
||
The requirement of never having an invalid type instantiation within the runtime type system means that if we're compiling `Do<int>` (where we cannot allow `Constrained<int>` to exist), the code generator cannot ask questions about all locals (IL locals are scoped to the method, not to a basic block), exception handling regions, or any IL within the `IsAssignableFrom` check. While it may be reasonable to expect the `IsAssignableFrom` check gets optimized out, it would likely not happen for unoptimized code. Locals and EH regions pose additional programs. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am wondering whether we can limit the patterns for bridging the constraints to just method calls. If there is more than just a simple method call that needs to execute with the more stringent constraint, the code can be factored out into a separate (aggressively inlined) method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we do that, we may not need the fine-grained runtime helpers for checking the constraints. We may limit the support to the following pattern:
call RuntimeHelpers.CheckConstraintsForMethodCallThatFollows
br.false ....
push arg0
push arg1
...
push argN
call CodeWithMoreStringentConstraints
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Imagine in C# code, a branch may only be taken if a stronger constraint is met:
interface IA {}
interface IB : IA {}
M<T>()
{
if (T is IB) // stronger than IA
{
M2<T>();
}
}
M2<T>() where T : IA {}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The compiler can introduce a compiler-generated method with the stronger constraint in this case.
|
||
The purpose of this document is to sketch out a possible implementation on the runtime side that would allow above code to work. | ||
|
||
This feature doesn't assume any kind of relaxation of constraint checks outside method bodies. E.g. allowing to derive from `class Base<T> where T : IFoo { }` with `class Derived<T> : Base<T> { }` to load for `Derived<IFoo>` is not in scope. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One of the common problems is a need to match and decompose generic type:
void m<T>(T o)
{
if (T is List<U>) // If T is List of some U, give me the U. (This won't compile today.)
{
foreach (U e in o) Console.WriteLine(e);
}
else
{
Console.WriteLine(o);
}
}
We should mention it here at least. (It would be nice if the design can solve it too :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had a proposal for something like this, just FYI: dotnet/csharplang#1992
EDIT: oh wait, slightly different issue, but related and I imagine with some similar use cases
EDIT 2: er, there's something odd about the above. T is not an instance, its a type, but you are putting an instance into the a
var, so I dont know anymore. Should that not be o is List<U> a
>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
er, there's something odd about the above. T is not an instance, its a type, but you are putting an instance into the a var
Fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One of the common problems is a need to match and decompose generic type:
I'll add that as out of scope. Would need to rename this proposal because that's no longer "bridging constraints".
I don't see a way how we could encode that without actually extending the file format in a structurally-incompatible way. (So far our extensions to the file format didn't require changes to e.g. signature parsing, but this might).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It may be noteworthy that this feature would create an interesting issue that exists in my proposal as well that would need to be handled somehow:
class C : IEnumerable<int>, IEnumerable<string> { }
void M<T>(T o)
{
if (T is IEnumerable<U>) // what do we do here if T is C?
{
}
}
|
||
It would greatly simplify the codegen impact of this if we were to limit the amount of places within a method body that are allowed to break the constraints. The least impactful and potentially sufficiently powerful change would be allowing to break constraints on `call` instruction. | ||
|
||
The proposal is to: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was it considered to reuse some of the logic that exists for static virtuals in interfaces
?
That is, static virtuals in interfaces
works today via constrained <type-token> call <method-token>
and the runtime has special logic that allows this to resolve the method and invoke it.
Could a similar thing be done here such that the compiler does a relevant check and then simply emits a constrained call
allowing the runtime to do any additional work required to validate the type-token
is actually compatible with the required constraints for method-token
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What would be the type-token
in this case?
For constrained calls on instance methods, the type-token
is the type of the this
used in the interface call. For static methods, it's the type on which to search for the static implementation. I don't see a straightforward thing we could put there for constraint bridging.
We could potentially put there something non-sensical (like constrained void
) that would serve as a marker, as an alternative to what Jan proposed with call RuntimeHelpers.CheckConstraintsForMethodCallThatFollows
above, but I don't see anything meaningful we could put there.
@@ -0,0 +1,110 @@ | |||
# Bridging generic constraints | |||
|
|||
Since introduction of generics in .NET there hasn't been a way to go from less contrained type parameter T to a more constrained type parameter U. If U has any constraints that T doesn't have, a T cannot be substituted for U. We've seen many instances where it would be useful to allow this for concrete substitution of T that match U's constraints. There's no way to do this substitution at runtime besides using reflection right now. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since introduction of generics in .NET there hasn't been a way to go from less contrained type parameter T to a more constrained type parameter U. If U has any constraints that T doesn't have, a T cannot be substituted for U. We've seen many instances where it would be useful to allow this for concrete substitution of T that match U's constraints. There's no way to do this substitution at runtime besides using reflection right now. | |
Since introduction of generics in .NET there hasn't been a way to go from less constrained type parameter T to a more constrained type parameter U. If U has any constraints that T doesn't have, a T cannot be substituted for U. We've seen many instances where it would be useful to allow this for concrete substitution of T that match U's constraints. There's no way to do this substitution at runtime besides using reflection right now. |
|
||
### Potential shortcut: Stop validating constraints | ||
|
||
One more avenue could be to drop all constraint checks within the runtime and make constraints source compiler's concern only. We already have a constraint (`unmanaged` in C#) that is only known to the source compiler and not enforced at run time. This would likely require some thoughts around failure modes when invalid IL within a method body IL is encountered. This document is not going to explore this direction further since the assumption is that we do not want to drop constraint checks from the runtime, we just want to delay them. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something that came to me right as I went to bed last night: If we get the ability to check generic constraints (like unmanaged
) using runtime APIs, could that also open the door for new kinds of constraints? As a concrete example (pun half-intended) there's been places where it'd be real nice to constrain a type parameter to only concrete types (one such case has to do with static abstract
members in interfaces, another is when T
is something that would get instantiated with reflection such as in a DI container or the like).
The idea here is that for such a new constraint the compiler would emit a call to the appropriate helper method which enforces the appropriate behavior at runtime without needing a new IL encoding for said constraint.
Draft Pull Request was automatically closed for 30 days of inactivity. Please let us know if you'd like to reopen it. |
I pushed out a new version that uses Jan's suggestion from #97084 (comment). Instead of specialized helpers that check constrains and codegen/debugger/etc needs to map to the constraints themselves, there's just one helper that checks all constraints relevant to the runtime. This means that if a language comes up with new constraints that don't exist at the IL level (like |
call RuntimeHelpers.CheckConstraintsForMethodCallThatFollows | ||
br.false .... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really need something like this?
We already know statically if a check might be needed for a given callsite, so why can't we just do the validation implicitly as part of such a call
?
That is for a specialized generic (value type), we already statically know if a given T
meets the requirements for a more constrained call. We can simply allow the call or replace it with a throw if it isn't feasible.
Inversely, for a shared generic, we can see that the call might not be possible (we already throw an exception for this today), so we could just replace the throw with an implicit call to CheckConstraintsForMethodCallThatFollows
and throw if they aren't met.
There isn't really any "need" for it to be explicit in the IR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My general consideration here is that what we're doing is basically no different from bounds checking.
So it is entirely feasible for the JIT/AOT to just insert the implicit constraint check before any call where we need to see if a constraint is met or not.
This is overall less limiting to the IL, will make the IL smaller, and will more easily allow the JIT to hoist, fold, or otherwise optimize these type checks (just like with bounds checking).
This is no different from C# emitting the call itself, the JIT just does it implicitly as it does with bounds checking.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have been trying to maintain ability to verify IL type safety for safe C#.
If we do not introduce a marker for this, we will give up on ability to verify any constraints in IL or have good error handling when the constraints are violated unintentionally (e.g. by mismatch between ref and implementation).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
C# is already going to have concepts that can't be met via the constraint call (like unmanaged
) and so much like other new concepts, any verifier may already need to do things like recognize that if (!RuntimeHelpers.IsReferenceOrContainsReferences<T>())
So for IL verification it seems trivial (and goodness) for it recognize these higher level checks already.
While no such need actually exists for IL, which may even include unsafe or unverifiable code (perhaps the user has a private or internal method and already validates the call-sites do the right thing otherwise for example). So it doesn't seem fruitful (to me) to require a lot of additional IR.
Consider for example places where a different type check may already exist which C# doesn't quite understand but the user knows is valid. A simple example is in Vector128<T>
where every call is prefixed with ThrowHelper.ThrowForUnsupportedIntrinsicsVector128BaseType<T>();
and where the developer knows that this means the where T : INumber<T>
interface is definitely implemented.
We could make IR even bigger by inserting these runtime check calls, duplicating an extra throw, etc.
But we could also just have the JIT do the proper thing and make this work. Keep IL small, make it easier to optimize the IR (hoisting, folding repeated calls, etc). Make the overall IL more flexible for user patterns (whether verifiable or not), and make it so that the general concept works more generally and not with very stringent constraints around control flow, where the call must exist compared to other calls, ordering, etc
(The current requirements that it must exist before a call with no other calls and only basic operations can even make it very difficult to optimize or hoist other code and may not play well with inlining, helper calls, or other patterns that may be common).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
C# is already going to have concepts that can't be met via the constraint call (like unmanaged)
unmanaged
is outlier that was not implemented properly across language/runtime. I agree that we need to consider it in the design, but only as an after-through to make it work. It should not be the element that we design this around.
Keep IL small
The proposed marker is ~7 bytes of IL in the current proposal. 7 bytes extra for constraint bridging is nothing given overall cost of generic code.
make it easier to optimize the IR (hoisting, folding repeated calls, etc)
I disagree that the marker prevents optimizations or make optimizations harder.
The marker would be recognized as an intrinsic by the importer (just like the explicit type checks).
For fully specialized generic code, the marked would be imported as a constant that gets folded immediately (just like the explicit type checks).
For shared generic code, the importer would import the marker as constraint-validating dictionary lookup: Fetch the actual call target from the dictionary. If the call target is sentinel value, the constraints are not satisfied, and target call is skipped. Otherwise, the constraints are satisfied, and the target is called.
In alternative with explicit type checks, the type checks would be imported as separate dictionary lookups and separate calls. In theory, the JIT can do analysis to prove that these type checks are equivalent to the constraints and fold everything back to a single dictionary lookup. It would be fairly complex optimization: The JIT would have to collect the type conditions checked upfront in some form and pass them to the VM to validate whether the set of conditions is equivalent to the call constraints.
If the marker call that returns bool is undesirable, I would be fine with having a marker call that returns void
or with having some other explicit identification of constraint bridging calls. However, I think it is important to have explicit non-ambiguous identification of calls that are expected to bridge the generic constraints to avoid guessing whether the type safety violations are intentional or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
any verifier may already need to do things like recognize that
if (!RuntimeHelpers.IsReferenceOrContainsReferences<T>())
I call out the unmanaged
constraint specifically - IsReferenceOrContainsReferences
is not a sufficient check for unmanaged
constraint because it doesn't do the right thing for Nullable<T>
. C# would need to generate a call to IsReferenceOrContainsReferences
and and call to something that is recognized by the verifier as a thing that ensures it meets the struct, new()
constraint (the only relevant part of unmanaged
constraint are those two). Whether it's CheckConstraintsForMethodCallThatFollows
or default(T) != null
, or something else doesn't matter.
I don't think we need to teach runtimes/verifiers about unmanaged
constraint for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I disagree that the marker prevents optimizations or make optimizations harder.
I'm not sure. Various runtimes (RyuJIT has a few, but Mono has many) hit limitations when trying to make inlining observations due to the introduction of branches, particularly when those branches are in loops or similar.
It's definitely something we can fix longer term, but it's also one of those things where it seems an unnecessary cost to me. That being said, we can at least always decide to support it implicitly in the future if it does turn out to be a problem.
The proposed marker is ~7 bytes of IL in the current proposal. 7 bytes extra for constraint bridging is nothing given overall cost of generic code.
Notably this will be per call. Users can notably workaround that cost by defining their own helper to containerize the logic, but I expect it's going to be one of those cases where a more typical use-case will result in several calls each with their own 7-byte overhead and the JIT needing to do additional tracking to effectively associate the two calls together.
For fully specialized generic code, the marked would be imported as a constant that gets folded immediately (just like the explicit type checks).
This isn't really as trivial or even necessarily pay for play when it's explicit as when its implicit. Because its a separate IL opcode and doesn't actually work like an IL prefix, we functionally have to recognize it and then look ahead to at least the next opcode. However, in practice and based on the example, this could potentially be several opcodes ahead (past a branch and past several push/ld opcodes).
We're going to need quite a bit of additional tracking to basically recognize the call, track that we're now expecting a future constrained call to appear "next", fail if any incorrect opcodes appear between the check and the call, then check if the constraint can be met as a constant to determine if the special intrinsic node representing the check should actually be introduced or not. Inversely we could always introduce it, track it, and then remove that node at the call if it's not needed, but in either case this is quite a bit of additional work required in the main import loop to validate the user did everything correctly.
Inversely, making it implicit avoids all of this overhead. The only downside is that you don't know if the user "intended" for it to work that way or not. However, you could "cheat" and make that work with a new IL prefix on the call, using an existing IL prefix with a special marker (say constrained <RuntimeHelpers.DynamicConstraintCheck> call <methodToken>
), or even some attribute on the method. All of which are slightly more pay for play, less error prone to both produce and consume, and result in smaller IL.
In alternative with explicit type checks, the type checks would be imported as separate dictionary lookups and separate calls. In theory, the JIT can do analysis to prove that these type checks are equivalent to the constraints and fold everything back to a single dictionary lookup. It would be fairly complex optimization: The JIT would have to collect the type conditions checked upfront in some form and pass them to the VM to validate whether the set of conditions is equivalent to the call constraints.
We're going to have these explicit type checks from C# anyways, some of which are foldable and others (particularly in shared generic code) which won't be. I'm not recommending that the JIT recognize these as making the call safe to do, rather only that it tries to do the call rather than failing statically when the method has opted into that.
It's basically the exact same approach as proposed, just with a less intrusive and less error prone mechanism than inserting a separate call
before every single more constrained call we want to handle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Various runtimes (RyuJIT has a few, but Mono has many) hit limitations when trying to make inlining observations due to the introduction of branches, particularly when those branches are in loops or similar.
Yes, we are increasingly depending on great JIT optimizations in the feature designs, investing into RyuJIT heavily and the non-RuyJIT based code generators are falling behind. All the numerics work is a prime example of that. This feature is heading in the same direction.
I expect it's going to be one of those cases where a more typical use-case will result in several calls each with their own 7-byte overhead and the JIT needing to do additional tracking to effectively associate the two calls together.
The latest version of this proposal is explicitly scoping the type safety violations to calls and prohibits them everywhere else to avoid the issues discussed earlier in this PR. If your block has multiple calls, the IL codegen gets complicated as you need to avoid hitting other type safety violations. I expect that the Roslyn codegen will want to always move the whole block to a separate helper method with the more specialized constraint instead of dealing with figuring out whether it is type safe to do everything in inline IL or not.
We're going to need quite a bit of additional tracking to basically recognize the call, track that we're now expecting a future constrained call to appear "next"
We have a lot of prior art with doing look-ahead like this and it is not particularly complicated. I am not worried.
constrained <RuntimeHelpers.DynamicConstraintCheck>
Nit: This is 6 bytes of IL compared to 5 bytes of IL for void-returning marker call.
+ /// <summary> | ||
+ /// Represents a runtime feature where constraint validation can be delayed until run time. | ||
+ /// </summary> | ||
+ public const string DelayedConstraintCheck = nameof(DelayedConstraintCheck); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: This is not strictly required. The existence of this feature can be inferred from existence of the CheckConstraintsForMethodCallThatFollows
API.
} | ||
``` | ||
|
||
The requirement of never having an invalid type instantiation within the runtime type system means that if we're compiling `Do<int>` (where we cannot allow `Constrained<int>` to exist), the code generator or debugger cannot ask questions about all locals (IL locals are scoped to the method, not to a basic block), exception handling regions, or any IL within the `IsAssignableFrom` check. While it may be reasonable to expect the `IsAssignableFrom` check gets optimized out, it would likely not happen for unoptimized code. Locals and EH regions pose additional programs. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The requirement of never having an invalid type instantiation within the runtime type system means that if we're compiling `Do<int>` (where we cannot allow `Constrained<int>` to exist), the code generator or debugger cannot ask questions about all locals (IL locals are scoped to the method, not to a basic block), exception handling regions, or any IL within the `IsAssignableFrom` check. While it may be reasonable to expect the `IsAssignableFrom` check gets optimized out, it would likely not happen for unoptimized code. Locals and EH regions pose additional programs. | |
The requirement of never having an invalid type instantiation within the runtime type system means that if we're compiling `Do<int>` (where we cannot allow `Constrained<int>` to exist), the code generator or debugger cannot ask questions about all locals (IL locals are scoped to the method, not to a basic block), exception handling regions, or any IL within the `IsAssignableFrom` check. While it may be reasonable to expect the `IsAssignableFrom` check gets optimized out, it would likely not happen for unoptimized code. Locals and EH regions pose additional problems. |
?
call CodeWithMoreStringentConstraints | ||
``` | ||
|
||
The sequence must be within the same basic block and there must not be any other call/callvirt between `CheckConstraintsForMethodCallThatFollows` and the more constrained method call. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The sequence must be within the same basic block
It looks like this requirement is broken in the proposed IL. There is a br.false
following call RuntimeHelpers.CheckConstraintsForMethodCallThatFollows
. Basic blocks do not contain branches in the middle.
|
||
static void Do<T>() | ||
{ | ||
if (typeof(T).IsAssignableFrom(typeof(IInterface)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be typeof(T).IsAssignableTo(typeof(IInterface))
or typeof(IInterface).IsAssignableFrom(typeof(T))
.
I think this PR served it's purpose. I don't think we have a spot for "draft" design documents so no point merging this. A closed PR is as good as any storage location for the draft. This needs a committed plan on the C# language side to proceed in any way past this draft. |
(Only exploratory, no commitment whatsoever)