W# 0.1.1

Generics

Explicit type parameters on functions, structs and fn literals, all monomorphised.

Type parameters are written when they have to be named and inferred when they do not. Either way they are monomorphised: a generic function is compiled once per type it is used at, and a generic struct is laid out once per instantiation.

That is not an optimisation choice. It is forced by unboxing. i64, f64 and bool live in registers, so fn(T) T cannot be one function, and a ?T field is two slots or three depending on what T is, so Box[T] cannot be one layout.

// Explicit generic parameters, on functions, on structs, and on `fn`
// literals.
//
// All three are monomorphised: a generic function is compiled once per type it
// is used at, and a generic struct is laid out once per instantiation -- which
// it has to be, because a `?T` field is two slots or three depending on `T`.

const str = @import("std/str");

const Box = struct[T] { value: T };
const Pair = struct[A, B] { first: A, second: B };

fn unwrap[T](b: Box[T]) T { return b.value; }
fn swap[A, B](p: Pair[A, B]) Pair[B, A] {
    return Pair{ .first = p.second, .second = p.first };
}

// A recursive generic function. The call to `count` inside itself records no
// type arguments while its binding group is being inferred, and inference
// fills in the group's own variables once it has generalised.
fn count[T](a: []T, i: i64) i64 {
    if (i >= 3) { return 0; }
    return 1 + count(a, i + 1);
}

fn main() i64 {
    // A struct literal's type arguments come from its field values.
    const b = Box{ .value = 42 };
    print_int(unwrap(b));
    print(unwrap(Box{ .value = "boxed" }));

    const p = Pair{ .first = 1, .second = "one" };
    print(swap(p).first);
    print_int(swap(p).second);

    // An annotation reaches the literal's fields, so `5` coerces into `?i64`
    // exactly as it would in any other annotated binding.
    const maybe: Pair[?i64, str] = Pair{ .first = 5, .second = "five" };
    print_int(maybe.first orelse -1);

    print_int(count([]i64{ 1, 2, 3 }, 0));
    print(str.from_int(count([]str{ "a", "b", "c" }, 0)));

    // A `const` bound to a `fn` literal is a *definition*, not a value, so it
    // generalises exactly as a declaration does. It has to be: a closure value
    // is one code pointer, and these two uses need two.
    const first = fn [T](a: []T) T { return a[0]; };
    print_int(first([]i64{ 7, 8 }));
    print(first([]str{ "seven", "eight" }));

    // Without written parameters it is the same rule, and it may capture --
    // the captured value is shared by every instantiation, because its type
    // belongs to this frame rather than to the literal.
    const tag = "picked ";
    const pick = fn (a, b, c) { if (c) { print(tag); return a; } return b; };
    print_int(pick(1, 2, true));
    print(pick("x", "y", true));

    // What a *constraint* still owns stays monomorphic, exactly as it does for
    // a declaration: `Numeric` is solved with the binding group, and `add` is
    // `fn(i64, i64) i64` for the same reason `fn add(a, b)` is.
    const add = fn (a, b) { return a + b; };
    print_int(add(2, 3));
    return 0;
}
wsharp run examples/generics.ws
42
boxed
one
1
5
3
3
7
seven
picked
1
picked
x
5

The three forms

fn unwrap[T](b: Box[T]) T { return b.value; }     // on a function
const Box = struct[T] { value: T };               // on a struct
const first = fn [T](a: []T) T { return a[0]; };  // on a fn literal

A struct literal’s type arguments come from its field values, so Box{ .value = 42 } is a Box[i64] without being told. An annotation reaches the literal’s fields too, which is why Pair[?i64, str] accepts .first = 5: the 5 coerces into ?i64 exactly as it would in any other annotated binding.

A const bound to a fn literal is a definition

This is the subtle one, and the example leans on it twice:

const first = fn [T](a: []T) T { return a[0]; };
print_int(first([]i64{ 7, 8 }));
print(first([]str{ "seven", "eight" }));

first generalises exactly as a top-level declaration does, so those two calls get two specialised copies. It has to work this way: a closure value is one code pointer, and these two uses need two different ones.

The same holds without written parameters, and such a literal may still capture. The captured value is shared by every instantiation, because its type belongs to the enclosing frame rather than to the literal.

What a constraint still owns stays monomorphic, again exactly as for a declaration. const add = fn (a, b) { return a + b; }; is fn(i64, i64) i64 for the same reason fn add(a, b) is: + is solved with the binding group.

Recursion

count calls itself. The inner call records no type arguments while its binding group is being inferred, and inference fills in the group’s own variables once it has generalised. Mutual recursion works for the same reason, and so does an overload set, whose members all land in one group and generalise together.

Abstract types are generics with a constraint

Number and Integer are covered on the dispatch page, but they belong here too. A parameter annotated with an abstract type is a generic parameter constrained to that type’s members, compiled once per type it is used at. Nothing is tested at run time, because a scalar’s type is always known during compilation. The only difference from an unannotated parameter is that the set of types it will accept is written down and checked.

Last changed 8 September 2026. Improve this page

On this page