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::errA 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.