Generics
Generics in Slang enable parameterization of structures, interfaces, type aliases, functions and member functions, subscript operators, and constructors. A generic parameter can be a type, a Boolean value, an integer value, or an enumeration (TODO) value. In addition, Slang supports generic extension, covered in type extensions.
When the generic parameters are bound, a generic type or function is specialized. A specialized generic is a concrete type or function, which can be used like any other concrete type or function. Generic parameters are bound by providing arguments (explicit binding), by inference (implicit binding), or by a combination of both. Value-typed arguments to the generic parameters must be link-time constants (TODO). Conceptually, partial parameter binding can be done by defining a generic type alias for a generic type or function, but this does not specialize the generic.
📝 Remark 1: Slang does not support explicit specialization of generics where a Slang program would provide a definition for a specific combination of arguments. However, generic extension can be used to extend generic structures to similar effect.
📝 Remark 2: Slang does not currently support using interface-typed variables that require dynamic dispatch as generic parameters. See GitHub issue #10263.
Syntax
Generic structure declaration syntax:
'struct'[identifier] [generic-params-decl]
[':'bases-clause]
('where'where-clause)*
'{'member-list'}'
Generic interface declaration syntax:
'interface'identifier[generic-params-decl]
[':'bases-clause]
('where'where-clause)*
'{'member-list'}'
Generic type alias syntax:
'typealias'identifier[generic-params-decl]
'='simple-type-spec
('where'where-clause)*';'
Generic function and member function declaration (traditional syntax):
simple-type-specidentifier[generic-params-decl]
'('param-list')'
('where'where-clause)*
(';'|'{'body-stmt*'}')
Generic function and member function declaration (modern syntax):
'func'identifier[generic-params-decl]
'('param-list')'
['throws'simple-type-spec]
['->'simple-type-spec]
('where'where-clause)*
(';'|'{'body-stmt*'}')
Generic constructor declaration:
'__init'[generic-params-decl]'('param-list')'
('where'where-clause)*
(';'|'{'body-stmt*'}')
Generic subscript operator declaration:
'__subscript'[generic-params-decl]'('param-list')'
'->'simple-type-spec
('where'where-clause)*
(';'|'{'body-stmt*'}')
Generic parameters declaration:
generic-params-decl=
'<'[generic-param-decl(','generic-param-decl)* ]'>'
generic-param-decl=
generic-value-param-decl|
generic-value-param-pack-decl|
generic-value-param-trad-decl|
generic-value-param-pack-trad-decl|
generic-type-param-decl|
generic-type-param-pack-decl
generic-value-param-decl=
'let'identifier
[':'simple-type-spec]
['='init-expr]
generic-value-param-pack-decl=
'let''each'identifier
[':'simple-type-spec]
generic-value-param-trad-decl=
simple-type-spec
identifier
['='init-expr]
generic-value-param-pack-trad-decl=
'each'simple-type-spec
identifier
generic-type-param-decl=
['typename']identifier
[':'simple-type-spec]
['='simple-type-spec]
generic-type-param-pack-decl=
'each'identifier
[':'simple-type-spec]
Generic parameter constraint clause:
where-clause=
['optional']
where-clause-body
where-clause-body=
generic-non-empty-pack-constraint-decl|
generic-pack-count-constraint-decl|
(simple-type-spec
(generic-type-constraint-decl
|generic-type-constraint-eq-decl
|generic-type-constraint-coercion-decl))
generic-non-empty-pack-constraint-decl=
'nonempty''('identifier')'
generic-pack-count-constraint-decl=
'countof''('identifier')''=='expr
generic-type-constraint-decl=
':'
simple-type-spec
(','simple-type-spec)*
generic-type-constraint-eq-decl=
'=='
simple-type-spec
generic-type-constraint-coercion-decl=
'('
simple-type-spec
')'
['implicit']
📝 Remark: Generic enumeration declarations are also supported by the parser. However, they are not currently useful. See GitHub issue #10078.
Parameters
generic-params-decldeclares a list of generic parameters.generic-param-decldeclares a generic value parameter, type parameter, or type parameter pack.generic-value-param-decldeclares a generic value parameter.generic-value-param-pack-decldeclares a generic value parameter pack usinglet eachsyntax.generic-value-param-trad-decldeclares a generic value parameter using traditional syntax.generic-value-param-pack-trad-decldeclares a generic value parameter pack using traditional syntax.generic-type-param-decldeclares a generic type parameter.generic-type-param-pack-decldeclares a generic type parameter pack.where-clauseis a generic parameter constraint clause.generic-non-empty-pack-constraint-decldeclares a non-emptiness requirement on a generic type pack or value pack parameter.generic-pack-count-constraint-decldeclares an exact cardinality requirement on a generic type pack or value pack parameter.generic-type-constraint-decldeclares a generic type conformance constraint, requiring the left-hand-side type expression to conform to one or more constraining type expressions.generic-type-constraint-eq-decldeclares a generic type equality constraint, requiring the left-hand-side type expression to be equal to the right-hand-side type expression.generic-type-constraint-coercion-decldeclares a generic type coercion constraint, requiring the type expression in parentheses to be coercible to the type expression outside the parentheses. This constraint may be used only in generic extensions. See GitHub issue #10087.identifier: see the respective syntax for a description.bases-clause: see the respective syntax for a description.member-list: see the respective syntax for a description.simple-type-spec: see the respective syntax for a description.param-list: see the respective syntax for a description.body-stmt: see the respective syntax for a description.
Description
A generic parameter declaration list generic-params-decl adds any number of parameters to structures,
interfaces, type aliases, functions, subscript operators, and constructors. These parameterized constructs are
called generic structures, generic interfaces, generic type aliases, generic functions, generic
subscript operators, and generic constructors.
A generic parameter declaration is one of:
- Generic value parameter declaration
generic-value-param-declorgeneric-value-param-trad-decl, which adds a value parameter with an optional default value. The value type must be a Boolean, an integer, or an enumeration (TODO). - Generic value parameter pack declaration
generic-value-param-pack-declorgeneric-value-param-pack-trad-decl, which adds a variadic value parameter pack. A value parameter pack is a variable-length list of generic value parameters of a single declared type. - Generic type parameter declaration
generic-type-param-decl, which adds a type parameter with an optional type constraint and an optional default type. The keywordtypenameis optional. - Generic type parameter pack declaration
generic-type-param-pack-decl, which adds a type parameter pack. A type parameter pack is a variable-length list of types.
Types may be constrained by:
- Specifying an inline type constraint in
generic-type-param-declusing the formTypeParam : ConstrainingType. This adds a single conformance requirement such thatTypeParammust conform toConstrainingType. - Specifying one or more
whereclauses (where-clause). Awhereclause adds a single requirement using one of the following forms:- Conformance constraint declaration
generic-type-constraint-decladds a requirement that the left-hand-side type expression must conform to the right-hand-side type expressions. - Equivalence constraint declaration
generic-type-constraint-eq-decladds a requirement that the left-hand-side type expression must be equal to the right-hand-side type expression. - Coercion constraint declaration
generic-type-constraint-coercion-decladds a requirement that the parenthesized type expression must be coercible to the left-hand-side type expression.
- Conformance constraint declaration
Conformance and equivalence constraints may be declared as optional. When optional, the expression ParamType is
ConstrainingType returns true when ParamType conforms to or equals ConstrainingType. When the expression is used in
an if statement using the form if (ParamType is ConstrainingType) { ... }, then any variable of type ParamType may
be used as type ConstrainingType in the “then” branch.
The coercion requirement is usable only in generic extensions.
A constraint on a type parameter pack applies to every type in the pack.
Type and value parameter packs may also be constrained with where nonempty(P), which requires the pack P
to be non-empty at specialization time.
Type and value parameter packs may also be constrained with where countof(P) == IntExpr, which requires the
pack P to have exactly the compile-time integer count denoted by IntExpr at specialization time.
Value parameters that are not packs cannot be constrained.
📝 Remark 1: In Slang, a conformance requirement
TypeParam : ConstrainingTypemeans thatTypeParammust haveConstrainingTypeas a base (either directly or transitively), andConstrainingTypemust be an interface.
📝 Remark 2: Slang also has the
__genericmodifier, which can be used to declare generic parameters as an alternative togeneric-params-decl. Usinggeneric-params-declis recommended.
📝 Remark 3: Optional conformance constraints are currently an experimental feature. See GitHub issues #10078 and #10185.
Type Parameter Packs
A type parameter pack is declared using the each TypeIdentifier syntax. When a generic construct is
specialized, a (possibly empty) sequence of type arguments is bound to the parameter pack.
A type parameter pack is expanded using the expand/each construct with the following syntax:
Expand-expression:
expand-expr='expand'expr
Each-expression:
each-expr='each'expr
An expression in an each-expression evaluates to a type parameter pack, tuple, or variable whose type is a type parameter pack.
There must be at least one each-expression within an expand-expression. An each-expression must always be enclosed within an expand-expression except in a generic type declaration. If there are multiple each-expressions within an expand-expression, the referenced parameter packs must all have an equal number of parameters.
An expand-expression evaluates to a comma-separated value sequence whose length is the number of type parameters of the embedded each-expressions. Each element of the sequence is the expand expression with every embedded each-expression replaced by the Nth element of its corresponding pack.
That is, expand-expr is substituted with the following sequence:
expr, // every each-expr is substituted by pack element 0
expr, // every each-expr is substituted by pack element 1
expr, // every each-expr is substituted by pack element 2
...
expr // every each-expr is substituted by pack element N-1
In function parameter lists, expand/each constructs must come after all other parameters. There may be multiple declared expand/each parameters, in which case the type parameter packs must have equal lengths.
Pack queries
Slang provides the following pack-query operations for type packs, value packs, and tuple-like pack sources:
pack-query-expr=
pack-first-expr|pack-last-expr|pack-trim-first-expr|pack-trim-last-expr
pack-first-expr=
'__first''('expr')'
pack-last-expr=
'__last''('expr')'
pack-trim-first-expr=
'__trimFirst''('expr')'
pack-trim-last-expr=
'__trimLast''('expr')'
__first(P)__last(P)__trimFirst(P)__trimLast(P)
__first(P) and __last(P) are partial operations and require P to be known non-empty. For generic pack parameters, non-emptiness can be expressed with:
void foo<each T>() where nonempty(T)
{
// `__first(T)` is well-formed here.
}
__trimFirst(P) and __trimLast(P) are total operations and yield an empty pack when applied to an empty pack.
📝 Remark: The operand of
nonempty(...)must be a direct reference to a generic type pack or value pack parameter declared in the current generic declaration.optional nonempty(...)is parsed but rejected as invalid.
Pack count constraints
A pack count constraint has the form:
void foo<let N : int, each T>() where countof(T) == N
{
}
The operand of the left-side countof(...) must be a direct reference to a generic type pack or value pack
parameter declared in the current generic declaration. The right side must be a compile-time integer expression.
Pack count constraints are oriented. The constrained pack is always the pack named by the left-side
countof(...) expression. The following forms are not pack count constraints:
void reversed<let N : int, each T>() where N == countof(T) {}
void notEqual<let N : int, each T>() where countof(T) != N {}
void greaterEqual<let N : int, each T>() where countof(T) >= N {}
A right-side expression may itself use countof(...) as a compile-time integer expression. This allows an
inner generic to require that one of its packs has the same count as an outer pack:
struct Outer<let each D : int>
{
void inner<each TIndex>(TIndex indices)
where TIndex == int
where countof(TIndex) == countof(D)
{}
}
However, countof(D) == countof(TIndex) is not equivalent in this context, because the left-side pack D
is not declared by inner.
The exact oriented proof matters when checking generic bodies. A declared constraint countof(T) == countof(U)
can satisfy a callee requirement about T, but it is not used backwards to satisfy a requirement about U.
Type Checking
Type checking of parameterized types is performed based on their type constraints before specialization. In general, an operation on a parameterized generic type or a generic-typed variable is legal if it is legal for all possible concrete types conforming to the declared constraints.
The rules are as follows:
- If a parameterized type
Thas a type equality constraintT == U, typeTis considered to be typeUfor all intents and purposes. - If a parameterized type
Thas a type conformance constraintT : U, typeTis considered to conform toU. That is,Timplements all requirements ofU. - If a parameterized type
Thas a type constraintU(T), typeTmay be converted to typeU.
Type constraints may be declared for generic type parameters and type expressions that include generic type
parameters. For example, where T : IFace where T.AssocT == int requires that T.AssocT is int. Note that
IFace must declare associated type AssocT. (See interfaces for associated type
declarations.)
Pack count constraints are checked as exact shape requirements. During specialization, countof(P) must equal
the expected compile-time integer value. While checking an unspecialized generic body, a call that requires a
pack count constraint is valid only when an in-scope declared constraint provides the exact same oriented proof
after substitution.
No assumptions are made about generic value parameters other than their declared type.
📝 Remark: In contrast to C++ templates, type checking of Slang generics is performed before specialization. In C++, type checking is performed after template specialization and instantiation.
Parameter Binding
Arguments to generic parameters can be bound explicitly, implicitly, or as a combination of both. Binding is done at the call site.
In explicit binding, the arguments to the generic parameters are listed in angle brackets after the generic type or function identifier. Explicit binding cannot be used in constructs that do not use a named identifier at call sites (e.g., operator overloading).
In implicit binding, the arguments to the generic parameters are inferred. Inference is performed by matching the generic parameters against the call site argument expressions. It is an error if an argument to a generic parameter cannot be inferred from the call site.
If inference is ambiguous for a generic type parameter, the following rules are used to determine the type:
- If all inferred types are fundamental scalar types or
vector types of the same length, the element type with the highest promotion rank is
used. The promotion ranks from the lowest to the highest are:
int8_t,uint8_t,int16_t,uint16_t,int32_t,uint32_t,int64_t,uint64_t,float,double.- A fundamental type is promoted to a 1-dimensional vector if necessary.
- In all other cases, an ambiguous generic type argument is an error.
It is an error when inference yields multiple options for a generic value argument.
Pack count constraints do not infer generic value arguments. For example, where countof(T) == N does not infer
N from the number of arguments supplied for T; N must be bound explicitly or inferred by other ordinary
generic argument rules.
Mixing explicit and implicit parameter binding is allowed. The leftmost generic parameters use the provided generic arguments and the rest are inferred.
📝 Remark: If the generic argument inference is ambiguous and
boolis inferred as a fundamental or element type, the behavior is currently undefined. See GitHub issue #10164 for details.
Examples
Generic structure with type and value parameters
RWStructuredBuffer<float> outputBuffer;
struct TestStruct<T, let size : uint>
{
var arr : T[size];
}
[numthreads(10,1,1)]
void main(uint3 id : SV_DispatchThreadID)
{
TestStruct<float, 10> obj =
{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
outputBuffer[id.x] = obj.arr[id.x];
}
Generic type alias for partial type binding
struct ArrayOfElements<T, let size : uint>
{
typealias ElementType = T;
ElementType elems[size];
}
typealias ArrayOfFiveElements<T> = ArrayOfElements<T, 5>;
RWStructuredBuffer<float> outputBuffer;
[numthreads(5,1,1)]
void main(uint3 id : SV_DispatchThreadID)
{
ArrayOfFiveElements<float> myArray =
{ 1.0, 2.0, 3.0, 4.0, 5.0 };
outputBuffer[id.x] = myArray.elems[id.x];
}
Generic function with type parameter
RWStructuredBuffer<float> outputBuffer;
// Note: IFunc<ReturnType, Param1Type, Param2Type, ...>
func performOp<T>(IFunc<T, T, T> binaryOp, T a, T b) -> T
{
return binaryOp(a, b);
}
func add2<T : IArithmetic>(T a, T b) -> T
{
return a + b;
}
struct Adder : IFunc<int, int, int>
{
int bias;
int operator() (int a, int b)
{
return add2(add2(a, b), bias);
}
}
[numthreads(1,1,1)]
void main(uint3 id : SV_DispatchThreadID)
{
Adder addTwoWithBias = { 5 };
outputBuffer[id.x] = performOp(addTwoWithBias, 234, 456);
}
Generic constructor
RWStructuredBuffer<float> outputBuffer;
struct TestStruct
{
int val;
__init<T : IInteger>(T initval1, T initval2)
{
T tmp = initval1 + initval2;
val = tmp.toInt();
}
}
[numthreads(1,1,1)]
void main(uint3 id : SV_DispatchThreadID)
{
TestStruct obj = { 123, 345 };
outputBuffer[id.x] = obj.val;
}
Generic subscript
struct TestStruct
{
var arr : float[10];
// declare a 1-parameter subscript operator
__subscript<T> (T i) -> float where T : IInteger
{
get { return arr[i.toInt()]; }
set { arr[i.toInt()] = newValue; }
}
}
Type constraint
RWStructuredBuffer<float> outputBuffer;
func addTwo<T>(T a, T b) -> T where T : IArithmetic
{
return a + b;
}
[numthreads(1,1,1)]
void main(uint3 id : SV_DispatchThreadID)
{
outputBuffer[id.x] = addTwo(1, 2);
outputBuffer[id.x] += addTwo(1.1, 2.2);
}
Optional type constraint
RWStructuredBuffer<float> outputBuffer;
func addTwoIfInts<T>(T a, T b) -> T
where T : IArithmetic // T must be IArithmetic
where optional T : IInteger // T may also be IInteger
{
if (T is IInteger)
return a + b; // return sum if T is IInteger
else
return a - a; // return 0 otherwise
}
[numthreads(1,1,1)]
void main(uint3 id : SV_DispatchThreadID)
{
outputBuffer[id.x] = addTwoIfInts(1, 2);
outputBuffer[id.x] += addTwoIfInts(1.1, 2.2);
}
Type equality constraint for associated type
RWStructuredBuffer<float> outputBuffer;
interface IFace
{
associatedtype PropertyType;
property prop : PropertyType;
}
struct IntProperty : IFace
{
typealias PropertyType = int;
PropertyType prop;
}
struct FloatProperty : IFace
{
typealias PropertyType = float;
PropertyType prop;
}
int addTwoInts<T:IFace>(T a, T b) where T.PropertyType == int
{
return a.prop + b.prop;
}
[numthreads(1,1,1)]
void main(uint3 id : SV_DispatchThreadID)
{
IntProperty intObj1 = { 1 };
IntProperty intObj2 = { 2 };
FloatProperty floatObj1 = { 1.0 };
FloatProperty floatObj2 = { 2.0 };
outputBuffer[id.x] = addTwoInts(intObj1, intObj2);
// FloatProperty does not satisfy the equivalence requirement
// "T.PropertyType == int". Hence, the following line will
// not compile.
// outputBuffer[id.x] += addTwoInts(floatObj1, floatObj2);
}
Type coercion constraint
struct Foo<A>
{
}
// adds method to the struct if A is convertible to int
extension<A> Foo<A> where int(A)
{
int extensionMethod(int x)
{
return x + 42;
}
}
Type parameter pack
RWStructuredBuffer<float> outputBuffer;
void sumHelper(inout int acc, int term)
{
acc += term;
}
int sumInts<each T>(expand each T terms)
where T == int // every 'T' type pack member must be int
{
int acc = 0;
expand sumHelper(acc, each terms);
// expands to:
//
// sumHelper(acc, term0),
// sumHelper(acc, term1),
// ...
return acc;
}
[numthreads(1,1,1)]
void main(uint3 id : SV_DispatchThreadID)
{
outputBuffer[id.x] += sumInts(1, 2, 3, 4, 5, 6, 7);
}
Multiple function parameter packs
void dotProductHelper(float a, float b, inout float ret)
{
ret += a * b;
}
float dotProduct<each T>
(expand each T a, expand each T b)
where T == float
{
float r = 0.0f;
expand dotProductHelper(each a, each b, r);
return r;
}
RWStructuredBuffer<double> outputBuffer;
[numthreads(1,1,1)]
void main(uint3 id : SV_DispatchThreadID)
{
float a[3] = { 4, 5, 6 };
float b[3] = { 2, 1, 0 };
outputBuffer[0] =
dotProduct(
a[0], a[1], a[2],
b[0], b[1], b[2]);
}
Generic extension for generic types
struct GenericStruct<T>
{
}
extension<T> GenericStruct<T> where T : IFloat
{
int isInt() { return 0; }
}
extension<T> GenericStruct<T> where T : IInteger
{
int isInt() { return 1; }
}
RWStructuredBuffer<int> outputBuffer;
[numthreads(1,1,1)]
void main(uint3 id : SV_DispatchThreadID)
{
GenericStruct<float> floatObj = { };
GenericStruct<uint> uintObj = { };
outputBuffer[0] = floatObj.isInt();
outputBuffer[1] = uintObj.isInt();
}
📝 Remark: An extension cannot currently be used to override a more generic implementation. See GitHub issue #10146.
Explicit and implicit generic arguments
// Return type Ret is listed first, since it cannot be
// inferred.
func diffSingle<Ret : IInteger, T : IInteger, U : IInteger>
(T a, U b) -> Ret
{
return Ret(a.toInt64() - b.toInt64());
}
RWStructuredBuffer<int> outputBuffer;
[numthreads(1,1,1)]
void main(uint3 id : SV_DispatchThreadID)
{
int8_t a = 3;
int16_t b = 5;
// Return type Ret is bound explicitly. Parameter
// types T and U can be inferred from function
// arguments
outputBuffer[0] = diffSingle<int32_t>(a, b);
}
Implicit parameter binding for type and value
// Note: assumes N >= 1
ElementType sumElements<ElementType : IArithmetic, let N : uint>
(ElementType arr[N])
{
ElementType acc = arr[0];
for (uint i = 1; i < N; ++i)
acc = acc + arr[i];
return acc;
}
RWStructuredBuffer<int> outputBuffer;
[numthreads(1,1,1)]
void main(uint3 id : SV_DispatchThreadID)
{
int elements[7] = { 1,2,3,4,5,6,7 };
// generic parameters ElementType and N are bound implicitly
outputBuffer[0] = sumElements(elements);
}
Implicit parameter binding, ambiguous value argument
uint len<let N : uint>(int[N] arr, int[N] arr2)
{
return N;
}
RWStructuredBuffer<uint> outputBuffer;
[numthreads(1,1,1)]
void main(uint3 id : SV_DispatchThreadID)
{
int arr1[7] = { };
int arr2[6] = { };
// error: generic argument N cannot be inferred
outputBuffer[0] = len(arr1, arr2);
}
Implicit parameter binding, ambiguous type argument
interface IBase
{
}
struct A : IBase
{
}
struct B : IBase
{
}
void testFunc<T>(T x, T y)
{
}
[numthreads(1,1,1)]
void main(uint3 id : SV_DispatchThreadID)
{
A a = { };
B b = { };
// Explicit parameter type binding must be used,
// since inferred type for T is ambiguous and
// non-fundamental.
testFunc<IBase>(a, b);
}