Feature Proposal: Const Generics #89730

Description

@hez2010

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 of T with N 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 implement params {ReadOnly}Span<T>.

For [2], we can use the const type parameter to define a Span<T, int Dim>, so we can use Span 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 signature Matrix<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

structEpsilonFloating<T,T Epsilon>whereT:INumber<T>{publicstaticbooloperator==(EpsilonFloating<T,Epsilon>a,EpsilonFloating<T,Epsilon>b)=>T.Abs(a.value-b.value)<=Epsilon;}

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 type class Foo<int T> to represent 42 * (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:

structVector<T,int Size>{}staticclassVectorExtension{publicVector<int,4>Multiply<T>(thisVector<int,4>v,Vector<int,4>right){}// Vector64
public Vector<int,8>Multiply<T>(thisVector<int,8>v,Vector<int,8>right){}// Vector128
public Vector<int,16>Multiply<T>(thisVector<int,16>v,Vector<int,16>right){}// Vector256
public Vector<int,32>Multiply<T>(thisVector<int,32>v,Vector<int,32>right){}// Vector512
public Vector<int,Size>Multiply<int Size>(thisVector<int,Size>v,Vector<int,Size>right){}// For other sizes allowing a software fallback// ...
public Vector<T,Size>Multiply<T,int Size>(thisVector<T,Size>v,Vector<T,Size>right){}// For other types and sizes allowing a software fallback}

Design

Wording

  • Const type parameter: a type parameter that carries a const value.
  • Const type argument: the constant value for a type parameter in the instantiation.

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 a mdtGenericParamType and then emit the type of const type parameter with mdtGenericParamType, 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 with mdtTypeSpec using (token & ~mdtGenericParamType) | mdtTypeSpec. When loading generic constraints, if we see a generic constraint has type mdtGenericParamType, 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 a class Foo<int T> will be emitted to class 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:

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 reuse the existing generic metadata and rules, and add a Type token to the GenericParamRec schema. To determine whether a type parameter is a const type parameter or not, simply check the Type token to see if it's valid by using RidFromToken.

To summarize:

Added a column to GenericParameterRec to save a mdToken which represents the type of a const generic parameter.
Changed the reserved DWORD to mdToken to save the type of a const generic parameter.
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 GetGenericParamProps, both mdToken and the reserved DWORD are exactly DWORD.

BTW: actually we have another way without upgrading the existing metadata: we can downgrade the metadata version from the current v2.0 to v1.1, where in v1.1 metadata the GenericParamRec table has a Kind field which is exactly what we need for const generics.

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:

ELEMENT_TYPE_CTARG <element type of const value> <const value>

Note that the size of the const value is determined by its element type.
For example, an int 42 will be encoded as:

 ELEMENT_TYPE_CTARG ELEMENT_TYPE_I4 42
| 1 byte | 1 byte | 4 bytes |

While a double 3.1415926 will be encoded as:

 ELEMENT_TYPE_CTARG ELEMENT_TYPE_R8 3.1415926
| 1 byte | 1 byte | 8 bytes |

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 keyword literal 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, and type '(' 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 field m_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.

classConstValueTypeDesc : TypeDesc {
TypeHandle m_type;
uint64_t m_value;
};

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 support int128 or other types as primitive types in the future. Should we use size_t here instead? This can make sure we are always able to save a pointer here and in case the size of size_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:

enum {
CONST_VALUE_INLINE = 1,
CONST_VALUE_INDIRECT = 1 << 1,
};
classConstValueTypeDesc : TypeDesc {
TypeHandle m_type;
size_t m_value;
DWORD m_flag;
};
if ((m_flag & CONST_VALUE_INDIRECT) == CONST_VALUE_INDIRECT)
{
// get size and layout info from m_type// load the pointer from m_value// deference the pointer to get the value
}
else
{
// get size and layout info from m_type// load the value from m_value directly
}

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:

classConstValueTypeDesc : TypeDesc {
TypeHandle m_type;
mdToken m_value;
};

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 new ConstValueTypeDesc 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 the impResolveToken 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,

.class public auto ansi beforefieldinit Test`2<T, literal !T N>
{
.method public hidebysig newslot virtual instance void M<U, literal !!U V> () cil managed { }
}

Here we can leverage the type field in the GenericParamRec 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 with ValueArray<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.

call instance void Foo`1<float32 (42.42)>::.ctor(); // instantiate the Foo`1<float32 (42.42)>
call instance void Foo`1<int32 (42)>::.ctor(); // instantiate the Foo`1<int32 (42)>
call instance void Foo`1<int32 (42)>::A<int32 (42)>(); // calling the Foo`1<int32 (42)>::A<int32 (42)>()
call instance void Foo`1<int32 (42)>::A<float32 (42.42)>(); // calling the Foo`1<int32 (42)>::A<float32 (42.42)>()
.class public auto ansi beforefieldinit Foo`1<literal int32 N>
{
.method public hidebysig specialname rtspecialname instance void .ctor () cil managed { ... }
.method public hidebysig newslot virtual
instance void A<literal int32 X>() cil managed { ... }
.method public hidebysig newslot virtual
instance void A<literal float32 X>() cil managed { ... }
}
.class public auto ansi beforefieldinit Foo`1<literal float32 N>
{
// ...
}

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 a struct ValueArray<T, int Length> should not be less than 0.

We can add the below APIs to achieve arithmetic constraints.

namespaceSystem.Runtime.CompilerServices;publicabstractclassOperator{publicabstractclassUnaryOperator:Operator{// ...}publicabstractclassBinaryOperator:Operator{publicsealedclassAdditionOperator:BinaryOperator{}publicsealedclassSubtractionOperator:BinaryOperator{}publicsealedclassMultiplyOperator:BinaryOperator{}publicsealedclassDivisionOperator:BinaryOperator{}publicsealedclassEqualityOperator:BinaryOperator{}publicsealedclassLessThanOperator:BinaryOperator{}publicsealedclassConjunctionOperator:BinaryOperator{}publicsealedclassDisjunctionOperator:BinaryOperator{}// ...}}publicinterfaceIExpression{publicinterfaceIUnaryExpression<TOperator,TOprand>:IExpressionwhereTOperand:IExpressionwhereTOperator:UnaryOperator{}publicinterfaceIBinaryExpression<TOperator,TLeft,TRight>:IExpressionwhereTLeft:IExpressionwhereTRight:IExpressionwhereTOperator:BinaryOperator{}publicinterfaceIConstantExpression<TValue,TValueValue>:IExpression{}
...}

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:

classFoo<int T>whereT:>0,<20{}

And this got lowered to:

.class public auto ansi beforefieldinit Foo<literal int32 (
class BinaryExpression`3<
class Operator/BinaryOperator/GreaterThanOperator,
class IExpression/IConstantExpression`2<int32, !!T>,
class IExpression/IConstantExpression`2<int32, int32 (0)>
>,
class BinaryExpression`3<
class Operator/BinaryOperator/LessThanOperator,
class IExpression/IConstantExpression`2<int32, !!T>,
class IExpression/IConstantExpression`2<int32, int32 (20)>
>
) T>
extends [System.Runtime]System.Object { }

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, so class Foo<T, U, V> where V : == T + U where T : != 0 can be represented in IL as:

.class public auto ansi beforefieldinit Foo<literal int32 (constexpr (!T != int32 (0))) T, literal int32 U, literal int32 (constexpr (!V == !T + !U)) V>

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 of ValueArray<T, int N> type can be ValueArray<T, N + 1> Push(T elem), and the signature of a Concat method can be ValueArray<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 use ValueArray<T, U> Push<int U>(T elem) where U : (T + 1), and the constraint T + 1 can be expressed using IBinaryExpression<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 on ValueArray<int, 6>, the C# compiler may automatically infer the type of U so developers don't have to explicitly specify the value of U every time.

However, consider the below code:

classFoo<int T>{privateFoo<T+1>foo;}

Are we going to enforce users to introduce a new type parameter on Foo? I.e.,

classFoo<int T,int U>where ...{privateFoo<U>foo;}

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:

classFoo<int T>{typeN=T+1;privateFoo<N>foo;}

And also, if we have the support for defining an associated type inside a method, we can do:

classFoo{UMethod<intT>(){typeU=T+1;}}

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. A ValueArray is basically the InlineArray we already have today plus the ability to specify arbitrary length without the need to define a new InlineArray type.

Below is the dummy C# code for ValueArray:

structValueArray<T,int N>{privateTelem;// Repeat the field elem for N timespublicintLength{get;}// ldtoken !N; ret;publicrefTthis[intindex]{ ...}}

This can be used together with params:

Foo(1,2,3,4,5);// a method that only receives 5 int argumentsvoidFoo(paramsValueArray<int,5>args){}

Particularly, in C# we can lower all fixed buffer types to ValueArray, and it can perfectly serve all features like params Span<T> and stackalloc 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:

namespaceSystem;publicabstractclassType{publicvirtualboolIsConstValue{get;}publicvirtualobjectConstValue{get;}publicstaticTypeMakeConstValueType(objectvalue);}

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:

classFoo<T,int N>{}varfoo=newFoo<string,42>();foo.GetType();// Foo<string, int (42)>foo.GetType().GetGenericArguments()[0];// Type: System.Stringfoo.GetType().GetGenericArguments()[1].IsConstValue;// truefoo.GetType().GetGenericArguments()[1].HasElementType;// truefoo.GetType().GetGenericArguments()[1].ConstValue;// 42foo.GetType().GetGenericArguments()[1].GetElementType();// System.Int32vart=Type.MakeConstValue(42);vard=typeof(Foo<,>);d.GetGenericArguments()[1].IsConstValue;// falsed.GetGenericArguments()[1].HasElementType;// trued.GetGenericArguments()[1].ConstValue;// InvalidOperationExceptiond.GetGenericArguments()[1].GetElementType();// Type: System.Int32d.MakeGenericType(typeof(string),t);// Foo<string, int (42)>

An interesting idea is to allow typeof(value) for the Type.MakeConstValue, for example, typeof(42) to get a Type that contains a value 42.

This would either require us to:

  1. Use the 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 called ldctarg (load const type argument).
  2. Introduce a new instruction for this.
  3. No new instruction, and just compile it to 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:

  1. Matrix<T, int Row, int Col>: fixed-sized matrix to supersede Matrix3x3, Matrix4x4 and etc.
  2. Vector<T, int N>: fixed-sized vector to supersede Vector2, Vector3 and etc.
  3. Tensor<T, int Rank>: tensor types for AI/ML purpose
  4. Span<T, int Dim>: ND-span that can support multiple dimension arrays
  5. List<T, int N>, Array<T, int N>...: arbitrary list types can have a fixed size now
  6. ... and more

Future 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,

// value types
.class C`1<literal valuetype Foo T> { }
call C`1<valuetype Foo (bytearray ( 01 00 00 00 02 00 00 00 03 00 00 00))>::.ctor()
// string
.class D`1<literal string T> { }
call D`1<string ("hello world")>::.ctor()

where Foo is a Vector3<int>, so we are passing a Vector3<int> { X = 1, Y = 2, Z = 3 } here.

And as for the implementation, we can use the m_type in ConstValueTypeDesc to save the TypeHandle of the type, and m_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:

  • For strings, we encode the binary following the rule: ELEMENT_TYPE_CTARG ELEMENT_TYPE_STRING <constant record token>, or ELEMENT_TYPE_CTARG ELEMENT_TYPE_STRING <length> <qcompString>
  • For arbitrary value types: ELEMENT_TYPE_CTARG ELEMENT_TYPE_VALUETYPE <compressed type token> <constant record token>, or ELEMENT_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:

  1. Declare a const generic type, eg. class Foo<T, int N>.
  2. Use a const generic type, eg. new Foo<int, 42>().
  3. Declare a const generic method, eg. void Foo<int X>.
  4. Use a const generic method, eg. Foo<42>().
  5. Generics on const type parameter, eg. class Foo<T, T X>, then you can use it with Foo<int, 42> as well as Foo<float, 42.42424f>.
  6. Use const type parameter as constant directly. eg. calling Console.WriteLine(X) in the type class Foo<int X>.
  7. typeof support. eg. typeof(42).
  8. Casting support in const type argument. eg. new Foo<(short)42>, typeof((short)42)
  9. A built-in value type ValueArray<T, int X> that can be used as a fix-sized type with type T and length X.
  10. A niche syntax for declaring a ValueArray type, eg. int[42].
  11. 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()

Code Examples

A basic example

.assembly _ {}
.classpublicautoansibeforefieldinit Foo`2<T, literalint32 N>
extends[System.Runtime]System.Object
{
.methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
.maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
ret
}
.methodpublichidebysignewslotvirtualinstancevoid M<literalint32 V, literalint32 W> () cilmanaged {
.maxstack1.localsinit (
[0] int32 v
)
newobjinstancevoidclass Foo`2<string, int32 (42)>::.ctor()
callinstancevoidclass Foo`2<string, int32 (42)>::M<!!V, !!V>()
newobjinstancevoidclass Foo`2<string, !!V>::.ctor()
callinstancevoidclass Foo`2<string, !!V>::M<!N, int32 (42)>()
newobjinstancevoidclass Foo`2<string, !N>::.ctor()
callinstancevoidclass Foo`2<string, !N>::M<!!V, !!W>()
ldtoken !!V
callvoid[System.Console]System.Console::WriteLine(int32)
ldtoken !!W
callvoid[System.Console]System.Console::WriteLine(int32)
ldtoken !N
callvoid[System.Console]System.Console::WriteLine(int32)
ret
}
}

This can be interpreted to the following dummy C# code:

classFoo<T,int N>{publicvoidM<int V,int W>(){newFoo<string,42>().M<V,V>();newFoo<string,V>().M<N,42>();newFoo<string,N>().M<V,W>();Console.WriteLine(V);Console.WriteLine(W);Console.WriteLine(N);}}

Generic Virtual Method with Const Type Parameters

.assembly _ {}
.classprivateautoansibeforefieldinit Program
extends[System.Runtime]System.Object
{
.methodprivatehidebysigstaticvoid Main (
string[] args
) cilmanaged {
.maxstack8.entrypointnewobjinstancevoidclass Bar`2<string, int32( 42 )>::.ctor()
callinstancevoidclass Bar`2<string, int32( 42 )>::N<int32( 42 ), int32( 42 )>()
ret
}
.methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
.maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
ret
}
}
.classpublicautoansibeforefieldinit Foo`2<T, literalint32 N>
extends[System.Runtime]System.Object
{
.methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
.maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
ret
}
.methodpublichidebysignewslotvirtualinstancevoid M<literalint32 V, literalint32 W> () cilmanaged {
.maxstack8ldstr"From Foo::M"callvoid[System.Console]System.Console::WriteLine(string)
ldtoken !!V
callvoid[System.Console]System.Console::WriteLine(int32)
ldtoken !!W
callvoid[System.Console]System.Console::WriteLine(int32)
ldtoken !N
callvoid[System.Console]System.Console::WriteLine(int32)
ret
}
.methodpublichidebysignewslotvirtualinstancevoid N<literalint32 V, literalint32 W> () cilmanaged {
.maxstack8newobjinstancevoidclass Foo`2<string, int32( 42 )>::.ctor()
callinstancevoidclass Foo`2<string, int32 (42)>::M<!!V, !!V>()
newobjinstancevoidclass Foo`2<string, !!V>::.ctor()
callinstancevoidclass Foo`2<string, !!V>::M<!N, int32 (42)>()
newobjinstancevoidclass Foo`2<string, !N>::.ctor()
callinstancevoidclass Foo`2<string, !N>::M<!!V, !!W>()
ret
}
}
.classpublicautoansibeforefieldinit Bar`2<T, literalint32 N>
extendsclass Foo`2<!T, int32 (128)>
{
.methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
.maxstack8ldarg.0callinstancevoidclass Foo`2<!T, int32 (128)>::.ctor()
ret
}
.methodpublichidebysigvirtualinstancevoid M<literalint32 V, literalint32 W> () cilmanaged {
.maxstack8.localsinit (
[0] string v
)
ldstr"From Bar::M"callvoid[System.Console]System.Console::WriteLine(string)
ldtoken !!V
callvoid[System.Console]System.Console::WriteLine(int32)
ldtoken !!W
callvoid[System.Console]System.Console::WriteLine(int32)
ldtoken !N
callvoid[System.Console]System.Console::WriteLine(int32)
ret
}
.methodpublichidebysigvirtualinstancevoid N<literalint32 V, literalint32 W> () cilmanaged {
.maxstack8ldarg.0callinstancevoidclass Foo`2<!T, int32 (128)>::M<!!V, !!W>()
ldarg.0callvirtinstancevoidclass Foo`2<!T, !N>::M<!!V, !!W>()
ret
}
}

This will yield the below execution result:

From Foo::M
42
42
128
From Bar::M
42
42
42

Generic Virtual Method with Generic on Const Type Parameters

.assembly _ { }
.classprivateautoansibeforefieldinit Program
extends[System.Runtime]System.Object
{
.methodprivatehidebysigstaticvoid Main (
string[] args
) cilmanaged {
.maxstack8.entrypointnewobjinstancevoidclass Bar`2<float32, int32( 42 )>::.ctor()
callinstancevoidclass Bar`2<float32, int32( 42 )>::N<float32( 42.42 ), int32( 42 )>()
ret
}
.methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
.maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
ret
}
}
.classpublicautoansibeforefieldinit Foo`2<T, literalint32 N>
extends[System.Runtime]System.Object
{
.methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
.maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
ret
}
.methodpublichidebysignewslotvirtualinstancevoid M<literal !T V, literalint32 W> () cilmanaged {
.maxstack8ldstr"From Foo::M"callvoid[System.Console]System.Console::WriteLine(string)
ldtoken !!V
box !T
callvoid[System.Console]System.Console::WriteLine(object)
ldtoken !!W
callvoid[System.Console]System.Console::WriteLine(int32)
ldtoken !N
callvoid[System.Console]System.Console::WriteLine(int32)
ret
}
.methodpublichidebysignewslotvirtualinstancevoid N<literal !T V, literalint32 W> () cilmanaged {
.maxstack8newobjinstancevoidclass Foo`2<int32, int32( 42 )>::.ctor()
callinstancevoidclass Foo`2<int32, int32 (42)>::M<!!V, !!V>()
newobjinstancevoidclass Foo`2<int32, !!V>::.ctor()
callinstancevoidclass Foo`2<int32, !!V>::M<!N, int32 (42)>()
newobjinstancevoidclass Foo`2<int32, !N>::.ctor()
callinstancevoidclass Foo`2<int32, !N>::M<!!V, !!W>()
ret
}
}
.classpublicautoansibeforefieldinit Bar`2<T, literalint32 N>
extendsclass Foo`2<!T, int32 (128)>
{
.methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
.maxstack8ldarg.0callinstancevoidclass Foo`2<!T, int32 (128)>::.ctor()
ret
}
.methodpublichidebysigvirtualinstancevoid M<literal !T V, literalint32 W> () cilmanaged {
.maxstack8.localsinit (
[0] string v
)
ldstr"From Bar::M"callvoid[System.Console]System.Console::WriteLine(string)
ldtoken !!V
box !T
callvoid[System.Console]System.Console::WriteLine(object)
ldtoken !!W
callvoid[System.Console]System.Console::WriteLine(int32)
ldtoken !N
callvoid[System.Console]System.Console::WriteLine(int32)
ret
}
.methodpublichidebysigvirtualinstancevoid N<literal !T V, literalint32 W> () cilmanaged {
.maxstack8ldarg.0callinstancevoidclass Foo`2<!T, int32 (128)>::M<!!V, !!W>()
ldarg.0callvirtinstancevoidclass Foo`2<!T, !N>::M<!!V, !!W>()
ret
}
}

This will yield the below execution result:

From Foo::M
42.42
42
128
From Bar::M
42.42
42
42

Minimal ValueArray Type Implementation

.class public sequential ansi sealed beforefieldinit System.ValueArray`2<T, literal int32 Length>
extends [System.Runtime]System.ValueType
{
.field private !T elem
.method public hidebysig specialname instance !T& get_Item (int32 index) cil managed {
.custom instance void [System.Runtime]System.Diagnostics.CodeAnalysis.UnscopedRefAttribute::.ctor() = (01 00 00 00)
.maxstack 8
ldarg.1
ldc.i4.0
blt.s OutOfRange
ldarg.1
ldarg.0
call instance int32 valuetype System.ValueArray`2<!T, !Length>::get_Length()
blt.s GetItem
OutOfRange:
call void valuetype System.ValueArray`2<!T, !Length>::ThrowIndexOutOfRange()
GetItem:
ldarg.0
ldflda !0 valuetype System.ValueArray`2<!T, !Length>::elem
ldarg.1
call !!0& [System.Runtime]System.Runtime.CompilerServices.Unsafe::Add<!T>(!!0&, int32)
ret
}
.method public hidebysig specialname instance int32 get_Length () cil managed {
.maxstack 8
ldtoken !Length
ret
}
.method private hidebysig static void ThrowIndexOutOfRange () cil managed {
.maxstack 8
newobj instance void [System.Runtime]System.IndexOutOfRangeException::.ctor()
throw
}
.property instance !T& Item(int32 index)
{
.get instance !0& System.ValueArray`2::get_Item(int32)
}
.property instance int32 Length()
{
.get instance int32 System.ValueArray`2::get_Length()
}
}

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
       blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
      }
      } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
      })();
      (function(){
      try {
      var __m = "github.com";
      var __re = new RegExp('^' + "github\\.com" + '
      
      Skip to content

      Feature Proposal: Const Generics #89730

      Description

      @hez2010

      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 of T with N 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 implement params {ReadOnly}Span<T>.

      For [2], we can use the const type parameter to define a Span<T, int Dim>, so we can use Span 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 signature Matrix<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

      structEpsilonFloating<T,T Epsilon>whereT:INumber<T>{publicstaticbooloperator==(EpsilonFloating<T,Epsilon>a,EpsilonFloating<T,Epsilon>b)=>T.Abs(a.value-b.value)<=Epsilon;}

      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 type class Foo<int T> to represent 42 * (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:

      structVector<T,int Size>{}staticclassVectorExtension{publicVector<int,4>Multiply<T>(thisVector<int,4>v,Vector<int,4>right){}// Vector64
      public Vector<int,8>Multiply<T>(thisVector<int,8>v,Vector<int,8>right){}// Vector128
      public Vector<int,16>Multiply<T>(thisVector<int,16>v,Vector<int,16>right){}// Vector256
      public Vector<int,32>Multiply<T>(thisVector<int,32>v,Vector<int,32>right){}// Vector512
      public Vector<int,Size>Multiply<int Size>(thisVector<int,Size>v,Vector<int,Size>right){}// For other sizes allowing a software fallback// ...
      public Vector<T,Size>Multiply<T,int Size>(thisVector<T,Size>v,Vector<T,Size>right){}// For other types and sizes allowing a software fallback}

      Design

      Wording

      • Const type parameter: a type parameter that carries a const value.
      • Const type argument: the constant value for a type parameter in the instantiation.

      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 a mdtGenericParamType and then emit the type of const type parameter with mdtGenericParamType, 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 with mdtTypeSpec using (token & ~mdtGenericParamType) | mdtTypeSpec. When loading generic constraints, if we see a generic constraint has type mdtGenericParamType, 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 a class Foo<int T> will be emitted to class 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:

      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 reuse the existing generic metadata and rules, and add a Type token to the GenericParamRec schema. To determine whether a type parameter is a const type parameter or not, simply check the Type token to see if it's valid by using RidFromToken.

      To summarize:

      Added a column to GenericParameterRec to save a mdToken which represents the type of a const generic parameter.
      Changed the reserved DWORD to mdToken to save the type of a const generic parameter.
      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 GetGenericParamProps, both mdToken and the reserved DWORD are exactly DWORD.

      BTW: actually we have another way without upgrading the existing metadata: we can downgrade the metadata version from the current v2.0 to v1.1, where in v1.1 metadata the GenericParamRec table has a Kind field which is exactly what we need for const generics.

      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:

      ELEMENT_TYPE_CTARG <element type of const value> <const value>
      

      Note that the size of the const value is determined by its element type.
      For example, an int 42 will be encoded as:

       ELEMENT_TYPE_CTARG ELEMENT_TYPE_I4 42
      | 1 byte | 1 byte | 4 bytes |
      

      While a double 3.1415926 will be encoded as:

       ELEMENT_TYPE_CTARG ELEMENT_TYPE_R8 3.1415926
      | 1 byte | 1 byte | 8 bytes |
      

      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 keyword literal 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, and type '(' 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 field m_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.

      classConstValueTypeDesc : TypeDesc {
      TypeHandle m_type;
      uint64_t m_value;
      };

      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 support int128 or other types as primitive types in the future. Should we use size_t here instead? This can make sure we are always able to save a pointer here and in case the size of size_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:

      enum {
      CONST_VALUE_INLINE = 1,
      CONST_VALUE_INDIRECT = 1 << 1,
      };
      classConstValueTypeDesc : TypeDesc {
      TypeHandle m_type;
      size_t m_value;
      DWORD m_flag;
      };
      if ((m_flag & CONST_VALUE_INDIRECT) == CONST_VALUE_INDIRECT)
      {
      // get size and layout info from m_type// load the pointer from m_value// deference the pointer to get the value
      }
      else
      {
      // get size and layout info from m_type// load the value from m_value directly
      }

      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:

      classConstValueTypeDesc : TypeDesc {
      TypeHandle m_type;
      mdToken m_value;
      };

      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 new ConstValueTypeDesc 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 the impResolveToken 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,

      .class public auto ansi beforefieldinit Test`2<T, literal !T N>
      {
      .method public hidebysig newslot virtual instance void M<U, literal !!U V> () cil managed { }
      }
      

      Here we can leverage the type field in the GenericParamRec 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 with ValueArray<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.

      call instance void Foo`1<float32 (42.42)>::.ctor(); // instantiate the Foo`1<float32 (42.42)>
      call instance void Foo`1<int32 (42)>::.ctor(); // instantiate the Foo`1<int32 (42)>
      call instance void Foo`1<int32 (42)>::A<int32 (42)>(); // calling the Foo`1<int32 (42)>::A<int32 (42)>()
      call instance void Foo`1<int32 (42)>::A<float32 (42.42)>(); // calling the Foo`1<int32 (42)>::A<float32 (42.42)>()
      .class public auto ansi beforefieldinit Foo`1<literal int32 N>
      {
      .method public hidebysig specialname rtspecialname instance void .ctor () cil managed { ... }
      .method public hidebysig newslot virtual
      instance void A<literal int32 X>() cil managed { ... }
      .method public hidebysig newslot virtual
      instance void A<literal float32 X>() cil managed { ... }
      }
      .class public auto ansi beforefieldinit Foo`1<literal float32 N>
      {
      // ...
      }
      

      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 a struct ValueArray<T, int Length> should not be less than 0.

      We can add the below APIs to achieve arithmetic constraints.

      namespaceSystem.Runtime.CompilerServices;publicabstractclassOperator{publicabstractclassUnaryOperator:Operator{// ...}publicabstractclassBinaryOperator:Operator{publicsealedclassAdditionOperator:BinaryOperator{}publicsealedclassSubtractionOperator:BinaryOperator{}publicsealedclassMultiplyOperator:BinaryOperator{}publicsealedclassDivisionOperator:BinaryOperator{}publicsealedclassEqualityOperator:BinaryOperator{}publicsealedclassLessThanOperator:BinaryOperator{}publicsealedclassConjunctionOperator:BinaryOperator{}publicsealedclassDisjunctionOperator:BinaryOperator{}// ...}}publicinterfaceIExpression{publicinterfaceIUnaryExpression<TOperator,TOprand>:IExpressionwhereTOperand:IExpressionwhereTOperator:UnaryOperator{}publicinterfaceIBinaryExpression<TOperator,TLeft,TRight>:IExpressionwhereTLeft:IExpressionwhereTRight:IExpressionwhereTOperator:BinaryOperator{}publicinterfaceIConstantExpression<TValue,TValueValue>:IExpression{}
      ...}

      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:

      classFoo<int T>whereT:>0,<20{}

      And this got lowered to:

      .class public auto ansi beforefieldinit Foo<literal int32 (
      class BinaryExpression`3<
      class Operator/BinaryOperator/GreaterThanOperator,
      class IExpression/IConstantExpression`2<int32, !!T>,
      class IExpression/IConstantExpression`2<int32, int32 (0)>
      >,
      class BinaryExpression`3<
      class Operator/BinaryOperator/LessThanOperator,
      class IExpression/IConstantExpression`2<int32, !!T>,
      class IExpression/IConstantExpression`2<int32, int32 (20)>
      >
      ) T>
      extends [System.Runtime]System.Object { }
      

      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, so class Foo<T, U, V> where V : == T + U where T : != 0 can be represented in IL as:

      .class public auto ansi beforefieldinit Foo<literal int32 (constexpr (!T != int32 (0))) T, literal int32 U, literal int32 (constexpr (!V == !T + !U)) V>
      

      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 of ValueArray<T, int N> type can be ValueArray<T, N + 1> Push(T elem), and the signature of a Concat method can be ValueArray<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 use ValueArray<T, U> Push<int U>(T elem) where U : (T + 1), and the constraint T + 1 can be expressed using IBinaryExpression<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 on ValueArray<int, 6>, the C# compiler may automatically infer the type of U so developers don't have to explicitly specify the value of U every time.

      However, consider the below code:

      classFoo<int T>{privateFoo<T+1>foo;}

      Are we going to enforce users to introduce a new type parameter on Foo? I.e.,

      classFoo<int T,int U>where ...{privateFoo<U>foo;}

      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:

      classFoo<int T>{typeN=T+1;privateFoo<N>foo;}

      And also, if we have the support for defining an associated type inside a method, we can do:

      classFoo{UMethod<intT>(){typeU=T+1;}}

      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. A ValueArray is basically the InlineArray we already have today plus the ability to specify arbitrary length without the need to define a new InlineArray type.

      Below is the dummy C# code for ValueArray:

      structValueArray<T,int N>{privateTelem;// Repeat the field elem for N timespublicintLength{get;}// ldtoken !N; ret;publicrefTthis[intindex]{ ...}}

      This can be used together with params:

      Foo(1,2,3,4,5);// a method that only receives 5 int argumentsvoidFoo(paramsValueArray<int,5>args){}

      Particularly, in C# we can lower all fixed buffer types to ValueArray, and it can perfectly serve all features like params Span<T> and stackalloc 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:

      namespaceSystem;publicabstractclassType{publicvirtualboolIsConstValue{get;}publicvirtualobjectConstValue{get;}publicstaticTypeMakeConstValueType(objectvalue);}

      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:

      classFoo<T,int N>{}varfoo=newFoo<string,42>();foo.GetType();// Foo<string, int (42)>foo.GetType().GetGenericArguments()[0];// Type: System.Stringfoo.GetType().GetGenericArguments()[1].IsConstValue;// truefoo.GetType().GetGenericArguments()[1].HasElementType;// truefoo.GetType().GetGenericArguments()[1].ConstValue;// 42foo.GetType().GetGenericArguments()[1].GetElementType();// System.Int32vart=Type.MakeConstValue(42);vard=typeof(Foo<,>);d.GetGenericArguments()[1].IsConstValue;// falsed.GetGenericArguments()[1].HasElementType;// trued.GetGenericArguments()[1].ConstValue;// InvalidOperationExceptiond.GetGenericArguments()[1].GetElementType();// Type: System.Int32d.MakeGenericType(typeof(string),t);// Foo<string, int (42)>

      An interesting idea is to allow typeof(value) for the Type.MakeConstValue, for example, typeof(42) to get a Type that contains a value 42.

      This would either require us to:

      1. Use the 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 called ldctarg (load const type argument).
      2. Introduce a new instruction for this.
      3. No new instruction, and just compile it to 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:

      1. Matrix<T, int Row, int Col>: fixed-sized matrix to supersede Matrix3x3, Matrix4x4 and etc.
      2. Vector<T, int N>: fixed-sized vector to supersede Vector2, Vector3 and etc.
      3. Tensor<T, int Rank>: tensor types for AI/ML purpose
      4. Span<T, int Dim>: ND-span that can support multiple dimension arrays
      5. List<T, int N>, Array<T, int N>...: arbitrary list types can have a fixed size now
      6. ... and more

      Future 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,

      // value types
      .class C`1<literal valuetype Foo T> { }
      call C`1<valuetype Foo (bytearray ( 01 00 00 00 02 00 00 00 03 00 00 00))>::.ctor()
      // string
      .class D`1<literal string T> { }
      call D`1<string ("hello world")>::.ctor()
      

      where Foo is a Vector3<int>, so we are passing a Vector3<int> { X = 1, Y = 2, Z = 3 } here.

      And as for the implementation, we can use the m_type in ConstValueTypeDesc to save the TypeHandle of the type, and m_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:

      • For strings, we encode the binary following the rule: ELEMENT_TYPE_CTARG ELEMENT_TYPE_STRING <constant record token>, or ELEMENT_TYPE_CTARG ELEMENT_TYPE_STRING <length> <qcompString>
      • For arbitrary value types: ELEMENT_TYPE_CTARG ELEMENT_TYPE_VALUETYPE <compressed type token> <constant record token>, or ELEMENT_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:

      1. Declare a const generic type, eg. class Foo<T, int N>.
      2. Use a const generic type, eg. new Foo<int, 42>().
      3. Declare a const generic method, eg. void Foo<int X>.
      4. Use a const generic method, eg. Foo<42>().
      5. Generics on const type parameter, eg. class Foo<T, T X>, then you can use it with Foo<int, 42> as well as Foo<float, 42.42424f>.
      6. Use const type parameter as constant directly. eg. calling Console.WriteLine(X) in the type class Foo<int X>.
      7. typeof support. eg. typeof(42).
      8. Casting support in const type argument. eg. new Foo<(short)42>, typeof((short)42)
      9. A built-in value type ValueArray<T, int X> that can be used as a fix-sized type with type T and length X.
      10. A niche syntax for declaring a ValueArray type, eg. int[42].
      11. 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()

      Code Examples

      A basic example

      .assembly _ {}
      .classpublicautoansibeforefieldinit Foo`2<T, literalint32 N>
      extends[System.Runtime]System.Object
      {
      .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
      .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
      ret
      }
      .methodpublichidebysignewslotvirtualinstancevoid M<literalint32 V, literalint32 W> () cilmanaged {
      .maxstack1.localsinit (
      [0] int32 v
      )
      newobjinstancevoidclass Foo`2<string, int32 (42)>::.ctor()
      callinstancevoidclass Foo`2<string, int32 (42)>::M<!!V, !!V>()
      newobjinstancevoidclass Foo`2<string, !!V>::.ctor()
      callinstancevoidclass Foo`2<string, !!V>::M<!N, int32 (42)>()
      newobjinstancevoidclass Foo`2<string, !N>::.ctor()
      callinstancevoidclass Foo`2<string, !N>::M<!!V, !!W>()
      ldtoken !!V
      callvoid[System.Console]System.Console::WriteLine(int32)
      ldtoken !!W
      callvoid[System.Console]System.Console::WriteLine(int32)
      ldtoken !N
      callvoid[System.Console]System.Console::WriteLine(int32)
      ret
      }
      }

      This can be interpreted to the following dummy C# code:

      classFoo<T,int N>{publicvoidM<int V,int W>(){newFoo<string,42>().M<V,V>();newFoo<string,V>().M<N,42>();newFoo<string,N>().M<V,W>();Console.WriteLine(V);Console.WriteLine(W);Console.WriteLine(N);}}

      Generic Virtual Method with Const Type Parameters

      .assembly _ {}
      .classprivateautoansibeforefieldinit Program
      extends[System.Runtime]System.Object
      {
      .methodprivatehidebysigstaticvoid Main (
      string[] args
      ) cilmanaged {
      .maxstack8.entrypointnewobjinstancevoidclass Bar`2<string, int32( 42 )>::.ctor()
      callinstancevoidclass Bar`2<string, int32( 42 )>::N<int32( 42 ), int32( 42 )>()
      ret
      }
      .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
      .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
      ret
      }
      }
      .classpublicautoansibeforefieldinit Foo`2<T, literalint32 N>
      extends[System.Runtime]System.Object
      {
      .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
      .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
      ret
      }
      .methodpublichidebysignewslotvirtualinstancevoid M<literalint32 V, literalint32 W> () cilmanaged {
      .maxstack8ldstr"From Foo::M"callvoid[System.Console]System.Console::WriteLine(string)
      ldtoken !!V
      callvoid[System.Console]System.Console::WriteLine(int32)
      ldtoken !!W
      callvoid[System.Console]System.Console::WriteLine(int32)
      ldtoken !N
      callvoid[System.Console]System.Console::WriteLine(int32)
      ret
      }
      .methodpublichidebysignewslotvirtualinstancevoid N<literalint32 V, literalint32 W> () cilmanaged {
      .maxstack8newobjinstancevoidclass Foo`2<string, int32( 42 )>::.ctor()
      callinstancevoidclass Foo`2<string, int32 (42)>::M<!!V, !!V>()
      newobjinstancevoidclass Foo`2<string, !!V>::.ctor()
      callinstancevoidclass Foo`2<string, !!V>::M<!N, int32 (42)>()
      newobjinstancevoidclass Foo`2<string, !N>::.ctor()
      callinstancevoidclass Foo`2<string, !N>::M<!!V, !!W>()
      ret
      }
      }
      .classpublicautoansibeforefieldinit Bar`2<T, literalint32 N>
      extendsclass Foo`2<!T, int32 (128)>
      {
      .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
      .maxstack8ldarg.0callinstancevoidclass Foo`2<!T, int32 (128)>::.ctor()
      ret
      }
      .methodpublichidebysigvirtualinstancevoid M<literalint32 V, literalint32 W> () cilmanaged {
      .maxstack8.localsinit (
      [0] string v
      )
      ldstr"From Bar::M"callvoid[System.Console]System.Console::WriteLine(string)
      ldtoken !!V
      callvoid[System.Console]System.Console::WriteLine(int32)
      ldtoken !!W
      callvoid[System.Console]System.Console::WriteLine(int32)
      ldtoken !N
      callvoid[System.Console]System.Console::WriteLine(int32)
      ret
      }
      .methodpublichidebysigvirtualinstancevoid N<literalint32 V, literalint32 W> () cilmanaged {
      .maxstack8ldarg.0callinstancevoidclass Foo`2<!T, int32 (128)>::M<!!V, !!W>()
      ldarg.0callvirtinstancevoidclass Foo`2<!T, !N>::M<!!V, !!W>()
      ret
      }
      }

      This will yield the below execution result:

      From Foo::M
      42
      42
      128
      From Bar::M
      42
      42
      42
      

      Generic Virtual Method with Generic on Const Type Parameters

      .assembly _ { }
      .classprivateautoansibeforefieldinit Program
      extends[System.Runtime]System.Object
      {
      .methodprivatehidebysigstaticvoid Main (
      string[] args
      ) cilmanaged {
      .maxstack8.entrypointnewobjinstancevoidclass Bar`2<float32, int32( 42 )>::.ctor()
      callinstancevoidclass Bar`2<float32, int32( 42 )>::N<float32( 42.42 ), int32( 42 )>()
      ret
      }
      .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
      .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
      ret
      }
      }
      .classpublicautoansibeforefieldinit Foo`2<T, literalint32 N>
      extends[System.Runtime]System.Object
      {
      .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
      .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
      ret
      }
      .methodpublichidebysignewslotvirtualinstancevoid M<literal !T V, literalint32 W> () cilmanaged {
      .maxstack8ldstr"From Foo::M"callvoid[System.Console]System.Console::WriteLine(string)
      ldtoken !!V
      box !T
      callvoid[System.Console]System.Console::WriteLine(object)
      ldtoken !!W
      callvoid[System.Console]System.Console::WriteLine(int32)
      ldtoken !N
      callvoid[System.Console]System.Console::WriteLine(int32)
      ret
      }
      .methodpublichidebysignewslotvirtualinstancevoid N<literal !T V, literalint32 W> () cilmanaged {
      .maxstack8newobjinstancevoidclass Foo`2<int32, int32( 42 )>::.ctor()
      callinstancevoidclass Foo`2<int32, int32 (42)>::M<!!V, !!V>()
      newobjinstancevoidclass Foo`2<int32, !!V>::.ctor()
      callinstancevoidclass Foo`2<int32, !!V>::M<!N, int32 (42)>()
      newobjinstancevoidclass Foo`2<int32, !N>::.ctor()
      callinstancevoidclass Foo`2<int32, !N>::M<!!V, !!W>()
      ret
      }
      }
      .classpublicautoansibeforefieldinit Bar`2<T, literalint32 N>
      extendsclass Foo`2<!T, int32 (128)>
      {
      .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
      .maxstack8ldarg.0callinstancevoidclass Foo`2<!T, int32 (128)>::.ctor()
      ret
      }
      .methodpublichidebysigvirtualinstancevoid M<literal !T V, literalint32 W> () cilmanaged {
      .maxstack8.localsinit (
      [0] string v
      )
      ldstr"From Bar::M"callvoid[System.Console]System.Console::WriteLine(string)
      ldtoken !!V
      box !T
      callvoid[System.Console]System.Console::WriteLine(object)
      ldtoken !!W
      callvoid[System.Console]System.Console::WriteLine(int32)
      ldtoken !N
      callvoid[System.Console]System.Console::WriteLine(int32)
      ret
      }
      .methodpublichidebysigvirtualinstancevoid N<literal !T V, literalint32 W> () cilmanaged {
      .maxstack8ldarg.0callinstancevoidclass Foo`2<!T, int32 (128)>::M<!!V, !!W>()
      ldarg.0callvirtinstancevoidclass Foo`2<!T, !N>::M<!!V, !!W>()
      ret
      }
      }

      This will yield the below execution result:

      From Foo::M
      42.42
      42
      128
      From Bar::M
      42.42
      42
      42
      

      Minimal ValueArray Type Implementation

      .class public sequential ansi sealed beforefieldinit System.ValueArray`2<T, literal int32 Length>
      extends [System.Runtime]System.ValueType
      {
      .field private !T elem
      .method public hidebysig specialname instance !T& get_Item (int32 index) cil managed {
      .custom instance void [System.Runtime]System.Diagnostics.CodeAnalysis.UnscopedRefAttribute::.ctor() = (01 00 00 00)
      .maxstack 8
      ldarg.1
      ldc.i4.0
      blt.s OutOfRange
      ldarg.1
      ldarg.0
      call instance int32 valuetype System.ValueArray`2<!T, !Length>::get_Length()
      blt.s GetItem
      OutOfRange:
      call void valuetype System.ValueArray`2<!T, !Length>::ThrowIndexOutOfRange()
      GetItem:
      ldarg.0
      ldflda !0 valuetype System.ValueArray`2<!T, !Length>::elem
      ldarg.1
      call !!0& [System.Runtime]System.Runtime.CompilerServices.Unsafe::Add<!T>(!!0&, int32)
      ret
      }
      .method public hidebysig specialname instance int32 get_Length () cil managed {
      .maxstack 8
      ldtoken !Length
      ret
      }
      .method private hidebysig static void ThrowIndexOutOfRange () cil managed {
      .maxstack 8
      newobj instance void [System.Runtime]System.IndexOutOfRangeException::.ctor()
      throw
      }
      .property instance !T& Item(int32 index)
      {
      .get instance !0& System.ValueArray`2::get_Item(int32)
      }
      .property instance int32 Length()
      {
      .get instance int32 System.ValueArray`2::get_Length()
      }
      }
      

      Metadata

      Metadata

      Assignees

      No one assigned

        Type

        No type

        Projects

        No projects

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
          Skip to content

          Feature Proposal: Const Generics #89730

          Description

          @hez2010

          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 of T with N 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 implement params {ReadOnly}Span<T>.

          For [2], we can use the const type parameter to define a Span<T, int Dim>, so we can use Span 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 signature Matrix<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

          structEpsilonFloating<T,T Epsilon>whereT:INumber<T>{publicstaticbooloperator==(EpsilonFloating<T,Epsilon>a,EpsilonFloating<T,Epsilon>b)=>T.Abs(a.value-b.value)<=Epsilon;}

          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 type class Foo<int T> to represent 42 * (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:

          structVector<T,int Size>{}staticclassVectorExtension{publicVector<int,4>Multiply<T>(thisVector<int,4>v,Vector<int,4>right){}// Vector64
          public Vector<int,8>Multiply<T>(thisVector<int,8>v,Vector<int,8>right){}// Vector128
          public Vector<int,16>Multiply<T>(thisVector<int,16>v,Vector<int,16>right){}// Vector256
          public Vector<int,32>Multiply<T>(thisVector<int,32>v,Vector<int,32>right){}// Vector512
          public Vector<int,Size>Multiply<int Size>(thisVector<int,Size>v,Vector<int,Size>right){}// For other sizes allowing a software fallback// ...
          public Vector<T,Size>Multiply<T,int Size>(thisVector<T,Size>v,Vector<T,Size>right){}// For other types and sizes allowing a software fallback}

          Design

          Wording

          • Const type parameter: a type parameter that carries a const value.
          • Const type argument: the constant value for a type parameter in the instantiation.

          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 a mdtGenericParamType and then emit the type of const type parameter with mdtGenericParamType, 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 with mdtTypeSpec using (token & ~mdtGenericParamType) | mdtTypeSpec. When loading generic constraints, if we see a generic constraint has type mdtGenericParamType, 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 a class Foo<int T> will be emitted to class 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:

          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 reuse the existing generic metadata and rules, and add a Type token to the GenericParamRec schema. To determine whether a type parameter is a const type parameter or not, simply check the Type token to see if it's valid by using RidFromToken.

          To summarize:

          Added a column to GenericParameterRec to save a mdToken which represents the type of a const generic parameter.
          Changed the reserved DWORD to mdToken to save the type of a const generic parameter.
          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 GetGenericParamProps, both mdToken and the reserved DWORD are exactly DWORD.

          BTW: actually we have another way without upgrading the existing metadata: we can downgrade the metadata version from the current v2.0 to v1.1, where in v1.1 metadata the GenericParamRec table has a Kind field which is exactly what we need for const generics.

          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:

          ELEMENT_TYPE_CTARG <element type of const value> <const value>
          

          Note that the size of the const value is determined by its element type.
          For example, an int 42 will be encoded as:

           ELEMENT_TYPE_CTARG ELEMENT_TYPE_I4 42
          | 1 byte | 1 byte | 4 bytes |
          

          While a double 3.1415926 will be encoded as:

           ELEMENT_TYPE_CTARG ELEMENT_TYPE_R8 3.1415926
          | 1 byte | 1 byte | 8 bytes |
          

          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 keyword literal 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, and type '(' 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 field m_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.

          classConstValueTypeDesc : TypeDesc {
          TypeHandle m_type;
          uint64_t m_value;
          };

          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 support int128 or other types as primitive types in the future. Should we use size_t here instead? This can make sure we are always able to save a pointer here and in case the size of size_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:

          enum {
          CONST_VALUE_INLINE = 1,
          CONST_VALUE_INDIRECT = 1 << 1,
          };
          classConstValueTypeDesc : TypeDesc {
          TypeHandle m_type;
          size_t m_value;
          DWORD m_flag;
          };
          if ((m_flag & CONST_VALUE_INDIRECT) == CONST_VALUE_INDIRECT)
          {
          // get size and layout info from m_type// load the pointer from m_value// deference the pointer to get the value
          }
          else
          {
          // get size and layout info from m_type// load the value from m_value directly
          }

          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:

          classConstValueTypeDesc : TypeDesc {
          TypeHandle m_type;
          mdToken m_value;
          };

          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 new ConstValueTypeDesc 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 the impResolveToken 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,

          .class public auto ansi beforefieldinit Test`2<T, literal !T N>
          {
          .method public hidebysig newslot virtual instance void M<U, literal !!U V> () cil managed { }
          }
          

          Here we can leverage the type field in the GenericParamRec 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 with ValueArray<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.

          call instance void Foo`1<float32 (42.42)>::.ctor(); // instantiate the Foo`1<float32 (42.42)>
          call instance void Foo`1<int32 (42)>::.ctor(); // instantiate the Foo`1<int32 (42)>
          call instance void Foo`1<int32 (42)>::A<int32 (42)>(); // calling the Foo`1<int32 (42)>::A<int32 (42)>()
          call instance void Foo`1<int32 (42)>::A<float32 (42.42)>(); // calling the Foo`1<int32 (42)>::A<float32 (42.42)>()
          .class public auto ansi beforefieldinit Foo`1<literal int32 N>
          {
          .method public hidebysig specialname rtspecialname instance void .ctor () cil managed { ... }
          .method public hidebysig newslot virtual
          instance void A<literal int32 X>() cil managed { ... }
          .method public hidebysig newslot virtual
          instance void A<literal float32 X>() cil managed { ... }
          }
          .class public auto ansi beforefieldinit Foo`1<literal float32 N>
          {
          // ...
          }
          

          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 a struct ValueArray<T, int Length> should not be less than 0.

          We can add the below APIs to achieve arithmetic constraints.

          namespaceSystem.Runtime.CompilerServices;publicabstractclassOperator{publicabstractclassUnaryOperator:Operator{// ...}publicabstractclassBinaryOperator:Operator{publicsealedclassAdditionOperator:BinaryOperator{}publicsealedclassSubtractionOperator:BinaryOperator{}publicsealedclassMultiplyOperator:BinaryOperator{}publicsealedclassDivisionOperator:BinaryOperator{}publicsealedclassEqualityOperator:BinaryOperator{}publicsealedclassLessThanOperator:BinaryOperator{}publicsealedclassConjunctionOperator:BinaryOperator{}publicsealedclassDisjunctionOperator:BinaryOperator{}// ...}}publicinterfaceIExpression{publicinterfaceIUnaryExpression<TOperator,TOprand>:IExpressionwhereTOperand:IExpressionwhereTOperator:UnaryOperator{}publicinterfaceIBinaryExpression<TOperator,TLeft,TRight>:IExpressionwhereTLeft:IExpressionwhereTRight:IExpressionwhereTOperator:BinaryOperator{}publicinterfaceIConstantExpression<TValue,TValueValue>:IExpression{}
          ...}

          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:

          classFoo<int T>whereT:>0,<20{}

          And this got lowered to:

          .class public auto ansi beforefieldinit Foo<literal int32 (
          class BinaryExpression`3<
          class Operator/BinaryOperator/GreaterThanOperator,
          class IExpression/IConstantExpression`2<int32, !!T>,
          class IExpression/IConstantExpression`2<int32, int32 (0)>
          >,
          class BinaryExpression`3<
          class Operator/BinaryOperator/LessThanOperator,
          class IExpression/IConstantExpression`2<int32, !!T>,
          class IExpression/IConstantExpression`2<int32, int32 (20)>
          >
          ) T>
          extends [System.Runtime]System.Object { }
          

          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, so class Foo<T, U, V> where V : == T + U where T : != 0 can be represented in IL as:

          .class public auto ansi beforefieldinit Foo<literal int32 (constexpr (!T != int32 (0))) T, literal int32 U, literal int32 (constexpr (!V == !T + !U)) V>
          

          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 of ValueArray<T, int N> type can be ValueArray<T, N + 1> Push(T elem), and the signature of a Concat method can be ValueArray<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 use ValueArray<T, U> Push<int U>(T elem) where U : (T + 1), and the constraint T + 1 can be expressed using IBinaryExpression<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 on ValueArray<int, 6>, the C# compiler may automatically infer the type of U so developers don't have to explicitly specify the value of U every time.

          However, consider the below code:

          classFoo<int T>{privateFoo<T+1>foo;}

          Are we going to enforce users to introduce a new type parameter on Foo? I.e.,

          classFoo<int T,int U>where ...{privateFoo<U>foo;}

          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:

          classFoo<int T>{typeN=T+1;privateFoo<N>foo;}

          And also, if we have the support for defining an associated type inside a method, we can do:

          classFoo{UMethod<intT>(){typeU=T+1;}}

          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. A ValueArray is basically the InlineArray we already have today plus the ability to specify arbitrary length without the need to define a new InlineArray type.

          Below is the dummy C# code for ValueArray:

          structValueArray<T,int N>{privateTelem;// Repeat the field elem for N timespublicintLength{get;}// ldtoken !N; ret;publicrefTthis[intindex]{ ...}}

          This can be used together with params:

          Foo(1,2,3,4,5);// a method that only receives 5 int argumentsvoidFoo(paramsValueArray<int,5>args){}

          Particularly, in C# we can lower all fixed buffer types to ValueArray, and it can perfectly serve all features like params Span<T> and stackalloc 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:

          namespaceSystem;publicabstractclassType{publicvirtualboolIsConstValue{get;}publicvirtualobjectConstValue{get;}publicstaticTypeMakeConstValueType(objectvalue);}

          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:

          classFoo<T,int N>{}varfoo=newFoo<string,42>();foo.GetType();// Foo<string, int (42)>foo.GetType().GetGenericArguments()[0];// Type: System.Stringfoo.GetType().GetGenericArguments()[1].IsConstValue;// truefoo.GetType().GetGenericArguments()[1].HasElementType;// truefoo.GetType().GetGenericArguments()[1].ConstValue;// 42foo.GetType().GetGenericArguments()[1].GetElementType();// System.Int32vart=Type.MakeConstValue(42);vard=typeof(Foo<,>);d.GetGenericArguments()[1].IsConstValue;// falsed.GetGenericArguments()[1].HasElementType;// trued.GetGenericArguments()[1].ConstValue;// InvalidOperationExceptiond.GetGenericArguments()[1].GetElementType();// Type: System.Int32d.MakeGenericType(typeof(string),t);// Foo<string, int (42)>

          An interesting idea is to allow typeof(value) for the Type.MakeConstValue, for example, typeof(42) to get a Type that contains a value 42.

          This would either require us to:

          1. Use the 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 called ldctarg (load const type argument).
          2. Introduce a new instruction for this.
          3. No new instruction, and just compile it to 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:

          1. Matrix<T, int Row, int Col>: fixed-sized matrix to supersede Matrix3x3, Matrix4x4 and etc.
          2. Vector<T, int N>: fixed-sized vector to supersede Vector2, Vector3 and etc.
          3. Tensor<T, int Rank>: tensor types for AI/ML purpose
          4. Span<T, int Dim>: ND-span that can support multiple dimension arrays
          5. List<T, int N>, Array<T, int N>...: arbitrary list types can have a fixed size now
          6. ... and more

          Future 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,

          // value types
          .class C`1<literal valuetype Foo T> { }
          call C`1<valuetype Foo (bytearray ( 01 00 00 00 02 00 00 00 03 00 00 00))>::.ctor()
          // string
          .class D`1<literal string T> { }
          call D`1<string ("hello world")>::.ctor()
          

          where Foo is a Vector3<int>, so we are passing a Vector3<int> { X = 1, Y = 2, Z = 3 } here.

          And as for the implementation, we can use the m_type in ConstValueTypeDesc to save the TypeHandle of the type, and m_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:

          • For strings, we encode the binary following the rule: ELEMENT_TYPE_CTARG ELEMENT_TYPE_STRING <constant record token>, or ELEMENT_TYPE_CTARG ELEMENT_TYPE_STRING <length> <qcompString>
          • For arbitrary value types: ELEMENT_TYPE_CTARG ELEMENT_TYPE_VALUETYPE <compressed type token> <constant record token>, or ELEMENT_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:

          1. Declare a const generic type, eg. class Foo<T, int N>.
          2. Use a const generic type, eg. new Foo<int, 42>().
          3. Declare a const generic method, eg. void Foo<int X>.
          4. Use a const generic method, eg. Foo<42>().
          5. Generics on const type parameter, eg. class Foo<T, T X>, then you can use it with Foo<int, 42> as well as Foo<float, 42.42424f>.
          6. Use const type parameter as constant directly. eg. calling Console.WriteLine(X) in the type class Foo<int X>.
          7. typeof support. eg. typeof(42).
          8. Casting support in const type argument. eg. new Foo<(short)42>, typeof((short)42)
          9. A built-in value type ValueArray<T, int X> that can be used as a fix-sized type with type T and length X.
          10. A niche syntax for declaring a ValueArray type, eg. int[42].
          11. 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()

          Code Examples

          A basic example

          .assembly _ {}
          .classpublicautoansibeforefieldinit Foo`2<T, literalint32 N>
          extends[System.Runtime]System.Object
          {
          .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
          .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
          ret
          }
          .methodpublichidebysignewslotvirtualinstancevoid M<literalint32 V, literalint32 W> () cilmanaged {
          .maxstack1.localsinit (
          [0] int32 v
          )
          newobjinstancevoidclass Foo`2<string, int32 (42)>::.ctor()
          callinstancevoidclass Foo`2<string, int32 (42)>::M<!!V, !!V>()
          newobjinstancevoidclass Foo`2<string, !!V>::.ctor()
          callinstancevoidclass Foo`2<string, !!V>::M<!N, int32 (42)>()
          newobjinstancevoidclass Foo`2<string, !N>::.ctor()
          callinstancevoidclass Foo`2<string, !N>::M<!!V, !!W>()
          ldtoken !!V
          callvoid[System.Console]System.Console::WriteLine(int32)
          ldtoken !!W
          callvoid[System.Console]System.Console::WriteLine(int32)
          ldtoken !N
          callvoid[System.Console]System.Console::WriteLine(int32)
          ret
          }
          }

          This can be interpreted to the following dummy C# code:

          classFoo<T,int N>{publicvoidM<int V,int W>(){newFoo<string,42>().M<V,V>();newFoo<string,V>().M<N,42>();newFoo<string,N>().M<V,W>();Console.WriteLine(V);Console.WriteLine(W);Console.WriteLine(N);}}

          Generic Virtual Method with Const Type Parameters

          .assembly _ {}
          .classprivateautoansibeforefieldinit Program
          extends[System.Runtime]System.Object
          {
          .methodprivatehidebysigstaticvoid Main (
          string[] args
          ) cilmanaged {
          .maxstack8.entrypointnewobjinstancevoidclass Bar`2<string, int32( 42 )>::.ctor()
          callinstancevoidclass Bar`2<string, int32( 42 )>::N<int32( 42 ), int32( 42 )>()
          ret
          }
          .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
          .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
          ret
          }
          }
          .classpublicautoansibeforefieldinit Foo`2<T, literalint32 N>
          extends[System.Runtime]System.Object
          {
          .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
          .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
          ret
          }
          .methodpublichidebysignewslotvirtualinstancevoid M<literalint32 V, literalint32 W> () cilmanaged {
          .maxstack8ldstr"From Foo::M"callvoid[System.Console]System.Console::WriteLine(string)
          ldtoken !!V
          callvoid[System.Console]System.Console::WriteLine(int32)
          ldtoken !!W
          callvoid[System.Console]System.Console::WriteLine(int32)
          ldtoken !N
          callvoid[System.Console]System.Console::WriteLine(int32)
          ret
          }
          .methodpublichidebysignewslotvirtualinstancevoid N<literalint32 V, literalint32 W> () cilmanaged {
          .maxstack8newobjinstancevoidclass Foo`2<string, int32( 42 )>::.ctor()
          callinstancevoidclass Foo`2<string, int32 (42)>::M<!!V, !!V>()
          newobjinstancevoidclass Foo`2<string, !!V>::.ctor()
          callinstancevoidclass Foo`2<string, !!V>::M<!N, int32 (42)>()
          newobjinstancevoidclass Foo`2<string, !N>::.ctor()
          callinstancevoidclass Foo`2<string, !N>::M<!!V, !!W>()
          ret
          }
          }
          .classpublicautoansibeforefieldinit Bar`2<T, literalint32 N>
          extendsclass Foo`2<!T, int32 (128)>
          {
          .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
          .maxstack8ldarg.0callinstancevoidclass Foo`2<!T, int32 (128)>::.ctor()
          ret
          }
          .methodpublichidebysigvirtualinstancevoid M<literalint32 V, literalint32 W> () cilmanaged {
          .maxstack8.localsinit (
          [0] string v
          )
          ldstr"From Bar::M"callvoid[System.Console]System.Console::WriteLine(string)
          ldtoken !!V
          callvoid[System.Console]System.Console::WriteLine(int32)
          ldtoken !!W
          callvoid[System.Console]System.Console::WriteLine(int32)
          ldtoken !N
          callvoid[System.Console]System.Console::WriteLine(int32)
          ret
          }
          .methodpublichidebysigvirtualinstancevoid N<literalint32 V, literalint32 W> () cilmanaged {
          .maxstack8ldarg.0callinstancevoidclass Foo`2<!T, int32 (128)>::M<!!V, !!W>()
          ldarg.0callvirtinstancevoidclass Foo`2<!T, !N>::M<!!V, !!W>()
          ret
          }
          }

          This will yield the below execution result:

          From Foo::M
          42
          42
          128
          From Bar::M
          42
          42
          42
          

          Generic Virtual Method with Generic on Const Type Parameters

          .assembly _ { }
          .classprivateautoansibeforefieldinit Program
          extends[System.Runtime]System.Object
          {
          .methodprivatehidebysigstaticvoid Main (
          string[] args
          ) cilmanaged {
          .maxstack8.entrypointnewobjinstancevoidclass Bar`2<float32, int32( 42 )>::.ctor()
          callinstancevoidclass Bar`2<float32, int32( 42 )>::N<float32( 42.42 ), int32( 42 )>()
          ret
          }
          .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
          .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
          ret
          }
          }
          .classpublicautoansibeforefieldinit Foo`2<T, literalint32 N>
          extends[System.Runtime]System.Object
          {
          .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
          .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
          ret
          }
          .methodpublichidebysignewslotvirtualinstancevoid M<literal !T V, literalint32 W> () cilmanaged {
          .maxstack8ldstr"From Foo::M"callvoid[System.Console]System.Console::WriteLine(string)
          ldtoken !!V
          box !T
          callvoid[System.Console]System.Console::WriteLine(object)
          ldtoken !!W
          callvoid[System.Console]System.Console::WriteLine(int32)
          ldtoken !N
          callvoid[System.Console]System.Console::WriteLine(int32)
          ret
          }
          .methodpublichidebysignewslotvirtualinstancevoid N<literal !T V, literalint32 W> () cilmanaged {
          .maxstack8newobjinstancevoidclass Foo`2<int32, int32( 42 )>::.ctor()
          callinstancevoidclass Foo`2<int32, int32 (42)>::M<!!V, !!V>()
          newobjinstancevoidclass Foo`2<int32, !!V>::.ctor()
          callinstancevoidclass Foo`2<int32, !!V>::M<!N, int32 (42)>()
          newobjinstancevoidclass Foo`2<int32, !N>::.ctor()
          callinstancevoidclass Foo`2<int32, !N>::M<!!V, !!W>()
          ret
          }
          }
          .classpublicautoansibeforefieldinit Bar`2<T, literalint32 N>
          extendsclass Foo`2<!T, int32 (128)>
          {
          .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
          .maxstack8ldarg.0callinstancevoidclass Foo`2<!T, int32 (128)>::.ctor()
          ret
          }
          .methodpublichidebysigvirtualinstancevoid M<literal !T V, literalint32 W> () cilmanaged {
          .maxstack8.localsinit (
          [0] string v
          )
          ldstr"From Bar::M"callvoid[System.Console]System.Console::WriteLine(string)
          ldtoken !!V
          box !T
          callvoid[System.Console]System.Console::WriteLine(object)
          ldtoken !!W
          callvoid[System.Console]System.Console::WriteLine(int32)
          ldtoken !N
          callvoid[System.Console]System.Console::WriteLine(int32)
          ret
          }
          .methodpublichidebysigvirtualinstancevoid N<literal !T V, literalint32 W> () cilmanaged {
          .maxstack8ldarg.0callinstancevoidclass Foo`2<!T, int32 (128)>::M<!!V, !!W>()
          ldarg.0callvirtinstancevoidclass Foo`2<!T, !N>::M<!!V, !!W>()
          ret
          }
          }

          This will yield the below execution result:

          From Foo::M
          42.42
          42
          128
          From Bar::M
          42.42
          42
          42
          

          Minimal ValueArray Type Implementation

          .class public sequential ansi sealed beforefieldinit System.ValueArray`2<T, literal int32 Length>
          extends [System.Runtime]System.ValueType
          {
          .field private !T elem
          .method public hidebysig specialname instance !T& get_Item (int32 index) cil managed {
          .custom instance void [System.Runtime]System.Diagnostics.CodeAnalysis.UnscopedRefAttribute::.ctor() = (01 00 00 00)
          .maxstack 8
          ldarg.1
          ldc.i4.0
          blt.s OutOfRange
          ldarg.1
          ldarg.0
          call instance int32 valuetype System.ValueArray`2<!T, !Length>::get_Length()
          blt.s GetItem
          OutOfRange:
          call void valuetype System.ValueArray`2<!T, !Length>::ThrowIndexOutOfRange()
          GetItem:
          ldarg.0
          ldflda !0 valuetype System.ValueArray`2<!T, !Length>::elem
          ldarg.1
          call !!0& [System.Runtime]System.Runtime.CompilerServices.Unsafe::Add<!T>(!!0&, int32)
          ret
          }
          .method public hidebysig specialname instance int32 get_Length () cil managed {
          .maxstack 8
          ldtoken !Length
          ret
          }
          .method private hidebysig static void ThrowIndexOutOfRange () cil managed {
          .maxstack 8
          newobj instance void [System.Runtime]System.IndexOutOfRangeException::.ctor()
          throw
          }
          .property instance !T& Item(int32 index)
          {
          .get instance !0& System.ValueArray`2::get_Item(int32)
          }
          .property instance int32 Length()
          {
          .get instance int32 System.ValueArray`2::get_Length()
          }
          }
          

          Metadata

          Metadata

          Assignees

          No one assigned

            Type

            No type

            Projects

            No projects

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              Feature Proposal: Const Generics #89730

              Description

              @hez2010

              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 of T with N 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 implement params {ReadOnly}Span<T>.

              For [2], we can use the const type parameter to define a Span<T, int Dim>, so we can use Span 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 signature Matrix<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

              structEpsilonFloating<T,T Epsilon>whereT:INumber<T>{publicstaticbooloperator==(EpsilonFloating<T,Epsilon>a,EpsilonFloating<T,Epsilon>b)=>T.Abs(a.value-b.value)<=Epsilon;}

              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 type class Foo<int T> to represent 42 * (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:

              structVector<T,int Size>{}staticclassVectorExtension{publicVector<int,4>Multiply<T>(thisVector<int,4>v,Vector<int,4>right){}// Vector64
              public Vector<int,8>Multiply<T>(thisVector<int,8>v,Vector<int,8>right){}// Vector128
              public Vector<int,16>Multiply<T>(thisVector<int,16>v,Vector<int,16>right){}// Vector256
              public Vector<int,32>Multiply<T>(thisVector<int,32>v,Vector<int,32>right){}// Vector512
              public Vector<int,Size>Multiply<int Size>(thisVector<int,Size>v,Vector<int,Size>right){}// For other sizes allowing a software fallback// ...
              public Vector<T,Size>Multiply<T,int Size>(thisVector<T,Size>v,Vector<T,Size>right){}// For other types and sizes allowing a software fallback}

              Design

              Wording

              • Const type parameter: a type parameter that carries a const value.
              • Const type argument: the constant value for a type parameter in the instantiation.

              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 a mdtGenericParamType and then emit the type of const type parameter with mdtGenericParamType, 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 with mdtTypeSpec using (token & ~mdtGenericParamType) | mdtTypeSpec. When loading generic constraints, if we see a generic constraint has type mdtGenericParamType, 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 a class Foo<int T> will be emitted to class 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:

              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 reuse the existing generic metadata and rules, and add a Type token to the GenericParamRec schema. To determine whether a type parameter is a const type parameter or not, simply check the Type token to see if it's valid by using RidFromToken.

              To summarize:

              Added a column to GenericParameterRec to save a mdToken which represents the type of a const generic parameter.
              Changed the reserved DWORD to mdToken to save the type of a const generic parameter.
              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 GetGenericParamProps, both mdToken and the reserved DWORD are exactly DWORD.

              BTW: actually we have another way without upgrading the existing metadata: we can downgrade the metadata version from the current v2.0 to v1.1, where in v1.1 metadata the GenericParamRec table has a Kind field which is exactly what we need for const generics.

              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:

              ELEMENT_TYPE_CTARG <element type of const value> <const value>
              

              Note that the size of the const value is determined by its element type.
              For example, an int 42 will be encoded as:

               ELEMENT_TYPE_CTARG ELEMENT_TYPE_I4 42
              | 1 byte | 1 byte | 4 bytes |
              

              While a double 3.1415926 will be encoded as:

               ELEMENT_TYPE_CTARG ELEMENT_TYPE_R8 3.1415926
              | 1 byte | 1 byte | 8 bytes |
              

              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 keyword literal 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, and type '(' 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 field m_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.

              classConstValueTypeDesc : TypeDesc {
              TypeHandle m_type;
              uint64_t m_value;
              };

              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 support int128 or other types as primitive types in the future. Should we use size_t here instead? This can make sure we are always able to save a pointer here and in case the size of size_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:

              enum {
              CONST_VALUE_INLINE = 1,
              CONST_VALUE_INDIRECT = 1 << 1,
              };
              classConstValueTypeDesc : TypeDesc {
              TypeHandle m_type;
              size_t m_value;
              DWORD m_flag;
              };
              if ((m_flag & CONST_VALUE_INDIRECT) == CONST_VALUE_INDIRECT)
              {
              // get size and layout info from m_type// load the pointer from m_value// deference the pointer to get the value
              }
              else
              {
              // get size and layout info from m_type// load the value from m_value directly
              }

              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:

              classConstValueTypeDesc : TypeDesc {
              TypeHandle m_type;
              mdToken m_value;
              };

              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 new ConstValueTypeDesc 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 the impResolveToken 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,

              .class public auto ansi beforefieldinit Test`2<T, literal !T N>
              {
              .method public hidebysig newslot virtual instance void M<U, literal !!U V> () cil managed { }
              }
              

              Here we can leverage the type field in the GenericParamRec 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 with ValueArray<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.

              call instance void Foo`1<float32 (42.42)>::.ctor(); // instantiate the Foo`1<float32 (42.42)>
              call instance void Foo`1<int32 (42)>::.ctor(); // instantiate the Foo`1<int32 (42)>
              call instance void Foo`1<int32 (42)>::A<int32 (42)>(); // calling the Foo`1<int32 (42)>::A<int32 (42)>()
              call instance void Foo`1<int32 (42)>::A<float32 (42.42)>(); // calling the Foo`1<int32 (42)>::A<float32 (42.42)>()
              .class public auto ansi beforefieldinit Foo`1<literal int32 N>
              {
              .method public hidebysig specialname rtspecialname instance void .ctor () cil managed { ... }
              .method public hidebysig newslot virtual
              instance void A<literal int32 X>() cil managed { ... }
              .method public hidebysig newslot virtual
              instance void A<literal float32 X>() cil managed { ... }
              }
              .class public auto ansi beforefieldinit Foo`1<literal float32 N>
              {
              // ...
              }
              

              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 a struct ValueArray<T, int Length> should not be less than 0.

              We can add the below APIs to achieve arithmetic constraints.

              namespaceSystem.Runtime.CompilerServices;publicabstractclassOperator{publicabstractclassUnaryOperator:Operator{// ...}publicabstractclassBinaryOperator:Operator{publicsealedclassAdditionOperator:BinaryOperator{}publicsealedclassSubtractionOperator:BinaryOperator{}publicsealedclassMultiplyOperator:BinaryOperator{}publicsealedclassDivisionOperator:BinaryOperator{}publicsealedclassEqualityOperator:BinaryOperator{}publicsealedclassLessThanOperator:BinaryOperator{}publicsealedclassConjunctionOperator:BinaryOperator{}publicsealedclassDisjunctionOperator:BinaryOperator{}// ...}}publicinterfaceIExpression{publicinterfaceIUnaryExpression<TOperator,TOprand>:IExpressionwhereTOperand:IExpressionwhereTOperator:UnaryOperator{}publicinterfaceIBinaryExpression<TOperator,TLeft,TRight>:IExpressionwhereTLeft:IExpressionwhereTRight:IExpressionwhereTOperator:BinaryOperator{}publicinterfaceIConstantExpression<TValue,TValueValue>:IExpression{}
              ...}

              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:

              classFoo<int T>whereT:>0,<20{}

              And this got lowered to:

              .class public auto ansi beforefieldinit Foo<literal int32 (
              class BinaryExpression`3<
              class Operator/BinaryOperator/GreaterThanOperator,
              class IExpression/IConstantExpression`2<int32, !!T>,
              class IExpression/IConstantExpression`2<int32, int32 (0)>
              >,
              class BinaryExpression`3<
              class Operator/BinaryOperator/LessThanOperator,
              class IExpression/IConstantExpression`2<int32, !!T>,
              class IExpression/IConstantExpression`2<int32, int32 (20)>
              >
              ) T>
              extends [System.Runtime]System.Object { }
              

              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, so class Foo<T, U, V> where V : == T + U where T : != 0 can be represented in IL as:

              .class public auto ansi beforefieldinit Foo<literal int32 (constexpr (!T != int32 (0))) T, literal int32 U, literal int32 (constexpr (!V == !T + !U)) V>
              

              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 of ValueArray<T, int N> type can be ValueArray<T, N + 1> Push(T elem), and the signature of a Concat method can be ValueArray<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 use ValueArray<T, U> Push<int U>(T elem) where U : (T + 1), and the constraint T + 1 can be expressed using IBinaryExpression<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 on ValueArray<int, 6>, the C# compiler may automatically infer the type of U so developers don't have to explicitly specify the value of U every time.

              However, consider the below code:

              classFoo<int T>{privateFoo<T+1>foo;}

              Are we going to enforce users to introduce a new type parameter on Foo? I.e.,

              classFoo<int T,int U>where ...{privateFoo<U>foo;}

              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:

              classFoo<int T>{typeN=T+1;privateFoo<N>foo;}

              And also, if we have the support for defining an associated type inside a method, we can do:

              classFoo{UMethod<intT>(){typeU=T+1;}}

              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. A ValueArray is basically the InlineArray we already have today plus the ability to specify arbitrary length without the need to define a new InlineArray type.

              Below is the dummy C# code for ValueArray:

              structValueArray<T,int N>{privateTelem;// Repeat the field elem for N timespublicintLength{get;}// ldtoken !N; ret;publicrefTthis[intindex]{ ...}}

              This can be used together with params:

              Foo(1,2,3,4,5);// a method that only receives 5 int argumentsvoidFoo(paramsValueArray<int,5>args){}

              Particularly, in C# we can lower all fixed buffer types to ValueArray, and it can perfectly serve all features like params Span<T> and stackalloc 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:

              namespaceSystem;publicabstractclassType{publicvirtualboolIsConstValue{get;}publicvirtualobjectConstValue{get;}publicstaticTypeMakeConstValueType(objectvalue);}

              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:

              classFoo<T,int N>{}varfoo=newFoo<string,42>();foo.GetType();// Foo<string, int (42)>foo.GetType().GetGenericArguments()[0];// Type: System.Stringfoo.GetType().GetGenericArguments()[1].IsConstValue;// truefoo.GetType().GetGenericArguments()[1].HasElementType;// truefoo.GetType().GetGenericArguments()[1].ConstValue;// 42foo.GetType().GetGenericArguments()[1].GetElementType();// System.Int32vart=Type.MakeConstValue(42);vard=typeof(Foo<,>);d.GetGenericArguments()[1].IsConstValue;// falsed.GetGenericArguments()[1].HasElementType;// trued.GetGenericArguments()[1].ConstValue;// InvalidOperationExceptiond.GetGenericArguments()[1].GetElementType();// Type: System.Int32d.MakeGenericType(typeof(string),t);// Foo<string, int (42)>

              An interesting idea is to allow typeof(value) for the Type.MakeConstValue, for example, typeof(42) to get a Type that contains a value 42.

              This would either require us to:

              1. Use the 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 called ldctarg (load const type argument).
              2. Introduce a new instruction for this.
              3. No new instruction, and just compile it to 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:

              1. Matrix<T, int Row, int Col>: fixed-sized matrix to supersede Matrix3x3, Matrix4x4 and etc.
              2. Vector<T, int N>: fixed-sized vector to supersede Vector2, Vector3 and etc.
              3. Tensor<T, int Rank>: tensor types for AI/ML purpose
              4. Span<T, int Dim>: ND-span that can support multiple dimension arrays
              5. List<T, int N>, Array<T, int N>...: arbitrary list types can have a fixed size now
              6. ... and more

              Future 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,

              // value types
              .class C`1<literal valuetype Foo T> { }
              call C`1<valuetype Foo (bytearray ( 01 00 00 00 02 00 00 00 03 00 00 00))>::.ctor()
              // string
              .class D`1<literal string T> { }
              call D`1<string ("hello world")>::.ctor()
              

              where Foo is a Vector3<int>, so we are passing a Vector3<int> { X = 1, Y = 2, Z = 3 } here.

              And as for the implementation, we can use the m_type in ConstValueTypeDesc to save the TypeHandle of the type, and m_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:

              • For strings, we encode the binary following the rule: ELEMENT_TYPE_CTARG ELEMENT_TYPE_STRING <constant record token>, or ELEMENT_TYPE_CTARG ELEMENT_TYPE_STRING <length> <qcompString>
              • For arbitrary value types: ELEMENT_TYPE_CTARG ELEMENT_TYPE_VALUETYPE <compressed type token> <constant record token>, or ELEMENT_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:

              1. Declare a const generic type, eg. class Foo<T, int N>.
              2. Use a const generic type, eg. new Foo<int, 42>().
              3. Declare a const generic method, eg. void Foo<int X>.
              4. Use a const generic method, eg. Foo<42>().
              5. Generics on const type parameter, eg. class Foo<T, T X>, then you can use it with Foo<int, 42> as well as Foo<float, 42.42424f>.
              6. Use const type parameter as constant directly. eg. calling Console.WriteLine(X) in the type class Foo<int X>.
              7. typeof support. eg. typeof(42).
              8. Casting support in const type argument. eg. new Foo<(short)42>, typeof((short)42)
              9. A built-in value type ValueArray<T, int X> that can be used as a fix-sized type with type T and length X.
              10. A niche syntax for declaring a ValueArray type, eg. int[42].
              11. 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()

              Code Examples

              A basic example

              .assembly _ {}
              .classpublicautoansibeforefieldinit Foo`2<T, literalint32 N>
              extends[System.Runtime]System.Object
              {
              .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
              .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
              ret
              }
              .methodpublichidebysignewslotvirtualinstancevoid M<literalint32 V, literalint32 W> () cilmanaged {
              .maxstack1.localsinit (
              [0] int32 v
              )
              newobjinstancevoidclass Foo`2<string, int32 (42)>::.ctor()
              callinstancevoidclass Foo`2<string, int32 (42)>::M<!!V, !!V>()
              newobjinstancevoidclass Foo`2<string, !!V>::.ctor()
              callinstancevoidclass Foo`2<string, !!V>::M<!N, int32 (42)>()
              newobjinstancevoidclass Foo`2<string, !N>::.ctor()
              callinstancevoidclass Foo`2<string, !N>::M<!!V, !!W>()
              ldtoken !!V
              callvoid[System.Console]System.Console::WriteLine(int32)
              ldtoken !!W
              callvoid[System.Console]System.Console::WriteLine(int32)
              ldtoken !N
              callvoid[System.Console]System.Console::WriteLine(int32)
              ret
              }
              }

              This can be interpreted to the following dummy C# code:

              classFoo<T,int N>{publicvoidM<int V,int W>(){newFoo<string,42>().M<V,V>();newFoo<string,V>().M<N,42>();newFoo<string,N>().M<V,W>();Console.WriteLine(V);Console.WriteLine(W);Console.WriteLine(N);}}

              Generic Virtual Method with Const Type Parameters

              .assembly _ {}
              .classprivateautoansibeforefieldinit Program
              extends[System.Runtime]System.Object
              {
              .methodprivatehidebysigstaticvoid Main (
              string[] args
              ) cilmanaged {
              .maxstack8.entrypointnewobjinstancevoidclass Bar`2<string, int32( 42 )>::.ctor()
              callinstancevoidclass Bar`2<string, int32( 42 )>::N<int32( 42 ), int32( 42 )>()
              ret
              }
              .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
              .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
              ret
              }
              }
              .classpublicautoansibeforefieldinit Foo`2<T, literalint32 N>
              extends[System.Runtime]System.Object
              {
              .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
              .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
              ret
              }
              .methodpublichidebysignewslotvirtualinstancevoid M<literalint32 V, literalint32 W> () cilmanaged {
              .maxstack8ldstr"From Foo::M"callvoid[System.Console]System.Console::WriteLine(string)
              ldtoken !!V
              callvoid[System.Console]System.Console::WriteLine(int32)
              ldtoken !!W
              callvoid[System.Console]System.Console::WriteLine(int32)
              ldtoken !N
              callvoid[System.Console]System.Console::WriteLine(int32)
              ret
              }
              .methodpublichidebysignewslotvirtualinstancevoid N<literalint32 V, literalint32 W> () cilmanaged {
              .maxstack8newobjinstancevoidclass Foo`2<string, int32( 42 )>::.ctor()
              callinstancevoidclass Foo`2<string, int32 (42)>::M<!!V, !!V>()
              newobjinstancevoidclass Foo`2<string, !!V>::.ctor()
              callinstancevoidclass Foo`2<string, !!V>::M<!N, int32 (42)>()
              newobjinstancevoidclass Foo`2<string, !N>::.ctor()
              callinstancevoidclass Foo`2<string, !N>::M<!!V, !!W>()
              ret
              }
              }
              .classpublicautoansibeforefieldinit Bar`2<T, literalint32 N>
              extendsclass Foo`2<!T, int32 (128)>
              {
              .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
              .maxstack8ldarg.0callinstancevoidclass Foo`2<!T, int32 (128)>::.ctor()
              ret
              }
              .methodpublichidebysigvirtualinstancevoid M<literalint32 V, literalint32 W> () cilmanaged {
              .maxstack8.localsinit (
              [0] string v
              )
              ldstr"From Bar::M"callvoid[System.Console]System.Console::WriteLine(string)
              ldtoken !!V
              callvoid[System.Console]System.Console::WriteLine(int32)
              ldtoken !!W
              callvoid[System.Console]System.Console::WriteLine(int32)
              ldtoken !N
              callvoid[System.Console]System.Console::WriteLine(int32)
              ret
              }
              .methodpublichidebysigvirtualinstancevoid N<literalint32 V, literalint32 W> () cilmanaged {
              .maxstack8ldarg.0callinstancevoidclass Foo`2<!T, int32 (128)>::M<!!V, !!W>()
              ldarg.0callvirtinstancevoidclass Foo`2<!T, !N>::M<!!V, !!W>()
              ret
              }
              }

              This will yield the below execution result:

              From Foo::M
              42
              42
              128
              From Bar::M
              42
              42
              42
              

              Generic Virtual Method with Generic on Const Type Parameters

              .assembly _ { }
              .classprivateautoansibeforefieldinit Program
              extends[System.Runtime]System.Object
              {
              .methodprivatehidebysigstaticvoid Main (
              string[] args
              ) cilmanaged {
              .maxstack8.entrypointnewobjinstancevoidclass Bar`2<float32, int32( 42 )>::.ctor()
              callinstancevoidclass Bar`2<float32, int32( 42 )>::N<float32( 42.42 ), int32( 42 )>()
              ret
              }
              .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
              .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
              ret
              }
              }
              .classpublicautoansibeforefieldinit Foo`2<T, literalint32 N>
              extends[System.Runtime]System.Object
              {
              .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
              .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
              ret
              }
              .methodpublichidebysignewslotvirtualinstancevoid M<literal !T V, literalint32 W> () cilmanaged {
              .maxstack8ldstr"From Foo::M"callvoid[System.Console]System.Console::WriteLine(string)
              ldtoken !!V
              box !T
              callvoid[System.Console]System.Console::WriteLine(object)
              ldtoken !!W
              callvoid[System.Console]System.Console::WriteLine(int32)
              ldtoken !N
              callvoid[System.Console]System.Console::WriteLine(int32)
              ret
              }
              .methodpublichidebysignewslotvirtualinstancevoid N<literal !T V, literalint32 W> () cilmanaged {
              .maxstack8newobjinstancevoidclass Foo`2<int32, int32( 42 )>::.ctor()
              callinstancevoidclass Foo`2<int32, int32 (42)>::M<!!V, !!V>()
              newobjinstancevoidclass Foo`2<int32, !!V>::.ctor()
              callinstancevoidclass Foo`2<int32, !!V>::M<!N, int32 (42)>()
              newobjinstancevoidclass Foo`2<int32, !N>::.ctor()
              callinstancevoidclass Foo`2<int32, !N>::M<!!V, !!W>()
              ret
              }
              }
              .classpublicautoansibeforefieldinit Bar`2<T, literalint32 N>
              extendsclass Foo`2<!T, int32 (128)>
              {
              .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
              .maxstack8ldarg.0callinstancevoidclass Foo`2<!T, int32 (128)>::.ctor()
              ret
              }
              .methodpublichidebysigvirtualinstancevoid M<literal !T V, literalint32 W> () cilmanaged {
              .maxstack8.localsinit (
              [0] string v
              )
              ldstr"From Bar::M"callvoid[System.Console]System.Console::WriteLine(string)
              ldtoken !!V
              box !T
              callvoid[System.Console]System.Console::WriteLine(object)
              ldtoken !!W
              callvoid[System.Console]System.Console::WriteLine(int32)
              ldtoken !N
              callvoid[System.Console]System.Console::WriteLine(int32)
              ret
              }
              .methodpublichidebysigvirtualinstancevoid N<literal !T V, literalint32 W> () cilmanaged {
              .maxstack8ldarg.0callinstancevoidclass Foo`2<!T, int32 (128)>::M<!!V, !!W>()
              ldarg.0callvirtinstancevoidclass Foo`2<!T, !N>::M<!!V, !!W>()
              ret
              }
              }

              This will yield the below execution result:

              From Foo::M
              42.42
              42
              128
              From Bar::M
              42.42
              42
              42
              

              Minimal ValueArray Type Implementation

              .class public sequential ansi sealed beforefieldinit System.ValueArray`2<T, literal int32 Length>
              extends [System.Runtime]System.ValueType
              {
              .field private !T elem
              .method public hidebysig specialname instance !T& get_Item (int32 index) cil managed {
              .custom instance void [System.Runtime]System.Diagnostics.CodeAnalysis.UnscopedRefAttribute::.ctor() = (01 00 00 00)
              .maxstack 8
              ldarg.1
              ldc.i4.0
              blt.s OutOfRange
              ldarg.1
              ldarg.0
              call instance int32 valuetype System.ValueArray`2<!T, !Length>::get_Length()
              blt.s GetItem
              OutOfRange:
              call void valuetype System.ValueArray`2<!T, !Length>::ThrowIndexOutOfRange()
              GetItem:
              ldarg.0
              ldflda !0 valuetype System.ValueArray`2<!T, !Length>::elem
              ldarg.1
              call !!0& [System.Runtime]System.Runtime.CompilerServices.Unsafe::Add<!T>(!!0&, int32)
              ret
              }
              .method public hidebysig specialname instance int32 get_Length () cil managed {
              .maxstack 8
              ldtoken !Length
              ret
              }
              .method private hidebysig static void ThrowIndexOutOfRange () cil managed {
              .maxstack 8
              newobj instance void [System.Runtime]System.IndexOutOfRangeException::.ctor()
              throw
              }
              .property instance !T& Item(int32 index)
              {
              .get instance !0& System.ValueArray`2::get_Item(int32)
              }
              .property instance int32 Length()
              {
              .get instance int32 System.ValueArray`2::get_Length()
              }
              }
              

              Metadata

              Metadata

              Assignees

              No one assigned

                Type

                No type

                Projects

                No projects

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
                  Skip to content

                  Feature Proposal: Const Generics #89730

                  Description

                  @hez2010

                  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 of T with N 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 implement params {ReadOnly}Span<T>.

                  For [2], we can use the const type parameter to define a Span<T, int Dim>, so we can use Span 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 signature Matrix<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

                  structEpsilonFloating<T,T Epsilon>whereT:INumber<T>{publicstaticbooloperator==(EpsilonFloating<T,Epsilon>a,EpsilonFloating<T,Epsilon>b)=>T.Abs(a.value-b.value)<=Epsilon;}

                  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 type class Foo<int T> to represent 42 * (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:

                  structVector<T,int Size>{}staticclassVectorExtension{publicVector<int,4>Multiply<T>(thisVector<int,4>v,Vector<int,4>right){}// Vector64
                  public Vector<int,8>Multiply<T>(thisVector<int,8>v,Vector<int,8>right){}// Vector128
                  public Vector<int,16>Multiply<T>(thisVector<int,16>v,Vector<int,16>right){}// Vector256
                  public Vector<int,32>Multiply<T>(thisVector<int,32>v,Vector<int,32>right){}// Vector512
                  public Vector<int,Size>Multiply<int Size>(thisVector<int,Size>v,Vector<int,Size>right){}// For other sizes allowing a software fallback// ...
                  public Vector<T,Size>Multiply<T,int Size>(thisVector<T,Size>v,Vector<T,Size>right){}// For other types and sizes allowing a software fallback}

                  Design

                  Wording

                  • Const type parameter: a type parameter that carries a const value.
                  • Const type argument: the constant value for a type parameter in the instantiation.

                  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 a mdtGenericParamType and then emit the type of const type parameter with mdtGenericParamType, 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 with mdtTypeSpec using (token & ~mdtGenericParamType) | mdtTypeSpec. When loading generic constraints, if we see a generic constraint has type mdtGenericParamType, 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 a class Foo<int T> will be emitted to class 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:

                  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 reuse the existing generic metadata and rules, and add a Type token to the GenericParamRec schema. To determine whether a type parameter is a const type parameter or not, simply check the Type token to see if it's valid by using RidFromToken.

                  To summarize:

                  Added a column to GenericParameterRec to save a mdToken which represents the type of a const generic parameter.
                  Changed the reserved DWORD to mdToken to save the type of a const generic parameter.
                  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 GetGenericParamProps, both mdToken and the reserved DWORD are exactly DWORD.

                  BTW: actually we have another way without upgrading the existing metadata: we can downgrade the metadata version from the current v2.0 to v1.1, where in v1.1 metadata the GenericParamRec table has a Kind field which is exactly what we need for const generics.

                  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:

                  ELEMENT_TYPE_CTARG <element type of const value> <const value>
                  

                  Note that the size of the const value is determined by its element type.
                  For example, an int 42 will be encoded as:

                   ELEMENT_TYPE_CTARG ELEMENT_TYPE_I4 42
                  | 1 byte | 1 byte | 4 bytes |
                  

                  While a double 3.1415926 will be encoded as:

                   ELEMENT_TYPE_CTARG ELEMENT_TYPE_R8 3.1415926
                  | 1 byte | 1 byte | 8 bytes |
                  

                  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 keyword literal 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, and type '(' 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 field m_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.

                  classConstValueTypeDesc : TypeDesc {
                  TypeHandle m_type;
                  uint64_t m_value;
                  };

                  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 support int128 or other types as primitive types in the future. Should we use size_t here instead? This can make sure we are always able to save a pointer here and in case the size of size_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:

                  enum {
                  CONST_VALUE_INLINE = 1,
                  CONST_VALUE_INDIRECT = 1 << 1,
                  };
                  classConstValueTypeDesc : TypeDesc {
                  TypeHandle m_type;
                  size_t m_value;
                  DWORD m_flag;
                  };
                  if ((m_flag & CONST_VALUE_INDIRECT) == CONST_VALUE_INDIRECT)
                  {
                  // get size and layout info from m_type// load the pointer from m_value// deference the pointer to get the value
                  }
                  else
                  {
                  // get size and layout info from m_type// load the value from m_value directly
                  }

                  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:

                  classConstValueTypeDesc : TypeDesc {
                  TypeHandle m_type;
                  mdToken m_value;
                  };

                  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 new ConstValueTypeDesc 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 the impResolveToken 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,

                  .class public auto ansi beforefieldinit Test`2<T, literal !T N>
                  {
                  .method public hidebysig newslot virtual instance void M<U, literal !!U V> () cil managed { }
                  }
                  

                  Here we can leverage the type field in the GenericParamRec 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 with ValueArray<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.

                  call instance void Foo`1<float32 (42.42)>::.ctor(); // instantiate the Foo`1<float32 (42.42)>
                  call instance void Foo`1<int32 (42)>::.ctor(); // instantiate the Foo`1<int32 (42)>
                  call instance void Foo`1<int32 (42)>::A<int32 (42)>(); // calling the Foo`1<int32 (42)>::A<int32 (42)>()
                  call instance void Foo`1<int32 (42)>::A<float32 (42.42)>(); // calling the Foo`1<int32 (42)>::A<float32 (42.42)>()
                  .class public auto ansi beforefieldinit Foo`1<literal int32 N>
                  {
                  .method public hidebysig specialname rtspecialname instance void .ctor () cil managed { ... }
                  .method public hidebysig newslot virtual
                  instance void A<literal int32 X>() cil managed { ... }
                  .method public hidebysig newslot virtual
                  instance void A<literal float32 X>() cil managed { ... }
                  }
                  .class public auto ansi beforefieldinit Foo`1<literal float32 N>
                  {
                  // ...
                  }
                  

                  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 a struct ValueArray<T, int Length> should not be less than 0.

                  We can add the below APIs to achieve arithmetic constraints.

                  namespaceSystem.Runtime.CompilerServices;publicabstractclassOperator{publicabstractclassUnaryOperator:Operator{// ...}publicabstractclassBinaryOperator:Operator{publicsealedclassAdditionOperator:BinaryOperator{}publicsealedclassSubtractionOperator:BinaryOperator{}publicsealedclassMultiplyOperator:BinaryOperator{}publicsealedclassDivisionOperator:BinaryOperator{}publicsealedclassEqualityOperator:BinaryOperator{}publicsealedclassLessThanOperator:BinaryOperator{}publicsealedclassConjunctionOperator:BinaryOperator{}publicsealedclassDisjunctionOperator:BinaryOperator{}// ...}}publicinterfaceIExpression{publicinterfaceIUnaryExpression<TOperator,TOprand>:IExpressionwhereTOperand:IExpressionwhereTOperator:UnaryOperator{}publicinterfaceIBinaryExpression<TOperator,TLeft,TRight>:IExpressionwhereTLeft:IExpressionwhereTRight:IExpressionwhereTOperator:BinaryOperator{}publicinterfaceIConstantExpression<TValue,TValueValue>:IExpression{}
                  ...}

                  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:

                  classFoo<int T>whereT:>0,<20{}

                  And this got lowered to:

                  .class public auto ansi beforefieldinit Foo<literal int32 (
                  class BinaryExpression`3<
                  class Operator/BinaryOperator/GreaterThanOperator,
                  class IExpression/IConstantExpression`2<int32, !!T>,
                  class IExpression/IConstantExpression`2<int32, int32 (0)>
                  >,
                  class BinaryExpression`3<
                  class Operator/BinaryOperator/LessThanOperator,
                  class IExpression/IConstantExpression`2<int32, !!T>,
                  class IExpression/IConstantExpression`2<int32, int32 (20)>
                  >
                  ) T>
                  extends [System.Runtime]System.Object { }
                  

                  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, so class Foo<T, U, V> where V : == T + U where T : != 0 can be represented in IL as:

                  .class public auto ansi beforefieldinit Foo<literal int32 (constexpr (!T != int32 (0))) T, literal int32 U, literal int32 (constexpr (!V == !T + !U)) V>
                  

                  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 of ValueArray<T, int N> type can be ValueArray<T, N + 1> Push(T elem), and the signature of a Concat method can be ValueArray<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 use ValueArray<T, U> Push<int U>(T elem) where U : (T + 1), and the constraint T + 1 can be expressed using IBinaryExpression<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 on ValueArray<int, 6>, the C# compiler may automatically infer the type of U so developers don't have to explicitly specify the value of U every time.

                  However, consider the below code:

                  classFoo<int T>{privateFoo<T+1>foo;}

                  Are we going to enforce users to introduce a new type parameter on Foo? I.e.,

                  classFoo<int T,int U>where ...{privateFoo<U>foo;}

                  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:

                  classFoo<int T>{typeN=T+1;privateFoo<N>foo;}

                  And also, if we have the support for defining an associated type inside a method, we can do:

                  classFoo{UMethod<intT>(){typeU=T+1;}}

                  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. A ValueArray is basically the InlineArray we already have today plus the ability to specify arbitrary length without the need to define a new InlineArray type.

                  Below is the dummy C# code for ValueArray:

                  structValueArray<T,int N>{privateTelem;// Repeat the field elem for N timespublicintLength{get;}// ldtoken !N; ret;publicrefTthis[intindex]{ ...}}

                  This can be used together with params:

                  Foo(1,2,3,4,5);// a method that only receives 5 int argumentsvoidFoo(paramsValueArray<int,5>args){}

                  Particularly, in C# we can lower all fixed buffer types to ValueArray, and it can perfectly serve all features like params Span<T> and stackalloc 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:

                  namespaceSystem;publicabstractclassType{publicvirtualboolIsConstValue{get;}publicvirtualobjectConstValue{get;}publicstaticTypeMakeConstValueType(objectvalue);}

                  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:

                  classFoo<T,int N>{}varfoo=newFoo<string,42>();foo.GetType();// Foo<string, int (42)>foo.GetType().GetGenericArguments()[0];// Type: System.Stringfoo.GetType().GetGenericArguments()[1].IsConstValue;// truefoo.GetType().GetGenericArguments()[1].HasElementType;// truefoo.GetType().GetGenericArguments()[1].ConstValue;// 42foo.GetType().GetGenericArguments()[1].GetElementType();// System.Int32vart=Type.MakeConstValue(42);vard=typeof(Foo<,>);d.GetGenericArguments()[1].IsConstValue;// falsed.GetGenericArguments()[1].HasElementType;// trued.GetGenericArguments()[1].ConstValue;// InvalidOperationExceptiond.GetGenericArguments()[1].GetElementType();// Type: System.Int32d.MakeGenericType(typeof(string),t);// Foo<string, int (42)>

                  An interesting idea is to allow typeof(value) for the Type.MakeConstValue, for example, typeof(42) to get a Type that contains a value 42.

                  This would either require us to:

                  1. Use the 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 called ldctarg (load const type argument).
                  2. Introduce a new instruction for this.
                  3. No new instruction, and just compile it to 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:

                  1. Matrix<T, int Row, int Col>: fixed-sized matrix to supersede Matrix3x3, Matrix4x4 and etc.
                  2. Vector<T, int N>: fixed-sized vector to supersede Vector2, Vector3 and etc.
                  3. Tensor<T, int Rank>: tensor types for AI/ML purpose
                  4. Span<T, int Dim>: ND-span that can support multiple dimension arrays
                  5. List<T, int N>, Array<T, int N>...: arbitrary list types can have a fixed size now
                  6. ... and more

                  Future 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,

                  // value types
                  .class C`1<literal valuetype Foo T> { }
                  call C`1<valuetype Foo (bytearray ( 01 00 00 00 02 00 00 00 03 00 00 00))>::.ctor()
                  // string
                  .class D`1<literal string T> { }
                  call D`1<string ("hello world")>::.ctor()
                  

                  where Foo is a Vector3<int>, so we are passing a Vector3<int> { X = 1, Y = 2, Z = 3 } here.

                  And as for the implementation, we can use the m_type in ConstValueTypeDesc to save the TypeHandle of the type, and m_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:

                  • For strings, we encode the binary following the rule: ELEMENT_TYPE_CTARG ELEMENT_TYPE_STRING <constant record token>, or ELEMENT_TYPE_CTARG ELEMENT_TYPE_STRING <length> <qcompString>
                  • For arbitrary value types: ELEMENT_TYPE_CTARG ELEMENT_TYPE_VALUETYPE <compressed type token> <constant record token>, or ELEMENT_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:

                  1. Declare a const generic type, eg. class Foo<T, int N>.
                  2. Use a const generic type, eg. new Foo<int, 42>().
                  3. Declare a const generic method, eg. void Foo<int X>.
                  4. Use a const generic method, eg. Foo<42>().
                  5. Generics on const type parameter, eg. class Foo<T, T X>, then you can use it with Foo<int, 42> as well as Foo<float, 42.42424f>.
                  6. Use const type parameter as constant directly. eg. calling Console.WriteLine(X) in the type class Foo<int X>.
                  7. typeof support. eg. typeof(42).
                  8. Casting support in const type argument. eg. new Foo<(short)42>, typeof((short)42)
                  9. A built-in value type ValueArray<T, int X> that can be used as a fix-sized type with type T and length X.
                  10. A niche syntax for declaring a ValueArray type, eg. int[42].
                  11. 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()

                  Code Examples

                  A basic example

                  .assembly _ {}
                  .classpublicautoansibeforefieldinit Foo`2<T, literalint32 N>
                  extends[System.Runtime]System.Object
                  {
                  .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
                  .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
                  ret
                  }
                  .methodpublichidebysignewslotvirtualinstancevoid M<literalint32 V, literalint32 W> () cilmanaged {
                  .maxstack1.localsinit (
                  [0] int32 v
                  )
                  newobjinstancevoidclass Foo`2<string, int32 (42)>::.ctor()
                  callinstancevoidclass Foo`2<string, int32 (42)>::M<!!V, !!V>()
                  newobjinstancevoidclass Foo`2<string, !!V>::.ctor()
                  callinstancevoidclass Foo`2<string, !!V>::M<!N, int32 (42)>()
                  newobjinstancevoidclass Foo`2<string, !N>::.ctor()
                  callinstancevoidclass Foo`2<string, !N>::M<!!V, !!W>()
                  ldtoken !!V
                  callvoid[System.Console]System.Console::WriteLine(int32)
                  ldtoken !!W
                  callvoid[System.Console]System.Console::WriteLine(int32)
                  ldtoken !N
                  callvoid[System.Console]System.Console::WriteLine(int32)
                  ret
                  }
                  }

                  This can be interpreted to the following dummy C# code:

                  classFoo<T,int N>{publicvoidM<int V,int W>(){newFoo<string,42>().M<V,V>();newFoo<string,V>().M<N,42>();newFoo<string,N>().M<V,W>();Console.WriteLine(V);Console.WriteLine(W);Console.WriteLine(N);}}

                  Generic Virtual Method with Const Type Parameters

                  .assembly _ {}
                  .classprivateautoansibeforefieldinit Program
                  extends[System.Runtime]System.Object
                  {
                  .methodprivatehidebysigstaticvoid Main (
                  string[] args
                  ) cilmanaged {
                  .maxstack8.entrypointnewobjinstancevoidclass Bar`2<string, int32( 42 )>::.ctor()
                  callinstancevoidclass Bar`2<string, int32( 42 )>::N<int32( 42 ), int32( 42 )>()
                  ret
                  }
                  .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
                  .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
                  ret
                  }
                  }
                  .classpublicautoansibeforefieldinit Foo`2<T, literalint32 N>
                  extends[System.Runtime]System.Object
                  {
                  .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
                  .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
                  ret
                  }
                  .methodpublichidebysignewslotvirtualinstancevoid M<literalint32 V, literalint32 W> () cilmanaged {
                  .maxstack8ldstr"From Foo::M"callvoid[System.Console]System.Console::WriteLine(string)
                  ldtoken !!V
                  callvoid[System.Console]System.Console::WriteLine(int32)
                  ldtoken !!W
                  callvoid[System.Console]System.Console::WriteLine(int32)
                  ldtoken !N
                  callvoid[System.Console]System.Console::WriteLine(int32)
                  ret
                  }
                  .methodpublichidebysignewslotvirtualinstancevoid N<literalint32 V, literalint32 W> () cilmanaged {
                  .maxstack8newobjinstancevoidclass Foo`2<string, int32( 42 )>::.ctor()
                  callinstancevoidclass Foo`2<string, int32 (42)>::M<!!V, !!V>()
                  newobjinstancevoidclass Foo`2<string, !!V>::.ctor()
                  callinstancevoidclass Foo`2<string, !!V>::M<!N, int32 (42)>()
                  newobjinstancevoidclass Foo`2<string, !N>::.ctor()
                  callinstancevoidclass Foo`2<string, !N>::M<!!V, !!W>()
                  ret
                  }
                  }
                  .classpublicautoansibeforefieldinit Bar`2<T, literalint32 N>
                  extendsclass Foo`2<!T, int32 (128)>
                  {
                  .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
                  .maxstack8ldarg.0callinstancevoidclass Foo`2<!T, int32 (128)>::.ctor()
                  ret
                  }
                  .methodpublichidebysigvirtualinstancevoid M<literalint32 V, literalint32 W> () cilmanaged {
                  .maxstack8.localsinit (
                  [0] string v
                  )
                  ldstr"From Bar::M"callvoid[System.Console]System.Console::WriteLine(string)
                  ldtoken !!V
                  callvoid[System.Console]System.Console::WriteLine(int32)
                  ldtoken !!W
                  callvoid[System.Console]System.Console::WriteLine(int32)
                  ldtoken !N
                  callvoid[System.Console]System.Console::WriteLine(int32)
                  ret
                  }
                  .methodpublichidebysigvirtualinstancevoid N<literalint32 V, literalint32 W> () cilmanaged {
                  .maxstack8ldarg.0callinstancevoidclass Foo`2<!T, int32 (128)>::M<!!V, !!W>()
                  ldarg.0callvirtinstancevoidclass Foo`2<!T, !N>::M<!!V, !!W>()
                  ret
                  }
                  }

                  This will yield the below execution result:

                  From Foo::M
                  42
                  42
                  128
                  From Bar::M
                  42
                  42
                  42
                  

                  Generic Virtual Method with Generic on Const Type Parameters

                  .assembly _ { }
                  .classprivateautoansibeforefieldinit Program
                  extends[System.Runtime]System.Object
                  {
                  .methodprivatehidebysigstaticvoid Main (
                  string[] args
                  ) cilmanaged {
                  .maxstack8.entrypointnewobjinstancevoidclass Bar`2<float32, int32( 42 )>::.ctor()
                  callinstancevoidclass Bar`2<float32, int32( 42 )>::N<float32( 42.42 ), int32( 42 )>()
                  ret
                  }
                  .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
                  .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
                  ret
                  }
                  }
                  .classpublicautoansibeforefieldinit Foo`2<T, literalint32 N>
                  extends[System.Runtime]System.Object
                  {
                  .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
                  .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
                  ret
                  }
                  .methodpublichidebysignewslotvirtualinstancevoid M<literal !T V, literalint32 W> () cilmanaged {
                  .maxstack8ldstr"From Foo::M"callvoid[System.Console]System.Console::WriteLine(string)
                  ldtoken !!V
                  box !T
                  callvoid[System.Console]System.Console::WriteLine(object)
                  ldtoken !!W
                  callvoid[System.Console]System.Console::WriteLine(int32)
                  ldtoken !N
                  callvoid[System.Console]System.Console::WriteLine(int32)
                  ret
                  }
                  .methodpublichidebysignewslotvirtualinstancevoid N<literal !T V, literalint32 W> () cilmanaged {
                  .maxstack8newobjinstancevoidclass Foo`2<int32, int32( 42 )>::.ctor()
                  callinstancevoidclass Foo`2<int32, int32 (42)>::M<!!V, !!V>()
                  newobjinstancevoidclass Foo`2<int32, !!V>::.ctor()
                  callinstancevoidclass Foo`2<int32, !!V>::M<!N, int32 (42)>()
                  newobjinstancevoidclass Foo`2<int32, !N>::.ctor()
                  callinstancevoidclass Foo`2<int32, !N>::M<!!V, !!W>()
                  ret
                  }
                  }
                  .classpublicautoansibeforefieldinit Bar`2<T, literalint32 N>
                  extendsclass Foo`2<!T, int32 (128)>
                  {
                  .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
                  .maxstack8ldarg.0callinstancevoidclass Foo`2<!T, int32 (128)>::.ctor()
                  ret
                  }
                  .methodpublichidebysigvirtualinstancevoid M<literal !T V, literalint32 W> () cilmanaged {
                  .maxstack8.localsinit (
                  [0] string v
                  )
                  ldstr"From Bar::M"callvoid[System.Console]System.Console::WriteLine(string)
                  ldtoken !!V
                  box !T
                  callvoid[System.Console]System.Console::WriteLine(object)
                  ldtoken !!W
                  callvoid[System.Console]System.Console::WriteLine(int32)
                  ldtoken !N
                  callvoid[System.Console]System.Console::WriteLine(int32)
                  ret
                  }
                  .methodpublichidebysigvirtualinstancevoid N<literal !T V, literalint32 W> () cilmanaged {
                  .maxstack8ldarg.0callinstancevoidclass Foo`2<!T, int32 (128)>::M<!!V, !!W>()
                  ldarg.0callvirtinstancevoidclass Foo`2<!T, !N>::M<!!V, !!W>()
                  ret
                  }
                  }

                  This will yield the below execution result:

                  From Foo::M
                  42.42
                  42
                  128
                  From Bar::M
                  42.42
                  42
                  42
                  

                  Minimal ValueArray Type Implementation

                  .class public sequential ansi sealed beforefieldinit System.ValueArray`2<T, literal int32 Length>
                  extends [System.Runtime]System.ValueType
                  {
                  .field private !T elem
                  .method public hidebysig specialname instance !T& get_Item (int32 index) cil managed {
                  .custom instance void [System.Runtime]System.Diagnostics.CodeAnalysis.UnscopedRefAttribute::.ctor() = (01 00 00 00)
                  .maxstack 8
                  ldarg.1
                  ldc.i4.0
                  blt.s OutOfRange
                  ldarg.1
                  ldarg.0
                  call instance int32 valuetype System.ValueArray`2<!T, !Length>::get_Length()
                  blt.s GetItem
                  OutOfRange:
                  call void valuetype System.ValueArray`2<!T, !Length>::ThrowIndexOutOfRange()
                  GetItem:
                  ldarg.0
                  ldflda !0 valuetype System.ValueArray`2<!T, !Length>::elem
                  ldarg.1
                  call !!0& [System.Runtime]System.Runtime.CompilerServices.Unsafe::Add<!T>(!!0&, int32)
                  ret
                  }
                  .method public hidebysig specialname instance int32 get_Length () cil managed {
                  .maxstack 8
                  ldtoken !Length
                  ret
                  }
                  .method private hidebysig static void ThrowIndexOutOfRange () cil managed {
                  .maxstack 8
                  newobj instance void [System.Runtime]System.IndexOutOfRangeException::.ctor()
                  throw
                  }
                  .property instance !T& Item(int32 index)
                  {
                  .get instance !0& System.ValueArray`2::get_Item(int32)
                  }
                  .property instance int32 Length()
                  {
                  .get instance int32 System.ValueArray`2::get_Length()
                  }
                  }
                  

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Type

                    No type

                    Projects

                    No projects

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                      Skip to content

                      Feature Proposal: Const Generics #89730

                      Description

                      @hez2010

                      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 of T with N 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 implement params {ReadOnly}Span<T>.

                      For [2], we can use the const type parameter to define a Span<T, int Dim>, so we can use Span 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 signature Matrix<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

                      structEpsilonFloating<T,T Epsilon>whereT:INumber<T>{publicstaticbooloperator==(EpsilonFloating<T,Epsilon>a,EpsilonFloating<T,Epsilon>b)=>T.Abs(a.value-b.value)<=Epsilon;}

                      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 type class Foo<int T> to represent 42 * (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:

                      structVector<T,int Size>{}staticclassVectorExtension{publicVector<int,4>Multiply<T>(thisVector<int,4>v,Vector<int,4>right){}// Vector64
                      public Vector<int,8>Multiply<T>(thisVector<int,8>v,Vector<int,8>right){}// Vector128
                      public Vector<int,16>Multiply<T>(thisVector<int,16>v,Vector<int,16>right){}// Vector256
                      public Vector<int,32>Multiply<T>(thisVector<int,32>v,Vector<int,32>right){}// Vector512
                      public Vector<int,Size>Multiply<int Size>(thisVector<int,Size>v,Vector<int,Size>right){}// For other sizes allowing a software fallback// ...
                      public Vector<T,Size>Multiply<T,int Size>(thisVector<T,Size>v,Vector<T,Size>right){}// For other types and sizes allowing a software fallback}

                      Design

                      Wording

                      • Const type parameter: a type parameter that carries a const value.
                      • Const type argument: the constant value for a type parameter in the instantiation.

                      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 a mdtGenericParamType and then emit the type of const type parameter with mdtGenericParamType, 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 with mdtTypeSpec using (token & ~mdtGenericParamType) | mdtTypeSpec. When loading generic constraints, if we see a generic constraint has type mdtGenericParamType, 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 a class Foo<int T> will be emitted to class 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:

                      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 reuse the existing generic metadata and rules, and add a Type token to the GenericParamRec schema. To determine whether a type parameter is a const type parameter or not, simply check the Type token to see if it's valid by using RidFromToken.

                      To summarize:

                      Added a column to GenericParameterRec to save a mdToken which represents the type of a const generic parameter.
                      Changed the reserved DWORD to mdToken to save the type of a const generic parameter.
                      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 GetGenericParamProps, both mdToken and the reserved DWORD are exactly DWORD.

                      BTW: actually we have another way without upgrading the existing metadata: we can downgrade the metadata version from the current v2.0 to v1.1, where in v1.1 metadata the GenericParamRec table has a Kind field which is exactly what we need for const generics.

                      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:

                      ELEMENT_TYPE_CTARG <element type of const value> <const value>
                      

                      Note that the size of the const value is determined by its element type.
                      For example, an int 42 will be encoded as:

                       ELEMENT_TYPE_CTARG ELEMENT_TYPE_I4 42
                      | 1 byte | 1 byte | 4 bytes |
                      

                      While a double 3.1415926 will be encoded as:

                       ELEMENT_TYPE_CTARG ELEMENT_TYPE_R8 3.1415926
                      | 1 byte | 1 byte | 8 bytes |
                      

                      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 keyword literal 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, and type '(' 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 field m_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.

                      classConstValueTypeDesc : TypeDesc {
                      TypeHandle m_type;
                      uint64_t m_value;
                      };

                      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 support int128 or other types as primitive types in the future. Should we use size_t here instead? This can make sure we are always able to save a pointer here and in case the size of size_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:

                      enum {
                      CONST_VALUE_INLINE = 1,
                      CONST_VALUE_INDIRECT = 1 << 1,
                      };
                      classConstValueTypeDesc : TypeDesc {
                      TypeHandle m_type;
                      size_t m_value;
                      DWORD m_flag;
                      };
                      if ((m_flag & CONST_VALUE_INDIRECT) == CONST_VALUE_INDIRECT)
                      {
                      // get size and layout info from m_type// load the pointer from m_value// deference the pointer to get the value
                      }
                      else
                      {
                      // get size and layout info from m_type// load the value from m_value directly
                      }

                      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:

                      classConstValueTypeDesc : TypeDesc {
                      TypeHandle m_type;
                      mdToken m_value;
                      };

                      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 new ConstValueTypeDesc 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 the impResolveToken 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,

                      .class public auto ansi beforefieldinit Test`2<T, literal !T N>
                      {
                      .method public hidebysig newslot virtual instance void M<U, literal !!U V> () cil managed { }
                      }
                      

                      Here we can leverage the type field in the GenericParamRec 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 with ValueArray<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.

                      call instance void Foo`1<float32 (42.42)>::.ctor(); // instantiate the Foo`1<float32 (42.42)>
                      call instance void Foo`1<int32 (42)>::.ctor(); // instantiate the Foo`1<int32 (42)>
                      call instance void Foo`1<int32 (42)>::A<int32 (42)>(); // calling the Foo`1<int32 (42)>::A<int32 (42)>()
                      call instance void Foo`1<int32 (42)>::A<float32 (42.42)>(); // calling the Foo`1<int32 (42)>::A<float32 (42.42)>()
                      .class public auto ansi beforefieldinit Foo`1<literal int32 N>
                      {
                      .method public hidebysig specialname rtspecialname instance void .ctor () cil managed { ... }
                      .method public hidebysig newslot virtual
                      instance void A<literal int32 X>() cil managed { ... }
                      .method public hidebysig newslot virtual
                      instance void A<literal float32 X>() cil managed { ... }
                      }
                      .class public auto ansi beforefieldinit Foo`1<literal float32 N>
                      {
                      // ...
                      }
                      

                      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 a struct ValueArray<T, int Length> should not be less than 0.

                      We can add the below APIs to achieve arithmetic constraints.

                      namespaceSystem.Runtime.CompilerServices;publicabstractclassOperator{publicabstractclassUnaryOperator:Operator{// ...}publicabstractclassBinaryOperator:Operator{publicsealedclassAdditionOperator:BinaryOperator{}publicsealedclassSubtractionOperator:BinaryOperator{}publicsealedclassMultiplyOperator:BinaryOperator{}publicsealedclassDivisionOperator:BinaryOperator{}publicsealedclassEqualityOperator:BinaryOperator{}publicsealedclassLessThanOperator:BinaryOperator{}publicsealedclassConjunctionOperator:BinaryOperator{}publicsealedclassDisjunctionOperator:BinaryOperator{}// ...}}publicinterfaceIExpression{publicinterfaceIUnaryExpression<TOperator,TOprand>:IExpressionwhereTOperand:IExpressionwhereTOperator:UnaryOperator{}publicinterfaceIBinaryExpression<TOperator,TLeft,TRight>:IExpressionwhereTLeft:IExpressionwhereTRight:IExpressionwhereTOperator:BinaryOperator{}publicinterfaceIConstantExpression<TValue,TValueValue>:IExpression{}
                      ...}

                      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:

                      classFoo<int T>whereT:>0,<20{}

                      And this got lowered to:

                      .class public auto ansi beforefieldinit Foo<literal int32 (
                      class BinaryExpression`3<
                      class Operator/BinaryOperator/GreaterThanOperator,
                      class IExpression/IConstantExpression`2<int32, !!T>,
                      class IExpression/IConstantExpression`2<int32, int32 (0)>
                      >,
                      class BinaryExpression`3<
                      class Operator/BinaryOperator/LessThanOperator,
                      class IExpression/IConstantExpression`2<int32, !!T>,
                      class IExpression/IConstantExpression`2<int32, int32 (20)>
                      >
                      ) T>
                      extends [System.Runtime]System.Object { }
                      

                      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, so class Foo<T, U, V> where V : == T + U where T : != 0 can be represented in IL as:

                      .class public auto ansi beforefieldinit Foo<literal int32 (constexpr (!T != int32 (0))) T, literal int32 U, literal int32 (constexpr (!V == !T + !U)) V>
                      

                      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 of ValueArray<T, int N> type can be ValueArray<T, N + 1> Push(T elem), and the signature of a Concat method can be ValueArray<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 use ValueArray<T, U> Push<int U>(T elem) where U : (T + 1), and the constraint T + 1 can be expressed using IBinaryExpression<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 on ValueArray<int, 6>, the C# compiler may automatically infer the type of U so developers don't have to explicitly specify the value of U every time.

                      However, consider the below code:

                      classFoo<int T>{privateFoo<T+1>foo;}

                      Are we going to enforce users to introduce a new type parameter on Foo? I.e.,

                      classFoo<int T,int U>where ...{privateFoo<U>foo;}

                      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:

                      classFoo<int T>{typeN=T+1;privateFoo<N>foo;}

                      And also, if we have the support for defining an associated type inside a method, we can do:

                      classFoo{UMethod<intT>(){typeU=T+1;}}

                      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. A ValueArray is basically the InlineArray we already have today plus the ability to specify arbitrary length without the need to define a new InlineArray type.

                      Below is the dummy C# code for ValueArray:

                      structValueArray<T,int N>{privateTelem;// Repeat the field elem for N timespublicintLength{get;}// ldtoken !N; ret;publicrefTthis[intindex]{ ...}}

                      This can be used together with params:

                      Foo(1,2,3,4,5);// a method that only receives 5 int argumentsvoidFoo(paramsValueArray<int,5>args){}

                      Particularly, in C# we can lower all fixed buffer types to ValueArray, and it can perfectly serve all features like params Span<T> and stackalloc 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:

                      namespaceSystem;publicabstractclassType{publicvirtualboolIsConstValue{get;}publicvirtualobjectConstValue{get;}publicstaticTypeMakeConstValueType(objectvalue);}

                      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:

                      classFoo<T,int N>{}varfoo=newFoo<string,42>();foo.GetType();// Foo<string, int (42)>foo.GetType().GetGenericArguments()[0];// Type: System.Stringfoo.GetType().GetGenericArguments()[1].IsConstValue;// truefoo.GetType().GetGenericArguments()[1].HasElementType;// truefoo.GetType().GetGenericArguments()[1].ConstValue;// 42foo.GetType().GetGenericArguments()[1].GetElementType();// System.Int32vart=Type.MakeConstValue(42);vard=typeof(Foo<,>);d.GetGenericArguments()[1].IsConstValue;// falsed.GetGenericArguments()[1].HasElementType;// trued.GetGenericArguments()[1].ConstValue;// InvalidOperationExceptiond.GetGenericArguments()[1].GetElementType();// Type: System.Int32d.MakeGenericType(typeof(string),t);// Foo<string, int (42)>

                      An interesting idea is to allow typeof(value) for the Type.MakeConstValue, for example, typeof(42) to get a Type that contains a value 42.

                      This would either require us to:

                      1. Use the 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 called ldctarg (load const type argument).
                      2. Introduce a new instruction for this.
                      3. No new instruction, and just compile it to 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:

                      1. Matrix<T, int Row, int Col>: fixed-sized matrix to supersede Matrix3x3, Matrix4x4 and etc.
                      2. Vector<T, int N>: fixed-sized vector to supersede Vector2, Vector3 and etc.
                      3. Tensor<T, int Rank>: tensor types for AI/ML purpose
                      4. Span<T, int Dim>: ND-span that can support multiple dimension arrays
                      5. List<T, int N>, Array<T, int N>...: arbitrary list types can have a fixed size now
                      6. ... and more

                      Future 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,

                      // value types
                      .class C`1<literal valuetype Foo T> { }
                      call C`1<valuetype Foo (bytearray ( 01 00 00 00 02 00 00 00 03 00 00 00))>::.ctor()
                      // string
                      .class D`1<literal string T> { }
                      call D`1<string ("hello world")>::.ctor()
                      

                      where Foo is a Vector3<int>, so we are passing a Vector3<int> { X = 1, Y = 2, Z = 3 } here.

                      And as for the implementation, we can use the m_type in ConstValueTypeDesc to save the TypeHandle of the type, and m_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:

                      • For strings, we encode the binary following the rule: ELEMENT_TYPE_CTARG ELEMENT_TYPE_STRING <constant record token>, or ELEMENT_TYPE_CTARG ELEMENT_TYPE_STRING <length> <qcompString>
                      • For arbitrary value types: ELEMENT_TYPE_CTARG ELEMENT_TYPE_VALUETYPE <compressed type token> <constant record token>, or ELEMENT_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:

                      1. Declare a const generic type, eg. class Foo<T, int N>.
                      2. Use a const generic type, eg. new Foo<int, 42>().
                      3. Declare a const generic method, eg. void Foo<int X>.
                      4. Use a const generic method, eg. Foo<42>().
                      5. Generics on const type parameter, eg. class Foo<T, T X>, then you can use it with Foo<int, 42> as well as Foo<float, 42.42424f>.
                      6. Use const type parameter as constant directly. eg. calling Console.WriteLine(X) in the type class Foo<int X>.
                      7. typeof support. eg. typeof(42).
                      8. Casting support in const type argument. eg. new Foo<(short)42>, typeof((short)42)
                      9. A built-in value type ValueArray<T, int X> that can be used as a fix-sized type with type T and length X.
                      10. A niche syntax for declaring a ValueArray type, eg. int[42].
                      11. 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()

                      Code Examples

                      A basic example

                      .assembly _ {}
                      .classpublicautoansibeforefieldinit Foo`2<T, literalint32 N>
                      extends[System.Runtime]System.Object
                      {
                      .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
                      .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
                      ret
                      }
                      .methodpublichidebysignewslotvirtualinstancevoid M<literalint32 V, literalint32 W> () cilmanaged {
                      .maxstack1.localsinit (
                      [0] int32 v
                      )
                      newobjinstancevoidclass Foo`2<string, int32 (42)>::.ctor()
                      callinstancevoidclass Foo`2<string, int32 (42)>::M<!!V, !!V>()
                      newobjinstancevoidclass Foo`2<string, !!V>::.ctor()
                      callinstancevoidclass Foo`2<string, !!V>::M<!N, int32 (42)>()
                      newobjinstancevoidclass Foo`2<string, !N>::.ctor()
                      callinstancevoidclass Foo`2<string, !N>::M<!!V, !!W>()
                      ldtoken !!V
                      callvoid[System.Console]System.Console::WriteLine(int32)
                      ldtoken !!W
                      callvoid[System.Console]System.Console::WriteLine(int32)
                      ldtoken !N
                      callvoid[System.Console]System.Console::WriteLine(int32)
                      ret
                      }
                      }

                      This can be interpreted to the following dummy C# code:

                      classFoo<T,int N>{publicvoidM<int V,int W>(){newFoo<string,42>().M<V,V>();newFoo<string,V>().M<N,42>();newFoo<string,N>().M<V,W>();Console.WriteLine(V);Console.WriteLine(W);Console.WriteLine(N);}}

                      Generic Virtual Method with Const Type Parameters

                      .assembly _ {}
                      .classprivateautoansibeforefieldinit Program
                      extends[System.Runtime]System.Object
                      {
                      .methodprivatehidebysigstaticvoid Main (
                      string[] args
                      ) cilmanaged {
                      .maxstack8.entrypointnewobjinstancevoidclass Bar`2<string, int32( 42 )>::.ctor()
                      callinstancevoidclass Bar`2<string, int32( 42 )>::N<int32( 42 ), int32( 42 )>()
                      ret
                      }
                      .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
                      .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
                      ret
                      }
                      }
                      .classpublicautoansibeforefieldinit Foo`2<T, literalint32 N>
                      extends[System.Runtime]System.Object
                      {
                      .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
                      .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
                      ret
                      }
                      .methodpublichidebysignewslotvirtualinstancevoid M<literalint32 V, literalint32 W> () cilmanaged {
                      .maxstack8ldstr"From Foo::M"callvoid[System.Console]System.Console::WriteLine(string)
                      ldtoken !!V
                      callvoid[System.Console]System.Console::WriteLine(int32)
                      ldtoken !!W
                      callvoid[System.Console]System.Console::WriteLine(int32)
                      ldtoken !N
                      callvoid[System.Console]System.Console::WriteLine(int32)
                      ret
                      }
                      .methodpublichidebysignewslotvirtualinstancevoid N<literalint32 V, literalint32 W> () cilmanaged {
                      .maxstack8newobjinstancevoidclass Foo`2<string, int32( 42 )>::.ctor()
                      callinstancevoidclass Foo`2<string, int32 (42)>::M<!!V, !!V>()
                      newobjinstancevoidclass Foo`2<string, !!V>::.ctor()
                      callinstancevoidclass Foo`2<string, !!V>::M<!N, int32 (42)>()
                      newobjinstancevoidclass Foo`2<string, !N>::.ctor()
                      callinstancevoidclass Foo`2<string, !N>::M<!!V, !!W>()
                      ret
                      }
                      }
                      .classpublicautoansibeforefieldinit Bar`2<T, literalint32 N>
                      extendsclass Foo`2<!T, int32 (128)>
                      {
                      .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
                      .maxstack8ldarg.0callinstancevoidclass Foo`2<!T, int32 (128)>::.ctor()
                      ret
                      }
                      .methodpublichidebysigvirtualinstancevoid M<literalint32 V, literalint32 W> () cilmanaged {
                      .maxstack8.localsinit (
                      [0] string v
                      )
                      ldstr"From Bar::M"callvoid[System.Console]System.Console::WriteLine(string)
                      ldtoken !!V
                      callvoid[System.Console]System.Console::WriteLine(int32)
                      ldtoken !!W
                      callvoid[System.Console]System.Console::WriteLine(int32)
                      ldtoken !N
                      callvoid[System.Console]System.Console::WriteLine(int32)
                      ret
                      }
                      .methodpublichidebysigvirtualinstancevoid N<literalint32 V, literalint32 W> () cilmanaged {
                      .maxstack8ldarg.0callinstancevoidclass Foo`2<!T, int32 (128)>::M<!!V, !!W>()
                      ldarg.0callvirtinstancevoidclass Foo`2<!T, !N>::M<!!V, !!W>()
                      ret
                      }
                      }

                      This will yield the below execution result:

                      From Foo::M
                      42
                      42
                      128
                      From Bar::M
                      42
                      42
                      42
                      

                      Generic Virtual Method with Generic on Const Type Parameters

                      .assembly _ { }
                      .classprivateautoansibeforefieldinit Program
                      extends[System.Runtime]System.Object
                      {
                      .methodprivatehidebysigstaticvoid Main (
                      string[] args
                      ) cilmanaged {
                      .maxstack8.entrypointnewobjinstancevoidclass Bar`2<float32, int32( 42 )>::.ctor()
                      callinstancevoidclass Bar`2<float32, int32( 42 )>::N<float32( 42.42 ), int32( 42 )>()
                      ret
                      }
                      .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
                      .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
                      ret
                      }
                      }
                      .classpublicautoansibeforefieldinit Foo`2<T, literalint32 N>
                      extends[System.Runtime]System.Object
                      {
                      .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
                      .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
                      ret
                      }
                      .methodpublichidebysignewslotvirtualinstancevoid M<literal !T V, literalint32 W> () cilmanaged {
                      .maxstack8ldstr"From Foo::M"callvoid[System.Console]System.Console::WriteLine(string)
                      ldtoken !!V
                      box !T
                      callvoid[System.Console]System.Console::WriteLine(object)
                      ldtoken !!W
                      callvoid[System.Console]System.Console::WriteLine(int32)
                      ldtoken !N
                      callvoid[System.Console]System.Console::WriteLine(int32)
                      ret
                      }
                      .methodpublichidebysignewslotvirtualinstancevoid N<literal !T V, literalint32 W> () cilmanaged {
                      .maxstack8newobjinstancevoidclass Foo`2<int32, int32( 42 )>::.ctor()
                      callinstancevoidclass Foo`2<int32, int32 (42)>::M<!!V, !!V>()
                      newobjinstancevoidclass Foo`2<int32, !!V>::.ctor()
                      callinstancevoidclass Foo`2<int32, !!V>::M<!N, int32 (42)>()
                      newobjinstancevoidclass Foo`2<int32, !N>::.ctor()
                      callinstancevoidclass Foo`2<int32, !N>::M<!!V, !!W>()
                      ret
                      }
                      }
                      .classpublicautoansibeforefieldinit Bar`2<T, literalint32 N>
                      extendsclass Foo`2<!T, int32 (128)>
                      {
                      .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
                      .maxstack8ldarg.0callinstancevoidclass Foo`2<!T, int32 (128)>::.ctor()
                      ret
                      }
                      .methodpublichidebysigvirtualinstancevoid M<literal !T V, literalint32 W> () cilmanaged {
                      .maxstack8.localsinit (
                      [0] string v
                      )
                      ldstr"From Bar::M"callvoid[System.Console]System.Console::WriteLine(string)
                      ldtoken !!V
                      box !T
                      callvoid[System.Console]System.Console::WriteLine(object)
                      ldtoken !!W
                      callvoid[System.Console]System.Console::WriteLine(int32)
                      ldtoken !N
                      callvoid[System.Console]System.Console::WriteLine(int32)
                      ret
                      }
                      .methodpublichidebysigvirtualinstancevoid N<literal !T V, literalint32 W> () cilmanaged {
                      .maxstack8ldarg.0callinstancevoidclass Foo`2<!T, int32 (128)>::M<!!V, !!W>()
                      ldarg.0callvirtinstancevoidclass Foo`2<!T, !N>::M<!!V, !!W>()
                      ret
                      }
                      }

                      This will yield the below execution result:

                      From Foo::M
                      42.42
                      42
                      128
                      From Bar::M
                      42.42
                      42
                      42
                      

                      Minimal ValueArray Type Implementation

                      .class public sequential ansi sealed beforefieldinit System.ValueArray`2<T, literal int32 Length>
                      extends [System.Runtime]System.ValueType
                      {
                      .field private !T elem
                      .method public hidebysig specialname instance !T& get_Item (int32 index) cil managed {
                      .custom instance void [System.Runtime]System.Diagnostics.CodeAnalysis.UnscopedRefAttribute::.ctor() = (01 00 00 00)
                      .maxstack 8
                      ldarg.1
                      ldc.i4.0
                      blt.s OutOfRange
                      ldarg.1
                      ldarg.0
                      call instance int32 valuetype System.ValueArray`2<!T, !Length>::get_Length()
                      blt.s GetItem
                      OutOfRange:
                      call void valuetype System.ValueArray`2<!T, !Length>::ThrowIndexOutOfRange()
                      GetItem:
                      ldarg.0
                      ldflda !0 valuetype System.ValueArray`2<!T, !Length>::elem
                      ldarg.1
                      call !!0& [System.Runtime]System.Runtime.CompilerServices.Unsafe::Add<!T>(!!0&, int32)
                      ret
                      }
                      .method public hidebysig specialname instance int32 get_Length () cil managed {
                      .maxstack 8
                      ldtoken !Length
                      ret
                      }
                      .method private hidebysig static void ThrowIndexOutOfRange () cil managed {
                      .maxstack 8
                      newobj instance void [System.Runtime]System.IndexOutOfRangeException::.ctor()
                      throw
                      }
                      .property instance !T& Item(int32 index)
                      {
                      .get instance !0& System.ValueArray`2::get_Item(int32)
                      }
                      .property instance int32 Length()
                      {
                      .get instance int32 System.ValueArray`2::get_Length()
                      }
                      }
                      

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Type

                        No type

                        Projects

                        No projects

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                          Skip to content

                          Feature Proposal: Const Generics #89730

                          Description

                          @hez2010

                          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 of T with N 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 implement params {ReadOnly}Span<T>.

                          For [2], we can use the const type parameter to define a Span<T, int Dim>, so we can use Span 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 signature Matrix<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

                          structEpsilonFloating<T,T Epsilon>whereT:INumber<T>{publicstaticbooloperator==(EpsilonFloating<T,Epsilon>a,EpsilonFloating<T,Epsilon>b)=>T.Abs(a.value-b.value)<=Epsilon;}

                          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 type class Foo<int T> to represent 42 * (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:

                          structVector<T,int Size>{}staticclassVectorExtension{publicVector<int,4>Multiply<T>(thisVector<int,4>v,Vector<int,4>right){}// Vector64
                          public Vector<int,8>Multiply<T>(thisVector<int,8>v,Vector<int,8>right){}// Vector128
                          public Vector<int,16>Multiply<T>(thisVector<int,16>v,Vector<int,16>right){}// Vector256
                          public Vector<int,32>Multiply<T>(thisVector<int,32>v,Vector<int,32>right){}// Vector512
                          public Vector<int,Size>Multiply<int Size>(thisVector<int,Size>v,Vector<int,Size>right){}// For other sizes allowing a software fallback// ...
                          public Vector<T,Size>Multiply<T,int Size>(thisVector<T,Size>v,Vector<T,Size>right){}// For other types and sizes allowing a software fallback}

                          Design

                          Wording

                          • Const type parameter: a type parameter that carries a const value.
                          • Const type argument: the constant value for a type parameter in the instantiation.

                          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 a mdtGenericParamType and then emit the type of const type parameter with mdtGenericParamType, 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 with mdtTypeSpec using (token & ~mdtGenericParamType) | mdtTypeSpec. When loading generic constraints, if we see a generic constraint has type mdtGenericParamType, 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 a class Foo<int T> will be emitted to class 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:

                          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 reuse the existing generic metadata and rules, and add a Type token to the GenericParamRec schema. To determine whether a type parameter is a const type parameter or not, simply check the Type token to see if it's valid by using RidFromToken.

                          To summarize:

                          Added a column to GenericParameterRec to save a mdToken which represents the type of a const generic parameter.
                          Changed the reserved DWORD to mdToken to save the type of a const generic parameter.
                          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 GetGenericParamProps, both mdToken and the reserved DWORD are exactly DWORD.

                          BTW: actually we have another way without upgrading the existing metadata: we can downgrade the metadata version from the current v2.0 to v1.1, where in v1.1 metadata the GenericParamRec table has a Kind field which is exactly what we need for const generics.

                          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:

                          ELEMENT_TYPE_CTARG <element type of const value> <const value>
                          

                          Note that the size of the const value is determined by its element type.
                          For example, an int 42 will be encoded as:

                           ELEMENT_TYPE_CTARG ELEMENT_TYPE_I4 42
                          | 1 byte | 1 byte | 4 bytes |
                          

                          While a double 3.1415926 will be encoded as:

                           ELEMENT_TYPE_CTARG ELEMENT_TYPE_R8 3.1415926
                          | 1 byte | 1 byte | 8 bytes |
                          

                          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 keyword literal 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, and type '(' 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 field m_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.

                          classConstValueTypeDesc : TypeDesc {
                          TypeHandle m_type;
                          uint64_t m_value;
                          };

                          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 support int128 or other types as primitive types in the future. Should we use size_t here instead? This can make sure we are always able to save a pointer here and in case the size of size_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:

                          enum {
                          CONST_VALUE_INLINE = 1,
                          CONST_VALUE_INDIRECT = 1 << 1,
                          };
                          classConstValueTypeDesc : TypeDesc {
                          TypeHandle m_type;
                          size_t m_value;
                          DWORD m_flag;
                          };
                          if ((m_flag & CONST_VALUE_INDIRECT) == CONST_VALUE_INDIRECT)
                          {
                          // get size and layout info from m_type// load the pointer from m_value// deference the pointer to get the value
                          }
                          else
                          {
                          // get size and layout info from m_type// load the value from m_value directly
                          }

                          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:

                          classConstValueTypeDesc : TypeDesc {
                          TypeHandle m_type;
                          mdToken m_value;
                          };

                          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 new ConstValueTypeDesc 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 the impResolveToken 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,

                          .class public auto ansi beforefieldinit Test`2<T, literal !T N>
                          {
                          .method public hidebysig newslot virtual instance void M<U, literal !!U V> () cil managed { }
                          }
                          

                          Here we can leverage the type field in the GenericParamRec 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 with ValueArray<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.

                          call instance void Foo`1<float32 (42.42)>::.ctor(); // instantiate the Foo`1<float32 (42.42)>
                          call instance void Foo`1<int32 (42)>::.ctor(); // instantiate the Foo`1<int32 (42)>
                          call instance void Foo`1<int32 (42)>::A<int32 (42)>(); // calling the Foo`1<int32 (42)>::A<int32 (42)>()
                          call instance void Foo`1<int32 (42)>::A<float32 (42.42)>(); // calling the Foo`1<int32 (42)>::A<float32 (42.42)>()
                          .class public auto ansi beforefieldinit Foo`1<literal int32 N>
                          {
                          .method public hidebysig specialname rtspecialname instance void .ctor () cil managed { ... }
                          .method public hidebysig newslot virtual
                          instance void A<literal int32 X>() cil managed { ... }
                          .method public hidebysig newslot virtual
                          instance void A<literal float32 X>() cil managed { ... }
                          }
                          .class public auto ansi beforefieldinit Foo`1<literal float32 N>
                          {
                          // ...
                          }
                          

                          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 a struct ValueArray<T, int Length> should not be less than 0.

                          We can add the below APIs to achieve arithmetic constraints.

                          namespaceSystem.Runtime.CompilerServices;publicabstractclassOperator{publicabstractclassUnaryOperator:Operator{// ...}publicabstractclassBinaryOperator:Operator{publicsealedclassAdditionOperator:BinaryOperator{}publicsealedclassSubtractionOperator:BinaryOperator{}publicsealedclassMultiplyOperator:BinaryOperator{}publicsealedclassDivisionOperator:BinaryOperator{}publicsealedclassEqualityOperator:BinaryOperator{}publicsealedclassLessThanOperator:BinaryOperator{}publicsealedclassConjunctionOperator:BinaryOperator{}publicsealedclassDisjunctionOperator:BinaryOperator{}// ...}}publicinterfaceIExpression{publicinterfaceIUnaryExpression<TOperator,TOprand>:IExpressionwhereTOperand:IExpressionwhereTOperator:UnaryOperator{}publicinterfaceIBinaryExpression<TOperator,TLeft,TRight>:IExpressionwhereTLeft:IExpressionwhereTRight:IExpressionwhereTOperator:BinaryOperator{}publicinterfaceIConstantExpression<TValue,TValueValue>:IExpression{}
                          ...}

                          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:

                          classFoo<int T>whereT:>0,<20{}

                          And this got lowered to:

                          .class public auto ansi beforefieldinit Foo<literal int32 (
                          class BinaryExpression`3<
                          class Operator/BinaryOperator/GreaterThanOperator,
                          class IExpression/IConstantExpression`2<int32, !!T>,
                          class IExpression/IConstantExpression`2<int32, int32 (0)>
                          >,
                          class BinaryExpression`3<
                          class Operator/BinaryOperator/LessThanOperator,
                          class IExpression/IConstantExpression`2<int32, !!T>,
                          class IExpression/IConstantExpression`2<int32, int32 (20)>
                          >
                          ) T>
                          extends [System.Runtime]System.Object { }
                          

                          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, so class Foo<T, U, V> where V : == T + U where T : != 0 can be represented in IL as:

                          .class public auto ansi beforefieldinit Foo<literal int32 (constexpr (!T != int32 (0))) T, literal int32 U, literal int32 (constexpr (!V == !T + !U)) V>
                          

                          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 of ValueArray<T, int N> type can be ValueArray<T, N + 1> Push(T elem), and the signature of a Concat method can be ValueArray<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 use ValueArray<T, U> Push<int U>(T elem) where U : (T + 1), and the constraint T + 1 can be expressed using IBinaryExpression<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 on ValueArray<int, 6>, the C# compiler may automatically infer the type of U so developers don't have to explicitly specify the value of U every time.

                          However, consider the below code:

                          classFoo<int T>{privateFoo<T+1>foo;}

                          Are we going to enforce users to introduce a new type parameter on Foo? I.e.,

                          classFoo<int T,int U>where ...{privateFoo<U>foo;}

                          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:

                          classFoo<int T>{typeN=T+1;privateFoo<N>foo;}

                          And also, if we have the support for defining an associated type inside a method, we can do:

                          classFoo{UMethod<intT>(){typeU=T+1;}}

                          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. A ValueArray is basically the InlineArray we already have today plus the ability to specify arbitrary length without the need to define a new InlineArray type.

                          Below is the dummy C# code for ValueArray:

                          structValueArray<T,int N>{privateTelem;// Repeat the field elem for N timespublicintLength{get;}// ldtoken !N; ret;publicrefTthis[intindex]{ ...}}

                          This can be used together with params:

                          Foo(1,2,3,4,5);// a method that only receives 5 int argumentsvoidFoo(paramsValueArray<int,5>args){}

                          Particularly, in C# we can lower all fixed buffer types to ValueArray, and it can perfectly serve all features like params Span<T> and stackalloc 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:

                          namespaceSystem;publicabstractclassType{publicvirtualboolIsConstValue{get;}publicvirtualobjectConstValue{get;}publicstaticTypeMakeConstValueType(objectvalue);}

                          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:

                          classFoo<T,int N>{}varfoo=newFoo<string,42>();foo.GetType();// Foo<string, int (42)>foo.GetType().GetGenericArguments()[0];// Type: System.Stringfoo.GetType().GetGenericArguments()[1].IsConstValue;// truefoo.GetType().GetGenericArguments()[1].HasElementType;// truefoo.GetType().GetGenericArguments()[1].ConstValue;// 42foo.GetType().GetGenericArguments()[1].GetElementType();// System.Int32vart=Type.MakeConstValue(42);vard=typeof(Foo<,>);d.GetGenericArguments()[1].IsConstValue;// falsed.GetGenericArguments()[1].HasElementType;// trued.GetGenericArguments()[1].ConstValue;// InvalidOperationExceptiond.GetGenericArguments()[1].GetElementType();// Type: System.Int32d.MakeGenericType(typeof(string),t);// Foo<string, int (42)>

                          An interesting idea is to allow typeof(value) for the Type.MakeConstValue, for example, typeof(42) to get a Type that contains a value 42.

                          This would either require us to:

                          1. Use the 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 called ldctarg (load const type argument).
                          2. Introduce a new instruction for this.
                          3. No new instruction, and just compile it to 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:

                          1. Matrix<T, int Row, int Col>: fixed-sized matrix to supersede Matrix3x3, Matrix4x4 and etc.
                          2. Vector<T, int N>: fixed-sized vector to supersede Vector2, Vector3 and etc.
                          3. Tensor<T, int Rank>: tensor types for AI/ML purpose
                          4. Span<T, int Dim>: ND-span that can support multiple dimension arrays
                          5. List<T, int N>, Array<T, int N>...: arbitrary list types can have a fixed size now
                          6. ... and more

                          Future 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,

                          // value types
                          .class C`1<literal valuetype Foo T> { }
                          call C`1<valuetype Foo (bytearray ( 01 00 00 00 02 00 00 00 03 00 00 00))>::.ctor()
                          // string
                          .class D`1<literal string T> { }
                          call D`1<string ("hello world")>::.ctor()
                          

                          where Foo is a Vector3<int>, so we are passing a Vector3<int> { X = 1, Y = 2, Z = 3 } here.

                          And as for the implementation, we can use the m_type in ConstValueTypeDesc to save the TypeHandle of the type, and m_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:

                          • For strings, we encode the binary following the rule: ELEMENT_TYPE_CTARG ELEMENT_TYPE_STRING <constant record token>, or ELEMENT_TYPE_CTARG ELEMENT_TYPE_STRING <length> <qcompString>
                          • For arbitrary value types: ELEMENT_TYPE_CTARG ELEMENT_TYPE_VALUETYPE <compressed type token> <constant record token>, or ELEMENT_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:

                          1. Declare a const generic type, eg. class Foo<T, int N>.
                          2. Use a const generic type, eg. new Foo<int, 42>().
                          3. Declare a const generic method, eg. void Foo<int X>.
                          4. Use a const generic method, eg. Foo<42>().
                          5. Generics on const type parameter, eg. class Foo<T, T X>, then you can use it with Foo<int, 42> as well as Foo<float, 42.42424f>.
                          6. Use const type parameter as constant directly. eg. calling Console.WriteLine(X) in the type class Foo<int X>.
                          7. typeof support. eg. typeof(42).
                          8. Casting support in const type argument. eg. new Foo<(short)42>, typeof((short)42)
                          9. A built-in value type ValueArray<T, int X> that can be used as a fix-sized type with type T and length X.
                          10. A niche syntax for declaring a ValueArray type, eg. int[42].
                          11. 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()

                          Code Examples

                          A basic example

                          .assembly _ {}
                          .classpublicautoansibeforefieldinit Foo`2<T, literalint32 N>
                          extends[System.Runtime]System.Object
                          {
                          .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
                          .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
                          ret
                          }
                          .methodpublichidebysignewslotvirtualinstancevoid M<literalint32 V, literalint32 W> () cilmanaged {
                          .maxstack1.localsinit (
                          [0] int32 v
                          )
                          newobjinstancevoidclass Foo`2<string, int32 (42)>::.ctor()
                          callinstancevoidclass Foo`2<string, int32 (42)>::M<!!V, !!V>()
                          newobjinstancevoidclass Foo`2<string, !!V>::.ctor()
                          callinstancevoidclass Foo`2<string, !!V>::M<!N, int32 (42)>()
                          newobjinstancevoidclass Foo`2<string, !N>::.ctor()
                          callinstancevoidclass Foo`2<string, !N>::M<!!V, !!W>()
                          ldtoken !!V
                          callvoid[System.Console]System.Console::WriteLine(int32)
                          ldtoken !!W
                          callvoid[System.Console]System.Console::WriteLine(int32)
                          ldtoken !N
                          callvoid[System.Console]System.Console::WriteLine(int32)
                          ret
                          }
                          }

                          This can be interpreted to the following dummy C# code:

                          classFoo<T,int N>{publicvoidM<int V,int W>(){newFoo<string,42>().M<V,V>();newFoo<string,V>().M<N,42>();newFoo<string,N>().M<V,W>();Console.WriteLine(V);Console.WriteLine(W);Console.WriteLine(N);}}

                          Generic Virtual Method with Const Type Parameters

                          .assembly _ {}
                          .classprivateautoansibeforefieldinit Program
                          extends[System.Runtime]System.Object
                          {
                          .methodprivatehidebysigstaticvoid Main (
                          string[] args
                          ) cilmanaged {
                          .maxstack8.entrypointnewobjinstancevoidclass Bar`2<string, int32( 42 )>::.ctor()
                          callinstancevoidclass Bar`2<string, int32( 42 )>::N<int32( 42 ), int32( 42 )>()
                          ret
                          }
                          .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
                          .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
                          ret
                          }
                          }
                          .classpublicautoansibeforefieldinit Foo`2<T, literalint32 N>
                          extends[System.Runtime]System.Object
                          {
                          .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
                          .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
                          ret
                          }
                          .methodpublichidebysignewslotvirtualinstancevoid M<literalint32 V, literalint32 W> () cilmanaged {
                          .maxstack8ldstr"From Foo::M"callvoid[System.Console]System.Console::WriteLine(string)
                          ldtoken !!V
                          callvoid[System.Console]System.Console::WriteLine(int32)
                          ldtoken !!W
                          callvoid[System.Console]System.Console::WriteLine(int32)
                          ldtoken !N
                          callvoid[System.Console]System.Console::WriteLine(int32)
                          ret
                          }
                          .methodpublichidebysignewslotvirtualinstancevoid N<literalint32 V, literalint32 W> () cilmanaged {
                          .maxstack8newobjinstancevoidclass Foo`2<string, int32( 42 )>::.ctor()
                          callinstancevoidclass Foo`2<string, int32 (42)>::M<!!V, !!V>()
                          newobjinstancevoidclass Foo`2<string, !!V>::.ctor()
                          callinstancevoidclass Foo`2<string, !!V>::M<!N, int32 (42)>()
                          newobjinstancevoidclass Foo`2<string, !N>::.ctor()
                          callinstancevoidclass Foo`2<string, !N>::M<!!V, !!W>()
                          ret
                          }
                          }
                          .classpublicautoansibeforefieldinit Bar`2<T, literalint32 N>
                          extendsclass Foo`2<!T, int32 (128)>
                          {
                          .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
                          .maxstack8ldarg.0callinstancevoidclass Foo`2<!T, int32 (128)>::.ctor()
                          ret
                          }
                          .methodpublichidebysigvirtualinstancevoid M<literalint32 V, literalint32 W> () cilmanaged {
                          .maxstack8.localsinit (
                          [0] string v
                          )
                          ldstr"From Bar::M"callvoid[System.Console]System.Console::WriteLine(string)
                          ldtoken !!V
                          callvoid[System.Console]System.Console::WriteLine(int32)
                          ldtoken !!W
                          callvoid[System.Console]System.Console::WriteLine(int32)
                          ldtoken !N
                          callvoid[System.Console]System.Console::WriteLine(int32)
                          ret
                          }
                          .methodpublichidebysigvirtualinstancevoid N<literalint32 V, literalint32 W> () cilmanaged {
                          .maxstack8ldarg.0callinstancevoidclass Foo`2<!T, int32 (128)>::M<!!V, !!W>()
                          ldarg.0callvirtinstancevoidclass Foo`2<!T, !N>::M<!!V, !!W>()
                          ret
                          }
                          }

                          This will yield the below execution result:

                          From Foo::M
                          42
                          42
                          128
                          From Bar::M
                          42
                          42
                          42
                          

                          Generic Virtual Method with Generic on Const Type Parameters

                          .assembly _ { }
                          .classprivateautoansibeforefieldinit Program
                          extends[System.Runtime]System.Object
                          {
                          .methodprivatehidebysigstaticvoid Main (
                          string[] args
                          ) cilmanaged {
                          .maxstack8.entrypointnewobjinstancevoidclass Bar`2<float32, int32( 42 )>::.ctor()
                          callinstancevoidclass Bar`2<float32, int32( 42 )>::N<float32( 42.42 ), int32( 42 )>()
                          ret
                          }
                          .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
                          .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
                          ret
                          }
                          }
                          .classpublicautoansibeforefieldinit Foo`2<T, literalint32 N>
                          extends[System.Runtime]System.Object
                          {
                          .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
                          .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
                          ret
                          }
                          .methodpublichidebysignewslotvirtualinstancevoid M<literal !T V, literalint32 W> () cilmanaged {
                          .maxstack8ldstr"From Foo::M"callvoid[System.Console]System.Console::WriteLine(string)
                          ldtoken !!V
                          box !T
                          callvoid[System.Console]System.Console::WriteLine(object)
                          ldtoken !!W
                          callvoid[System.Console]System.Console::WriteLine(int32)
                          ldtoken !N
                          callvoid[System.Console]System.Console::WriteLine(int32)
                          ret
                          }
                          .methodpublichidebysignewslotvirtualinstancevoid N<literal !T V, literalint32 W> () cilmanaged {
                          .maxstack8newobjinstancevoidclass Foo`2<int32, int32( 42 )>::.ctor()
                          callinstancevoidclass Foo`2<int32, int32 (42)>::M<!!V, !!V>()
                          newobjinstancevoidclass Foo`2<int32, !!V>::.ctor()
                          callinstancevoidclass Foo`2<int32, !!V>::M<!N, int32 (42)>()
                          newobjinstancevoidclass Foo`2<int32, !N>::.ctor()
                          callinstancevoidclass Foo`2<int32, !N>::M<!!V, !!W>()
                          ret
                          }
                          }
                          .classpublicautoansibeforefieldinit Bar`2<T, literalint32 N>
                          extendsclass Foo`2<!T, int32 (128)>
                          {
                          .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
                          .maxstack8ldarg.0callinstancevoidclass Foo`2<!T, int32 (128)>::.ctor()
                          ret
                          }
                          .methodpublichidebysigvirtualinstancevoid M<literal !T V, literalint32 W> () cilmanaged {
                          .maxstack8.localsinit (
                          [0] string v
                          )
                          ldstr"From Bar::M"callvoid[System.Console]System.Console::WriteLine(string)
                          ldtoken !!V
                          box !T
                          callvoid[System.Console]System.Console::WriteLine(object)
                          ldtoken !!W
                          callvoid[System.Console]System.Console::WriteLine(int32)
                          ldtoken !N
                          callvoid[System.Console]System.Console::WriteLine(int32)
                          ret
                          }
                          .methodpublichidebysigvirtualinstancevoid N<literal !T V, literalint32 W> () cilmanaged {
                          .maxstack8ldarg.0callinstancevoidclass Foo`2<!T, int32 (128)>::M<!!V, !!W>()
                          ldarg.0callvirtinstancevoidclass Foo`2<!T, !N>::M<!!V, !!W>()
                          ret
                          }
                          }

                          This will yield the below execution result:

                          From Foo::M
                          42.42
                          42
                          128
                          From Bar::M
                          42.42
                          42
                          42
                          

                          Minimal ValueArray Type Implementation

                          .class public sequential ansi sealed beforefieldinit System.ValueArray`2<T, literal int32 Length>
                          extends [System.Runtime]System.ValueType
                          {
                          .field private !T elem
                          .method public hidebysig specialname instance !T& get_Item (int32 index) cil managed {
                          .custom instance void [System.Runtime]System.Diagnostics.CodeAnalysis.UnscopedRefAttribute::.ctor() = (01 00 00 00)
                          .maxstack 8
                          ldarg.1
                          ldc.i4.0
                          blt.s OutOfRange
                          ldarg.1
                          ldarg.0
                          call instance int32 valuetype System.ValueArray`2<!T, !Length>::get_Length()
                          blt.s GetItem
                          OutOfRange:
                          call void valuetype System.ValueArray`2<!T, !Length>::ThrowIndexOutOfRange()
                          GetItem:
                          ldarg.0
                          ldflda !0 valuetype System.ValueArray`2<!T, !Length>::elem
                          ldarg.1
                          call !!0& [System.Runtime]System.Runtime.CompilerServices.Unsafe::Add<!T>(!!0&, int32)
                          ret
                          }
                          .method public hidebysig specialname instance int32 get_Length () cil managed {
                          .maxstack 8
                          ldtoken !Length
                          ret
                          }
                          .method private hidebysig static void ThrowIndexOutOfRange () cil managed {
                          .maxstack 8
                          newobj instance void [System.Runtime]System.IndexOutOfRangeException::.ctor()
                          throw
                          }
                          .property instance !T& Item(int32 index)
                          {
                          .get instance !0& System.ValueArray`2::get_Item(int32)
                          }
                          .property instance int32 Length()
                          {
                          .get instance int32 System.ValueArray`2::get_Length()
                          }
                          }
                          

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Type

                            No type

                            Projects

                            No projects

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                              Skip to content

                              Feature Proposal: Const Generics #89730

                              Description

                              @hez2010

                              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 of T with N 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 implement params {ReadOnly}Span<T>.

                              For [2], we can use the const type parameter to define a Span<T, int Dim>, so we can use Span 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 signature Matrix<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

                              structEpsilonFloating<T,T Epsilon>whereT:INumber<T>{publicstaticbooloperator==(EpsilonFloating<T,Epsilon>a,EpsilonFloating<T,Epsilon>b)=>T.Abs(a.value-b.value)<=Epsilon;}

                              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 type class Foo<int T> to represent 42 * (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:

                              structVector<T,int Size>{}staticclassVectorExtension{publicVector<int,4>Multiply<T>(thisVector<int,4>v,Vector<int,4>right){}// Vector64
                              public Vector<int,8>Multiply<T>(thisVector<int,8>v,Vector<int,8>right){}// Vector128
                              public Vector<int,16>Multiply<T>(thisVector<int,16>v,Vector<int,16>right){}// Vector256
                              public Vector<int,32>Multiply<T>(thisVector<int,32>v,Vector<int,32>right){}// Vector512
                              public Vector<int,Size>Multiply<int Size>(thisVector<int,Size>v,Vector<int,Size>right){}// For other sizes allowing a software fallback// ...
                              public Vector<T,Size>Multiply<T,int Size>(thisVector<T,Size>v,Vector<T,Size>right){}// For other types and sizes allowing a software fallback}

                              Design

                              Wording

                              • Const type parameter: a type parameter that carries a const value.
                              • Const type argument: the constant value for a type parameter in the instantiation.

                              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 a mdtGenericParamType and then emit the type of const type parameter with mdtGenericParamType, 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 with mdtTypeSpec using (token & ~mdtGenericParamType) | mdtTypeSpec. When loading generic constraints, if we see a generic constraint has type mdtGenericParamType, 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 a class Foo<int T> will be emitted to class 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:

                              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 reuse the existing generic metadata and rules, and add a Type token to the GenericParamRec schema. To determine whether a type parameter is a const type parameter or not, simply check the Type token to see if it's valid by using RidFromToken.

                              To summarize:

                              Added a column to GenericParameterRec to save a mdToken which represents the type of a const generic parameter.
                              Changed the reserved DWORD to mdToken to save the type of a const generic parameter.
                              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 GetGenericParamProps, both mdToken and the reserved DWORD are exactly DWORD.

                              BTW: actually we have another way without upgrading the existing metadata: we can downgrade the metadata version from the current v2.0 to v1.1, where in v1.1 metadata the GenericParamRec table has a Kind field which is exactly what we need for const generics.

                              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:

                              ELEMENT_TYPE_CTARG <element type of const value> <const value>
                              

                              Note that the size of the const value is determined by its element type.
                              For example, an int 42 will be encoded as:

                               ELEMENT_TYPE_CTARG ELEMENT_TYPE_I4 42
                              | 1 byte | 1 byte | 4 bytes |
                              

                              While a double 3.1415926 will be encoded as:

                               ELEMENT_TYPE_CTARG ELEMENT_TYPE_R8 3.1415926
                              | 1 byte | 1 byte | 8 bytes |
                              

                              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 keyword literal 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, and type '(' 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 field m_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.

                              classConstValueTypeDesc : TypeDesc {
                              TypeHandle m_type;
                              uint64_t m_value;
                              };

                              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 support int128 or other types as primitive types in the future. Should we use size_t here instead? This can make sure we are always able to save a pointer here and in case the size of size_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:

                              enum {
                              CONST_VALUE_INLINE = 1,
                              CONST_VALUE_INDIRECT = 1 << 1,
                              };
                              classConstValueTypeDesc : TypeDesc {
                              TypeHandle m_type;
                              size_t m_value;
                              DWORD m_flag;
                              };
                              if ((m_flag & CONST_VALUE_INDIRECT) == CONST_VALUE_INDIRECT)
                              {
                              // get size and layout info from m_type// load the pointer from m_value// deference the pointer to get the value
                              }
                              else
                              {
                              // get size and layout info from m_type// load the value from m_value directly
                              }

                              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:

                              classConstValueTypeDesc : TypeDesc {
                              TypeHandle m_type;
                              mdToken m_value;
                              };

                              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 new ConstValueTypeDesc 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 the impResolveToken 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,

                              .class public auto ansi beforefieldinit Test`2<T, literal !T N>
                              {
                              .method public hidebysig newslot virtual instance void M<U, literal !!U V> () cil managed { }
                              }
                              

                              Here we can leverage the type field in the GenericParamRec 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 with ValueArray<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.

                              call instance void Foo`1<float32 (42.42)>::.ctor(); // instantiate the Foo`1<float32 (42.42)>
                              call instance void Foo`1<int32 (42)>::.ctor(); // instantiate the Foo`1<int32 (42)>
                              call instance void Foo`1<int32 (42)>::A<int32 (42)>(); // calling the Foo`1<int32 (42)>::A<int32 (42)>()
                              call instance void Foo`1<int32 (42)>::A<float32 (42.42)>(); // calling the Foo`1<int32 (42)>::A<float32 (42.42)>()
                              .class public auto ansi beforefieldinit Foo`1<literal int32 N>
                              {
                              .method public hidebysig specialname rtspecialname instance void .ctor () cil managed { ... }
                              .method public hidebysig newslot virtual
                              instance void A<literal int32 X>() cil managed { ... }
                              .method public hidebysig newslot virtual
                              instance void A<literal float32 X>() cil managed { ... }
                              }
                              .class public auto ansi beforefieldinit Foo`1<literal float32 N>
                              {
                              // ...
                              }
                              

                              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 a struct ValueArray<T, int Length> should not be less than 0.

                              We can add the below APIs to achieve arithmetic constraints.

                              namespaceSystem.Runtime.CompilerServices;publicabstractclassOperator{publicabstractclassUnaryOperator:Operator{// ...}publicabstractclassBinaryOperator:Operator{publicsealedclassAdditionOperator:BinaryOperator{}publicsealedclassSubtractionOperator:BinaryOperator{}publicsealedclassMultiplyOperator:BinaryOperator{}publicsealedclassDivisionOperator:BinaryOperator{}publicsealedclassEqualityOperator:BinaryOperator{}publicsealedclassLessThanOperator:BinaryOperator{}publicsealedclassConjunctionOperator:BinaryOperator{}publicsealedclassDisjunctionOperator:BinaryOperator{}// ...}}publicinterfaceIExpression{publicinterfaceIUnaryExpression<TOperator,TOprand>:IExpressionwhereTOperand:IExpressionwhereTOperator:UnaryOperator{}publicinterfaceIBinaryExpression<TOperator,TLeft,TRight>:IExpressionwhereTLeft:IExpressionwhereTRight:IExpressionwhereTOperator:BinaryOperator{}publicinterfaceIConstantExpression<TValue,TValueValue>:IExpression{}
                              ...}

                              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:

                              classFoo<int T>whereT:>0,<20{}

                              And this got lowered to:

                              .class public auto ansi beforefieldinit Foo<literal int32 (
                              class BinaryExpression`3<
                              class Operator/BinaryOperator/GreaterThanOperator,
                              class IExpression/IConstantExpression`2<int32, !!T>,
                              class IExpression/IConstantExpression`2<int32, int32 (0)>
                              >,
                              class BinaryExpression`3<
                              class Operator/BinaryOperator/LessThanOperator,
                              class IExpression/IConstantExpression`2<int32, !!T>,
                              class IExpression/IConstantExpression`2<int32, int32 (20)>
                              >
                              ) T>
                              extends [System.Runtime]System.Object { }
                              

                              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, so class Foo<T, U, V> where V : == T + U where T : != 0 can be represented in IL as:

                              .class public auto ansi beforefieldinit Foo<literal int32 (constexpr (!T != int32 (0))) T, literal int32 U, literal int32 (constexpr (!V == !T + !U)) V>
                              

                              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 of ValueArray<T, int N> type can be ValueArray<T, N + 1> Push(T elem), and the signature of a Concat method can be ValueArray<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 use ValueArray<T, U> Push<int U>(T elem) where U : (T + 1), and the constraint T + 1 can be expressed using IBinaryExpression<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 on ValueArray<int, 6>, the C# compiler may automatically infer the type of U so developers don't have to explicitly specify the value of U every time.

                              However, consider the below code:

                              classFoo<int T>{privateFoo<T+1>foo;}

                              Are we going to enforce users to introduce a new type parameter on Foo? I.e.,

                              classFoo<int T,int U>where ...{privateFoo<U>foo;}

                              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:

                              classFoo<int T>{typeN=T+1;privateFoo<N>foo;}

                              And also, if we have the support for defining an associated type inside a method, we can do:

                              classFoo{UMethod<intT>(){typeU=T+1;}}

                              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. A ValueArray is basically the InlineArray we already have today plus the ability to specify arbitrary length without the need to define a new InlineArray type.

                              Below is the dummy C# code for ValueArray:

                              structValueArray<T,int N>{privateTelem;// Repeat the field elem for N timespublicintLength{get;}// ldtoken !N; ret;publicrefTthis[intindex]{ ...}}

                              This can be used together with params:

                              Foo(1,2,3,4,5);// a method that only receives 5 int argumentsvoidFoo(paramsValueArray<int,5>args){}

                              Particularly, in C# we can lower all fixed buffer types to ValueArray, and it can perfectly serve all features like params Span<T> and stackalloc 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:

                              namespaceSystem;publicabstractclassType{publicvirtualboolIsConstValue{get;}publicvirtualobjectConstValue{get;}publicstaticTypeMakeConstValueType(objectvalue);}

                              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:

                              classFoo<T,int N>{}varfoo=newFoo<string,42>();foo.GetType();// Foo<string, int (42)>foo.GetType().GetGenericArguments()[0];// Type: System.Stringfoo.GetType().GetGenericArguments()[1].IsConstValue;// truefoo.GetType().GetGenericArguments()[1].HasElementType;// truefoo.GetType().GetGenericArguments()[1].ConstValue;// 42foo.GetType().GetGenericArguments()[1].GetElementType();// System.Int32vart=Type.MakeConstValue(42);vard=typeof(Foo<,>);d.GetGenericArguments()[1].IsConstValue;// falsed.GetGenericArguments()[1].HasElementType;// trued.GetGenericArguments()[1].ConstValue;// InvalidOperationExceptiond.GetGenericArguments()[1].GetElementType();// Type: System.Int32d.MakeGenericType(typeof(string),t);// Foo<string, int (42)>

                              An interesting idea is to allow typeof(value) for the Type.MakeConstValue, for example, typeof(42) to get a Type that contains a value 42.

                              This would either require us to:

                              1. Use the 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 called ldctarg (load const type argument).
                              2. Introduce a new instruction for this.
                              3. No new instruction, and just compile it to 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:

                              1. Matrix<T, int Row, int Col>: fixed-sized matrix to supersede Matrix3x3, Matrix4x4 and etc.
                              2. Vector<T, int N>: fixed-sized vector to supersede Vector2, Vector3 and etc.
                              3. Tensor<T, int Rank>: tensor types for AI/ML purpose
                              4. Span<T, int Dim>: ND-span that can support multiple dimension arrays
                              5. List<T, int N>, Array<T, int N>...: arbitrary list types can have a fixed size now
                              6. ... and more

                              Future 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,

                              // value types
                              .class C`1<literal valuetype Foo T> { }
                              call C`1<valuetype Foo (bytearray ( 01 00 00 00 02 00 00 00 03 00 00 00))>::.ctor()
                              // string
                              .class D`1<literal string T> { }
                              call D`1<string ("hello world")>::.ctor()
                              

                              where Foo is a Vector3<int>, so we are passing a Vector3<int> { X = 1, Y = 2, Z = 3 } here.

                              And as for the implementation, we can use the m_type in ConstValueTypeDesc to save the TypeHandle of the type, and m_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:

                              • For strings, we encode the binary following the rule: ELEMENT_TYPE_CTARG ELEMENT_TYPE_STRING <constant record token>, or ELEMENT_TYPE_CTARG ELEMENT_TYPE_STRING <length> <qcompString>
                              • For arbitrary value types: ELEMENT_TYPE_CTARG ELEMENT_TYPE_VALUETYPE <compressed type token> <constant record token>, or ELEMENT_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:

                              1. Declare a const generic type, eg. class Foo<T, int N>.
                              2. Use a const generic type, eg. new Foo<int, 42>().
                              3. Declare a const generic method, eg. void Foo<int X>.
                              4. Use a const generic method, eg. Foo<42>().
                              5. Generics on const type parameter, eg. class Foo<T, T X>, then you can use it with Foo<int, 42> as well as Foo<float, 42.42424f>.
                              6. Use const type parameter as constant directly. eg. calling Console.WriteLine(X) in the type class Foo<int X>.
                              7. typeof support. eg. typeof(42).
                              8. Casting support in const type argument. eg. new Foo<(short)42>, typeof((short)42)
                              9. A built-in value type ValueArray<T, int X> that can be used as a fix-sized type with type T and length X.
                              10. A niche syntax for declaring a ValueArray type, eg. int[42].
                              11. 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()

                              Code Examples

                              A basic example

                              .assembly _ {}
                              .classpublicautoansibeforefieldinit Foo`2<T, literalint32 N>
                              extends[System.Runtime]System.Object
                              {
                              .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
                              .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
                              ret
                              }
                              .methodpublichidebysignewslotvirtualinstancevoid M<literalint32 V, literalint32 W> () cilmanaged {
                              .maxstack1.localsinit (
                              [0] int32 v
                              )
                              newobjinstancevoidclass Foo`2<string, int32 (42)>::.ctor()
                              callinstancevoidclass Foo`2<string, int32 (42)>::M<!!V, !!V>()
                              newobjinstancevoidclass Foo`2<string, !!V>::.ctor()
                              callinstancevoidclass Foo`2<string, !!V>::M<!N, int32 (42)>()
                              newobjinstancevoidclass Foo`2<string, !N>::.ctor()
                              callinstancevoidclass Foo`2<string, !N>::M<!!V, !!W>()
                              ldtoken !!V
                              callvoid[System.Console]System.Console::WriteLine(int32)
                              ldtoken !!W
                              callvoid[System.Console]System.Console::WriteLine(int32)
                              ldtoken !N
                              callvoid[System.Console]System.Console::WriteLine(int32)
                              ret
                              }
                              }

                              This can be interpreted to the following dummy C# code:

                              classFoo<T,int N>{publicvoidM<int V,int W>(){newFoo<string,42>().M<V,V>();newFoo<string,V>().M<N,42>();newFoo<string,N>().M<V,W>();Console.WriteLine(V);Console.WriteLine(W);Console.WriteLine(N);}}

                              Generic Virtual Method with Const Type Parameters

                              .assembly _ {}
                              .classprivateautoansibeforefieldinit Program
                              extends[System.Runtime]System.Object
                              {
                              .methodprivatehidebysigstaticvoid Main (
                              string[] args
                              ) cilmanaged {
                              .maxstack8.entrypointnewobjinstancevoidclass Bar`2<string, int32( 42 )>::.ctor()
                              callinstancevoidclass Bar`2<string, int32( 42 )>::N<int32( 42 ), int32( 42 )>()
                              ret
                              }
                              .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
                              .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
                              ret
                              }
                              }
                              .classpublicautoansibeforefieldinit Foo`2<T, literalint32 N>
                              extends[System.Runtime]System.Object
                              {
                              .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
                              .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
                              ret
                              }
                              .methodpublichidebysignewslotvirtualinstancevoid M<literalint32 V, literalint32 W> () cilmanaged {
                              .maxstack8ldstr"From Foo::M"callvoid[System.Console]System.Console::WriteLine(string)
                              ldtoken !!V
                              callvoid[System.Console]System.Console::WriteLine(int32)
                              ldtoken !!W
                              callvoid[System.Console]System.Console::WriteLine(int32)
                              ldtoken !N
                              callvoid[System.Console]System.Console::WriteLine(int32)
                              ret
                              }
                              .methodpublichidebysignewslotvirtualinstancevoid N<literalint32 V, literalint32 W> () cilmanaged {
                              .maxstack8newobjinstancevoidclass Foo`2<string, int32( 42 )>::.ctor()
                              callinstancevoidclass Foo`2<string, int32 (42)>::M<!!V, !!V>()
                              newobjinstancevoidclass Foo`2<string, !!V>::.ctor()
                              callinstancevoidclass Foo`2<string, !!V>::M<!N, int32 (42)>()
                              newobjinstancevoidclass Foo`2<string, !N>::.ctor()
                              callinstancevoidclass Foo`2<string, !N>::M<!!V, !!W>()
                              ret
                              }
                              }
                              .classpublicautoansibeforefieldinit Bar`2<T, literalint32 N>
                              extendsclass Foo`2<!T, int32 (128)>
                              {
                              .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
                              .maxstack8ldarg.0callinstancevoidclass Foo`2<!T, int32 (128)>::.ctor()
                              ret
                              }
                              .methodpublichidebysigvirtualinstancevoid M<literalint32 V, literalint32 W> () cilmanaged {
                              .maxstack8.localsinit (
                              [0] string v
                              )
                              ldstr"From Bar::M"callvoid[System.Console]System.Console::WriteLine(string)
                              ldtoken !!V
                              callvoid[System.Console]System.Console::WriteLine(int32)
                              ldtoken !!W
                              callvoid[System.Console]System.Console::WriteLine(int32)
                              ldtoken !N
                              callvoid[System.Console]System.Console::WriteLine(int32)
                              ret
                              }
                              .methodpublichidebysigvirtualinstancevoid N<literalint32 V, literalint32 W> () cilmanaged {
                              .maxstack8ldarg.0callinstancevoidclass Foo`2<!T, int32 (128)>::M<!!V, !!W>()
                              ldarg.0callvirtinstancevoidclass Foo`2<!T, !N>::M<!!V, !!W>()
                              ret
                              }
                              }

                              This will yield the below execution result:

                              From Foo::M
                              42
                              42
                              128
                              From Bar::M
                              42
                              42
                              42
                              

                              Generic Virtual Method with Generic on Const Type Parameters

                              .assembly _ { }
                              .classprivateautoansibeforefieldinit Program
                              extends[System.Runtime]System.Object
                              {
                              .methodprivatehidebysigstaticvoid Main (
                              string[] args
                              ) cilmanaged {
                              .maxstack8.entrypointnewobjinstancevoidclass Bar`2<float32, int32( 42 )>::.ctor()
                              callinstancevoidclass Bar`2<float32, int32( 42 )>::N<float32( 42.42 ), int32( 42 )>()
                              ret
                              }
                              .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
                              .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
                              ret
                              }
                              }
                              .classpublicautoansibeforefieldinit Foo`2<T, literalint32 N>
                              extends[System.Runtime]System.Object
                              {
                              .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
                              .maxstack8ldarg.0callinstancevoid[System.Runtime]System.Object::.ctor()
                              ret
                              }
                              .methodpublichidebysignewslotvirtualinstancevoid M<literal !T V, literalint32 W> () cilmanaged {
                              .maxstack8ldstr"From Foo::M"callvoid[System.Console]System.Console::WriteLine(string)
                              ldtoken !!V
                              box !T
                              callvoid[System.Console]System.Console::WriteLine(object)
                              ldtoken !!W
                              callvoid[System.Console]System.Console::WriteLine(int32)
                              ldtoken !N
                              callvoid[System.Console]System.Console::WriteLine(int32)
                              ret
                              }
                              .methodpublichidebysignewslotvirtualinstancevoid N<literal !T V, literalint32 W> () cilmanaged {
                              .maxstack8newobjinstancevoidclass Foo`2<int32, int32( 42 )>::.ctor()
                              callinstancevoidclass Foo`2<int32, int32 (42)>::M<!!V, !!V>()
                              newobjinstancevoidclass Foo`2<int32, !!V>::.ctor()
                              callinstancevoidclass Foo`2<int32, !!V>::M<!N, int32 (42)>()
                              newobjinstancevoidclass Foo`2<int32, !N>::.ctor()
                              callinstancevoidclass Foo`2<int32, !N>::M<!!V, !!W>()
                              ret
                              }
                              }
                              .classpublicautoansibeforefieldinit Bar`2<T, literalint32 N>
                              extendsclass Foo`2<!T, int32 (128)>
                              {
                              .methodpublichidebysigspecialnamertspecialnameinstancevoid.ctor () cilmanaged {
                              .maxstack8ldarg.0callinstancevoidclass Foo`2<!T, int32 (128)>::.ctor()
                              ret
                              }
                              .methodpublichidebysigvirtualinstancevoid M<literal !T V, literalint32 W> () cilmanaged {
                              .maxstack8.localsinit (
                              [0] string v
                              )
                              ldstr"From Bar::M"callvoid[System.Console]System.Console::WriteLine(string)
                              ldtoken !!V
                              box !T
                              callvoid[System.Console]System.Console::WriteLine(object)
                              ldtoken !!W
                              callvoid[System.Console]System.Console::WriteLine(int32)
                              ldtoken !N
                              callvoid[System.Console]System.Console::WriteLine(int32)
                              ret
                              }
                              .methodpublichidebysigvirtualinstancevoid N<literal !T V, literalint32 W> () cilmanaged {
                              .maxstack8ldarg.0callinstancevoidclass Foo`2<!T, int32 (128)>::M<!!V, !!W>()
                              ldarg.0callvirtinstancevoidclass Foo`2<!T, !N>::M<!!V, !!W>()
                              ret
                              }
                              }

                              This will yield the below execution result:

                              From Foo::M
                              42.42
                              42
                              128
                              From Bar::M
                              42.42
                              42
                              42
                              

                              Minimal ValueArray Type Implementation

                              .class public sequential ansi sealed beforefieldinit System.ValueArray`2<T, literal int32 Length>
                              extends [System.Runtime]System.ValueType
                              {
                              .field private !T elem
                              .method public hidebysig specialname instance !T& get_Item (int32 index) cil managed {
                              .custom instance void [System.Runtime]System.Diagnostics.CodeAnalysis.UnscopedRefAttribute::.ctor() = (01 00 00 00)
                              .maxstack 8
                              ldarg.1
                              ldc.i4.0
                              blt.s OutOfRange
                              ldarg.1
                              ldarg.0
                              call instance int32 valuetype System.ValueArray`2<!T, !Length>::get_Length()
                              blt.s GetItem
                              OutOfRange:
                              call void valuetype System.ValueArray`2<!T, !Length>::ThrowIndexOutOfRange()
                              GetItem:
                              ldarg.0
                              ldflda !0 valuetype System.ValueArray`2<!T, !Length>::elem
                              ldarg.1
                              call !!0& [System.Runtime]System.Runtime.CompilerServices.Unsafe::Add<!T>(!!0&, int32)
                              ret
                              }
                              .method public hidebysig specialname instance int32 get_Length () cil managed {
                              .maxstack 8
                              ldtoken !Length
                              ret
                              }
                              .method private hidebysig static void ThrowIndexOutOfRange () cil managed {
                              .maxstack 8
                              newobj instance void [System.Runtime]System.IndexOutOfRangeException::.ctor()
                              throw
                              }
                              .property instance !T& Item(int32 index)
                              {
                              .get instance !0& System.ValueArray`2::get_Item(int32)
                              }
                              .property instance int32 Length()
                              {
                              .get instance int32 System.ValueArray`2::get_Length()
                              }
                              }
                              

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Type

                                No type

                                Projects

                                No projects

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions