[Draft Proposal]: Const Generics #7508
Replies: 4 comments 13 replies
-
Because of dupe they close my idea, so I paste here: I just saw this: #7508, it is a great proposal, but before reading it, I thought of a simple syntactic sugar, no need to modify the existing type system. The example code is roughly as follows.
As you can see, it almost exclusively uses the existing C#, except for This idea was inspired by [EDIT] Maybe we can let the user decide whether to expose the constant, like this: |
Beta Was this translation helpful? Give feedback.
-
I almost completely agree with what you said. It is because of this proposal that I believe However, I still believe in presenting my idea because I think combining this proposal with my idea might work better. So, what is For example, consider Here is another example (discussion in runtime: BitCount of IBinaryInteger):
In this example, I attempt to implement a collection where the capacity is determined by the number of binary bits in |
Beta Was this translation helpful? Give feedback.
-
Possibly fixed-size interfaces should implement variable-size interfaces, which is reasonable and avoids creating two parallel sets of interfaces, just like this:
|
Beta Was this translation helpful? Give feedback.
-
I hope it will be in .NET 9. |
Beta Was this translation helpful? Give feedback.
-
Const Generics
Summary
Const Generics is a feature that allows constant value to be used in a generic parameter or argument.
Motivation
This feature comes from the need of a type-safe and guaranteed constants.
Const Generics enables the use cases where developers need to pass a const value through a type parameter.
Typical use cases are templating for things like shuffle (its basically a guaranteed constant)
as well as for numerics, tensors, matrices and etc.
For example, fixed buffer and vector types [1], jagged arrays/spans [2], constrained shape of arrays [3], numeric types and multiplier types especially in graphics programming [4], expression abstractions [5], and value specialization [6].
For [1], we can have a type
struct ValueArray<T, int N>
to define a type of array ofT
withN
elements.This can also be useful in variadic parameters. For example, a
params ValueArray<int, 5>
can represent a variadic parameter that receives only 5 int arguments.Beside, we can also leverage the
ValueArray<T, int N>
type to implementparams {ReadOnly}Span<T>
.For [2], we can use the const type parameter to define a
Span<T, int Dim>
, so we can useSpan
for multi-dimension arrays as well.For [3], we can constrain the shape of an array. This is especially useful when you are dealing with matrix or vector computations.
For example, you now can define a matrix using
class Matrix<T, int Row, int Col>
. When you implement the multiplication algorithm, you can simply put a signatureMatrix<T, Row, NewCol> Multiply<NewCol>(Matrix<T, Col, NewCol> rMatrix)
. This can make sure users pass the correct shape of the matrix while doing multiplication operations.For [4], we can embed the coefficient into a multiplier type. This is especially useful in graphics programming. For example, when you are working with things about illumination, you will definitely want some multiplier types with coefficients (which are basically floating point numbers) that are guaranteed to be constants. While building AI/ML models, we are also often use such constant coefficients.
Also, we will be able to create a floating point type with user specified epsilon, such as
and then use it like
global using MyFloatWithEpsilon = EpsilonFloating<float, 1e-6f>
.For [5], we can have several types that can embed constant values to abstract an expression, then we can validate the expression at compile time, hence no runtime exception will happen. For instance, we can have below interface types:
abstract class BinOp
sealed class AddOp : BinOp
sealed class MulOp : BinOp
interface IExpr
interface IConstExpr<T, T Value> : IExpr
interface IBinExpr<TOp, TLeftExpr, TRightExpr> where TOp : BinOp where TLeftExpr : IExpr where TRightExpr IExpr
Then we can use
IBinExpr<MulOp, IBinExpr<AddOp, IConstExpr<int, 42>, IConstExpr<int, T>>, IConstExpr<int, 2>>
in a typeclass Foo<int T>
to represent42 * (T + 2)
, then we can use it like a type and let the compiler to verify whether the given const type argument satisfies the expression or not.For [6], we will be able to provide a generic
Vector
type and specialize SIMD-width types with extensions:Detailed design
Wording
Const type parameter
A const type parameter is, a type parameter that will carry a constant value of a specified type.
To declare a const type parameter, we use the syntax:
For example, a
int T
represent a type parameterT
that has constant value of typeint
, while aT Value
represent a type parameterValue
that has constant value of typeT
.The type of a const type parameter can be:
bool
,byte
,sbyte
,char
,ushort
,short
,uint
,int
,ulong
,long
,float
,double
or the type declared by a generic type parameter (see the section "Generics on const type parameter" below).Const type argument
A const type argument is, a constant value that will be passed to a const type parameter.
We use the following syntax to define a const type argument:
Note that we use
literal
instead ofconst
in the syntax tree because we are already usingliteral
keyword in the metadata to represent a constant value.For example, a
42
represents anint
const type argument with value42
, a42.42f
represents afloat
const type argument with value42.42
, atrue
represents abool
const type argument with valuetrue
, and a(short)42
represent ashort
const type argument with value42
.ValueArray
We added a type
struct ValueArray<T, int N>
in the BCL to represent a fix-sized array type that can be allocated on the stack.So we can support a niche syntax that can have fixed size on an array to be emitted as a
System.ValueArray
:which is the same with
System.ValueArray<int, 42> array = new()
.This can be used together with
params
as well:Particularly, in C# we can lower all fixed buffer types to
ValueArray
, and it can perfectly serve all features likeparams Span<T>
andstackalloc T[]
.Generics on const type parameter
This is useful because we don't allow overloading over generic constraints, so if the type of a const type parameter cannot be determined in advance, we need generics on it to leave the decision to end users.
Const value type
Due to the above changes, we will support const value as a type. But we don't want to support the general literal types that can be used without a type argument context, so we only support use const value as a type in
typeof
context, in order to support reflection.This is saying that we can use
typeof(value)
to create a runtime type handle that holds the constant value, so we can use it later inMakeGenericType
andMakeGenericMethod
purpose.Generic constraint over const type parameters
We can add the generic constraint support for const type parameters, so the value of a const type parameter can be constrained to avoid incorrect input.
This will allow us to write expressions with type parameters and const values as a type directly.
For instance,
Then we will be able to check the generic constraint with given instantiation at compile time, for example,
new Foo<1, 2, 3>
will be accpected, whilenew Foo<1, 2, 4>
andnew Foo<0, 1, 1>
be will rejected.Type const argument usesite
We allow a type const argument to be used as an R-value directly.
Therefore, we can use
Console.WriteLine(X)
in a methodvoid Foo<int X>()
.Lookup
We look up the members of a const type parameter by treating it as the type of the const type parameter.
For example, if we have a const type parameter
int T
, thenT
will have a typeint
in any usesite, hence the members ofT
are exactly the members ofint
.So
T.ToString()
will be resolved as an instance call toint.ToString
.Overload resolution
We treat different const type arguments as distinct types in overload resolution. Assuming we have a type
class Foo<T V>
, then we will take the constant value into account in overload resolution:Lowering
Generic constraint on const type parameters
To lower the expression of a generic constraint, we need some types defined in
System.Runtime.CompilerServices
that can be used to build an expression in a type so that we can use it for generic constraints.Note that they are special types that are not being implemented by any types. It's an implementation detail and we special case those type for verifying generic constraint purpose only.
For the case
where V : == T + U where T : != 0
, it will be lowered to:Then we can compute the expression at compile-time to make sure users pass the right value.
Emitting
Const type parameter
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 introduced amdtGenericParamType
and then emit the type of const type parameter withmdtGenericParamType
, and make sure it will always be the first entry in generic constraints.Const type argument
We added an element type
ELEMENT_TYPE_CTARG
standing forconst type argument
, while emitting a const type argument in the signature, we simply emitELEMENT_TYPE_CTARG Type Value
in the signature.For instance, a
42
can be emitted asELEMENT_TYPE_CTARG ELEMENT_TYPE_I4 42
.Const type parameter use sites
When reference a const type parameter, we use
ldtoken
to load the const type parameter. The JIT compiler in the runtime will import the value as a constant directly, which has the same result with usingldc.*
instructions that produce a constant value.typeof(value)
in context other than attributesWe add a
Type.MakeConstValueType(object? value)
to make a type of a constant value.So,
typeof(42)
will be emitted asType.MakeConstValueType(42)
, whiletypeof(T)
whereT
is a const type parameter, will be emitted asType.MakeConstValueType(T)
.typeof(value)
in attributes contextWe need to emit the type name while emitting
typeof
on an attribute. Here we use the metadata type representationType (hex value), Assembly
to represent a const value type.For instance, the type name of
42
intypeof(42)
that is in an attribute context[Attr(typeof(42))]
will be emitted asSystem.Int32(0x2a), System.Runtime, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3ad
, while a42.42
will be emitted asSystem.Double(0x4045364d7f0ed3d8), System.Runtime, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3ad
.It's important to have the const value to be emitted as hex string representation because this can make sure we can always get the accurate value from the string representation.
Proposal for the runtime and IL
See dotnet/runtime#89730
Fully Working Prototype
This prototype is based on the old design with a breaking change to the metadata, while the latest (current) design doesn't have any breaking changes to the metadata
I have done the fully working prototype of C# compiler, language server and CoreCLR runtime, and successfully built a SDK for it (Windows only).
If you want to have a try on const generics, you can download the SDK here: https://1drv.ms/u/s!ApWNk8G_rszRgrxP32IMKhW-V8iWug?e=JBn8wU
Be sure to follow the README.txt in the SDK.
Version: 20230912 Build 1
Checksum: a8c9ee29d1accd14797f60bedced312f9524391b
This prototype branch:
I may update the SDK without posting a new comment but change the version and checksum in the above, while the sharing link won't change.
This prototype supports all things in this proposal except generic constraints on const type parameter and const arithmetic.
For example, you can do the following things:
class Foo<T, int N>
.new Foo<int, 42>()
.void Foo<int X>
.Foo<42>()
.class Foo<T, T X>
, then you can use it withFoo<int, 42>
as well asFoo<float, 42.42424f>
.Console.WriteLine(X)
in the typeclass Foo<int X>
.typeof
support. eg.typeof(42)
.new Foo<(short)42>
,typeof((short)42)
ValueArray<T, int X>
that can be used as a fix-sized type with typeT
and lengthX
.ValueArray
type, eg.int[42]
.type.IsGenericParameter && type.HasElementType
.type.GetElementType()
.type.IsConstValue
.type.GetElementType()
.type.ConstValue
.Type.MakeConstValueType()
Drawbacks
Alternatives
Unresolved questions
Generic constraint on const type parameters emitting
Should we use
modreq
or something else to emit the generic constraint on const type parameters? Because those expression types are actually not being implemented by any types, but we still use them in the generic constraints which let them look like interface constraints but behave as expression evaluation, which is not intuitive.For example, we can add something like
constexpr
constraints in the metadata and allow it to be emitted directly, soclass Foo<T, U, V> where V : == T + U where T : != 0
can be represented in IL as:Const arithmetic
We want to support const arithmetic as well, so we can define a type that has an arithmetic relation to an existing const type parameter:
It's useful to have arithmetic support for const generics.
For example, the signature of a
Push
method ofValueArray<T, int N>
type can beValueArray<T, N + 1> Push(T elem)
, and the signature of aConcat
method can beValueArray<T, N + M> Concat<int M>(ValueArray<T, M> elems)
.This would require embedding the arithmetic operations in the type and implementing dependent/associated types, which is a non-trivial work.
While an alternative is to use constraints to achieve it. So for the example of
Push
method, we can useValueArray<T, U> Push<int U>(T elem) where U : (T + 1)
, and the constraintT + 1
can be expressed usingIBinaryExpression<Add, IConstantExpression<int, T>, IConstantExpression<int, 1>>
. Then we can validate the constraint at runtime.Although we need to specify the value such as
Push<7>(42)
while calling onValueArray<int, 6>
, the compiler may automatically infer the type ofU
so developers don't have to explicitly specify the value ofU
every time.However, consider the below code:
Are we going to enforce users to introduce a new type parameter on
Foo
? I.e.,If yes, whenever we want to introduce a new "computed" const type parameter on a method of the class, we will need to add it to the class signature, which will lead to breaking changes. This seems quite unfortunate, and unacceptable.
Therefore, we cannot just rely on generic constraints to serve const arithmetic.
However, if we have runtime support for dependent/associated types in the future, this can be simply resolved by using:
And also, if we have the support for defining an associated type inside a method, we can do:
We still need some discussion to design around here.
Maybe we can just skip const arithmetic for the first version, and implement const arithmetic in the future once we have proper runtime support?
More flexible const type argument emitting
The current const type argument emitting strategy limits the const type argument to be primitive types only, while actually we may want to support arbitrary value types too (considering
System.Half
,System.Int128
and etc.)Thus we may want to change the emitting strategy to be
ELEMENT_TYPE_CTARG <type-token> <constant-token>
instead.Beta Was this translation helpful? Give feedback.
All reactions