Introduction
Wyn is a functional, array-centric language for GPU programming. Function values are erased before execution, so programs run on GPU targets that have no first-class function pointers. The array surface unifies the several incompatible array kinds a GPU exposes — function-local arrays, vectors, storage buffers — into a single paradigm.
Design Goals
- GPU-targeted: language constraints (regular arrays, no recursion, no first-class function values) keep programs compatible with massively parallel hardware.
- Array-oriented: first-class support for multi-dimensional arrays and array operations.
- Type-safe: static type checking with type inference.
- Functional: immutable data structures and expression-based computation.
- Modular: encapsulation and reuse through first-class modules — signatures hide implementation details, and parameterized modules generalize components across types.
Key Features
- Second-order array operators:
map,reduce,scan,filter, and related combinators are the primary way to express bulk operations on arrays. - Uniqueness types:
*Tmarks a value as consuming-only, lettingarr with [i] = xmutate in place when the source is unique and copy when it is not. - Size-typed arrays: array lengths participate in the type system;
def f(xs: [n]i32) [n]i32declares a function whose output has the same length as its input. - Attribute-driven shader interface: attributes (
#[location],#[builtin],#[storage],#[uniform],#[texture],#[sampler], …) wire entry-point parameters and returns to GPU resources, built-ins, and inter-stage I/O.
Program Structure
Where other shader languages iterate, Wyn transforms.
Arrays are the primary data structure, aligning with the regular,
data-parallel organization of modern GPU hardware. Operators such as
map, reduce, scan, and filter consume and produce arrays,
while function values specialize each operator invocation by
capturing values from the surrounding scope.
Most non-trivial programs are compositions of these operators. Rather than writing execution pipelines explicitly, programmers describe transformations of arrays; the compiler constructs GPU pipelines that preserve the program’s semantics while exploiting the available parallelism. The programmer describes transformations; the compiler derives the pipeline.
A Wyn program is a sequence of declarations. The smallest interesting program is a single compute entry point:
#[compute]
entry double(arr: []f32) []f32 = map(|x| x * 2.0, arr)
entry marks a function as visible to the host runtime; the
#[compute] attribute (or #[vertex] / #[fragment]) selects the
GPU pipeline stage. Anything that’s not an entry point is an
ordinary function or value, defined with def:
def gravity: f32 = 9.81
def step(dt: f32, v: f32) f32 = v + gravity * dt
Functions are first-class within the program — they can be passed to
higher-order operators like map and reduce — but function values
are erased before execution and do not exist at runtime.
Arrays are the primary aggregate. Sizes participate in the type
system, so a function that takes an [n]f32 returns an array whose
length is bound to that same n:
def normalize(xs: [n]f32) [n]f32 =
let total = reduce(|a, b| a + b, 0.0, xs) in
map(|x| x / total, xs)
A typical graphics program splits across two entry points — a vertex stage that emits per-vertex position and varyings, and a fragment stage that consumes the matched varyings and writes a color:
#[vertex]
entry vs(#[builtin(vertex_index)] i: i32)
(#[builtin(position)] vec4f32, #[location(0)] vec3f32) =
let pos = if i == 0 then @[-0.5, -0.5, 0.0, 1.0]
else if i == 1 then @[ 0.5, -0.5, 0.0, 1.0]
else @[ 0.0, 0.5, 0.0, 1.0] in
let color: vec3f32 = @[1.0, 0.0, 0.0] in
(pos, color)
#[fragment]
entry fs(#[location(0)] color: vec3f32) #[location(0)] vec4f32 =
@[color.x, color.y, color.z, 1.0]
A single source entry may compile to multiple module entries. SOACs whose lowering requires more than one kernel — a parallel reduce that runs a per-workgroup partial fold followed by a tree-reduction across the partials, for example — split into separate entries that the host dispatches in sequence. The compiled module’s pipeline descriptor names every entry it produced; hosts iterate the descriptor rather than the source.
A Wyn program can span multiple files. Each file is implicitly a
module: declarations at file scope are members of that module. Files
reference each other with import, which loads a sibling file and
binds its declarations under the imported name; open brings a
module’s members into the current scope unqualified. Modules can also
be defined inline with module m = { ... }. Module types describe a
module’s interface; parameterized modules take other modules as
arguments.
A small standard library is automatically loaded. Top-level
declarations from its files — including the second-order array
operators (map, reduce, scan, filter, …) — are available
unqualified throughout the program. The standard library also defines
per-type modules (i32, f32, bool, …) that group operations on
their type; programs can open such a module to bring its members
into scope or address them by qualified name (f32.sqrt, i32.abs).