Tint uses intrinsic definition to generate intrinsic tables, end-to-end tests and C++ enums.
These definition files have a .def
extension and exist under the language subdirectories of src/tint/lang/
.
For example, the WGSL definition file can be found at src/tint/lang/wgsl/wgsl.def
.
A definition file can contain the following declarations:
Syntax: import "
project-relative-path "
An import is similar to a C++ #include
statement. It imports the content of the project-relative file into this definition file so that declarations can be shared between languages.
Example: import "src/tint/lang/core/access.def"
An named enumerator of symbols can be declared with enum
.
Example:
enum diagnostic_severity { error warning info off }
These enum declarations can be used by templates to generate an equivalent C++ enum, along with a parser and printer helper method, using the helper template src/tint/utils/templates/enums.tmpl.inc
.
See src/tint/lang/wgsl/extension.h.tmpl
as an example.
Enum declarations can also be used as constraints on type template parameters and overload template parameters.
Syntax: type
name [ <
type-or-number-templates >
]
Type declarations form the basic primitives are used to match intrinsic overloads. A type can be referenced by type matchers and by builtin overloads.
The simplest type declaration has the form: type name
A type can be templated with any number of sub-types and numbers, using a <
...>
suffix.
Examples:
Example | Description |
---|---|
type my_type | Declares a type called my_type |
type vec3<T> | Declares a type called vec3 which accepts a templated sub-type |
type vec<N: num, T> | Declares a type called vec which accepts a templated number N and sub-type T |
Types may be annotated with @precedence(N)
to prioritize which type will be picked when multiple types of a matcher match. This is typically used to ensure that abstract numerical types materialize to the concrete type with the lowest conversion rank. Types with higher precedence values will be matched first.
Example:
@precedence(5) type ia // This is preferred over all others @precedence(4) type fa @precedence(3) type i32 @precedence(2) type u32 @precedence(1) type f32 @precedence(0) type f16
Types may also be annotated with @display("
string")
to customize how the type is printed in diagnostic message. The string may contain curly-brace references to the templated types.
Example:
@display("vec{N}<{T}>") type vec<N: num, T>
Would display as vec2<i32>
if N=2
and T=i32
.
Syntax: matcher
name :
type_1 [ |
type_2 [ ... |
type_n ] ]
A type matcher can be used by overloads to match one or more types.
The overload declarations declare all the built-in functions, Value constructors, type conversions and unary and binary operators supported by the target language.
An overload declaration can declare a simple static-type declarations, as well as overload declarations that can match a number of different argument types via the use of template types, template numbers and type matchers.
All overload declarations share the same declaration patterns, but the kind of declaration has a different prefix.
fn
prefixctor
prefixconv
prefixop
prefixFor example: fn isInf(f32) -> bool
declares an overload of the function isInf
that accepts a single parameter of type f32
and returns a bool
.
All overload declarations use the same syntax for a parameter.
A parameter can be named or unnamed.
Unnamed parameters just use the name of constraint (type, type-matcher or template parameter). Named parameters have a name and constraint, separated with a colon. Example fn F(count : i32)
Code | Description |
---|---|
fn F(i32) | function F has a single unnamed parameter, of type or matcher i32 |
fn F(count: i32) | function F has a single parameter named count , of type or matcher i32 |
Note: Parameters may use a type-matcher directly, but for wgsl.def
it is encouraged to use an implicit template parameter. This helps the readability of the diagnostics produced. For example, instead of fn F(scalar)
, prefer: fn F[S: scalar](S)
.
Builtin function overload declarations can support explicit and implicit template parameters.
Each template parameter has the syntax: name [ :
constraint ] Where the optional constrant can be a type, type matcher, or : num
for a number template.
Explicit template parameters:
<
template_parameter_list >
Implicit template parameters:
[
template_parameter_list ]
Examples:
// A function overload that has no explicit or implicit template parameters fn F(i32) -> i32
// A function overload that accepts an explicit template parameter 'T', which matches any type. // The overload takes a single parameter of type 'T' and returns a value of type 'T' fn F<T>(T) -> T
// A function overload that accepts an implicit (inferred) template parameter 'T', which must be of // the type or type-matcher 'scalar'. // The overload takes a single parameter of type 'T' and returns a value of type 'T' fn F[T: scalar](T) -> T
// A function overload that accepts an explicit template parameter 'T', which must be of a 'vec' // type of the inferred element type 'E' and number 'N'. // 'E' is an implicit template type parameter, and must match the type or type-matcher 'scalar' // 'N' is an implicit template number parameter. // The overload takes a single parameter of type 'T' and returns a value of type 'T' fn F< T: vec<N, E> >[E: scalar, N: num](T) -> T
The goal of overload matching is to compare a function call's arguments and any explicitly provided template types in the program source against an overload declaration, to determine if the call satisfies the form and type constraints of the overload. If the call matches an overload, then the overload is added to the list of ‘overload candidates’ used for overload resolution (described below).
Prior to matching an overload, all template types are undefined.
Implicit template types are first defined with the type of the leftmost argument that matches against that template type name. Subsequent arguments that attempt to match against the template type name will either reject the overload or refine the template, in one of 3 ways:
To better understand, let's consider the following hypothetical overload declaration:
matcher scalar: f32 | i32 | u32 | bool fn foo[T: scalar](T, T)
Where:
Symbol | Description |
---|---|
T | is the template type name |
scalar | is a matcher for the types f32 , i32 , u32 or bool |
[T: scalar] | declares the implicit template parameter T , with the constraint that T must match one of f32 , i32 , u32 or bool . |
The process for resolving this overload is as follows:
T
. As the implicit template type T
has not been defined yet, T
is defined as the type of the first argument. There's no verification that the T
type is a scalar at this stage.T
is now defined as the first argument type, the second argument type is compared against the type of T
. Depending on the comparison of the argument type to the template type, either the actions of (a), (b) or (c) from above will occur.This algorithm for matching a single overload is less general than the algorithm described in the WGSL spec, but it makes the same decisions because the overloads defined by WGSL are monotonic in the sense that once a template parameter has been refined, there is never a need to backtrack and un-refine it to match a later argument.
The algorithm for matching template numbers is similar to matching template types, except numbers need to exactly match across all uses - there is no implicit conversion. Template numbers may match integer numbers or enumerators.
If multiple candidate overloads match a given set of arguments, then a final overload resolution pass needs to be performed. The arguments and overload parameter types for each candidate overload are compared, following the algorithm described at: https:www.w3.org/TR/WGSL/#overload-resolution-section
If the candidate list contains a single entry, then that single candidate is picked, and no overload resolution needs to be performed.
If the candidate list is empty, then the call fails to resolve and an error diagnostic is raised.
Intrinsics that can be used in constant evaluation expressions are annotated with @const
or @const(
name )
.
The templates use this attribute to associate the overload with a function in src/tint/lang/core/constant/eval.h
.
Syntax: fn
name [ <
explicit-template-parameters >
] [ [
implicit-template-parameters ]
](
parameters ) ->
return_type
Builtin function overloads support both implicit and explicit template parameters.
Builtin functions may be annotated with @must_use
to prevent the function being used as a call-statement in WGSL.
Code | Description |
---|---|
fn F() | Function called F . No template types or numbers, no parameters, no return value` |
fn F() -> R | Function with R as the return type |
fn F(f32, i32) | Two fixed-type, anonymous parameters |
fn F(U : f32) | Single parameter with name U |
fn F[T](T) | Single parameter of unconstrained implicit template type T (any type) |
fn F[T: scalar](T) | Single parameter of constrained implict template type T (must match scalar ) |
fn F[T: fiu32](T) -> T | Single parameter of constrained implicit template type T (must match fiu32 )Return type is of the parameter type |
fn F[T, N: num](vec<N, T>) | Single parameter of vec type with template number N and element template type T |
fn F[A: access](texture_storage_1d<f32_texel_format, A>) | Single parameter of type texture_storage_1d type with implicit template number A constrained to the enum access , and a texel format that that must match f32_texel_format |
Syntax: ctor
type-name [ [
implicit-template-parameters ]
](
parameters ) ->
return_type
Value constructors construct a value of the given type.
Syntax: conv
type-name [ [
implicit-template-parameters ]
](
parameters ) ->
return_type
Value conversions convert from one type to another.
Unary operator syntax: op
operator [ [
implicit-template-parameters ]
](
parameter ) ->
return_type
Binary operator syntax: op
operator [ [
implicit-template-parameters ]
](
lhs-parameter ,
rhs-parameter ) ->
return_type