Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Compound Types and Values

Grammar

type ::= qualname
       | array_type
       | tuple_type
       | record_type
       | sum_type
       | function_type
       | type_application
       | unique_type
       | existential_size

tuple_type ::= "(" ")" | "(" type ("," type)+ [","] ")"

array_type ::= "[" [exp] "]" type

sum_type ::= sum_variant ("|" sum_variant)*
sum_variant ::= constructor [ "(" type ("," type)* [","] ")" ]

record_type ::= "{" "}" | "{" fieldid ":" type ("," fieldid ":" type)* [","] "}"

type_application ::= qualname "<" type_argument ("," type_argument)* ">"
type_argument    ::= "[" [dim] "]" | type
unique_type      ::= "*" type

function_type ::= param_type "->" type
param_type    ::= type | "(" name ":" type ")"

stringlit  ::= '"' stringchar* '"'
stringchar ::= <any source character except "\" or newline or double quotes>

existential_size ::= "?" name+ "." type

Compound types can be constructed based on the primitive types. The Wyn type system is entirely structural, and type abbreviations are merely shorthands. The only exception is abstract types whose definition has been hidden via the module system.

Tuple Types

A tuple value or type is written as a sequence of comma-separated values or types enclosed in parentheses. For example, (0, 1) is a tuple value of type (i32, i32). The elements of a tuple need not have the same type – the value (false, 1, 2.0) is of type (bool, i32, f32). A tuple element can also be another tuple, as in ((1,2),(3,4)), which is of type ((i32, i32), (i32, i32)). A tuple cannot have just one element, but empty tuples are permitted, although they are not very useful. Empty tuples are written () and are of type ().

Array Types

An array value is written as a sequence of zero or more comma-separated values enclosed in square brackets: [1, 2, 3]. An array type is written as [d]t, where t is the element type of the array, and d is an expression of type i64 indicating the number of elements in the array. We can elide d and write just [] (an anonymous size), in which case the size will be inferred.

As an example, an array of three integers could be written as [1, 2, 3], and has type [3]i32. An empty array is written as [], and its type is inferred from its use. When writing Wyn values for testing purposes, empty arrays are written empty([0]t) for an empty array of type [0]t.

Multi-dimensional arrays are supported in Wyn, but they must be regular, meaning that all inner arrays must have the same shape. For example, [[1,2], [3,4], [5,6]] is a valid array of type [3][2]i32, but [[1,2], [3,4,5], [6,7]] is not, because we cannot come up with integers m and n such that [m][n]i32 describes the array. The restriction to regular arrays is rooted in low-level concerns about efficient compilation.

Vector Types

In addition to arrays, Wyn provides fixed-width vector types. They are distinct from arrays: they have a fixed component count and componentwise arithmetic.

Vector types use the naming convention vecNT where:

  • N is the number of components (2, 3, or 4)
  • T is the element type (i32, f32, etc.)

Common vector types:

Component Type2-component3-component4-component
i32 (signed)vec2i32vec3i32vec4i32
f32 (32-bit float)vec2f32vec3f32vec4f32

Vector types are distinct from array types and have different semantics:

  • Vectors are fixed-size, optimized for SIMD operations
  • Arrays are more general containers with runtime length operations

Example usage:

let position: vec3f32 = @[1.0, 2.0, 3.0]
let color: vec4f32 = @[1.0, 0.0, 0.0, 1.0]

Vector Swizzles

A vector’s components are accessed with field syntax (v.x). Wyn also supports swizzles: one to four letters drawn from a single “swizzle set”. The two sets and their component indices are:

Set0123
xyzwxyzw
rgbargba

rgba is an alias set for xyzw (r == x, g == y, b == z, a == w) — the two sets address the same underlying components and produce identical values. Mixing letters from the two sets in one swizzle (.xg, .rbw) is a type error.

A single-letter swizzle produces the scalar component; a 2-, 3-, or 4-letter swizzle produces a new vector of that length. Letters may repeat and may be in any order.

Each referenced component must lie within the source vector’s length — .z (or .b) on a vec2 is a type error.

let v: vec4f32 = @[1.0, 2.0, 3.0, 4.0]
let first: f32       = v.x        -- 1.0
let alpha: f32       = v.a        -- 4.0   (same as v.w)
let rgb:   vec3f32   = v.rgb      -- (1.0, 2.0, 3.0)
let rev:   vec4f32   = v.wzyx     -- (4.0, 3.0, 2.0, 1.0)
let splat: vec3f32   = v.xxx      -- (1.0, 1.0, 1.0)
Swizzle Update via with

Wyn extends the with operator to vec swizzles, so the GLSL idiom dir.yz *= rot(angle) translates directly:

let v1 = v0 with .yz = e          -- replace v0.y, v0.z with e.x, e.y
let v2 = v1 with .yz *= m         -- compound: same as `with .yz = v1.yz * m`
let v3 = v2 with .x = scalar      -- single-component LHS takes a scalar RHS

The compound forms *= += -= /= desugar to target with .swizzle = target.swizzle <op> rhs, with target evaluated once. The result is always a fresh vec — wyn is immutable, the original target is unchanged. Components on the LHS must be distinct (v with .xx = e is rejected); the RHS arity must match the swizzle length (a vec2 for .yz, a scalar for .x); and each component must be in range for the target’s size.

Vector Constructors

Within vector and array literals, a vector followed by ... expands its components in order. The source is evaluated once. Expansion accepts all fixed-width vector types, including multi-component swizzles; it does not accept scalars, arrays, or matrices. The expanded elements must have the same type, and a resulting vector must still have 2–4 components.

let normal = @[nrm..., 1.0]      -- nrm.x, nrm.y, nrm.z, 1.0
let planar = @[nrm.yx..., 1.0]  -- nrm.y, nrm.x, 1.0
let values = [nrm..., 1.0]      -- array of the same four components

Expansion is only allowed as a direct literal element. Inclusive ranges use a..=b; a..b and a..<b remain exclusive.

Vectors are constructed with the @[...] literal syntax:

let v1: vec3f32 = @[1.0, 2.0, 3.0]
let v2: vec4f32 = @[1.0, 0.0, 0.0, 1.0]

The element type is inferred from the arguments or the context.

Vector Arithmetic and Scalar Broadcasting

The binary arithmetic operators +, -, *, and / apply component-wise to vectors. When both operands are vectors they must have the same vector type (same length and element type); the result is that type and each component is combined independently:

let a: vec3f32 = @[1.0, 2.0, 3.0]
let b: vec3f32 = @[4.0, 5.0, 6.0]
let s: vec3f32 = a + b            -- (5.0, 7.0, 9.0)
let p: vec3f32 = a * b            -- (4.0, 10.0, 18.0), component-wise

When one operand is a scalar, it is broadcast against the vector: the scalar is applied to every component, and the result has the vector’s type. The scalar may appear on either side, so both v op scalar and scalar op v are accepted:

let v: vec3f32 = @[1.0, 2.0, 3.0]
let scaled: vec3f32 = v * 2.0         -- (2.0, 4.0, 6.0)
let shifted: vec3f32 = 3.0 - 2.0 * v  -- 3.0 - (2.0, 4.0, 6.0) = (1.0, -1.0, -3.0)

Broadcasting performs no implicit numeric conversion: the scalar’s type must equal the vector’s element type. Mixing a vec3f32 with an i32 scalar is a type error — write the scalar as f32. Likewise, component-wise vector arithmetic between two vectors of different element types or lengths is rejected.

For * specifically, matrix products (matrix×matrix, matrix×vector, vector×matrix, matrix×scalar) take priority over component-wise arithmetic; see Matrix Types.

Matrix Types

Wyn provides built-in matrix types. A matrix value has a fixed row count R, a fixed column count C, and a scalar element type — written matRxC<elem>. Square matrices have a shorthand alias matN<elem>:

ShapeSquare shorthandRectangular form
2×2mat2f32mat2x2f32
3×3mat3f32mat3x3f32
4×4mat4f32mat4x4f32
R×C (R ≠ C)n/amatRxCf32

Both the shorthand and rectangular form name the same type — mat2f32 = mat2x2f32.

Supported dimensions are R, C ∈ {2, 3, 4}. Supported element types are all of the SPIR-V scalar primitives: i8, i16, i32, i64, u8, u16, u32, u64, f16, f32, f64.

Matrix Literals

Matrices are written with the @[[...], [...], ...] literal syntax — outer brackets describe rows, inner brackets describe each row’s components:

let m: mat2x2f32 = @[[1.0, 0.0],
                     [0.0, 1.0]]

let rot: mat2f32 = @[[c, s],
                     [-s, c]]

Each inner array becomes one column of the matrix at SPIR-V emission — the literal above produces the GLSL equivalent mat2(c, s, -s, c) (column-major, matching GPU convention).

Matrices in Arrays

Matrix types are valid array elements, so [][N][M]T arrays and SOAC inputs work over matrix values in the same manner as they do over scalars.

Sum Types

Sum types are anonymous in Wyn, and are written as the constructors separated by vertical bars. Each constructor consists of a #-prefixed name and an optional parenthesised payload — a comma-separated list of payload types (or sub-patterns / sub-expressions, depending on the syntactic position). A constructor with no payload is written bare, with no parentheses.

Because sum types are structural, constructor names are not globally unique — they are tags inside a sum type, not declarations. The same name #left belongs to infinitely many possible sum types (#left(i32) | #right(f32), #left(bool) | #middle | #right, …), so a bare constructor expression like #left(3) is ambiguous in isolation. The type checker resolves it from context — the expected type at the use site, the type of an argument it’s being passed as, or an explicit annotation. When context doesn’t pin a single sum type down, an annotation is required:

let x: #left(i32) | #right(f32) = #left(3)

Note: Implementations are not required to optimize the representation of sum-typed values. A sum-typed value may carry storage for every constructor’s payload; sum types with multiple large-payload constructors can be costly.

Record Types

Records are mappings from field names to values, with the field names known statically. A tuple behaves in all respects like a record with numeric field names starting from zero, and vice versa. It is an error for a record type to name the same field twice. A trailing comma is permitted.

Records are structural: a field name x does not identify a single record type, so a bare projection r.x does not fully determine r’s type by itself. As with sum constructors, the type checker uses context — the inferred type of r from the surrounding expression, or an explicit annotation on r — to pick a single record type. When context doesn’t suffice, an annotation is required:

let r: { x: f32, y: f32 } = make_point() in r.x