Skip to content

Projections

Name a type the compiler already computed — a function's result, its error type, or the shape of one of its locals.

Updated View as Markdown

A type projection is a type spelled as function::something. It lets one declaration refer to a type the compiler derived somewhere else instead of restating it.

fn load_numbers(): []int {
    return [1, 2, 3]
}

struct Cache {
    values: load_numbers::result
}

fn main(): void {
    c := Cache { values: load_numbers() }
    for value in c.values {
        println("${value}")
    }
}
Form Names
function::result the success (or ordinary) return type
function::err the error type
function::binding the useful element type of the local named binding

Projections appear only in type positions. They are compile-time references, not runtime reflection: the compiler finalizes the target function’s signature and body types, then substitutes the resolved type.

The re-annotation rule

Read this before the rest of the page, because it shapes every example on it.

A projection is normalized late. A value whose declared type is a projection supports assignment, passing, storage, and plain field reads — but method resolution and trait selection both run before normalization, so anything beyond that (calling a method, iterating, interpolating a numeric field) fails. When the failure is a method it is ATOLL2003: no method len on type fn::result; when it is trait selection during lowering it is a much less helpful ATOLL5005: WasmIR could not lower continuation cont0.

The fix is one line: bind the value to a concretely annotated local first, and work with that local.

fn ids(): []int {
    return [1, 2, 3]
}

fn count(values: ids::result): int {
    xs: []int = values
    return xs.len()
}

fn main(): void {
    println("${count(ids())}")
}

Without the xs line, values.len() does not resolve:

fn ids(): []int {
    return [1, 2, 3]
}

fn count(values: ids::result): int {
    return values.len()
}

The projection still pays for itself — the signature tracks ids, and only the body names the concrete type — but treat the re-annotation as part of the idiom, not as an occasional workaround.

::result

::result is the function’s return type. It works whether the return is written out or inferred.

struct Endpoint {
    host: string
    port: int
}

fn default_endpoint(): Endpoint {
    return Endpoint { host: "localhost", port: 8080 }
}

type Target = default_endpoint::result

fn render(t: Target): string {
    e: Endpoint = t
    return "${e.host}:${e.port}"
}

fn main(): void {
    println(render(default_endpoint()))
}

For a fallible function, ::result names the success side only.

error LoadError { NotFound }

struct User { id: int, name: string }

fn load_user(id: int): User ! LoadError {
    if id < 0 { error NotFound }
    return User { id: id, name: "ada" }
}

// `LoadedUser` is `User`, not `Result[User, LoadError]`.
type LoadedUser = load_user::result

fn render(u: LoadedUser): string {
    concrete: User = u
    return "#${concrete.id} ${concrete.name}"
}

fn main(): void {
    println(render(User { id: 1, name: "ada" }))
}

Auto return types

::result is what makes an inferred return type usable elsewhere. A function may omit its return type entirely, including when it returns an anonymous record — and the projection gives that unnamed shape a name.

fn metrics() {
    return { total: 12, ok: true }
}

type Metrics = metrics::result

fn describe(m: Metrics): string {
    // There is no struct name to re-annotate to, so spell the record shape.
    r: { total: int, ok: bool } = m
    return "${r.total} ok=${r.ok}"
}

fn main(): void {
    println(describe(metrics()))
    // Any structurally matching record works.
    println(describe({ total: 1, ok: false }))
}

Without the projection there would be no way to write the parameter type of describe short of declaring a struct.

::err

::err names the error type, which is useful for an adapter that must track a lower-level API’s failure contract without copying it.

error StorageError { Disk, Network, Corrupt }

struct Blob { bytes: []byte }

fn read_blob(key: string): Blob ! StorageError {
    if key.is_empty() { error Disk }
    return Blob { bytes: [] }
}

type ReadFailure = read_blob::err

fn is_retryable(e: ReadFailure): bool {
    match e {
        Network => true
        _ => false
    }
}

fn main(): void {
    match read_blob("") {
        Ok(b) => println("${b.bytes.len()}")
        Err(e) => println("retry=${is_retryable(e)}")
    }
}

For a function whose error type is an inferred union of several callees’ failures, ::err denotes the whole union — not the error type of the first failing call.

Note that a value whose type arrives through a ::err alias loses variant exhaustiveness information at a match, so include a _ arm as above.

::binding

function::binding projects a named local out of the target function’s body, peeling one collection or optional wrapper to expose the element shape.

struct User { id: int, name: string }

fn all_users(): []User {
    users := [User { id: 1, name: "ada" }]
    return users
}

// `users` is `[]User`; the projection peels to `User`.
type OneUser = all_users::users

fn label(u: OneUser): string {
    return u.name
}

fn main(): void {
    println(label(User { id: 2, name: "bo" }))
}

The peeling rules:

Binding type Projected type
Result[T, E] continue peeling with T
Option[T] / T? T
List[T] / []T T
Set[T] T
Map[K, V] V
anything else T T, unchanged

One wrapper is removed after the Result step. A map projects its value shape, not its key or an entry pair. Every row of that table in one unit:

struct User { id: int, name: string }

fn shapes(): void {
    plain: int = 1
    opt: int? = Some(2)
    list: []User = []
    unique: Set[string] = Set.new()
    index: Map[string, User] = Map.new()
}

fn a(v: shapes::plain): int { return v }
fn b(v: shapes::opt): int { return v }
fn c(v: shapes::list): string { return v.name }
fn d(v: shapes::unique): string { return v }
fn e(v: shapes::index): string { return v.name }

fn main(): void {
    println("${a(1)}${b(2)}${c(User { id: 1, name: "x" })}${d("y")}${e(User { id: 2, name: "z" })}")
}

Binding lookup finds a named local. A destructured tuple or record does not provide one aggregate name — name the component explicitly, or return a named type when the shape is part of a stable contract.

Query row shapes

This is what binding projections were built for. A SELECT { ... } produces a compiler-generated record type with no source name; projecting the query’s local binding is how you refer to it.

schema shop

model Item {
    id: int
    name: string
    price: float
}

error QueryError { Failed }

fn summaries(): void ! QueryError {
    rows := FROM Item SELECT { id, name }?
}

// Names the generated `{ id: int, name: string }` row shape.
type Summary = summaries::rows

fn render(s: Summary): string {
    return "#${s.id} ${s.name}"
}

The projected type is the row, not the list of rows — the [] wrapper is peeled. A join keeps the types resolved from each qualified model field:

schema travel

model Booking {
    id: int
    schedule_id: int
}

model Schedule {
    id: int
    capacity: int
}

error QueryError { Failed }

fn capacities(): void ! QueryError {
    rows := FROM Booking
        JOIN Schedule ON Booking.schedule_id == Schedule.id
        SELECT { capacity: Schedule.capacity }?
}

type Capacity = capacities::rows

fn headroom(c: Capacity, used: int): int {
    return c.capacity - used
}

The compiler resolves Schedule.capacity against Schedule, then records that field — with its model-declared type — in the generated record.

Where projections carry, and where they stop

A projection carries across signatures and storage. It stops at the point where a concrete type is needed to select behavior. Both halves in one unit:

struct User { id: int, name: string }

fn all_users(): []User {
    return [User { id: 1, name: "ada" }]
}

// Carries: a struct field may be declared with a projection.
struct Snapshot {
    users: all_users::result
}

// Carries: so may a parameter.
fn first_name(users: all_users::result): string {
    // Stops: `for` needs the concrete element type.
    xs: []User = users
    for u in xs {
        return u.name
    }
    return "none"
}

fn main(): void {
    s := Snapshot { users: all_users() }
    everyone: []User = s.users
    for u in everyone {
        println("${u.id} ${u.name}")
    }
    println(first_name(s.users))
}

Two more sharp edges. An alias of a projection does not normalize during struct-literal checking — write the projection directly on the field (users: all_users::result) rather than users: SomeAlias, and keep aliases for parameter and return positions. And string interpolation of a numeric field counts as trait selection, so "${t.port}" on a projection-typed t needs the same re-annotation as a method call, even though t.port on its own is fine.

What is not diagnosed

The compiler does not currently reject an unknown target function, a binding name that does not exist in the target body, or ::err applied to an infallible function. Those projections resolve to an unusable placeholder type and surface later, as a mismatch at the point of use rather than at the projection itself.

fn total(): int {
    return 1
}

// Accepted today even though `total` cannot fail. Do not rely on it.
type Nothing = total::err

A projection in parameter position is also not enforced against the argument. The following compiles and runs even though label is declared to take a list of users:

struct User { id: int, name: string }

fn all_users(): []User {
    return [User { id: 1, name: "ada" }]
}

fn label(users: all_users::result): string {
    return "called"
}

fn main(): void {
    // A string is not `[]User`, and nothing says so.
    println(label("clearly not a list"))
}

So a projection is a convenience for the declaration, not a guarantee at the call site. Where the parameter type is a real contract that callers must respect, write the concrete type — or a named alias of it — instead.

Treat the projection spelling as something to get right at the source, and verify the resulting type by using it.

Coupling

A projection deliberately ties one declaration’s type to another function’s inferred body or query shape. Renaming a local, changing a return annotation, adding fallibility, changing a collection wrapper, or editing a SELECT list changes every consumer at compile time. That sensitivity is the point of the feature — and the reason to keep projections inside a closely maintained module boundary.

Use a named struct for a durable external contract. Use a projection when tracking a compiler-derived shape is exactly the behavior you want.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close