Open hez2010 opened 1 year ago
Tagging subscribers to this area: @dotnet/area-meta See info in area-owners.md if you want to be subscribed.
Author: | hez2010 |
---|---|
Assignees: | - |
Labels: | `area-Meta`, `untriaged`, `feature-request` |
Milestone: | - |
Thanks @hez2010 Appreciate this issue and the effort put into the prototype as well as details above. I'm going to move this to .NET 9 since there is no chance this is a .NET 8 request.
I will also tag a few people for visibility. Note that since we are closing down .NET 8, this is likely to receive little feedback right now. I would ping this issue in mid-September when people start mulling over vNext.
/cc @jaredpar @stephentoub @davidwrighton @tannergooding @jkotas @VSadov @tommcdon @MichalStrehovsky @lambdageek
This is interesting. There are quite a few details glossed over that I think are hiding serious work:
public int Length { get; } // ldtoken N; ret;
) suggests that ldtoken
could get you an actual int32
value instead of a RuntimeTypeHandle
, but I think there might be issues in the interaction with the "Generic on Const Generic Type Parameter" proposal - in that case I think we might want ldtoken N
to give us a TypedReference
. Two further comments
ldtoken
rather than adding a new opcode?why be stingy with IL opcodes and reuse
ldtoken
rather than adding a new opcode?
This was discussed fairly heavily in the C# discord. At various points, we'd talked about using ldtoken
followed by a RuntimeHelpers
call, and a separate opcode, but ldtoken
alone was what landed. The basic reason is that ldtoken const !0
doesn't really have a meaningful token to return, aside from the constant value itself. It could return the type token for the type of the parameter (to allow e.g. typeof
), but that seemed somewhat wrong, because the parameter is a value, not a type, so ldtoken
would be skipping over the value in that case. Plus, that information is already implicitly known by looking at the signature (even in the case where we have <TValue, const TValue Value>
; the type is statically known to be TValue
) so loading the type that way is redundant and unnecessary. The only other piece of information that would be useful to have for such a value is the value itself, thus ldtoken
loads that value.
Arithmetic Operations
There were also other alternatives discussed for doing arithmetic on values:
ADD CTARG !0 INT32 5
to generate a const type parameter representing !0 + 5
Define a subset of IL which is valid constexpr
, allow marking methods constexpr
, then add a signature CONST_METHOD_CALL <method token> <const value argument>...
to enable calling those.
constexpr
methods, and generally doing more complex/interesting operations.Presumably, this would be interpreted with some limited budget to prevent overly massive evaluation times. Even with that, however, I'd expect it to very easily cause throughput issues if not used carefully.
- If we're bumping the IL metadata version, why be stingy with IL opcodes and reuse
ldtoken
rather than adding a new opcode?
An out-of-scope topic here: if we are bumping IL metadata version, we may have a chance to change some new concepts encoded by custom attributes to new flags. Please consider overhauling the IL itself.
I imagine it should be possible to implement the ValueArray<,>
with pure managed code, just like Action<...>
or Func<...>
:
[System.Runtime.CompilerServices.InlineArray(N)]
public struct ValueArray<T, int N>
{
private T elem; // Repeat the field elem for N times
//public members omitted
}
Sadly, this has not been implemented even with conventional type generics. https://github.com/dotnet/csharplang/discussions/6923
I imagine it should be possible to implement the ValueArray<,> with pure managed code
Unfortunately it's not possible as attribute doesn't support using open generic type at all.
I created an IL implementation of Span`2<T, int Rank>
and Matrix`2<int Rows, int Cols>
and wrote two executable examples. This is not a language/API proposal, only a showcase of a feature and can be ilasm
'ed and corerun
'ed using current MVP implementation
Full IL listings here: https://gist.github.com/KirillAldashkin/62eee5a20f6cc3e71920ea7d40ba5cb2
C# dummy code (type[num]
== FixedBuffer<type, num>
):
readonly ref struct Span<T, int Rank>
{
private ref T _reference;
private int[Rank] _lengths;
Span(ref T reference, params int[Rank] lengths);
nuint TotalLength();
int Length(int rank);
ref T GetItem(params int[Rank] indices);
}
struct Matrix<int Rows, int Cols>
{
private int[Cols][Rows] _matrix;
ref float GetItem(int row, int col);
}
static class MatrixMath
{
static Matrix<Rows, Cols> Multiply<int Rows, int Cols>(Matrix<Rows, Cols> mat, float factor);
static Matrix<Rows, Cols> Multiply<int Rows, int Mids, int Cols>(Matrix<Rows, Mids> a, Matrix<Mids, Cols> b);
}
int Main_MatrixSample()
{
Matrix<2, 3> MatA = new([20, 0, 30],
[30, 0, 20]);
Console.WriteLine($"A = {MatA}");
Matrix<3, 4> MatB = new([5, 0, 7, 0],
[0, 0, 0, 0],
[0, 7, 0, 5]);
Console.WriteLine($"B = {MatA}");
MatA = MathMatrix.Multiply(MatA, 0.1f);
Console.WriteLine($"A = A * 0.1 = {MatA}");
Matrix<2, 4> MatC = MatrixMath.Multiply<2, 3, 4>(MatA, MatB);
Console.WriteLine($"C = A * B = {MatC}");
}
int Main_MultidimensionalSpanSample()
{
int* ptr = stackalloc int[1000];
for(int i = 0; i < 1000; i++) *(ptr+i) = i;
Span<int, 3> span = new(ptr, 10, 10, 10);
Console.WriteLine(span[5, 6, 7]);
}
I imagine it should be possible to implement the ValueArray<,> with pure managed code
Unfortunately it's not possible as attribute doesn't support using open generic type at all.
I think it's important to note that the custom attribute blob format can version separately from the greater ECMA metadata format.
@lambdageek I have changed part of the proposal.
why be stingy with IL opcodes and reuse ldtoken rather than adding a new opcode
In my earliest prototype, I added a new IL opcode ldctarg
for loading the const value of a const type argument, and keep the ldtoken
as is. But soon I found that a const type argument is exactly a const value, so it doesn't make sense to use ldtoken
to load the TypeHandle
of a const value, neither do we support typeof(42)
. So I end up using ldtoken
to load the const value from a const type argument.
About const arithmetic, maybe we could allow using a special static class for a const generic argument. To use a static class for a const generic argument, the static class shall have a special initonly field of the desired type initialized by the static constructor. For example:
[ConstGenericArgument(typeof(int))]
static class Sum<int N1, int N2>
{
public static readonly int __value = N1 + N2;
}
static ValueArray<T, Sum<N1, N2>> Concat<T, int N1, int N2>
(ValueArray<T, N1> array1, ValueArray<T, N2> array2) {...}
The common arithmetic operations could be included in the BCL, and C# (or other languages) provides shorthand syntaxes for them.
Constant const generic arguments could also be represented in this way like
static void Foo<int N>() {...}
[ConstGenericArgument(typeof(int))]
static class Const42
{
public const int __value = 42;
}
Foo<Const42>();
This way, it might not be necessary to bump the IL metadata version.
Const arithmetic leads to problems. Consider:
static ValueArray<T, Sum<N1, N2>> ConcatButWithReversedArgumentOrder<T, int N1, int N2>
(ValueArray<T, N1> array1, ValueArray<T, N2> array2)
=> Concat(array2, array1);
Concat(array2, array1)
would be of type ValueArray<T, Sum<N2, N1>>
, which is not "obviously" ValueArray<T, Sum<N1, N2>>
.
My opinion on this problem is simply to do nothing on the runtime side. It is up to C# (or other languages) and its compiler to decide whether they can verify this kind of type identity transforms statically, or they require some explicit conversion expression in the source code and possibly insert runtime checks in the IL.
About Constraints, I don't think they are really necessary on the runtime side or the runtime should enforce them. If an invalid const generic argument is given, just throw an exception in the constructor or method. As const generic is never shared, the AOT/JIT should be able to remove these checks.
What's more, I can't see how const arithmetic and statically enforced constraint could live together peacefully. Consider
static ValueArray<T, Sum<N1, N2>> Concat<T, int N1, int N2>
(ValueArray<T, N1> array1, ValueArray<T, N2> array2)
where N1 >= 0, N2 >= 0
{...}
static void Foo<T, int N1, int N2>
(ValueArray<T, N1> array1, ValueArray<T, N2> array2)
where N1 >= 0, N2 >= 0
{
ValueArray<T, Sum<N1, N2>> concat = Concat(array1, array2);//Let's say Foo concats them as an implementation detail.
...
}
If ValueArray<T, int N>
is constrained to N >= 0
, then ValueArray<T, Sum<N1, N2>>
would not compile, because N1 + N2
may overflow and be negative. It would have to add another constraint N1 + N2 >= 0
, which is really annoying, especially for Foo
as it only uses ValueArray<T, Sum<N1, N2>>
as an implementation detail.
It could be helpful to have some custom attributes describing the desired constraints or analyzers detecting invalid const generic arguments, though.
It may be helpful to also start an issue or discussion in dotnet/csharplang or repos of other languages.
Constant const generic arguments could also be represented in this way like
public void Foo(ValueArray<int, 42> arr)
in assembly A, and the type of arr
got lowered to an A.ValueArray42<int>
. Now, in another assembly B, I have the generated type B.ValueArray42<int>
as well. Those two ValueArray42<int>
s are not the same type, so we can not pass a B.ValueArray42<int>
to a parameter that expects an A.ValueArray42<int>
. If you want to say "We can use implicit casting for it", but still, they are not the same type and have different type tokens in the metadata, so we are not able to say whether those two types are the same at runtime. And implicit casting cannot handle cases like casting a Foo<A.ValueArray42<T>>
to Foo<B.ValueArray42<T>>
. Foo<1>
is just an instantiation of Foo<int T>
so we can instantiate Foo<2>
, Foo<3>
, etc. using reflection without emitting any new type at runtime. But with your approach, we need to emit new types Foo2
, Foo3
, etc. at runtime because we don't have a type called Foo2
or Foo3
in the assembly, which is also not supported by NativeAOT. This way, it might not be necessary to bump the IL metadata version.
We also have other features that we want to get in but are not able to do due to the metadata neck. This 21-year-old metadata v2.0 is limiting too many things today, and we can not afford consistently to apply a workaround upon a workaround in the runtime. Const generics is only one of them.
About Constraints, I don't think they are really necessary on the runtime side or the runtime should enforce them. If an invalid const generic argument is given, just throw an exception in the constructor or method.
With the constraint approach, we are already checking it only at the time an instantiated type is loading and has never been loaded before, so each type instantiation only needs to be checked once.
because N1 + N2 may overflow and be negative
This can be simply checked at runtime to make sure an exception will be thrown when an overflow is encountered.
It may be helpful to also start an issue or discussion in dotnet/csharplang or repos of other languages.
It's already on my mind and we will start to work on a design and spec for this once work on net8.0 RTM is done :)
My suggestion is only a representation of const generic arguments in IL. So ValueArray<int, Const42>
is ValueArray<int, 42>
, and ValueArray<int, Sum<42, 1>>
is ValueArray<int, 43>
. At runtime, these Const42
and Sum<42, 1>
vanish when used as const generic arguments.
A static class can not be used as a type argument
What I meant is to allow using a special static class to represent a const generic argument.
A generated type simply doesn't work because you can not expose it in ABI. Imagine you have a public void Foo(ValueArray<int, 42> arr) in assembly A, and the type of arr got lowered to an A.ValueArray42
. Now, in another assembly B, I have the generated type B.ValueArray42 as well. Those two ValueArray42 s are not the same type, so we can not pass a B.ValueArray42 to a parameter that expects an A.ValueArray42 . If you want to say "We can use implicit casting for it", but still, they are not the same type and have different type tokens in the metadata, so we are not able to say whether those two types are the same at runtime. And implicit casting cannot handle cases like casting a Foo<A.ValueArray42 > to Foo<B.ValueArray42 >.
I never said to generate types like ValueArray42<>
.
Generated types can not handle cases where N is not statically known, so your approach is blocking reflection usage. With the real const generics, a Foo<1> is just an instantiation of Foo
so we can instantiate Foo<2>, Foo<3>, etc. using reflection without emitting any new type at runtime. But with your approach, we need to emit new types Foo2, Foo3, etc. at runtime because we don't have a type called Foo2 or Foo3 in the assembly, which is also not supported by NativeAOT.
I never said to generate types like Foo2
or Foo3
. I said to generate special static classes like Const42
, but that is only used to represent the const generic argument in IL, and reflection may well instantiate Foo<2>
Foo<3>
without requiring a Const2
or Const3
.
Atrributes don't support using open generic type parameters so your approach is not able to support generic on const type parameter.
The attribute is not an essential thing of my suggestion. The attribute may be placed somewhere else like:
[ConstGenericArgumentClass]
static class Sum<int N1, int N2>
{
[ConstGenericArgumentField]
public static readonly int __value = N1 + N2;
}
Or we could make attributes support using open generic type parameters if we are already talking about big changes like const generic.
As a side point, I don't quite see how generic on const type parameter is useful. If the example of usage is struct FixedBuffer<T, TSize, const TSize Size>
, I don't see any problem with just struct FixedBuffer<T, long Size>
.
The main point of my suggestion is not to avoid bumping IL version, but to support const arithmetic.
If constraints are only checked at runtime, I don't see much necessity of them. For example, instead of
static ValueArray<T, N1 + N2> Concat<T, int N1, int N2>
(ValueArray<T, N1> array1, ValueArray<T, N2> array2)
where N1 >= 0, N2 >= 0, N1 + N2 >=0
{...}
why not just
static ValueArray<T, N1 + N2> Concat<T, int N1, int N2>
(ValueArray<T, N1> array1, ValueArray<T, N2> array2)
{
if (N1 < 0 || N2 < 0 || N1 + N2 < 0) throw new ArgumentException();
...
}
What I meant is to allow using a special static class to represent a const generic argument
Sorry for I misread your previous comment.
I don't quite see how generic on const type parameter is useful
This is useful especially in graphics programming, where the multiplier can be either float
or double
.
And in the design of generic constraint, we also need this feature otherwise we cannot define a IConstExpression<TValue, TValue Value>
.
Even your proposed Sum
type can be:
[ConstGenericArgumentClass]
static class Sum<T, T N1, T N2>
{
[ConstGenericArgumentField]
public static readonly T __value = N1 + N2;
}
Otherwise, we have to consider generic overloading (otherwise you cannot define Sum<int N1, int N2>
and Sum<float N1, float N2>
at the same time), which is also a non-trivial work and requires a metamodel change too.
If constraints are only checked at runtime
I implemented the prototype to check them at runtime, which doesn't mean we won't check them at compile time. We don't have the design for C# yet, so I cannot say anything about the compiler implementation.
I still have trouble imagining a situation where a const generic parameter of type other than int
is useful.
This is useful especially in graphics programming, where the multiplier can be either float or double.
Yes, the multiplier can be either float or double, but does the multiplier need to be a const generic parameter?
For [4], we can embed the coefficient into a multiplier type. This is especially useful in graphics programming. For example, when you are working with things about illumination, you will definitely want some multiplier types with coefficients (which are basically floating point numbers) that are guaranteed to be constants.
Is storing the coefficients into readonly fields not enough to guarantee them to be constant for the lifetime of the multiplier instance?
struct FixedBuffer<T, TSize, const TSize Size>
also sounds questionable. What is a FixedBuffer<int, long, 42424242424242>
? A pointer to a buffer of length 42424242424242? ValueArray<,>
is useful because it avoids heap allocations. Where is FixedBuffer
useful? Such large sizes often come from runtime calculations and cannot be easily provided as compile-time const generic arguments.
Is storing the coefficients into readonly fields not enough to guarantee them to be constant for the lifetime of the multiplier instance?
Then you will need to save them as a const field
to guarantee them to be constant for the runtime. But this means you cannot change them after you defined the type. While with const generics, you can change them by providing the const type argument then you can get a new instantiation that carries the constants you want. And if you need both float
and double
variants, without generics on type parameter you will need to define two distinct types because we don't support overloading on generics.
Another reason here is that we want to use generic on const type parameter to get rid of the need of overloading on generics, the latter is basically not achievable because implementing overloading on generics would break backward compatibility (an assembly already compiled by an older compiler won't be able to differentiate call targets while running on a new runtime).
And, actually, it's basically free to have the support for generic on const generic type parameter, we already have almost all the necessary infrastructure to support it. So why not?
What is a FixedBuffer<int, long, 42424242424242>?
It should be ValueArray
here, I just forgot to rename FixedBuffer
to ValueArray
in the proposal.
ValueArray<int, long, 42424242424242>
sounds problematic to me because there would be a StackOverflowException
every time a variable of this type is stored on stack. It would be only possible to use it indirectly by ref ValueArray<int, long, 42424242424242>
or ValueArray<int, long, 42424242424242>*
, which just sounds undesirable to me.
Speaking of const/literal field,
class MyClass<int N>
{
const int AnotherN = N;
}
class MyClass<int N>
{
const int NSquared = N * N;
}
class MyClass<double X>
{
const double CosX = Math.Cos(X);
}
Math.Cos(X)
is not even guaranteed to be exactly equal across different runtime setups, but surely this is desirable if const generic of type double
is supported, right?
There could be two answers to these problems.
const double CosX = Math.Cos(X);
should not be supported because Math.Cos(X)
is not strictly a constant, even at runtime. Just use static readonly fields for these scenarios.class MyClass1<double X>
{
...
}
class MyClass2<double X>
{
const double CosX = Math.Cos(X);
MyClass1<CosX> Foo() {...}
}
This looks like a similar problem with custom attribute arguments not allowed to be open generic types.
Speaking of const/literal field,
For 1, 2 and 3, you need to use static readonly
field instead of const
field. The const field in IL can only express a literal value so you cannot write anything except the literal value itself.
The JIT is able to fold a static readonly
field into constant at runtime.
This looks like a similar problem with custom attribute arguments not allowed to be open generic types.
Yeah this is a bit unfortunate. I would expect we can bring the support for const arithmetic later once we have associated types.
For someone who wants to try out the reflection APIs and ValueArray
support, I have them implemented in another branch: https://github.com/hez2010/runtime/tree/feature/const-generics-managed
The implementation of runtime support for ValueArray
is pretty straight-forward as we can reuse all the existing InlineArray
implementation.
3. If so, considering const arithmetic, is this supported?
class MyClass<int N> { const int NSquared = N * N; }
What happens if N * N
would overflow? This is well-defined at runtime, it depends on use of checked
or unchecked
, or a compiler flag to set the default.
For anyone who wants to try out const generics, I have the full CoreCLR and Roslyn implementation (prototype) in
If you want to give it a quick try, you can download the pre-built binaries and then you are ready to go (please follow the instructions in README.txt): https://1drv.ms/u/s!ApWNk8G_rszRgrxL0ch8e9h-gBiVaw?e=cwpUVD
You will need the latest preview version of Visual Studio 2022.
This is how it looks in Visual Studio:
To run the built artifacts, you need to use corerun in Core_Root.
Hi @hez2010
Incredible work you have done here. I've tried your solution in VS, but I have the following warning
Code | Description | File | Line |
---|---|---|---|
MSB3270 | There was a mismatch between the processor architecture of the project being built "MSIL" and the processor architecture of the reference "System.Private.CoreLib", "AMD64". This mismatch may cause runtime failures. Please consider changing the targeted processor architecture of your project through the Configuration Manager so as to align the processor architectures between your project and references, or take a dependency on references with a processor architecture that matches the targeted processor architecture of your project. | C:\Program Files\Microsoft Visual Studio\2022\Preview\MSBuild\ Current\Bin\amd64\ Microsoft.Common.CurrentVersion.targets | 2364 |
I installed every vsix
files (exept the arm64
one)
I have no other errors/warnings in VS and the project compile fine, but I can't run it, I have this error in the console
Unhandled exception. System.BadImageFormatException: Old version error. (0x80131107)
...\ConstGenerics.NET\ConstGenerics\bin\Release\net8.0\ConstGenerics.exe (process 32660) exited with code -532462766.
If you prefer, I can write an issue in one of your repo, just tell me which one.
@FaustVX The System.Private.CoreLib issue is because I built the coreroot with x64 architecture, which is not platform neutral. You can just ignore this warning. As for the latter issue, you need to run your code using corerun in Core_Root:
corerun.exe --clr-path <absolute_path/to/Core_Root> <path/to/your/assembly>
@hez2010 can you please send a checksum for that prebuilt binaries archive? 7zip says it's corrupted
Hey everyone, I've successfully made an SDK for it, so you can build and run your code directly just use Visual Studio or Visual Studio Code.
If anyone wants to try it, you can download the SDK, VS extensions and the language server here: https://1drv.ms/u/s!ApWNk8G_rszRgrxP32IMKhW-V8iWug?e=JBn8wU
Version: 20230912 Build 1 Checksum: a8c9ee29d1accd14797f60bedced312f9524391b
Please do follow the README.md.
It supports all things that has been marked with ⭕ or❗ in this feature proposal, for example:
class Foo<T, int N>
.new Foo<int, 42>()
.void Foo<int X>
.Foo<42>()
.class Foo<T, T X>
, then you can use it with Foo<int, 42>
as well as Foo<float, 42.42424f>
.Console.WriteLine(X)
in the type class Foo<int X>
.typeof
support. eg. typeof(42)
.new Foo<(short)42>
, typeof((short)42)
ValueArray<T, int X>
that can be used as a fix-sized type with type T
and length X
.ValueArray
type, eg. int[42]
.type.IsGenericParameter && type.HasElementType
.type.GetElementType()
.type.IsConstValue
.type.GetElementType()
.type.ConstValue
.Type.MakeConstValueType()
@hez2010 can you please send a checksum for that prebuilt binaries archive? 7zip says it's corrupted
I uploaded a new one (and fixed a bug) in the above post. Please use that instead.
Checksum 653667f63ad239cfd80c856f60ea2f91934ad654
Hey everyone, I've successfully made an SDK for it, so you can build and run your code directly just use Visual Studio or Visual Studio Code.
If anyone wants to try it, you can download the SDK, VS extensions and the language server here: https://1drv.ms/u/s!ApWNk8G_rszRgrxP32IMKhW-V8iWug?e=JBn8wU
Version: 20230903 Build 1 Checksum: 0e64e1425519774ae2166aed23eda6a7a0fe13da
Please do follow the README.md.
It supports all things that has been marked with ⭕ or❗ in this feature proposal, for example:
- Declare a const generic type, eg.
class Foo<T, int N>
.- Use a const generic type, eg.
new Foo<int, 42>()
.- Declare a const generic method, eg.
void Foo<int X>
.- Use a const generic method, eg.
Foo<42>()
.- Generics on const type parameter, eg.
class Foo<T, T X>
, then you can use it withFoo<int, 42>
as well asFoo<float, 42.42424f>
.- Use const type parameter as constant directly. eg. calling
Console.WriteLine(X)
in the typeclass Foo<int X>
.typeof
support. eg.typeof(42)
.- Casting support in const type argument. eg.
new Foo<(short)42>
,typeof((short)42)
- A built-in value type
ValueArray<T, int X>
that can be used as a fix-sized type with typeT
and lengthX
.- A niche syntax for declaring a
ValueArray
type, eg.int[42]
.Full reflection support.
- To check whether a type parameter is const type parameter, use
type.IsGenericParameter && type.HasElementType
.- To get the type of a const type parameter, use
type.GetElementType()
.- To check whether a type argument is const type argument, use
type.IsConstValue
.- To get the type of a const type argument, use
type.GetElementType()
.- To get the value of a const type argument, use
type.ConstValue
.- To make a const value type, use
Type.MakeConstValueType()
the VS code part works great. the VS extensions break the component cache of VS constantly (latest preview)
- To get the value of a const type argument, use
type.ConstValue
.- To make a const value type, use
Type.MakeConstValueType()
Maybe names should use Literal
to align with FieldInfo.IsLiteral
?
- To get the value of a const type argument, use
type.ConstValue
.- To make a const value type, use
Type.MakeConstValueType()
Maybe names should use
Literal
to align withFieldInfo.IsLiteral
?
Sounds great. We may want all const
words to be literal
to be consistent with the metadata.
@AaronRobinsonMSFT Ping. It's already mid-Sept :)
Yep, Thanks @hez2010. This is something I am watching closely. Your enthusiasm here is most welcome and we appreciate how much effort has been put into addressing many of the community concerns. It is also impressive how much you've been able to enable.
This is a rather fundamental change that is going to require substantial scrutiny. The changes here impact the entire .NET ecosystem due to the metadata changes and that I fear is likely to get serious push back. Not unlike the various other proposals that have attempted broad metadata changes. Related to the ecosystem impact is the scenarios this unblocks. There is no doubt this feature enables currently unsupported scenarios, but the question is about the impact to the ecosystem, the cost, relative to the unblocking of new scenarios, the benefit.
Please don't take my word as the official perspective here, but something this profound is going to need, at a minimum, everyone I mentioned at https://github.com/dotnet/runtime/issues/89730#issuecomment-1658861829 to weight in and that is going to take some time.
I agree with @AaronRobinsonMSFT, I've been traveling much of the summer, and unable to look deep into proposals like this, but the necessary step to actually getting general agreement to move the entire ecosystem forward is to come up with a reason to do so. When a change does not require performing an ecosystem shift, it is much easier to justify, as the costs are dramatically lower, and effectively restricted to the development effort to build a feature/support it. However, for anything that requires many components to change has significant costs to many people and organizations, so we need greater justification for any such change. Looking at your proposal, I see the main benefit proposed as providing for an excellent abstraction for the creation of fixed sized arrays, matrices and vectors. While I see that this would have applications in the development of AI, numeric computing, and graphics programs, I fail to see an analysis of what those improvements are, and whether or not they are really worth the cost to the ecosystem of continuing with this proposal as compared to taking other changes to improve those facets of computing in .NET (and frankly whether or not those scenarios are more or less important to the broader set of .NET developers which are developing web based applications or client line of business applications.)
For instance, we have been making other improvements that make it easier to develop manually specified constant array types (see https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-12.0/inline-arrays). While the InlineArray
certainly does not have all of the capabilities as the generalized const generics feature you have developed here, we expect the feature to enable the usage of Span<T>
over fixed size buffers, and we expect customers to develop multidimensional arrays, large stack variables, and other solutions that broadly resemble the many of the new capabilities that generic constants can also provide on top of the InlineArray
capability that is now present in .NET 8.
All of that said, I personally have wanted something like this for many years and am very excited to watch what you are building here to see if we can develop a reason for the wider ecosystem to embrace a change like this. Once we have a good set of reasons why we might want to build this feature, and scenarios enabled/unblocked, we will also need to take a really deep look into the costs, and those can be quite surprising to developers who focus only on enabling new capabilities. For instance, additional generic specialization seems like a great win, but it tends to come with significant costs to the startup of applications. It is possible that shipping a feature like this would require us to also change our model for handling canonicalization and specialization within the runtime to maintain acceptable startup performance. Some implementations of .NET may not be able to do that and may experience different performance characteristics as compared to the CoreCLR implementation, what impact does it have on Native AOT code generation (does it encourage code patterns which cannot effectively be compiled ahead of time, or require excessive precompiled binary sizes) etc.
@AaronRobinsonMSFT @jkotas @davidwrighton
While actually we are breaking metadata because we want the type of a const type parameter to be part of the type parameter (and it should), so I need to add a field to the GenericParamRec
table.
But if you can accept something like class Foo<[LiteralTypeParameter<int>] T>
or class Foo<T> where T : LiteralType<int>
to be a solution then we don't need to make any break changes to the metadata.
However, it exposes an issue where we have an almost non-extensible metadata, and we definitely have some work to do around this in the future (at least adding a field to an existing table shouldn't be a breaking change). I would like to see const generics can be a push for this work.
@hez2010 Based on your current work, do you think it's possible to implement https://github.com/dotnet/csharplang/discussions/1315
@hez2010 Based on your current work, do you think it's possible to implement dotnet/csharplang#1315
They're unrelated features and should be implemented independently.
I have updated the proposal and my MVP implementation. Now we no longer have any breaking changes to the existing metadata! All existing ecosystem and tooling can continue to be used without any major concern of compatibility.
@hez2010 The issue isn't that we can't break metadata. The issue is that we need a great deal of justification to make changes that cause significant fractions of the sets of tools in use with .NET to experience significant breaks, and even more so we need justification for anything that provides substantial new capabilities. Even if we avoid changing the metadata format for this sort of change, we still need to weigh the costs and risks to the platform and the impacts the new code will have on the ecosystem. The technical work to enable a feature to work or not work, is one thing, and you've done much of that research already, but we also need to have a good reason to bring new features into the platform. The more the feature will have viral impact on the ecosystem, the more justification we typically need. And my judgement, is that this is a fairly viral feature that may impact large amounts of code.
For instance, what scenarios (as in applications that can more easily/efficiently/performantly) be developed with the change you are presenting here? Why is this better than alternative approaches? What are the alternatives? These are all questions that need to be answered before I can even start to review the technical changes you've made here.
I think it's time to update metadata for the upcoming type system improvement of C#. Extension is tricky in today's type system. There are also other currently hard features like varadic generics, and HKT. It would be long-term hard task of course. I want to bring it to design discussion, together with C# language design.
Any update on this? I've long desired const generics. They allow you to express units of measurement very elegantly. With the current system, each unit you require needs its own dedicated type. For example, if you have Distance
, Time
, and Speed
, creating a Speed
from Distance/Time
requires dedicated operator overloads. Want to add Acceleration
? Now you require another type! There's also the minor annoyance of "which type does this overload go under? Is it overloading Distance
or is it overloading Time
?"
With const generics, you could instead have a base Unit<int NTime, int NDistance /* other bases */>
. Distance
would become aliased to Unit<0,1/*...*/>
, Speed
would become Unit<-1,1/*...*/>
, etc. and dividing two of them would yield Unit<NTimeA - NTimeB, NDistanceA - NDistanceB /* other bases */>
.
This is actually a major annoyance at my job; Unit handling is a major component of our applications. Specifically, we have at least ten different unit systems we need to keep track of — each with their own dedicated type and custom operator overloads. I've long wondered about ways to reduce the bloat. A dedicated source generator has helped, but it still doesn't solve all the problems.
For alternatives: I've considered a simple Unit
that brings along integers for each dimension (maximum of seven for all SI bases), but that would make a type that was previously four or eight bytes long (a single float
or double
) is now 4*N
(N=tracked dimensions) bytes larger, which destroys the ability to hold them in registers.
If you instead factor out the "dimension" struct into a record (reducing Unit
to sizeof(float)+sizeof(nint)
or sizeof(double)+sizeof(nint)
), you now allocate a new "dimension" struct with each multiplication or division. And every mathematical operator (even those that don't allocate) now has to chase a pointer to the dimension structs. Lastly, regardless of inline or extracted dimensions, math is performed for each dimension in addition to the desired one; Adding two Unit
values must verify they have the same dimension before it adds the inner values - all at runtime.
Const generics allow compile time checking of all of that.
The current state is captured in the last comment from @davidwrighton - https://github.com/dotnet/runtime/issues/89730#issuecomment-1725954556.
There is currently no plan to accept this proposal without starting with the thorough ecosystem analysis asked for in the aforementioned comment. Event after that analysis, it is possible the benefits will not be worth the breaking change.
Maybe you should add @hez2010 to the core team to push forward this proposal or I'm afraid it will be declined forever
In any case a business case would be required and an impact analysis on other active business cases today. There is just no reason for const generics to exist that warrants the cost of implementing it, and other languages having the feature is not a reason unto itself. C# is not an openly-developed language, in that a feature being designed and developed outside of Microsoft does not negate Microsoft's processes for ensuring all additions to it have well-understood benefits to important revenue-driving applications of .NET today.
This is not an authoritative view on .NET development and I am not employed by Microsoft, just trying to echo the views already expressed concisely.
Maybe you should add @hez2010 to the core team to push forward this proposal or I'm afraid it will be declined forever
This level of feature requires involvement from the various .NET Architects and other high level engineers across the runtime
, libraries
, and languages
(not just C# either, but also F#, C++/CLI, etc). It is not something that can be driven by a single individual, regardless of where they worked or what team they are on.
Const generics is a very interesting feature and there are a number of people that would enjoy seeing it and who may have uses for it (I know I have some in the BCL from the numerics side of things). But at the same time, it is just one of many features that people want and it starts from a position of much higher cost due to the potential need to version IL for such support to exist. So not only does it have to be prioritized with respect to every other feature that has to be done, but it also has to have significantly more justification to show the break is worthwhile or additional analysis has to be done to weigh the alternative approaches that may not require versioning IL and introducing such a break.
Throwing more people/money at the problem space doesn't solve it either and would likely cause the feature to take even longer to get looked at. Not everything is parallelizable or infinitely scalable, there are fundamental bottlenecks required as part of the design and development process due to the entire picture needing to be looked at, including with respect to every other feature actively being worked on.
It's also worth noting that while the work hez2010
did here is significant and their enthusiasm for this space is much appreciated, it is only a very small minority of the work that will actually be required. This is also why its typical for OSS repos to ask for developers to open an issue and engage with the correct teams first, so they don't spend a significant amount of time doing work around an area that may not actually make it in or which may go about it in the wrong way. Actually doing the implementation work is really the smallest portion of getting any feature into the ecosystem, and it is a far minority of the total work required. The actual bulk of the work comes from designing the feature. This includes, but is not limited to, doing a deep dive analysis into how it will impact the ecosystem (from both a backwards and forward thinking perspective), how it would integrate with other feature work (either ongoing or planned), what it means for tooling, what kind of breaks it may require, whether the cost/complexity of the feature justifies the benefits of the feature, who is going to use the feature and how it will be used, etc. -- The team and community has tons of brilliant engineers and many features can have an MVP knocked together within a week, but even simple ones really take a full year to get properly designed, tested, and integrated throughout the ecosystem. Others that are more complex (such as generic math, const generics, discriminated unions, etc) can take years due to the impact and broader considerations around the ecosystem.
Const Generics
"Const Generics" stands for allowing constant value to be used in a type parameter.
A fully working MVP implementation for CoreCLR can be found here: https://github.com/dotnet/runtime/pull/89636
And an implementation including the managed part can be found here: https://github.com/hez2010/runtime/tree/feature/const-generics-managed
Link to the language proposal: https://github.com/dotnet/csharplang/discussions/7508
Background and Use Cases
"Const Generics" enables the use cases where developers need to pass a const value through a type parameter.
Typical use cases are templating for things like shuffle (its basically a guaranteed constant) as well as for numerics, tensors, matrices and etc.
For example, fixed buffer and vector types [1], jagged arrays/spans [2], constrained shape of arrays [3], numeric types and multiplier types especially in graphics programming [4], expression abstractions [5], and value specialization [6].
For [1], we can have a type
struct ValueArray<T, int N>
to define a type of array ofT
withN
elements. This can also be useful in variadic parameters. For example, aparams ValueArray<int, 5>
can represent a variadic parameter that receives only 5 int arguments. Beside, we can also leverage theValueArray<T, int N>
type to implementparams {ReadOnly}Span<T>
.For [2], we can use the const type parameter to define a
Span<T, int Dim>
, so we can useSpan
for multi-dimension arrays as well.For [3], we can constrain the shape of an array. This is especially useful when you are dealing with matrix or vector computations. For example, you now can define a matrix using
class Matrix<T, int Row, int Col>
. When you implement the multiplication algorithm, you can simply put a signatureMatrix<T, Row, NewCol> Multiply<NewCol>(Matrix<T, Col, NewCol> rMatrix)
. This can make sure users pass the correct shape of the matrix while doing multiplication operations.For [4], we can embed the coefficient into a multiplier type. This is especially useful in graphics programming. For example, when you are working with things about illumination, you will definitely want some multiplier types with coefficients (which are basically floating point numbers) that are guaranteed to be constants. While building AI/ML models, we are also often use such constant coefficients. Also, we will be able to create a floating point type with user specified epsilon, such as
and then use it like
global using MyFloatWithEpsilon = EpsilonFloating<float, 1e-6f>
.For [5], we can have several types that can embed constant values to abstract an expression, then we can validate the expression at compile time, hence no runtime exception will happen. For instance, we can have below interface types:
abstract class BinOp
sealed class AddOp : BinOp
sealed class MulOp : BinOp
interface IExpr
interface IConstExpr<T, T Value> : IExpr
interface IBinExpr<TOp, TLeftExpr, TRightExpr> where TOp : BinOp where TLeftExpr : IExpr where TRightExpr IExpr
Then we can use
IBinExpr<MulOp, IBinExpr<AddOp, IConstExpr<int, 42>, IConstExpr<int, T>>, IConstExpr<int, 2>>
in a typeclass Foo<int T>
to represent42 * (T + 2)
, then we can use it like a type and let the compiler to verify whether the given const type argument satisfies the expression or not.For [6], we will be able to provide a generic
Vector
type and specialize SIMD-width types with extensions:Design
Wording
Const Type Parameter
⭕ This part is already implemented in the MVP implementation
New design:
To support const generics, we need a way to declare a const type parameter that will carry the const value after instantiation. Due to the fact that a const type parameter behaves no difference than a normal type parameter until instantiation, here we can treat the type of a const type parameter as a special generic constraint.
We want to emit the type of a const type parameter as
TypeSpec
, but in order to distinguish this type token from other generic constraints, we can introduce amdtGenericParamType
and then emit the type of const type parameter withmdtGenericParamType
, and make sure it will always be the first entry in generic constraints.To load the type of a type parameter, we simply look up the first entry in generic constraints and see if it's
mdtGenericParamType
. If yes, then replace it withmdtTypeSpec
using(token & ~mdtGenericParamType) | mdtTypeSpec
. When loading generic constraints, if we see a generic constraint has typemdtGenericParamType
, we can skip it directly.While an alternative approach (which is also the approach I preferred) is, use a type like
System.Runtime.CompilerServices.LiteralType<T>
as the generic constraint, and special case it. So aclass Foo<int T>
will be emitted toclass Foo<T> where T : LiteralType<int>
. But in the MVP implementation I don't touch the managed libraries so I don't have the type can be used for this.Old design:
Const Type Argument
⭕ This part is already implemented in the MVP implementation
A const type argument contains the actual constant value in the instantiation. Here we can introduce a new element type
ELEMENT_TYPE_CTARG
which stands for const type argument.A const type argument can be encoded as follows:
Note that the size of the const value is determined by its element type. For example, an
int 42
will be encoded as:While a
double 3.1415926
will be encoded as:While we'd better to save all constants to the constant table in the metadata, then instead of inlining the const value type and const value in the signature directly, we can use the constant token in the signature which is fix-sized and easier to decode, and use the type token instead of
CorElementType
so that we can also support const values of enums, int128, string and arbitrary value types as well.IL Parser
⭕ This part is already implemented in the MVP implementation
We can reuse the keyword
literal
in IL to indicate the type argument contains a const value. Particularly, we can use the keywordliteral
to differentiate a const type argument/parameter from a type argument/parameter. For example,literal int32 T
.For const type argument, we can simply use
int32 (42)
to express an int constant with the value 42.This is following the rule how we are expressing "const field" today.
We need to change the parser to parse
"literal" type typeName
as a const type parameter, andtype '(' value ')'
as a const type argument. You can define and use const generics as the examples at the bottom of this proposal.Type Desc
⭕ This part is already implemented in the MVP implementation
A const type parameter has no more difference than the additional type token, so we can reuse the
TypeVarTypeDesc
and add a fieldm_type
to save the type of const type if it's a const type parameter.A const type argument is exactly a constant value, so we need a separate
TypeDesc
for it. Therefore, aConstValueTypeDesc
can be added to save the type and the value of a const type argument.We can support up to 8 bytes of constant value if we use a
uint64_t
as the storage.To read the constant value from a
ConstValueTypeDesc
, we need to reinterpret the storage based on the type of constant value. For example, while reading a constant value which is a float, we can simply use*(float*)&m_value
.Actually I'm doubting whether an
uint64_t
is enough here, because we may supportint128
or other types as primitive types in the future. Should we usesize_t
here instead? This can make sure we are always able to save a pointer here and in case the size ofsize_t
is not enough for some types, we can allocate to save the value on the Non-GC heap and save its pointer to the Non-GC heap in this field:Or, if we go with the constant token approach which was mentioned in the "Const Type Argument" section, we may simply use the token of constant value instead:
But this soon brings another issue where making a new const value type using reflection APIs will create a new constant record that is not present in the metadata.
Method Table
⭕ This part is already implemented in the MVP implementation
Similar to function pointers, we don't need a
MethodTable
for const value.Type Loader
⭕ This part is already implemented in the MVP implementation
We can always load constant values in the CoreLib module because a constant value is independent from the assembly, the same constant value can be served from any assembly. To avoid loading the same constant value other than once, once we load a constant value, we can save it into a hash table
m_pAvailableParamTypes
. Whenever we load a constant value, we first lookup in the hash table, if found then we load theTypeHandle
from the hash table directly, otherwise we allocate a newConstValueTypeDesc
for it.Value Loading
⭕ This part is already implemented in the MVP implementation
We need to use the const value from a type parameter, here we can reuse the
ldtoken
instruction to achieve this. Instead of loading theTypeHandle
of the type parameter, we need to load the constant value and push it to the stack directly when we see the type parameter is a const type parameter.JIT
⭕ This part is already implemented in the MVP implementation
We only need to handle
ldtoken
here, so we can change theimpResolveToken
to resolve the information about the const value as well, and then use the information to determine whether we should load a type handle or a const value to the stack. So we only need a minor necessary change in the importation phase.Further changes would probably necessary after we introduce types like
Vector<T, int Length>
, as the JIT needs to recognize it to allow hardware acceleration.Generic Sharing
⭕ This part is already implemented in the MVP implementation
We don't share the implementation among const generic type parameters. Each const type argument gets specialized so we can always import the const type argument as a real type-rich constant value anytime.
Type Unloadability
⭕ This part is already implemented in the MVP implementation
They are just constant values and can be reused by any other assemblies, so we don't need to unload them at all.
Type Validation
⭕ This part is already implemented in the MVP implementation
We need to validate whether the const value type can be passed to a const type parameter. We can do it during checking the generic constraints: whenever we meet a const value, we can simply check whether the const value type is equivalent to the type saved in generic param props. Alternatively, we can also do it at the token resolution.
Generic on Const Generic Type Parameter
⭕ This part is already implemented in the MVP implementation
We can also support generic type on a const generic type parameter.
For example,
Here we can leverage the
type
field in theGenericParamRec
to save a type spec, then we will be able to look up the type parameter.This will allow us to write something like
struct ValueArray<T, TSize, literal TSize Size>
and use it withValueArray<int, int, 42424242>
,ValueArray<int, long, 42424242424242>
, and etc.Also we can leverage this feature to define a
ConstValueExpression<TValue, TValue Value>
and use it while implementing a compiler/interpreter.Overloading
❌ This part is NOT yet implemented in the MVP implementation 🚧 This part still needs more discussions to reach a conclusion
In this design, we are differentiating the calling target at the call site, so we can support overloading on const generic type parameters without any issues.
This would require us to consider the type of a type parameter while resolving tokens, i.e., making the type of a const type parameter part of the signature. We need to decide whether to support it or not before we are actually shipping const generics, because once we ship const generics, we can't afford a breaking change around signature encoding.
While given the fact that we can support generics on const generic type parameter, the overloading support is not so much necessary IMO.
Constraints
❌ This part is NOT yet implemented in the MVP implementation
It's useful to constraint a const type parameter. For example, the dimension of a nd-Span
ref struct Span<T, int Dimension>
should not be less than 1, and the length of astruct ValueArray<T, int Length>
should not be less than 0.We can add the below APIs to achieve arithmetic constraints.
Then we can evaluate the expression when we validate the generic constraints. For example, to constraint
N
to be greater than 0 and less than 20, we can use:And this got lowered to:
I have a naive prototype commit in another branch for show case only: https://github.com/hez2010/runtime/commit/e1fa0c307d5a38d0dd27e2d827a8752777d2e0d7
However, those expression types are actually not being implemented by any types, but we still use them in the generic constraints which let them look like interface constraints but behave as expression evaluation, which is not intuitive.
For example, we can add something like
constexpr
constraints in the metadata and allow it to be emitted directly, soclass Foo<T, U, V> where V : == T + U where T : != 0
can be represented in IL as:Const Arithmetic
❌ This part is NOT yet implemented in the MVP implementation 🚧 This part still needs more discussions to reach a conclusion
It's useful to have arithmetic support for const generics.
For example, the signature of a
Push
method ofValueArray<T, int N>
type can beValueArray<T, N + 1> Push(T elem)
, and the signature of aConcat
method can beValueArray<T, N + M> Concat<int M>(ValueArray<T, M> elems)
.This would require embedding the arithmetic operations in the type and implementing dependent/associated types, which is a non-trivial work.
While an alternative is to use constraints to achieve it. So for the example of
Push
method, we can useValueArray<T, U> Push<int U>(T elem) where U : (T + 1)
, and the constraintT + 1
can be expressed usingIBinaryExpression<Add, IConstantExpression<int, T>, IConstantExpression<int, 1>>
. Then we can validate the constraint at runtime.Although we need to specify the value such as
Push<7>(42)
while calling onValueArray<int, 6>
, the C# compiler may automatically infer the type ofU
so developers don't have to explicitly specify the value ofU
every time.However, consider the below code:
Are we going to enforce users to introduce a new type parameter on
Foo
? I.e.,If yes, whenever we want to introduce a new "computed" const type parameter on a method of the class, we will need to add it to the class signature, which will lead to breaking changes. This seems quite unfortunate, and unacceptable.
Therefore, we cannot just rely on generic constraints to serve const arithmetic.
However, if we have runtime support for dependent/associated types in the future, this can be simply resolved by using:
And also, if we have the support for defining an associated type inside a method, we can do:
We still need some discussion to design around here.
Maybe we can just skip const arithmetic for the first version, and implement const arithmetic in the future once we have proper runtime support?
Built-in
ValueArray
Intrinsic Type❗ The implementation can be found here, though this part is not included in the MVP implementation
We need a built-in
ValueArray
, aka.FixedBuffer
type for use, and it will play an important role in public APIs. AValueArray
is basically theInlineArray
we already have today plus the ability to specify arbitrary length without the need to define a newInlineArray
type.Below is the dummy C# code for
ValueArray
:This can be used together with
params
:Particularly, in C# we can lower all fixed buffer types to
ValueArray
, and it can perfectly serve all features likeparams Span<T>
andstackalloc T[]
.Reflection APIs
❗ The implementation can be found here, though this part is not included in the MVP implementation
To support reflection, we need something like
MakeGenericType
for a const value as well, so I have the below API proposal:This can make sure we can instantiate a type/method that contains const type parameters, and also get the const value from a constructed type argument.
Some use patterns of reflection:
An interesting idea is to allow
typeof(value)
for theType.MakeConstValue
, for example,typeof(42)
to get aType
that contains a value42
.This would either require us to:
ldtoken
instruction for this, and we will need to introduce a new instruction for loading a const type argument to the stack, for example, an instruction calledldctarg
(load const type argument).Type.MakeConstValue
.Changes to ECMA-335
Basically the new element type
ELEMENT_TYPE_CTARG
.Compatibility Concerns
Tooling
Disassembler
Both ILSpy and dnSpy should able to special case the
mdtGenericParamType
while loading generic constraints.Profilers and Debuggers
They need to support decoding new types or methods which contain
ELEMENT_TYPE_CTARG
/CORINFO_TYPE_CTARG
on the signature.As for debuggers, they need to add support for the extended
ldtoken
instruction.EnC
We don't support modifying generic type signatures today, so no actions are needed.
Other 3rd Party Tools
With the new design, we are not breaking the metadata so no concern here.
Other Useful APIs
Other many APIs can make use of const generics to provide valuable features and abilities for users:
Matrix<T, int Row, int Col>
: fixed-sized matrix to supersedeMatrix3x3
,Matrix4x4
and etc.Vector<T, int N>
: fixed-sized vector to supersedeVector2
,Vector3
and etc.Tensor<T, int Rank>
: tensor types for AI/ML purposeSpan<T, int Dim>
: ND-span that can support multiple dimension arraysList<T, int N>
,Array<T, int N>
...: arbitrary list types can have a fixed size nowFuture Considerations
Support for Strings and Arbitrary Value Types
This can be done by changing the parser to allow strings and arbitrary value types as well.
For example,
where
Foo
is aVector3<int>
, so we are passing aVector3<int> { X = 1, Y = 2, Z = 3 }
here.And as for the implementation, we can use the
m_type
inConstValueTypeDesc
to save theTypeHandle
of the type, andm_value
to save the address or constant record token. In this way, we can extend Const Generics to strings and arbitrary value types as well.We only need to extend the encoding of const type arguments as following:
ELEMENT_TYPE_CTARG ELEMENT_TYPE_STRING <constant record token>
, orELEMENT_TYPE_CTARG ELEMENT_TYPE_STRING <length> <qcompString>
ELEMENT_TYPE_CTARG ELEMENT_TYPE_VALUETYPE <compressed type token> <constant record token>
, orELEMENT_TYPE_CTARG ELEMENT_TYPE_VALUETYPE <compressed type token> <length> <bytearray>
This won't be a breaking change so we can do this later.
Fully Working Prototype
This prototype is based on the old design with a breaking change to the metadata, while the latest (current) design doesn't have any breaking changes to the metadata
I have done the fully working prototype of C# compiler, language server and CoreCLR runtime, and successfully built a SDK for it (Windows only).
If you want to have a try on const generics, you can download the SDK here: https://1drv.ms/u/s!ApWNk8G_rszRgrxP32IMKhW-V8iWug?e=JBn8wU
Be sure to follow the README.txt in the SDK.
Version: 20230912 Build 1 Checksum: a8c9ee29d1accd14797f60bedced312f9524391b
This prototype branch:
I may update the SDK without posting a new comment but change the version and checksum in the above, while the sharing link won't change.
This prototype supports all things in this proposal except generic constraints on const type parameter and const arithmetic. For example, you can do the following things:
class Foo<T, int N>
.new Foo<int, 42>()
.void Foo<int X>
.Foo<42>()
.class Foo<T, T X>
, then you can use it withFoo<int, 42>
as well asFoo<float, 42.42424f>
.Console.WriteLine(X)
in the typeclass Foo<int X>
.typeof
support. eg.typeof(42)
.new Foo<(short)42>
,typeof((short)42)
ValueArray<T, int X>
that can be used as a fix-sized type with typeT
and lengthX
.ValueArray
type, eg.int[42]
.type.IsGenericParameter && type.HasElementType
.type.GetElementType()
.type.IsConstValue
.type.GetElementType()
.type.ConstValue
.Type.MakeConstValueType()
Code Examples
A basic example
This can be interpreted to the following dummy C# code:
Generic Virtual Method with Const Type Parameters
This will yield the below execution result:
Generic Virtual Method with Generic on Const Type Parameters
This will yield the below execution result:
Minimal ValueArray Type Implementation