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:
Nis the number of components (2, 3, or 4)Tis the element type (i32, f32, etc.)
Common vector types:
| Component Type | 2-component | 3-component | 4-component |
|---|---|---|---|
i32 (signed) | vec2i32 | vec3i32 | vec4i32 |
f32 (32-bit float) | vec2f32 | vec3f32 | vec4f32 |
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:
| Set | 0 | 1 | 2 | 3 |
|---|---|---|---|---|
| xyzw | x | y | z | w |
| rgba | r | g | b | a |
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>:
| Shape | Square shorthand | Rectangular form |
|---|---|---|
| 2×2 | mat2f32 | mat2x2f32 |
| 3×3 | mat3f32 | mat3x3f32 |
| 4×4 | mat4f32 | mat4x4f32 |
| R×C (R ≠ C) | n/a | matRxCf32 |
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