Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Spec §5 — The type system

5.1 Types

The type forms (grammar §3.9) denote:

  • Nominal types: structs and enums, possibly generic (Task, Option<i32>, HashMap<str, List<i32>>). Two nominal types are equal iff they name the same declaration and their arguments are equal. There is no structural typing of nominals.
  • Primitives: bool, str, i8 i16 i32 i53 u8 u16 u32 u53, usize, f32 f64, BigInt. Declared in std as external structs; nominally distinct (no implicit numeric conversions, §5.8).
  • Tuples: (T, U, …); structural: equal iff element-wise equal. () and one-element tuples do not exist as distinct types ((T) is T; the unit is void) — save the one-slot LABELLED tuple (x: T), whose label makes it one. A tuple’s slots may carry labels, (x: f64, y: f64); labels name positions and are never part of the type’s identity (§5.9, labelled tuples).
  • Closure types: |T, U| R, || R, || void; structural in their parameter and return types. An async closure type (§7.4) is distinct from its plain counterpart. A context clause (§8.5) is carried by the type too, but is not part of what makes two closure types compatible: a closure LITERAL is born clause-less and takes the clause of the position it lands in, and the restriction on where an injected value may then flow is what keeps it honest.
  • View types: &T, &mut T (§6). Views are second-class: these types appear in parameter and return positions and in short-lived locals only.
  • void: the unit; one value, also written void.
  • any: the dynamic top type, produced at host boundaries; it unifies with every type (absorbing).
  • Never: the type of expressions that never produce a value — panic(..), ret .., jump break/continue, a for { … } nothing breaks out of, and any block, if or match whose every path is one of those. Never unifies by erasure: it yields to whatever it meets, so a diverging expression drops out of every join it takes part in — if/else arms, match arms, a block’s tail against the type expected of it, an operator’s operands (never + i32 is i32), a ?-chain, a call’s argument against its parameter — and a Never value satisfies any expected type, a declared return type included (fun f(): i32 { panic("x"); } checks). Erasure removes the diverging expression from the join and nothing else: whatever remains must still agree, so let v: str = if c { 1 } else { panic("no") } is a mismatch at the 1, not at the panic. No Never value exists at run time — the expression that had the type leaves before anything can read it. Internal; not written in source.
  • Fixed arrays: [T; n], n values of T with the length part of the type, so [i32; 3] and [i32; 4] are distinct and neither resizes. A length is a TYPE-LEVEL NUMBER: a literal, a name bound to one (const let SIZE = 4; makes [u8; SIZE] a [u8; 4]), or a length parameter (§5.6) that each instance binds to one; two lengths are the same exactly when the numbers are, and diagnostics print the number. An array is a value aggregate (copied on assignment), iterates at T (for x in arr), indexes like a List (a literal index the length proves out of range is a compile error), and arr.len() is a usize folded from the type. It is an impl subject like any other (§5.4), and std implements PartialEq, Hashable, Debug and Items<T> for every length.
  • Generics: a bound type parameter in scope (T) is a type; it is abstract within its binder’s body.

Naming a type through a module

A nominal type is written as a path (grammar §3.9’s type-path): either a bare name, or the name qualified by the modules that declare it. Style, style::Style and std::web::style::Style name the same type; the segments before the last select namespaces and are resolved exactly as an expression path’s are (names §4.2), so a module in scope reaches its types the way it reaches its values. This holds in every type position — return type, let annotation, parameter, struct field, impl subject, trait bound, generic argument, and nested inside another type form:

import std::reactive;
import std::web::style;

struct Card {
    style: style::Style,
    hits: reactive::SignalCell<i32>,
}

fun render(card: &Card): style::Style {
    card.style
}

fun main() {
    let card = Card {
        style = style::style(),
        hits = reactive::Signal::new(0),
    };
    print(render(&card).class_list());
}

Generic arguments attach to the last segment, the only one that names a type (reactive::SignalCell<i32>); earlier segments are modules and take none.

A path addresses exactly what its namespace declares, so a segment naming a member that is not a type — a fun, a let — is refused where it stands rather than resolving to that member’s own type. The unqualified form differs here: a bare name in type position may skip a value binding and keep looking outward (names §4.5), which a qualified one has nowhere to do.

5.2 null

null is not a member of ordinary types. It exists for host interoperability (an extern that may return JS null); std APIs flatten it at the boundary (Option, or a documented sentinel like storage::get’s ""). A conforming program cannot assign null to a non-host type.

5.3 Declarations

A struct introduces a nominal product type; field types are mandatory in non-external structs. An enum introduces a nominal sum type; each variant is a constructor (with payload types) and a static member of the enum. An external struct declares a host type: no fields, its surface defined entirely by externs in impls.

An extern’s [extern(…)] attribute takes one further trailing flag, retains, declaring that the host keeps an argument past the call rather than reading it only until the call returns; it composes with every binding form. It is the one part of an extern’s contract the signature cannot express, and omitting it where it applies is a use-after-free the compiler cannot warn about — §6.8’s Externs and retention defines it.

Enum representation

An enum has one of two runtime representations, and which one is a property of the whole declaration, not of a variant. An enum is backed when both of these hold:

  1. every variant is data-less, and
  2. at least one variant has an explicit backing value.

A backed enum lowers to the bare backing value: Ordering::Greater is the value 1 and Align::Start is the string "flex-start", comparisons and equality are native scalar comparisons, and a match tests subject === 1 / subject === "flex-start". Every other enum lowers to the tagged form [index, …payload], and a match tests the tag slot.

The conjunction is the part worth stating outright, because both halves are easy to trip over:

enum Level { Low, Mid, High }           // tagged: no explicit backing value
enum Level { Low = 0, Mid, High }       // backed: ONE `= 0` converts all three
enum Level { Low = 0, Mid(i32) }        // rejected: a payload and a backing value

Adding = 0 to a single variant changes the runtime shape of the entire enum — including how its values cross a host boundary, since a backed enum reaches an external fun as a plain number or string. Backing values must be unique across the enum, counting the values implicitly continued from the previous variant; see the grammar chapter for the full rule.

An integer backing value must lie in -9007199254740991 ..= 9007199254740991 (i53), because that is the widest integer a runtime number holds exactly and the variant is that number: a discriminant past the bound would cross a host boundary as a different value than the source wrote. The continuation stops at the same edge.

Backed-enum conversions

Every backed enum gets two members, synthesized by the compiler — no derive marker, because writing = "start" is already the opt-in:

enum Align { Start = "flex-start", End = "flex-end" }

Align::Start.value()            // "flex-start" — the backing type
Align::parse("flex-start")      // Some(Align::Start)
Align::parse("middle")          // None

value() returns the enum’s backing type (str, or the narrowest of i32/i53 that holds every discriminant) and costs nothing: the receiver already is that value, so the call lowers to the receiver. parse is a static returning Option<Self> — the house form for a fallible parse, matching str::parse_i32. Declaring your own value or parse on a backed enum is a duplicate-member error.

Every variant takes part, including one that continued the sequence instead of writing a value. There is one rule about what a variant is worth, and the conversions read the same answer the lowering does:

import std::option::Option::{ self, Some, None };

enum Level { Low = 0, Mid, High }

fun main() {
	print(Level::Mid.value());  // 1 — continued from Low
	print(match Level::parse(2) {
		Some(let level) => level.value(),
		None => -1,
	});                         // 2 — High
}

(A string backing is the exception, and not a new one: there is no successor of "start", so every variant must write its own.)

A backed enum is also Hashable, implemented by the compiler on the same opt-in and for the same reason value() costs nothing: the enum IS its backing value, and that value is already a key.

mut widths: HashMap<Align, i32> = HashMap::new();
widths.insert(Align::Start, 1);          // keyed by "flex-start"

So HashMap<Align, V> and HashSet<Align> need no [derive(Hashable)], and Align::Start.hash() is Align::Start.value().hash(). Writing the derive anyway is harmless and does nothing; a hand-written impl Align with Hashable is a duplicate-impl error, because the compiler’s is already there. An unbacked enum is unaffected — it lowers to the tagged array, so it is an aggregate and needs the derive like a struct.

A resource enum gets none of the three — no value(), no parse, no Hashable. Its identity is not its copyable backing value.

Two rules follow from the backing value being a representation rather than a second name for the variant. A match still matches variants, not values — match align { "flex-start" => … } is an error, exactly as match ordering { 1 => … } already was. And <, <=, >, >= are rejected on a string backing: they would compare the strings lexicographically (Size::Large < Size::Small because "lg" < "sm"), and ordering by declaration index cannot be offered, because bare lowering erases the index. Integer backings order as before.

The trap arm

A backed enum lowers to a bare host value, so its runtime domain is the host’s and not the variant set. Exhaustiveness is checked over that variant set, by name — which is a proof about the vilan side of the boundary and never was one about the value. So an exhaustive match over a backed enum tests every variant, including the last, and its else traps:

match align {
    Align::Start => "s",
    Align::End   => "e",       // tested, not assumed
}
// a value outside the set panics:
//   Align: "middle" is not one of its values

The trap follows the backed enum wherever the pattern tests it, not only when it is the subject. A backed enum reached through a payload is the same value on the same boundary:

match pair {
    Pair::Of(Align::Start) => "s",
    Pair::Of(Align::End)   => "e",   // tested, not assumed
}
// an out-of-set payload panics, naming the payload's own value:
//   Align: "middle" is not one of its values

If one arm tests more than one backed enum, the panic names whichever value actually left its set.

A backed test can also live in an EARLIER arm than the one that becomes the else — a different variant’s payload, tested across several arms with no arm of its own left over for it:

match pair {
    Pair::Of(Align::Start) => "s",
    Pair::Of(Align::End)   => "e",   // together, `Of`'s only handler
    Pair::Other            => "o",
}
// an out-of-set `Of` payload traps instead of silently answering `Other`:
//   Align: "middle" is not one of its values

Other’s own arm carries no backed test, so the exhaustiveness proof that drops its condition is still a proof about Pair’s VARIANT set, not about Align’s runtime domain — the same gap the payload form above closes, one level up. The two Of arms above are the only place Align is ever tested, so reaching the else with the subject’s tag actually Of is possible only when its payload left Align’s set; the trap fires there, naming Align and the raw value, and Other’s own arm still answers for a genuine Pair::Other.

Only the exhaustive form is affected. A match you gave a _ arm keeps it — an out-of-set value takes the arm you wrote, which is the answer you asked for — and is and == compare against a literal, so they answer false outside the set, as they always did. An enum with no backing value keeps the tagged array form and no trap: the language itself writes that tag, so there its exhaustiveness proof is a proof about the value. That holds nested too — a match whose arms test only unbacked enums emits exactly what it always did, at any depth.

An external fun may both take and return a backed enum, and so may a callback it is handed. Nothing checks the boundary: a host value outside the set enters unremarked, exactly as it does for an external fun f(): i32 that answers "hello". What the trap arm buys is that such a value can no longer become a confident variant — the first exhaustive match to meet it panics with the raw value.

Which of the two shapes to write is a question about the value, not about safety:

[extern("getAlign")]
external fun get_align(): Align;              // out of set is a BUG — trap

[extern("getAlign")]
external fun get_align_raw(): str;            // out of set is an INPUT
fun read_align(): Option<Align> { Align::parse(get_align_raw()) }

Return the enum where the host’s set is genuinely closed and a value outside it means something is wrong; bind the backing type and parse where an unrecognized value is one of the answers you expect.

5.4 Impls

impl Subject { … } adds inherent members to Subject; impl Subject with Trait { … } provides Trait for Subject. The subject is a type pattern whose type X: Bounds binders declare the impl’s generics:

impl List<type T: PartialEq> { … }      // for every List<T> where T: PartialEq
impl SignalCell<SignalCell<type U>> { … }       // only for nested signals
impl type T: Display { … }              // blanket: every T that is Display

An impl applies to a concrete type when the pattern matches it and every binder’s bounds hold. Members may be functions (with or without self). A function without self is a static, reached as Subject::name(…).

A binder may be written inside another binder’s bound, and it declares the impl’s generics exactly like one written in the subject’s own arguments. Its scope is the whole head — the sibling bounds, the with clause, and every member signature:

impl type S: Source<type T> with MaybeSignal<T> { … }

reads “for every S that is a Source of some T”, and binds that T per receiver: a Words: Source<str> instantiates the impl at T = str, a Counts: Source<i32> at T = i32. A name in a bound that no binder declares is still unresolved, and is reported where it is written.

A bare trait in subject position means the same thing with the binder left implicit: impl Source<type T> { … } is impl type S: Source<type T> { … } — the universal reading every other bare-trait position has — so self inside the body is the implementing type, not a value of the trait, and calls on it dispatch through the bound like any other bounded parameter. The implicit binder has no name; a body that needs to say it writes Self, and a body that spells a name out is told which head would declare it.

A binder the head never mentions again needs no name, and is written _ — the wildcard of Some(_) and let _, in the impl-subject position, with the same optional bound:

impl Source<Option<_: Source<type U>>> { … }    // a signal of an optional signal
impl Pair<_, _> { … }                           // for every Pair, whatever it holds

Each _ is a parameter of its OWN: two of them in one head are two parameters, never one written twice, so nothing about the head says the two positions agree. Write a name where they must. type _ means the same thing and stays accepted; vilan fmt prints it as _. Outside an impl subject there is no head to introduce a parameter into, so _ in an annotation (let x: List<_>) is refused — it is a binder, not an inference placeholder.

A trait has one implementation per subject: writing impl Bag with Show twice is a compile error at the second one, since nothing would rank them and the second would never run. Two impls are the same implementation when they name the same trait with the same arguments for the same subject, up to the naming of their own binders — so impl Pair<type T> with Show and impl Pair<type U> with Show are one impl written twice, and an argument left to a = Self default is the type it defaults to (with Combine is with Combine<Bag> on subject Bag). A trait parameterized differently is a different implementation: impl Bag with Into<Cup> and impl Bag with Into<Mug> both stand.

The rule refuses exact repeats, not OVERLAP: a blanket impl and a specific one that both match a type — your own impl type T with Show beside impl Bag with Show, or two conditional impls with different bounds — both stand, and a call is answered by the more specific of them, never by whichever was declared first. More specific means, in order: the impl whose subject pattern the other’s matches and not conversely (Box<i32> and Box<List<i32>> over Box<type T>, and any named type over a bare type T), then — for subjects of the same shape — the impl whose binders carry the stronger bounds (Box<type T: Display> over Box<type T>). The winner brings its own member, which may be the trait’s default body where it declares none.

Two impls that neither subsumes the other — Box<type T: Display> and Box<type U: Ord> for a Box<i32> that satisfies both — are not ranked. They are legal to write, and a call that reaches both is reported at the call site; narrowing one subject is the fix. Two impls with the same subject are refused at the second declaration instead, since no type could ever separate them — so a second blanket for one trait (impl type T with Show beside impl type U with Show) never reaches a call site at all.

Which trait instantiation a call means is decided before specificity is consulted: impl Bag with Into<Cup> and impl Bag with Into<Mug> are two implementations, so bag.into() is answered by the one whose result fits the type the call site expects (let cup: Cup = bag.into()). A call with no such expectation, or one that fits both or neither, is reported rather than resolved.

Dispatch through a bound

A call on a generic parameter (fun f<V: Show>(v: V) { v.show(); }) resolves through the parameter’s bound and selects an implementation at monomorphization, by the same order — applicability, then instantiation, then specificity — with the bound supplying the instantiation the call site would otherwise have to. That order is total wherever the program compiles: every such call reaches one implementation’s member, or the trait’s default where the winning impl declares none, or the program is refused before it runs. A binding whose implementations the order cannot rank is reported at the call that made it, naming both subjects.

A blanket implementation is reachable this way like any other — it is simply the least specific tier — which is what makes one trait cover a static value and a reactive one at once:

trait MaybeSignal<T> { fun bind(self, react: |T| void); }

impl type T with MaybeSignal<T> {              // every type, statically
	fun bind(self, react: |T| void) { react(self); }
}
impl SignalCell<type T> with MaybeSignal<T> {      // signals, reactively
	fun bind(self, react: |T| void) { let _watching = self.sub(|value| react(value)); }
}

fun badge<V: MaybeSignal<str>>(label: V) { … }  // takes both, no ceremony

A blanket implementation satisfies a bound for a concrete type this way, and never for an abstract one. Whether a generic parameter satisfies a bound is answered from that parameter’s own declared bounds alone — no impl is consulted, blanket or otherwise:

trait Wrap<T> { fun unwrap(self): T; }
impl type T with Wrap<T> { fun unwrap(self): T { self } }

fun consume<T: Tag, W: Wrap<T>>(wrapped: W): str { wrapped.unwrap().tag() }

fun main() { consume(3); }                             // fine: `i32` is concrete
fun wrapper<T: Tag>(value: T): str { consume(value) }  // error: `T` lacks `: Wrap<T>`
fun ok<T: Tag + Wrap<T>>(value: T): str { consume(value) }   // declare it, and it holds

The blanket covers T at every instantiation, so reading it as satisfied in the abstract body would be sound only until a more specific impl appears — and the specificity order above ranks the blanket last, so the body would have been checked against an implementation the call does not reach. Monomorphization is where the question has a real answer, so the concrete check is the one that counts and a declared bound is what an abstract call may lean on. The refusal names the parameter and the bound it lacks, which is the edit that fixes it.

A blanket’s methods are the other direction, and they are reachable from an abstract parameter. A member that a blanket written over the parameter’s declared bounds provides answers a call on it — impl type S: Src<type T> { fun twice(self): (T, T) { … } } answers s.twice() inside fun f<S: Src<i32>>(s: S) — because every instantiation satisfies those bounds and so reaches the blanket. The bounds must entail the blanket’s, arguments included: a blanket over Src<str> is not reached through S: Src<i32>. Through the bound the parameter stays opaque. An inherent blanket member is called as written, and the concrete type’s own same-named inherent member is out of scope there — while at a CONCRETE receiver that member outranks the blanket’s, whichever was declared or loaded first; a trait member the blanket provides dispatches at monomorphization to the most specific impl of that trait, as every call through a bound does.

Because the instantiation is decided first, a SignalCell<str> reaches the Signal impl under a MaybeSignal<str> bound and the blanket under a MaybeSignal<SignalCell<str>> bound, where it is a static value of that type. The two impls never overlap at one bound: the blanket provides MaybeSignal<SignalCell<str>> there and the Signal impl provides MaybeSignal<str>, and T = SignalCell<T> has no solution at any finite T. There is no way to write “every type except a signal” — a negative bound would make adding an implementation a breaking change for everyone who wrote one — and by this order there is no need to.

Implementation note (tracked): derive-based checks (Wire, Json) verify field trees syntactically rather than through trait bounds.

5.5 Traits

A trait declares required methods (signature-only) and defaults (with bodies). trait X with Y makes Y a supertrait: implementing X requires Y — from the same impl, a separate impl on the same subject, or, for a blanket, the subject’s own bound (impl type S: Y with X reaches only types that are already Y, at the ARGUMENTS the bound writes: impl type S: Y<List<type T>> with X<T> does not reach a type that is a Y<i32>, so a Y default called on that type is answered by its own impl). A supertrait’s members are reachable through the sub-trait — a T: Ord value may call eq — and they are typed at the arguments the sub-trait passes the supertrait, not at the supertrait’s own parameters: under trait Sig<T> with Src<T>, a S: Sig<u32> bound sees Src’s get(): T as get(): u32. A trait’s generic parameters may carry defaults (trait PartialEq<B = Self>) and bounds (trait Holder<T: Bound>); Self in a trait body denotes the implementing type. Traits are used as bounds; a trait is not a type, and there are no trait objects: no value ever has a trait as its type.

That rule is enforced at the annotation, in every value position — a return type, a generic argument (List<Display>) — and reported where the trait’s name is written, whether or not the declaration is ever used. A trait’s name stays legal in the positions that name a bound or a namespace rather than a value’s type: a generic parameter’s bound (<T: Display>), a supertrait, an impl subject (impl Iterator<type T>, which blankets over a bound), and the head of a qualified path (Display::show(x)). Inside a trait’s own declaration, write Self for “the implementing type”; a generic parameter defaulted to it (trait PartialEq<B = Self>) is a parameter, not the trait, and is unaffected.

A trait annotation on a binding

A let binding’s annotation is one of three exceptions — the others are a parameter’s and a struct field’s, below — and none is an exception to the rule above but an application of it: a trait written here is a checked constraint, not the binding’s type.

let count: Signal<i32> = SignalCell::new(1);

count’s type is SignalCell<i32> — the type its initializer infers, exactly as if nothing had been written. The annotation neither widens it nor boxes it; it asserts that whatever type the initializer produces implements Signal<i32>, and is a compile error when it does not. This is the bounded-generic rule (§5.6) in binding position: checked wide, kept narrow, one concrete type per binding. Reading count’s members therefore reaches SignalCell’s own — its fields included — and a reassignment must still be a SignalCell<i32>.

The reading is universal: any trait name in this position, for every trait — and it reaches a trait NESTED inside the annotation too (let b: Signal<Option<Signal<i32>>> = …, let o: Option<Source<i32>> = …, List<Display>, a tuple’s element). Each nested mention stands for ONE type that implements it, grounded by the initializer: Signal<Option< Signal<i32>>> reads “some Signal of an Option of some one Signal<i32>”, and the binding keeps the initializer’s concrete type. One type per mention is what keeps a heterogeneous container impossible: every element of a List<Source<i32>> is the same source type, a list literal mixing two is refused with the steer to List<dyn Source<i32>>, and the object is the spelling for values of different types.

An if needs no rule of its own. Its arms unify first (§5.11), and the constraint meets the one type that unification produced:

// legal — both arms are SignalCell<i32>
let cell: Signal<i32> = if c { SignalCell::new(1) } else { SignalCell::new(2) };
// refused at the ARMS, as an ordinary mismatch: two concrete types,
// each implementing Signal<i32>, still do not unify
let cell: Signal<i32> = if c { SignalCell::new(1) } else { OtherSignal::new(2) };

A trait annotation on a parameter

A parameter’s annotation is the second position that takes a trait name, and it means something different from a binding’s: an implicit generic parameter.

fun render(cell: Signal<i32>): str { ... }   // == fun render<T: Signal<i32>>(cell: T)

The two readings differ because their quantification does. A binding has one initializer and so one concrete type, which it keeps. A parameter has one type per call site, so the function is checked once, against the bound, and monomorphized per call like any generic — which means the body reaches the trait’s members and not the argument’s own. Everything else follows from the desugaring, and nothing about it is new:

  • Each annotation is its own generic. fun f(a: Show, b: Show) has two type parameters and its two arguments may be different types; a function that needs them equal writes one generic and uses it twice.
  • The trait’s arguments are the bound’s: x: Signal<i32> bounds the parameter by Signal<i32>, not by Signal<T> for a free T.
  • The implicit parameter is appended after every generic the declaration writes, so explicit generic arguments keep their meaning.
  • Written and implicit generics mix freely in one signature.
  • A closure parameter takes no trait: a closure has no generic parameters, so the annotation is refused there like any other value position.

A trait NESTED inside a parameter’s annotation is read the same way, one implicit generic per mention: fun total(sources: List<Source<i32>>) is fun total<S: Source<i32>>(sources: List<S>), and fun first(source: Source<List<Source<i32>>>) has two, the inner bounding the outer’s argument. Every element of such a list is the same type (List<dyn Source<i32>> holds different ones). A & is not a nesting — it is a call convention, erased before the annotation is read — so &Display is “a view of something implementing Display” at a parameter and at a binding alike.

A trait annotation on a return

A free fun’s or an inherent method’s return annotation takes a trait name too, and there it means the one type the body picks:

fun counter(start: i32): Source<i32> { SignalCell::new(start) }

The function types exactly as if its return were unannotated — the return is the body’s own type, per instantiation of a generic function — and the annotation asserts that type implements Source<i32>, a compile error when it does not. Dispatch is static: a caller of counter holds a SignalCell<i32>. The return is not hidden: callers see the concrete type and may reach its own members (opacity, where callers see only the trait, is a later design). Every branch must produce that one type, so branches of different types are refused, with the steer to dyn Source<i32>, the trait object. A TRAIT method’s return (declared in a trait, or implemented in an impl … with block) takes no trait name: each impl would pick its own type, which is an associated type vilan does not have — it returns dyn Trait or a concrete type.

A trait annotation on a struct field

A struct field’s annotation is the third position a trait name can reach, and there it is refused, with a steer to the trait object:

struct Store { count: Signal<i32> }       // error: a field holds a value
struct Store { count: dyn Signal<i32> }   // one type, whatever it holds (§5.12)
struct Store<S: Signal<i32>> { count: S } // or the parameter, written

A field is part of a type every value of the struct shares, so the two readings the other positions give do not fit it: a binding’s constraint has no initializer to keep, and a parameter’s implicit generic would be a parameter of the STRUCT that no one wrote — grounded per literal, so a struct over one implementation and the same struct over another would be two types that no one list could hold, and viral through every struct that embeds one. dyn Trait is the value the field wants when what varies is the implementation; a written parameter is the answer when one instance should keep its concrete type. The refusal is the same on an attributed ([derive(..)], [service(..)]) declaration, where dyn Trait is a spelling a generator can read, and on a trait NESTED in a field’s annotation (List<Display>), which the binding and parameter readings do not reach for the same reason.

Associated functions

A trait may declare associated functions: funs with no self receiver, with or without a default body. They are a namespace, not a dispatch — there is no receiver to select an implementation with.

trait SignalCell<T> {
    fun new(initial: T): SignalCell<T> { SignalCell { value = initial } }
}

Trait::func(..) calls the trait’s own default body, always. An impl may override an associated function, and that override is reached through the implementing type’s path, Type::func(..) — never by re-pointing the trait’s spelling at it. (This is the opposite of Trait::method(receiver) for a self method, §5.7, which exists precisely to name the trait a receiver dispatches through: with a receiver there is something to select, and without one there is not.)

Two consequences follow, and both are compile errors rather than surprises. Trait::func(..) where the trait declares func without a default body names a per-impl requirement with nothing behind it, and is refused, naming both spellings. And a default body is not inherited onto an implementing type’s path: Type::func(..) reaches that type’s own declaration or nothing, and the refusal names the trait’s spelling.

The trait’s own generic parameters bind from the call, like any generic function’s: Signal::new(7) binds T = i32.

An impl’s parameters bind at a Type::func(..) path the same way — from the type arguments written on the path (Holder<A>::tag()), from the arguments, or from where the result lands. A path written with the BARE type name inside one of that type’s own impls means Self::func(..): the enclosing instance’s arguments (Holder::tag() inside impl Holder<type T: Label> is Holder<T>::tag()). Outside every impl of the type, a bounded parameter that none of those binds is refused (cannot infer 'T' for this call) — there is no instance to dispatch its bound through.

A trait parameter’s bound is in scope inside the trait’s own default bodies, exactly as a function’s or impl’s is inside theirs (§5.6): a default may call the bound trait’s members on a value of that parameter’s type. Each impl supplies the parameter’s argument (impl DogBox with Holder<Dog>), whose conformance is checked there, and the default monomorphizes per implementing type — so the call reaches that type’s argument’s implementation, never the bound trait’s abstract member.

Trait members resolve on a value when exactly one visible impl provides the name; a trait default is inherited by impls that don’t override it.

5.6 Generic binding and inference

Type checking is expectation-directed: every expression is checked against an expected type (possibly unknown), and expectations flow inward (a let annotation to its initializer, a parameter type to its argument, a field’s declared type to its initializer value).

A written type application supplies exactly the arity its declaration declares, in every position an annotation can occupy — a parameter, a return type, a let annotation, a field, a generic argument, an impl head subject, a trait bound’s argument. Naming Holder for a struct Holder<S> is an error, not a request to infer S: annotations are checked, never inferred, so a missing argument has nothing to come from. A parameter with a default (<B = Self>, §5.5) supplies itself, so an application may omit exactly the trailing arguments whose parameters default. Supplying too many is the same error. The head of a qualified path (Option::None, List::new()) names a namespace rather than a type and carries no arity of its own.

Length parameters (const N)

A length parameter stands for an array length (grammar §3.3): <const N> in a generic list, or _ / const N in an impl subject’s length position (impl [type T; _] with Count, impl [type T; const N]). It is a generic like any other — it binds, substitutes and keys an instance as a type argument does — whose KIND is a length: it may stand only where a length does, and a length position holds only a number or a length parameter. N written as a type (let x: N) and a type written as a length ([T; T]) are refused, as are a type given to a const parameter (zeros<i32>()) and a number given to a type parameter (List<4>).

A length binds wherever a type argument binds: from an ARGUMENT (total(row) with row: [i32; 3] binds N = 3), from a DIRECTED literal by its count (total([1, 2, 3]) — the literal’s type carries its own length), from the EXPECTED type (let z: [i32; 4] = zeros();), and EXPLICITLY (zeros<4>()). Two uses of one N must agree; two parameters are independent. A length nothing binds is refused at the call, the steer naming the numeric spelling.

Inside a length-generic body N is a usize VALUE, and self.len() (any spelling of an array’s length) is the same value, both folded per instance; [value; N] repeats it. The length is RIGID there, as a type parameter is: the body’s [T; N] is no [T; 3]. A literal index keeps its runtime bounds check (there is no length to check it against), and two forms are refused: a destructuring let [a, b] = self (it needs the length), and a const expression reading N (it evaluates once per site, and N differs per instance). Each length a program uses is an instance of its own on both backends.

For a call f(a₁ … aₙ) where f has generic parameters:

  1. Each parameter type is unified with its argument’s type; unification of a generic parameter with a concrete (or caller-generic) type binds it. Bindings are per-call.
  2. A generic mentioned only in the return type is bound by unifying the declared return type against the call’s expected type (let c: Cell<i32> = Cell::fresh() binds T := i32). Only binders that are free at this call participate: neither a caller-side generic introduced by substitution, nor a binder of a declaration enclosing the call — the latter is fixed by the enclosing instantiation, and callee and caller can share one outright (a trait’s parameter, in a call between two of its own members).
  3. A generic mentioned only in a SIBLING parameter’s parameterized bound (fun f<T: PartialEq, S: Source<T>>(source: S)) is bound once that sibling is: the bound’s arguments are recovered from the implementation the sibling’s type provides for the bound trait (SignalCell<i32>: Source<i32> binds T := i32). The arguments are read at that implementation’s OWN binders, so an argument whose type is caller-generic comes through as the caller’s parameter — bounds intact — exactly as a concrete one comes through as the concrete type: under impl SignalCell<type Z> with Source<Z>, a SignalCell<T> receiver binds Z := T and the bound’s argument is the caller’s T, never the impl’s Z.
  4. After binding, every bound’s satisfaction is checked; an unsatisfied bound is an error naming the parameter and bound.
  5. A call whose generics cannot all be grounded (no argument or expectation determines them) is an error at the call, naming the generic its result is typed by and the two places a type can be written: the binding the result lands in (let v: Option<i32> = nothing();) or the call’s type argument (nothing<i32>()). One exception grounds itself: a method’s own generic whose only role in the result is to RE-TYPE one of the receiver’s own parameters — the result is the receiver’s type with the generic where the receiver has a parameter — takes the receiver’s argument when nothing else binds it. err.or_else(|e| Ok(7)) on a Result<i32, str> is a Result<i32, str>: the error type of a closure that produces no error is the input’s. A closure parameter written _ is never what a call waits on, and never the thing a refusal asks to annotate.

A generic parameter is rigid inside its own body. The caller chose it, once, for this instantiation; nothing in the body may choose again. So a parameter’s type unifies with itself and with nothing else: assigning a value of one parameter’s type to a binding of another’s is a mismatch, and passing it where a concrete type is declared is a mismatch, exactly as str and i32 are. The parameters a site may bind are the ones it is inferring — a callee’s at a call, a struct’s at a literal, an enum’s at a constructor, a declaration’s own at an impl head — never one an enclosing fun, impl or trait declares. A parameter that carries a bound still satisfies a position declared as that trait (the bound is what it promises); satisfying is not binding.

A bound names a trait. A struct or an enum written in bound position (fun f<T: S>) is an error naming the sort, not a silent parameter with an unusable constraint.

Bounds cut both ways. At a call they are an obligation (4 above); at a declaration they are an assumption: inside the body of whatever declares the parameter — a function, an impl, or a trait — the bound trait’s members are in scope on that parameter’s type, and a call to one resolves through the bound and dispatches at monomorphization to the implementation the parameter is bound to there.

Method calls additionally bind the receiver’s impl binders from the receiver’s type before the parameters are considered. Closure arguments participate: a closure’s parameter types take the callee’s expectations, and its return type may ground the callee’s generics; resolution defers until the closure’s body has typed.

Generic code is monomorphized: each distinct binding vector of a generic function/impl produces its own specialization; dispatch is static. A program that would require an unbounded set of specializations (polymorphic recursion) is not required to compile.

Return-type inference. A function with no declared return type takes its type from its body’s return positions — the tail, when the body can reach it, and every ret — and they must agree. A tail the body cannot reach is not a return position: the paths before it all leave, by ret, by jump, by a panic(..), or into a for { … } nothing breaks out of (§5.1). So fun f(x: bool) { ret 1; } is i32; a tail it can reach is a return position, so a ret 1 beside an if with no else disagrees with the void that path produces. A bare ret is a void return; it agrees only with a void body. A disagreeing ret is an error at that ret, naming both types and where the inferred one came from — the function then has no type, so the error is not repeated at its calls. A call the function makes to itself contributes nothing (its type is the one being inferred) — a binding in between changes nothing, so let x = g(n - 1); x + 1 reads exactly as g(n - 1) + 1 does; a function whose only return positions are such calls is Never. Declaring the return type replaces inference with checking (every position against the declaration) — and an unreachable tail owes no value there either, so fun f(): i32 { panic("x"); } and fun serve(): i32 { for { tick(); } } both check without one. A tail is unreachable as soon as ANY statement before it leaves, not only the last one: fun g(): i32 { ret 1; log("dead"); } owes nothing, because nothing after the ret runs. A closure (and an async block) infers the same way: its return type is the unification of its reachable tail and every ret, so |x| { ret x * 2; } is |i32| i32, and a ret that disagrees — with the tail, an earlier ret, or a body path that ends without a value — is an error at that ret. When the closure’s return type is known ahead of the body (its own annotation, or the call site’s expectation), the rets check against that type instead, exactly as a declared function’s do.

fun sign(x: i32) {
	if x > 0 {
		ret 1;
	}
	if x < 0 {
		ret -1;
	}
	0
}

fun main() {
	let s: i32 = sign(-4);
	print(s);
}
fun f(x: bool) { if x { ret "s"; } 2 }   // error at `ret "s"`: the tail is i32
fun g(x: bool) { if x { ret; } 2 }       // error at `ret`: a bare ret is void
fun h(x: bool) { if x { ret 1; } }       // error at `ret 1`: the if can produce void

5.7 Operator and method dispatch

The operators dispatch through lang-item traits (appendix §A.4): + - * / % and the bit/shift operators through Add/Sub/…; == != through PartialEq; < <= > >= through PartialOrd. The left operand’s type selects the impl; the trait’s B parameter types the right operand (default Self); the result type is the impl’s (for the arithmetic traits, Self). Compound assignment x op= e is exactly x = x op e with x’s place evaluated once.

The left operand’s shape does not enter into it. A tuple or an array is an impl subject like any other (impl (i32, i32) with PartialEq), and one without the impl is the same error a struct without it is — the operators are never the host’s. void is refused outright: an expression that produces no value has no operand to be.

A method dispatches on the receiver’s type by the same rule, and the receiver’s shape does not enter into it either: a member an impl declares on a tuple subject is callable by name on a tuple receiver, and a trait default inherited by that impl is reached the same way. So one impl serves both spellings — impl (i32, i32) with Add gives (1, 2) + (3, 4) and (1, 2).add((3, 4)) alike. A tuple’s arity is part of its type, so an impl answers only for the arity its subject names, and a tuple has no members of its own: with no impl declaring one, a method call on it is the same “no method” error any other type gets.

The primitives do not dispatch — native machine operators are their semantics — so their admitted operand pairs are stated rather than read off an impl. For + those pairs are exactly two:

  • str + x, concatenation, where x is a str, a numeric primitive, or a bool — the types whose value is already their rendering — or a generic parameter whose bound provides to_string(self): str, which is that same rendering promised rather than built in. This is the rule an interpolated string is checked against, since i"a{x}b" is a + chain (§2.3).
  • T + T for a numeric primitive T, ordinary addition, with no implicit conversions (§5.8).

Anything else is an error. A struct, an enum, a List, an Option or a tuple has no string form, so it does not concatenate: render it first with to_string(), implementing Display if it has none. The order matters, because the expression takes its type from the left operand: "n=" + count concatenates, count + "n=" does not. And bool and backed enums, native though they are for == and <, have no Add at all — a backing value is a lowering detail, not a number to compute with.

A generic parameter admits exactly what its bounds promise, and no more: a declaration is checked once for all its instantiations, so the bounds are the whole of what the operand is known to be. Whether that is enough is decided by the LEFT operand, because what the right one has to show is membership of the set the left admits — and a bound can show membership only where a trait names that set.

str’s set has one. A bound providing to_string(self): str promises the string form, so the parameter concatenates, and the concatenation calls that implementation at each instantiation: the promise is kept, not assumed. An unbounded parameter promises nothing and a bound promising something else (T: Add) promises the wrong thing; both are refused, for the same reason.

A number’s set has no such trait. i32 + i32 is the only admitted pair, nothing names “is an i32”, and a bound promises a trait’s methods rather than a type — Add’s B defaults to Self, which is the parameter, not the left operand. So a parameter is never an admissible right operand of a native operator over a number: not of +, and not of any sibling (-, *, the bit and shift operators, ==, <), whatever it is bounded to. The same holds for str’s own comparisons, which want a str and have no trait naming that either. Convert where the type is known and declare the operand concretely.

A parameter on the LEFT is a different question, and there the bound is required. The operator dispatches through its left operand, so the bound is what selects the implementation to run; a parameter without one has no implementation to dispatch to, and the operator is refused. What admits it is a bound that provides the operator’s method — the parameter’s own bound, or one reached through a supertrait: + needs T: Add, - needs T: Sub, == and != need T: PartialEq, the four orderings need T: PartialOrd, and so on for every operator that models a trait. Merely being bounded is not enough, for the same reason it is not enough on the right: T: Display promises to_string, not add.

The bound then decides the right operand too, the way a dispatching impl does below: the operand must be admitted by the B that bound declares. A bare T: Add is Add<B = Self>, so the admitted operand is a T, and only a T — a second parameter is not one, whatever it is bounded to, because a bound promises a trait’s methods and never that the parameter is the declared B. A parameterized bound says otherwise outright: T: Add<U> declares that a T takes a U there, and is the spelling that makes a two-parameter shape legal.

fun sum<P: Add, Q>(a: P, b: Q): P { a + b }      // error: `P`'s `add` accepts `P`
fun sum<P: Add, Q: Add>(a: P, b: Q): P { a + b } // error: same — a bound is not membership
fun sum<P: Add>(a: P, b: P): P { a + b }         // one parameter twice: the same type
fun sum<Q, P: Add<Q>>(a: P, b: Q): P { a + b }   // the bound declares the operand

Inside a trait’s own default body the rule is unchanged, and the parameter is then the trait’s. The bound goes on the trait, so the refusal names it: adding it there moves every impl of the trait and every bound that mentions it, which is not a local edit the way a function’s own parameter is.

&& and || are the one family with no spelling that works. They admit bool, they model no operator trait at all, and no trait names that set — so a parameter is refused on either side, whatever it is bounded to, and the fix is to change the type rather than to add a bound.

An if or for condition is that same set with no operator at all. It takes a bool, nothing names bool’s set, and there is no truthiness: a parameter the enclosing declaration owns is refused there too, bounded or not, because the body is checked once for every instantiation and the caller is what fixed the type. Test the value and branch on the bool the test produces.

fun check<T>(x: T) { if x { … } }             // error: a condition takes a `bool`
fun check<T: Display>(x: T) { for x { … } }   // error: no bound can prove `bool`
fun check<T: PartialEq>(x: T, y: T) { if x == y { … } }  // the test produces one
"n=" + count                 // str + i32 — concatenation
"p=" + point                 // error: `Point` has no string form
"p=" + point.to_string()     // the fix the error names
count + "n="                 // error: only a `str` LEFT operand concatenates
1.5 + count                  // error: f64 and i32; no implicit conversions
index - count                // error: usize and i32 — `-`, `*`, `/`, `%` as `+`
count * true                 // error: `*` computes on two numbers, and `bool` is not one
count * "2"                  // error: the same, for `str`
flags & index                // error: i32 and usize — `&`, `|`, `^` take one integer type
count << index               // a shift: the right operand is a count, any integer type
ratio & 1                    // error: a bitwise operator over a float
fun show<T>(value: T): str { "v=" + value }           // error: `T` is unbounded
fun show<T: Add>(value: T): str { "v=" + value }      // error: no bound provides `to_string`
fun show<T: Display>(value: T): str { "v=" + value }  // the impl is called
fun show<T: Display>(value: T): str { i"v={value}" }  // the hole is the same expression
fun bump<T: Add>(total: i32, value: T): i32 { total + value }  // error: `T` is wider
fun bump<T: Add>(total: i32, value: T): bool { total < value } // error: same, for `<`
fun sum<T: Add>(first: T, second: T): T { first + second }     // the parameter is on the LEFT
fun sum<T>(a: T, b: T): T { a + b }                 // error: `+` on `T` needs `T: Add`
fun sum<T: Display>(a: T, b: T): T { a + b }        // error: `Display` does not declare `add`
fun sum<T: Add>(a: T, b: T): T { a + b }            // the bound provides `add`
fun same<T: PartialEq>(a: T, b: T): bool { a == b } // one bound per operator
fun smaller<T: Ord>(a: T, b: T): bool { a <= b }    // a supertrait's `le` counts
fun both<T>(a: T, b: bool): bool { a && b }         // error: no bound can prove `bool`

trait Doubler<T> {                                  // error: the bound goes on the TRAIT
	fun once(self): T;
	fun twice(self): T { self.once() + self.once() }
}
trait Doubler<T: Add> { … }                         // the spelling that works

Where the left operand does dispatch, the same membership question is answered by the impl instead of by this section: the right operand must be admitted by the B that impl declares, checked against the signature the dispatch will actually run. That B is whatever the impl wrote — a type of its own (impl Meters with Add<Feet> admits a Feet and nothing else, Meters included), one of the impl’s own parameters (impl Bag<type T> with Add<T>, whose admitted operand is whatever the subject bound T to), or Self, spelled or reached through the trait’s B = Self default. A generic right operand refuses here for the reason it refuses over a number, one level along: a bound promises a trait’s methods, never that the parameter is the declared B. The spelling that works is the one where the impl’s B is that same parameter, which is what a generic subject’s own impl gives.

counter + point                  // error: `Counter`'s `add` accepts `Counter`
metres + feet                    // ok:    `impl Meters with Add<Feet>`
metres + metres                  // error: that impl's `B` is `Feet`
bag_of_i32 + "x"                 // error: `Bag<i32>`'s `add` accepts `i32`

fun bump<T: Add>(counter: Counter, value: T): Counter { counter + value }
// error: `Counter`'s `add` accepts `Counter`, and a bound cannot prove `T` is one
fun bump<T: Add>(bag: Bag<T>, value: T): Bag<T> { bag + value }
// ok: `impl Bag<type T> with Add<T>` declares the very parameter as its `B`

is (§3.7 level 10) tests a value against a match pattern and yields bool, and a let binding inside the pattern captures. Its scope is everywhere the test is known to have passed, and nowhere else: the then-branch, and the rest of the condition to the right of an &&. Not the else branch, where the test failed; not after the if, where nothing is known (except the guard clause below); and not the other arm of a ||, which runs exactly when the test failed — nor, for a capture under a ||, the then-branch, since reaching it proves only that some arm was true. Outside its scope the name is simply unbound, and reading it is the ordinary “cannot find” error.

The rule follows the condition’s boolean spine — !, &&, || and the is test itself — and stops there. A capture reached through anything else (a call argument, a nested if) is bound by an evaluation the condition’s truth says nothing about: always(x is P(let n)) is true whether or not P matched, so n reaches neither branch. Only the && rule survives the step off the spine, and only inside the subtree it stepped into, because && short-circuits wherever it is written.

That short-circuit is also a promise about evaluation, not only about names. && and || evaluate their right operand only when the left operand did not already settle the test, and that covers everything the operand needs to run — the subject of an is, the copy a capture takes, a call, a nested if — not merely the comparison it ends in. So a is Some(let x) && x.on_end is Some(let y) never reads x.on_end when a is None, and flag || probe() is Some(let n) never calls probe when flag is true. It holds wherever the operator is written — a condition, an initializer, an operand of another operator — and the same “only once it is reached” applies to an else if’s condition, which runs only after every earlier branch of its chain has missed.

A negation swaps the two branches, and nothing else: !(x is P) is true exactly where P failed, so the capture is not in that if’s then-branch — it is in the else branch (and on down an else if chain), which is reached precisely when the pattern matched. Two negations cancel. The && rule is unchanged inside the negation — !(x is P && …) still binds the right operand, which the short-circuit reached by matching — but a negated capture does not cross an && it sits to the left of, because && carries only its left operand’s true side. A capture under a || stays unbound in both branches.

The guard clause is the one place a capture outlives its if. When a condition binds captures on its FALSE path — the negated shapes above — the if has no else, and its then-branch diverges (every path out of it leaves — the Never rule of §5.1), then the only way past the if is that false path, where the pattern matched. Those captures are therefore in scope for the rest of the enclosing block, as ordinary declarations there: a later let of the name shadows them, and the block they belong to is the one the if was written in, not the function.

All three conditions are load-bearing. An unnegated if x is P(let n) { ret; } reaches its continuation exactly when the pattern didn’t match. A then-branch that can fall through reaches it on a miss. And an if with an else reaches it through whichever arm did not diverge, which is a different question and is not part of this rule.

“Diverges” is the whole of §5.1’s rule, every leaf included: a guard ending in ret, in a jump, in a panic(..) or in a for { .. } nothing breaks out of publishes its captures the same way, because the language has one answer to “does this leave?” and this is it.

Everything above is about a CONDITION — an if’s, a while-shaped for’s, a match guard’s — because a condition is the only thing that selects on a test’s answer, and all three read the same way: a plain capture reaches the body the test selects, a negated one does not. An is written anywhere else is an ordinary expression yielding an ordinary bool, and its captures reach the rest of that expression and nothing after it: let ok = x is Some(let n); binds n for nowhere, and a later read of it is refused. There is no narrowing that could make one work — the language has no flow typing, so “n where ok is true” is not a thing it can say — and admitting the read would emit a payload load the test never proved. The && rule holds here as everywhere, wherever the operator is written: let ok = x is Some(let n) && n > 1; binds n for its own right operand. To use a capture past the test, put the test where a branch depends on it.

if slot is Some(let n) { use(n); }                // yes: the test passed
if slot is Some(let n) && n > 0 { use(n); }       // yes: `&&` short-circuits
if slot is Some(let n) { … } else { use(n); }     // error: unbound here
if slot is Some(let n) || n > 0 { … }             // error: unbound here
if slot is Some(let n) { … } use(n);              // error: the `if` ended
if always(slot is Some(let n)) { use(n); }        // error: off the spine
if always(slot is Some(let n) && n > 0) { … }     // yes for `n > 0`: `&&` still holds

if !(slot is Some(let n)) { use(n); }             // error: the test failed
if !(slot is Some(let n)) { … } else { use(n); }  // yes: it matched
if !(!(slot is Some(let n))) { use(n); }          // yes: the negations cancel
if !(slot is Some(let n) && n > 0) { … }          // yes for `n > 0`, no for the branch
if !(slot is Some(let n)) && n > 0 { … }          // error: unbound here

if !(slot is Some(let n)) { ret; } use(n);        // yes: the guard clause
if !(slot is Some(let n)) { … } use(n);           // error: it can fall through
if slot is Some(let n) { ret; } use(n);           // error: the test failed here
if !(slot is Some(let n)) { ret; } else { … } use(n);  // error: it has an `else`

let ok = slot is Some(let n); use(n);             // error: not a condition
let ok = slot is Some(let n) && n > 0;            // yes: `&&`, wherever written
f(slot is Some(let n)); use(n);                   // error: not a condition

for slot is Some(let n) { use(n); }               // yes: the test passed
for !(slot is Some(let n)) { use(n); }            // error: the test failed
match x { _ if slot is Some(let n) => use(n) }    // yes: the guard held
match x { _ if !(slot is Some(let n)) => use(n) } // error: the test failed
if f(|| slot is Some(let n)) { use(n); }          // error: a closure body is not the condition

A pattern is checked against the type of the value it matches, so an enum-variant pattern requires that type to be that enum. A generic parameter of an enclosing declaration is not: T is whatever each instantiation binds it to, and the declaration is checked once for all of them, so value is Colour::Red on a T-typed value is a compile error — in a generic function’s body, and in a trait default over the trait’s own parameter. Match a value of the enum’s own type, or move the match to where the parameter is concrete. A bound does not change this: a trait bound cannot make a parameter be one particular enum.

5.8 Conversions and coercions

There are no implicit conversions between numeric types; use the as_* methods (value-converting, Rust-as semantics). Every binary operator over two numbers takes one type on both sides, with one exception taken from Rust: a shift’s right operand (<<, >>) is a count and may be any integer type. A bitwise operator never takes a float. There is one implicit coercion: a reference to a plain named function coerces to a matching closure type:

let transform: |str| i32 = measure;    // fun measure(text: str): i32
words.map(measure);

Eligibility: a non-generic, non-method, non-async, non-external fun whose signature equals the target closure type. Everything else (generics, methods, async functions, externs) requires an explicit wrapping closure.

The same eligibility decides what a function-typed binding can do. A binding that takes its type from the reference rather than an annotation keeps the function’s own type, and calling it calls that function:

let f = measure;                       // fun measure(text: str): i32
let n = f("abc");                      // 3 — arity and argument types
                                       // are the declaration's

An ineligible fun has no value form at all, so it can be neither stored nor called this way; the error names which rule it hit.

A tuple variant named without a call coerces the same way, where a closure type is expected: Some against |i32| Option<i32> is |x| Some(x), one parameter per payload, the enum’s generics taken from the expected type ([1, 2].map(Some), build(Shape::Rect) for a |i32, i32| Shape parameter). Only where a closure is expected — let f = Some; has no type to take the generics from, and a payload variant named as a value anywhere else is refused (call it, Some(x)). A unit variant (None) is already a value and never coerces.

any unifies with every type in both directions (it is produced by panic and host boundaries; it absorbs rather than converts).

Ascription: EXP as T

EXP as T names the type a value has, in place, where no let is at hand. It is typed exactly as the initializer of let tmp: T = EXP is: the type flows into EXP (an unsuffixed literal, an empty list, a bare None and a generic call’s result take it), every coercion an annotated binding performs is performed (a value erased to dyn T, §5.12; a named function or a tuple variant re-typed as a closure, above; a closure literal’s parameters adopting the type’s modes; Never yielding), a bare trait is checked and the value keeps its concrete type (§5.5, as at a binding), and a mismatch is refused. The result has type T — or the concrete type, under a bare trait.

SignalCell::new([1, 2, 3] as List<usize>)
make() as List<str>
match pick { true => Source::constant(1), false => cell.derive(f) } as dyn Flow<i32>

It is not a binding: nothing is copied that the unascribed value would not copy (f(xs as List<i32>) passes xs exactly as f(xs) does, and let ys = xs as List<i32> copies as let ys = xs does), it has no drop point and no name, and a resource moves exactly when the unascribed expression would. It is a value, never a place: an assignment through one, a &mut view of one and a mutating method called on one are refused ((p as Point).x = 1 — write p.x = 1).

The method converts, the keyword constrains. n.as_f64() makes an f64; n as f64 says n already is one, and with n: i32 it is refused with the conversion that exists. vilan has no cast: a dyn value never narrows back (§5.12), so as cannot downcast either.

auto: types the toolchain keeps

auto T on a function’s return, a let/mut binding (a module’s or a local), or an ascription (as auto T) is a type the toolchain writes and keeps current. It is a signature, not a constraint: the body — a function’s, a binding’s initializer, an ascription’s value — is inferred exactly as if nothing were written, so T never directs it (fun f(): auto f64 { 5 } infers i32); everything outside reads T — a function’s callers, a binding’s uses, hover; and vilan check (and build, run, test) compares the two and refuses a difference as stale, naming how many callers were checked against the written type in the meantime. vilan check --fix rewrites it, and a bare auto, which promised nothing yet, is a warning the same command fills. In a program that checks, deleting every auto changes nothing it does.

fun load(): auto List<str> { … }         // callers read List<str>
mut names: auto List<str> = [];           // a use elsewhere cannot widen it
fun doubled(cell): auto Pipe<i32> { … }   // a stage type, by its trait

Two types agree when their resolved forms are equal, except that a bare trait anywhere in the written type is met by any type that implements it (§5.5, as at a binding) — which is how a long pipe-stage type is written: as the inlay hint shows it, ~Pipe<T> written as the bare trait Pipe<T>. Such an item is no firewall: its callers still hold the concrete type. A labelled tuple is written with its labels, and labels agree as §5.9 reconciles them — except one label at two different slots, which callers would read from the wrong slot. fmt prints an auto canonically and never fills one; auto stands nowhere a type is not inferred — a parameter, a field, a generic argument, a trait member’s return is refused.

5.9 Variadic tuples

A generic parameter with a tuple bound ranges over tuples: T: (2..) (arity ≥ 2), T: (..: Display) (every element Display). A mapped type (U in T: F<U>) denotes the tuple obtained by mapping each element U of T to F<U>; combine’s signature is the canonical use:

fun combine<T: (2..)>(sources: (U in T: SignalCell<U>)): SignalCell<T>

A tuple comprehension (x in xs => e) is the value-level mapping form. Its source is a tuple family: a mapped tuple, or a VALUE of a tuple-bounded parameter, which is the identity mapping (U in T: U). The body is checked ONCE, with x at the element template over a binder U fresh to that comprehension and rigid everywhere: two walks over one family range over different positions at once, so their elements are different types (U and U'), and nothing outside a walk may bind its U. The answer is (U in T: <body type>). A concrete tuple is not a source (its elements have types of their own and no template).

Two or more bindings zip: (a in aa, b in bb => e) walks its sources’ positions in step, each binder at its own source’s element, one U for the walk. The sources must be ONE family — each a mapping of the same T, or a value of it — which is what makes their arities equal by construction; two families (T and S, both (2..)) are refused, because their arities are independent. (key in whole.keys(), value in whole => …) pairs each key with its element.

fun writes<T: (2..)>(targets: (U in T: SignalCell<U>), values: T) {
	let _done = (target in targets, value in values => target.set(value));
}

A mapped type’s binder carries its pack’s element bound: over T: (2..: PartialEq), the U of (U in T: F<U>) is PartialEq, because it ranges over exactly the elements the bound constrains — so a comprehension body may compare two Us, or call what the bound provides, and each element dispatches to its own implementation. The bound is the pack’s alone: a mapped type over a second, unbounded pack gets nothing from it.

Tuple bounds are enforced at every binding site, alongside trait bounds: the bound value must be a tuple, its arity must fall inside the declared range (endpoints inclusive), and every element must satisfy the element bound. A generic parameter forwarded into a tuple-bounded position satisfies it only through its own declared tuple bound: a contained arity range whose element bound names the same trait or a subtrait.

A tuple bound also declares the trait of a blanket written over a tuple bound that contains it — by the same containment, a range inside the blanket’s and an element bound naming its trait or a subtrait. Over std’s impl type T: (2..: PartialEq) with PartialEq, a parameter T: (2..: PartialEq) is PartialEq: two Ts compare with ==, and a callee bounded U: PartialEq takes one. The containment is a proof about every instantiation, not an impl search, so an abstract value’s declared bounds stay its only answer (§5.4); and the dispatch stays per instance — at T = (i32, i32) a program’s own impl (i32, i32) with PartialEq outranks the blanket (B588).

A spread parameter ...items: T is a call convention over an ordinary tuple parameter — the call site writes the pack’s elements out flat, and they are collected into that one tuple argument:

fun log<T: (..: Display)>(...items: T)      //  log(1, "hi") collects the pack (1, "hi")
fun gather<T: (2..)>(...sources: (U in T: SignalCell<U>)): SignalCell<T>

T is the pack — the tuple of the collected arguments’ types — not each element’s type; a uniform requirement is spelled as the element bound. The callee side is unchanged in every respect, so a spread parameter’s legal call arities are exactly the arities its declared type admits, by the rule above: (2..) refuses a one-argument call, and (..) accepts a zero-argument one (the empty pack ()). A spread parameter is what makes 0- and 1-arity tuple values reachable; tuple types already admit them. Since the convention lives on the declaration, a spread function used as a value has its tuple type, and is called with a tuple.

A direct call by name always collects, whatever its arguments’ types: log((1, "hi")) passes ONE argument, so its pack is the one-slot ((1, "hi")), and against fun draw(...at: (f64, f64)) the call draw((3, 4)) is refused. A tuple already built is passed AS the pack with a tuple-value spread (below): draw(..pair), log(..(1, "hi")) (B581).

Grammar and the positions where ... is rejected: §3.3.

A tuple-value spread ..e is an entry of a tuple construction that contributes the elements of e’s tuple type rather than e itself, so the construction’s type is the concatenation of its entries, in written order:

let pair = (1, 2);
let lead = (..pair, 3);      //  (i32, i32, i32)
let mid  = (0, ..pair, 9);   //  (i32, i32, i32, i32)
let both = (..pair, ..pair); //  (i32, i32, i32, i32)

Spreads may appear in any position, in any number, mixed freely with ordinary entries, and a construction whose only entry is a spread is still a construction ((..pair) is the concatenation of one). The operand must be a tuple; anything else is an error naming the type. Concatenation is one level deep — a spread operand’s own nested elements keep their nesting, since it contributes its elements, not its slots.

A spread is also a call argument, which is what lets a pack be forwarded: since f(a, b) means f((a, b)), f(..pair) means f((..pair)), and the arity bound is checked on the concatenation. Passing a spread to a function that has no spread parameter builds no tuple and is an error; write the construction (f((..pair))) instead.

A tuple that is still an abstract generic pack has no known element sequence while the body is checked, so it may be spread only where there is nothing to concatenate it with — alone, as inner(..items), which is how a pack is forwarded to another spread function. keyof and the type-level spread (..T, U) are recorded future work.

Walking a family: std::tuple. Importing the Tuple trait (import std::tuple::Tuple;) gives every tuple of arity two or more — a concrete one, a value of T: (2..), or a mapped tuple over one — four members: len(), the arity; keys(), one TupleKey<T, U> per position, each at its own element type; entries(), (key, value) per position; and get(key), the element a key names. A key carries its FAMILY (T) and the element type at its position (U), and a key for T is a key for every tuple mapped from T, at the mapped element: TupleKey<T, U> indexes a (V in T: F<V>) at F<U>, through any number of mappings. Two unrelated families never share keys, and a key has no public constructor, so it always names a position its family has. t.map(|x| e) is the comprehension (x in t => e) spelled as a call: it takes a closure LITERAL (a closure value has one type, and a tuple needs one per position) whose parameter is the binder. A for over a family binds the template and checks its body once (§3.5).

fun cells<T: (2..)>(whole: T): (U in T: SignalCell<U>) {
	whole.map(|x| SignalCell::new(x))
}
fun reads<T: (2..)>(whole: T, cells: (U in T: SignalCell<U>)): T {
	(key in whole.keys() => cells.get(key).get())   // `cells.get(key)`: SignalCell<U>
}

Positional access t.0, t.1 (chaining as t.0.1) types as that element and, through a mut binding, assigns it. A family whose arity is still abstract has no numbered positions; it is read at a key. Tuples store flat: a tuple-typed element occupies its elements’ slots, so accessing one yields its region as a value (destructuring reads the same layout). Positional access and destructuring are the element-wise spellings a tuple has: a tuple is not iterable, and for x in t is refused because the binder would have no single type to take (§3.5).

Labelled tuples

A tuple’s positions may carry labels (B569, named-tuple-fields.md): let p: (x: f64, y: f64) = (x = 5, y = 7); — every slot labelled, or none. A label is a name for a POSITION, read as p.x (the slot p.0 is, and p.0 keeps working) and written through a mut binding as p.x = 3;. Labels are carried by the type and never compared: two tuple types are compatible when their elements are, whatever their labels, with ONE exception below. So:

  • a labelled literal against a labelled type of its arity matches BY NAME — let q: (x: f64, y: f64) = (y = 7, x = 5); is (5, 7) — and must name exactly that type’s labels; its entries are evaluated as WRITTEN, then stored in the type’s order. Against an unlabelled type, or with no expected type, it is positional ((y = 7, x = 5) alone is (y: i32, x: i32)).
  • an unlabelled value into a labelled position takes the position’s labels; a labelled one into an unlabelled position drops them; two DIFFERENTLY named label sets reconcile by position — (w: f64, h: f64) into (x: f64, y: f64) — and a binding, parameter, field or return takes the labels of its declared type.
  • The contradiction is refused: two tuple types where some label sits at DIFFERENT positions — (x: f64, y: f64) into (y: f64, x: f64), and the one-rename twin (x: f64, y: f64) into (y: f64, z: f64). The two readings (by name, by position) give different values, so neither is picked; the refusal spells both rewrites for a value that is a place: (y = p.y, x = p.x) by name, (p.0, p.1) by position.
  • a join types a later arm against a labelled first arm, so labelled literal arms and list elements match it by name.

A tuple PATTERN may name its elements too — let (y = top, x = left) = p;, match q { (x = 0, y = let v) => … } — and is placed by the labels of the value it matches, which it names exactly; a positional pattern ignores them. dbg prints a labelled tuple as its literal, p = (x = 5.0, y = 7.0); a tuple a generic body prints through its parameter prints positionally, since labels erase at the instance (below). Debug’s debug() is positional.

A spread parameter over a labelled tuple takes NAMED arguments: the call collects its arguments into one tuple literal (§5.9’s desugar), so draw(x = 1, y = 2) and draw(y = 2, x = 1) against fun draw(...at: (x: f64, y: f64)) both collect a literal that matches the pack by name, and draw(1, 2) still collects one positionally. A fixed parameter before the pack is passed by position; the collected arguments are all named or none; a name nothing collects is refused; and a slot cannot be left out — there are no defaults (a struct with field defaults is the shape for those).

Labels flow through generics as part of the type a parameter binds (id(p).x), and a mapped tuple keeps its source’s labels position by position — combine((x = a, y = b)) derives over (x: i32, y: i32). They are erased at monomorphization (f<(x: i32, y: i32)> and f<(i32, i32)> are one instance), in both emitters (a label is the slot offset), and in a service’s contract hash. A tuple’s future Wire or Json encoding is positional. keys() is unchanged: a key is a position. An impl cannot name a labelled tuple — labels are not an identity, so it would answer for every tuple of its slot types — and a shape with methods, derives, an identity across signatures or a wire format is a struct. (x = 5) is a one-slot labelled tuple ((x: i32), arity 1, so it does not meet T: (2..)); (5) stays a group.

5.10 !, ?. and ?

All three dispatch through lang-item traits and desugar per expression:

  • e!: try-assert. With v = e of a type implementing Try<T, B>: if v.verdict() is Good(t), the value is t; if Bad(b), the enclosing function returns R::from_bad(b) where R is its declared return type (which must implement Try<_, B> compatibly). Option and Result implement Try in std.
  • e?.m…: lift. With v = e of a container type implementing Lift + Try: if v is good with value t, the continuation (.m and the following plain postfixes, §3.6) applies to t; the result re-wraps in the container, unless the continuation itself yields the container type, in which case it is returned as-is (flattening). If v is bad, the container passes through unchanged.
  • e? with no . after it: expression lift. The lifted expression is the whole enclosing slot — a let/mut initializer, a call argument, a field value in a struct literal, a list or tuple element, an index, a ret value, a condition, a match subject, or a block tail — with every bare ? under that slot joining one region. The slot therefore receives the CONTAINER, not the element: let v = probe()? > 0 binds a Result<bool, E>, not a bool, and the position of the ? inside the slot changes nothing about where the lift lands. Parentheses delimit a slot of their own. !, not ?, is the operator that yields the element and returns the bad half from the enclosing function; a ? region does not early-return past its slot.
import std::option::Option::{ self, Some, None };

fun main() {
	let word: Option<str> = Some("dune");
	print((word?.len()).unwrap_or(0));       // 4  — lift + rewrap
	let missing: Option<str> = None;
	print((missing?.len()).unwrap_or(0));    // 0  — bad passes through
}

5.11 Type errors of note

Normative rejection cases (each is a compile error):

  • Using a trait as a type, in any value position — a parameter, a return type, a field, a generic argument (fun f(v: Display), fun make(): Display, struct H { item: Display }, let xs: List<Display> = …). Reported at the annotation (§5.5). A let binding’s OWN annotation is not this case: there a trait is a checked constraint on the inferred type (§5.5).
  • A let binding whose inferred type does not implement the trait its annotation names (let x: Display = bag, Bag having no Display impl). Reported at the annotation (§5.5).
  • Trait::func(..) for an associated function the trait declares without a default body, and Type::func(..) for one the type’s impl does not declare (§5.5).
  • A type application whose argument count is not the arity its declaration declares — too few (fun read(h: Holder) for struct Holder<S>) or too many — in any annotation position. Reported at the annotation, naming the arity and the spelling that fixes it (§5.6). Trailing arguments whose parameters have defaults may be omitted.
  • An enum-variant pattern matched against a generic parameter of an enclosing declaration (§5.7).
  • A generic enum variant constructed with a payload that does not match the instantiation the position expects (let x: Result<i32, str> = Ok(true), and the same call as an argument, a field value, or a return). The expected type may guide how the payload is typed — Ok(decode()) types decode() against the expected element — but it never overrides what the payload turned out to be, so the constructor is the enum at the arguments its payload binds and the mismatch is reported where the value lands.
  • An unsatisfied bound at a call (generic parameter 'T' is missing the bound …).
  • A match whose VALUE legs’ types don’t unify, and — by the same rule — a value if (one with a final else, §5.1) whose arms’ don’t. An else if chain is one construct: every arm unifies with the rest, not just with its neighbour. Diverging legs and arms (ret, panic, jump) are Never and don’t participate (§5.1).
  • An i53/i32 operand mix (no implicit widening; suffix the literal) — two different integer types as the operands of +, -, *, /, %, a comparison or == (B579).
  • A binding whose type still holds a part nothing determines once inference is done — let xs = [] read only through len(), let o = None — is refused at its initializer, naming where to write the type (xs: List<…>); a later use that states it is what inference reads, and only an annotation can stand in for one (B580). A use states the missing part when it is an argument at a parameter that names it (take(o) with take(value: Option<str>), through a bare-trait or bounded parameter by the one impl the value’s type has, and after every argument of the call has bound what it binds), a receiver whose call binds it (o.unwrap_or("d"), cell.set(Some(3)), xs.push(1)), or a reassignment; the FIRST such use decides, and a later one that disagrees is a mismatch at that use. An any parameter states nothing (fill-from-use, B599).
  • A value written into a struct field it does not match, through either door: the literal S { field = v } and the assignment s.field = v are governed by ONE rule, and a place chain of any depth (a.b.c = v, list[i].f = v) is the same door. A compound s.field op= v is checked on what lands — the result of op (§5.7) — not on v. The error is reported at the value.
  • A value assigned to ANY place it does not match. Every place takes the plain mut local’s rule, checked against what the place holds: a subscript (list[i] = v), a tuple position (pair.0 = v), a &mut/own parameter or a &mut view binding, and a call answering &mut T (cell.write() = v against Shared<T>’s T). References are transparent, so the place’s type is T, never &mut T: a bare 5 written through a Shared<Option<i32>> is refused with Expected Option<i32>, but got i32 exactly as mut x: Option<i32>; x = 5 is — write Some(5).

Implementation note (tracked gaps): a closure bound to a local and called directly does not infer its parameter types from the call, and effect’s unannotated closure parameter can type against the impl’s abstract T (B23). Each has a pinned test; the workaround is an annotation or a binding. (A closure passed to a method’s own generic parameter was a third such gap — its body reached the checker with the parameter still abstract, so a pattern inside it was not checked at all. Both call paths now bind from the non-closure arguments and defer before typing any closure, so the substitution has landed by the time the body is read.)

5.12 Trait objects (dyn Trait)

dyn Trait is a trait object: a value whose concrete type has been erased, carrying its trait’s members in a table beside it. It is a type in its own right and may stand wherever a type stands — a let annotation, a parameter, a struct field, an element type, a generic argument.

The keyword is required. A bare trait name in a value position is an error (§5.11); a trait object is written out, because coercing a value into one changes which member a call runs: a vtable holds the trait’s tier, and an inherent member of the same name outranks the trait’s on a concrete receiver (§5.7).

Object safety. dyn Trait is legal only when every member the trait requires — one with no default body — can occupy a table slot:

  • it takes a receiver (self). A static has nothing to select an implementation with;
  • it names no Self in its signature. Neither side can be supplied or received once the type is gone;
  • it is not generic. One slot cannot hold an unbounded family of specializations.

A trait’s default members are not slots: a default body is the trait’s own code and reaches the object through the same two rules a blanket over T: Trait does. A trait whose supertrait is not object-safe is not object-safe either.

A member that is generic, or that names Self, is unreachable through an object even when the trait is object-safe; the call is refused by name, and the fix is a generic parameter, where the type is known.

Coercion is explicit and positional. A value becomes an object only where the position’s type is a dyn: an annotated binding, a parameter, a field, an element of a list whose element type is a dyn. There is no implicit coercion between two concrete types, and no coercion out of an object: a dyn Trait never narrows back to the type it erased.

The coercion is per VALUE, at the position where the value lands. A literal hands each of its elements the position — [Root { .. }] at a List<dyn Src>, Some(root) at an Option<dyn Src>, || Root { .. } at a || dyn Src, Boxed { value = root } under a Boxed<dyn Src> annotation — and a generic position bound to an object is one (push on a List<dyn Src>). A value already BUILT with concrete elements inside does not become an object container as a whole: a List<Root> binding or call result passed as List<dyn Src> is refused, and so is a built Option<Root>, a Boxed<Root>, and a || Root closure held in a binding. Rebuild it element by element under the object type (let objects: List<dyn Src> = built.map(|element| element);), or wrap a closure in a literal (|| make()). A TUPLE is the exception, because its elements are a fixed set: a built (Root, Root) landing at a (dyn Src, dyn Src) position (or at a mapped (U in T: dyn Source<U>)) is re-built by projection, each element becoming its object where it lands, exactly as (t.0, t.1) would. The reverse, nested (List<dyn Src> where List<Root> is wanted), is the narrowing, refused one level down as at the top.

Resources. A resource with a Drop anywhere inside it may not be coerced into a trait object: teardown through a table would make the destructor dynamic where the rest of the language keeps it static (memory.md R7/R10), so the coercion is refused and the resource is held in a struct field of its own. A resource with NO Drop inside has no teardown to dispatch, and may become the object of a trait declared [resource] ([resource] trait Flow<T> { … }): such a trait says its objects may hold a resource, so dyn Flow<T> — whatever landed in it, a data value included — is itself a resource, moved and never copied. The attribute is the declaring trait’s alone: a trait extending a [resource] trait has data objects unless it declares the attribute too. Erasing a Drop-free resource into the object of an undeclared trait is refused with the steer to declare it; dyn of an undeclared trait stays data. The rules hold at every instantiation: a generic parameter erased into an object (fun erase<S: Src>(own source: S): dyn Src { source }) is judged at the call that binds it to a resource, directly or through a caller that forwards its own parameter. Where an if or a match is expected to produce an object, each arm is erased where it lands, so arms of different types meet in the object.

What reaches an object. The members its trait and that trait’s supertraits declare, through the table. Beyond those, only what is written over a bound the object satisfies: a blanket impl (impl type S: Trait { .. }, or impl type S: Trait with Other) and a generic function over S: Trait, each with S bound to the object. Nothing else the erased value implements is reachable — not an inherent member, not another trait’s member, and not another trait’s bound: dyn Shape does not satisfy T: Named because the value it holds happens to.

The ARGUMENTS an object provides a trait at follow the same lines. dyn Trait<A> provides Trait at A, and each supertrait at the arguments the with clauses pass down from A (dyn Source<i32> is a Flow<i32> under trait Source<T> with Flow<T>). Any other trait it provides only through a blanket, at what that blanket provides with its binder bound to the object; a concrete implementor of the trait says nothing about an object.

Copies. A trait object is a value, and copying one copies what it erased (§6.1, rule 1), exactly as the concrete value would be copied.

Asyncness. A call through an object is compiled once, against the member’s declaration, for every value the object may hold — so the declaration’s asyncness binds there, where through a generic bound an implementation may differ from it. A value whose implementation of a member is async cannot be coerced into an object whose trait declares that member sync; an async declaration admits both.