Declaring Functions and Values
Grammar
def_bind ::= "def" def_name [generics] [def_signature] "=" exp
def_name ::= name | "(" symbol ")"
def_signature ::= "(" param ("," param)* ")" type -- function form
| ":" type -- typed constant
param ::= name ":" type
generics ::= "<" generic_param ("," generic_param)* ">"
generic_param ::= "[" name "]" | UpperName
entry_bind ::= "entry" name "(" [entry_param ("," entry_param)*] ")"
entry_return "=" exp
entry_param ::= ["#[" attr "]"] name ":" type
entry_return ::= type | "(" entry_output ("," entry_output)* ")"
entry_output ::= ["#[" attr "]"] type
UpperName is an identifier whose first character is uppercase.
Description
A def declaration binds a value or function to a name at module
scope. The body is an arbitrary expression that may use only names
already in scope at the point of binding; forward references are
not permitted, and functions may not be recursive.
A def may take parameters and a return type, in which case it
defines a function; without parameters it is a constant.
def gravity: f32 = 9.81
def step(dt: f32, v: f32) f32 = v + gravity * dt
Type Inference
Hindley-Milner-style type inference fills in argument and return types when context allows. Explicit annotations are required only when inference cannot determine a unique type. Sizes participate in the type system; see Size Types for the rules.
def step(dt, v) = v + 9.81 * dt
Here dt, v, and the return type are all inferred as f32:
9.81 defaults to f32, the multiplication constrains dt, and
the addition constrains v.
Polymorphic Functions
A function may be polymorphic over types and sizes through the
<...> generics list. Size parameters are written [n]; type
parameters are uppercase identifiers:
def reverse<[n], A>(xs: [n]A) [n]A = ???
Generics need not cover the type of every parameter. Any argument whose type isn’t tied to a declared generic is given a fresh type variable by inference:
def pair<A>(x: A, y) = (x, y)
A fresh type variable is invented for y.
Type Parameter Resolution
Type and size parameters are inferred from arguments at call sites
— they are not passed explicitly. If the same type parameter A
appears in multiple parameter positions, all arguments bound to it
must agree in both shape and element type. For example:
def pair<A>(x: A, y: A) = (x, y)
pair([1], [2, 3]) is ill-typed because the two arguments bind
A to arrays of different sizes.
Aliasing Restrictions
To simplify the handling of in-place updates (see In-place Updates), the value returned by a function may not alias any global variables.
Shader Entry Points
A shader entry point is declared with the entry keyword. It may
be annotated with a #[vertex], #[fragment], or #[compute]
attribute identifying the pipeline stage:
#[vertex]
entry vertex_main() #[builtin(position)] vec4f32 =
@[0.0, 0.0, 0.0, 1.0]
#[fragment]
entry fragment_main() #[location(0)] vec4f32 =
@[1.0, 0.0, 0.0, 1.0]
#[compute]
entry double(arr: []f32) []f32 = map(|x| x * 2.0, arr)
Entry-point declarations differ from def in three ways:
- Parameters require the
name: typeform; pattern destructuring is not allowed. - Parameters and return positions accept attributes
(
#[builtin(...)],#[location(n)],#[storage],#[uniform],#[texture],#[sampler],#[storage_image]) that wire them to GPU resources, built-ins, and inter-stage I/O. - An empty parameter list still requires the parentheses:
entry foo() ret = ....
The name of an entry point must not contain an apostrophe ('),
even though apostrophes are otherwise permitted in identifiers.