-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
Feature Proposal: Const Generics #89730
Comments
Tagging subscribers to this area: @dotnet/area-meta Issue DetailsConst Generics"Const Generics" stands for allowing constant value to be used in a type parameter. An MVP implementation for CoreCLR can be found here: #89636 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) For example, fixed buffers [1], multi-dimension arrays/spans [2] and constrained shape of arrays [3]. For [1], we can have a type For [2], we can use const type parameter to define a For [3], we can constraint the shape of an array. This is especially useful when you are dealing with matrix/vector computations. DesignWording
Const Type ParameterTo support const generics, we need a way to declare a const type parameter that will carry the const value after instantiation. To summarize:
This requires a change to the existing metamodel. But worth to note that we don't need a new COM interface as we are reusing the reserved parameter in Const Type ArgumentA const type argument contains the actual constant value in the instantiation. A const type argument can be encoded as follows:
Note that the size of const value is determined by its element type.
While a
IL ParserWe can add a keyword We need to change the parser to parse Metamodel VersionGiven that we are changing the metamodel, we need to bump the version of metamodel. To summarize what we need to do:
Type DescA const type parameter has no more difference than the additional type token, so we can reuse the A const type argument is exactly a constant value, so we need a separate We can support up to 8 bytes of constant value if we use a class ConstValueTypeDesc : TypeDesc {
TypeHandle m_type;
uint64_t m_value;
}; To read the constant value from a Method TableSimilar to function pointers, we don't need a Type LoaderWe can always load constant values in the CoreLib module because a constant value is independent from the assembly, a same constant value can be served from any assembly. Value LoadingWe may need to use the const value from a type parameter, here we can reuse the JITWe only need to handle Generic SharingWe don't share the implementation among const generic type parameters. Each const type argument get specialized so we can always import const type argument as a real type-rich constant value anytime. OverloadingIn this design, we are differentiate the calling target at the callsite, so we can support overloading on const generic type parameters without any issue.
The Built-in
|
Author: | hez2010 |
---|---|
Assignees: | - |
Labels: |
|
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:
|
This was discussed fairly heavily in the C# discord. At various points, we'd talked about using
There were also other alternatives discussed for doing arithmetic on values:
|
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 [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. dotnet/csharplang#6923 |
Unfortunately it's not possible as attribute doesn't support using open generic type at all. |
I created an IL implementation of Full IL listings here: C# dummy code ( 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 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.
In my earliest prototype, I added a new IL opcode |
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);
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 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. |
This comment was marked as outdated.
This comment was marked as outdated.
My suggestion is only a representation of const generic arguments in IL. So
What I meant is to allow using a special static class to represent a const generic argument.
I never said to generate types like
I never said to generate types like
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 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();
...
} |
Sorry for I misread your previous comment.
This is useful especially in graphics programming, where the multiplier can be either Even your proposed [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
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
Yes, the multiplier can be either float or double, but does the multiplier need to be a const generic parameter?
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 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?
It should be |
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);
}
There could be two answers to these problems.
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. |
For 1, 2 and 3, you need to use
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 |
This comment was marked as outdated.
This comment was marked as outdated.
Hi @hez2010 Incredible work you have done here.
I installed every
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. 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 |
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
the VS code part works great. the VS extensions break the component cache of VS constantly (latest preview) |
Maybe names should use |
Sounds great. We may want all |
@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 #89730 (comment) 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 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 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 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. |
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 With const generics, you could instead have a base 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 If you instead factor out the "dimension" struct into a record (reducing Const generics allow compile time checking of all of that. |
The current state is captured in the last comment from @davidwrighton - #89730 (comment). 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. |
This level of feature requires involvement from the various .NET Architects and other high level engineers across the 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 |
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: #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: dotnet/csharplang#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, a
params ValueArray<int, 5>
can represent a variadic parameter that receives only 5 int arguments.Beside, we can also leverage the
ValueArray<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, a
ConstValueTypeDesc
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 the
TypeHandle
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 the
TypeHandle
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: hez2010@e1fa0c3
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
The text was updated successfully, but these errors were encountered: