Generics

Generics

Sun supports generic classes and functions, parameterized by type. This allows you to write reusable code that works with different types while maintaining type safety.

Generic Functions

Generic functions are declared with type parameters in angle brackets after the function name:

function identity<T>(x: T) T {
    return x;
}
 
function main() i32 {
    var a = identity<i32>(42);     // T = i32, returns 42
    var b = identity<f64>(3.14);   // T = f64, returns 3.14
    return a;
}

Multiple Type Parameters

Functions can have multiple type parameters:

function pair_sum<A, B>(a: A, b: B) A {
    return a + b;
}
 
function main() i32 {
    return pair_sum<i32, i32>(10, 20);  // 30
}

Inferred Type Arguments

When a type parameter appears in a parameter's type, the call can leave it out and the argument supplies it. A call may also write only the leading type arguments; the rest are inferred. This is how a function whose return type is a type parameter is called:

function identity<T>(x: T) T { return x; }
function narrow<T, U>(x: U) T { return _convert<T>(x); }
 
function main() i32 {
    var a = identity(42);          // T = i32, from the argument
    var ms: i64 = 200;
    var b = narrow<i32>(ms);       // T = i32 as written, U = i64 from ms
    return a + b;
}

A type argument that is written always wins over what the argument suggests; a type parameter that appears in no parameter must be written. Generic methods work the same way:

class Box<T> {
    var v: T;
    init(v: T) { this.v = v; }
    method twice<U>(x: U) U { return x + x; }
    method as<R, U>(x: U) R { return _convert<R>(x); }
}
 
var b = Box<i64>(1);
var n = b.twice(21);      // U = i32
var m = b.as<i32>(ms);    // R = i32 as written, U = i64 from ms

Constraints

A type parameter can require something of the type it stands for. Write the requirement after a colon:

function twice<T: _Numeric>(x: T) T { return x + x; }
 
function main() i32 {
    return twice<i32>(21);   // 42
    // twice<bool>(true) is rejected: bool is not numeric
}

The constraint is checked wherever the generic is used with a real type:

type argument 'bool' does not satisfy constraint '_Numeric' on type parameter 'T' of generic function 'twice'

Two kinds of requirement can appear after the colon: a built-in trait, or the name of an interface the type argument must implement. The built-in traits are:

TraitSatisfied by
_Integeri8–i64, u8–u64
_Signedi8–i64
_Unsignedu8–u64
_Floatf32, f64
_Numeric_Integer and _Float
_Primitive_Numeric and bool
_Lambdaany closure type
_Functiona named-function value (function (Args) _Result)
_Callable_Lambda and _Function

Constraints work on every generic declaration, not only functions:

class Box<T: _Numeric> { var v: T; }
enum Maybe<T: _Numeric> { Some(T), None }
interface IStore<T: _Numeric> { public method get() T; }
function run<F: _Lambda>(f: F) i32 { ... }

An interface constraint can include type arguments, including the constrained parameter itself:

interface IHandler<Self> {
    /** Returns an explicit copy of the handler. */
    public const method clone() Self;
}
 
class Server<H: IHandler<H>> {
    var handler: H;
    init(handler: H) { this.handler = handler; }
 
    /** Returns an explicit copy of the stored handler. */
    public const method cloneHandler() H { return this.handler.clone(); }
}

Server<MyHandler> requires MyHandler to implement IHandler<MyHandler>. Self is an ordinary interface type parameter: substituting MyHandler makes clone() return MyHandler. The requirement applies to H, not to Server. Interface arguments can also refer to other parameters, as in <H: IValue<T>, T>. The exact interface arguments must match; implementing IValue<i32> does not satisfy IValue<i64>.

Only some parameters need constraining — <T, U: _Numeric> leaves T open.

A constraint and _is<T> ask the same question and use the same vocabulary; the difference is where the question is asked. A constraint states a requirement in the signature, so a bad type argument is rejected at the call. _is<T> asks in the body, so the function can branch on what it was given:

function describe<T: _Numeric>(x: T) i32 {
    if (_is<_Float>(x)) { return 2; }   // T is numeric for sure...
    return 1;                            // ...but which kind is a body question
}

An interface constraint also says what the body may do with the value. Whatever T turns out to be, it implements the interface, so the interface's methods and fields are reachable on a value of type T:

interface IShape { public method area() i32; }
 
class Square implements IShape {
    var side: i32;
    init(s: i32) { this.side = s; }
    public method area() i32 { return this.side * this.side; }
}
 
function measure<T: IShape>(s: ref T) i32 {
    return s.area();      // IShape promised this method
}
 
function main() i32 {
    var sq = Square(6);
    return measure(sq);   // 36
}

Without a constraint there is nothing to go on, and reaching for a member is an error — the compiler has no idea what T will be. A trait constraint such as _Numeric says which types are allowed, not what members they carry, so it does not open up member access either. Only an interface does.

Value Packs

A parameter list can end in a pack: one name standing for however many arguments the call supplies. Write it as a name followed by ...:

function make<T>(args...: _params_of<T>) raw_ptr<T> {
    var size: i64 = _sizeof<T>();
    var memory: raw_ptr<i8> = unsafe { _malloc(size); };
    unsafe { _init<T>(memory, args...); };
    return memory;
}

args... at the call passes the whole pack along, in order. It may only appear in a call's argument list.

The pack is not a type, and there is no way to ask how long it is or to index it. It is monomorphized: each distinct tuple of argument types gets its own compiled function. That is what lets one call site pick one constructor and another pick a different one:

class Point {
    var x: i32;
    var y: i32;
    init(x: i32, y: i32) { this.x = x; this.y = y; }
    init(v: i32) { this.x = v; this.y = v; }
}
 
var a = make<Point>(3, 4);   // selects init(i32, i32)
var b = make<Point>(9);      // selects init(i32) — a separate function

A pack must come last, and ordinary parameters may precede it. Everything past those parameters fills the pack:

function scaled<T>(factor: i32, args...: _params_of<T>) i32 { ... }
 
scaled<Point>(2, 3, 4);   // factor = 2, pack = (3, 4)

The annotation after the colon says what parameter list the pack stands for. _params_of<T> reads it off T: for a class, the parameters of its init (any overload may match); for a lambda, the parameters that lambda takes. The call's arguments are checked against it.

function apply<F: _Lambda>(f: F, args...: _params_of<F>) i32 {
    return f(args...);
}
 
function main() i32 {
    var add = (a: i32, b: i32) => i32 { return a + b; };
    return apply(add, 3, 4);   // 7 — F is inferred from `add`
}

That is the shape the standard library's spawn takes. Paired with _return_type_of<F>, which names what F returns, it lets one function forward any lambda and any arguments to a new thread:

public function spawn<F: _Lambda>(fn: F, args...: _params_of<F>)
    Thread<_return_type_of<F>>

The annotation is optional. args... on its own accepts whatever the call passes, and so does _params_of<T> for a T that is neither a class nor a lambda.

A pack makes a declaration a template on its own, whether or not it also has type parameters — its arity comes from the call, so there is one compiled function per argument tuple either way. So a pack is fine wherever the type it names is in scope, including one borrowed from an enclosing generic:

function outer<T>() i32 {
    function build(args...: _params_of<T>) i32 { ... }
    return build(3, 4) + build(9);
}

A pack on a class method still needs its type argument written out — alloc.create<Point>(3, 4), never alloc.create(3, 4). Free functions infer it from their fixed arguments.

Generic Functions with Type Checking

Combine generic functions with _is<T> for type-specific behavior:

function processValue<T>(x: T) i32 {
    if (_is<_Integer>(x)) {
        return 1;
    }
    if (_is<_Float>(x)) {
        return 2;
    }
    return 0;
}
 
function main() i32 {
    var a = processValue<i32>(42);    // Returns 1
    var b = processValue<f64>(3.14);  // Returns 2
    return a + b;  // 3
}

Since Sun uses monomorphization, each instantiation of a generic function compiles to specialized code with dead branches eliminated by LLVM.

Lambda types instantiate generics like any other type, and the <'_> marker is part of the type: Box<() => i32> and Box<<'_>() => i32> are two distinct instantiations. One instantiated over a <'_> lambda type is frame-carrying as a whole — it works freely inside the frame that built it, but cannot be returned or become a global (see Memory Safety).

Lifetime parameters share the angle brackets, written first (function pick<'a, T>), but they are not type parameters: they never distinguish types, never mint a specialization, and never appear in emitted symbol names. <'a>() => i32 and <'b>() => i32 are one type, and an explicit instantiation names only the type arguments (combine<i32>(...) — lifetime arguments are always inferred).

Generic Classes

Basic Generic Class

class Box<T> {
  var value: T;
 
  init(v: T) {
    this.value = v;
  }
 
  method get() T {
    return this.value;
  }
}
 
function main() i32 {
    var intBox = Box<i32>(42);
    var floatBox = Box<f64>(3.14);
    return intBox.get();  // 42
}

Type Parameters

Type parameters are specified in angle brackets after the class name. When instantiating a generic class, you must provide concrete types:

var intBox = Box<i32>(42);      // T = i32
var floatBox = Box<f64>(3.14);  // T = f64

Multiple Type Parameters

Classes can have multiple type parameters:

class Pair<A, B> {
    var first: A;
    var second: B;
 
    init(a: A, b: B) {
        this.first = a;
        this.second = b;
    }
 
    method getFirst() A {
        return this.first;
    }
 
    method getSecond() B {
        return this.second;
    }
}
 
function main() i32 {
    var p = Pair<i32, f64>(42, 3.14);
    return p.getFirst();  // 42
}

Generic Classes Implementing Interfaces

Generic classes can implement interfaces, forwarding type parameters:

using std;
 
// `items` views the caller's array, so a Container is a reference holder:
// it borrows that array and cannot outlive its frame
class Container<T> implements IIterator<T, Container<T>> {
    var items: ref array<T>;
    var index: i32;
    var size: i32;
 
    init(arr: ref array<T>, sz: i32) {
        this.items = arr;
        this.index = 0;
        this.size = sz;
    }
 
    method next(self: ref Container<T>) Option<T> {
        if (this.index >= this.size) {
            return Option.None;
        }
        var result = this.items[this.index];
        this.index = this.index + 1;
        return Option.Some(result);
    }
}

Monomorphization

All generics in Sun are monomorphized at compile time. A separate version of the class or function is generated for each type combination used. This provides zero-cost abstractions — generic code runs at the same speed as hand-written type-specific code.

// Using Box<i32> and Box<f64> generates two separate struct types:
var a = Box<i32>(42);     // Generates Box_i32
var b = Box<f64>(3.14);   // Generates Box_f64