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)isT; the unit isvoid) — 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. Anasyncclosure type (§7.4) is distinct from its plain counterpart. Acontextclause (§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 writtenvoid.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, afor { … }nothing breaks out of, and any block,iformatchwhose every path is one of those.Neverunifies by erasure: it yields to whatever it meets, so a diverging expression drops out of every join it takes part in —if/elsearms,matcharms, a block’s tail against the type expected of it, an operator’s operands (never + i32isi32), a?-chain, a call’s argument against its parameter — and aNevervalue 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, solet v: str = if c { 1 } else { panic("no") }is a mismatch at the1, not at thepanic. NoNevervalue 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],nvalues ofTwith 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 atT(for x in arr), indexes like aList(a literal index the length proves out of range is a compile error), andarr.len()is ausizefolded from the type. It is an impl subject like any other (§5.4), and std implementsPartialEq,Hashable,DebugandItems<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:
- every variant is data-less, and
- 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 bySignal<i32>, not bySignal<T>for a freeT. - 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:
- 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.
- 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()bindsT := 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). - 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>bindsT := 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: underimpl SignalCell<type Z> with Source<Z>, aSignalCell<T>receiver bindsZ := Tand the bound’s argument is the caller’sT, never the impl’sZ. - After binding, every bound’s satisfaction is checked; an unsatisfied bound is an error naming the parameter and bound.
- 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 aResult<i32, str>is aResult<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, wherexis astr, a numeric primitive, or abool— the types whose value is already their rendering — or a generic parameter whose bound providesto_string(self): str, which is that same rendering promised rather than built in. This is the rule an interpolated string is checked against, sincei"a{x}b"is a+chain (§2.3).T + Tfor a numeric primitiveT, 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. Withv = eof a type implementingTry<T, B>: ifv.verdict()isGood(t), the value ist; ifBad(b), the enclosing function returnsR::from_bad(b)whereRis its declared return type (which must implementTry<_, B>compatibly).OptionandResultimplementTryin std.e?.m…: lift. Withv = eof a container type implementingLift+Try: ifvis good with valuet, the continuation (.mand the following plain postfixes, §3.6) applies tot; the result re-wraps in the container, unless the continuation itself yields the container type, in which case it is returned as-is (flattening). Ifvis bad, the container passes through unchanged.e?with no.after it: expression lift. The lifted expression is the whole enclosing slot — alet/mutinitializer, a call argument, a field value in a struct literal, a list or tuple element, an index, aretvalue, 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()? > 0binds aResult<bool, E>, not abool, 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). Aletbinding’s OWN annotation is not this case: there a trait is a checked constraint on the inferred type (§5.5). - A
letbinding whose inferred type does not implement the trait its annotation names (let x: Display = bag,Baghaving noDisplayimpl). Reported at the annotation (§5.5). Trait::func(..)for an associated function the trait declares without a default body, andType::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)forstruct 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())typesdecode()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
matchwhose VALUE legs’ types don’t unify, and — by the same rule — a valueif(one with a finalelse, §5.1) whose arms’ don’t. Anelse ifchain is one construct: every arm unifies with the rest, not just with its neighbour. Diverging legs and arms (ret,panic,jump) areNeverand don’t participate (§5.1). - An
i53/i32operand 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 throughlen(),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)withtake(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. Ananyparameter 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 assignments.field = vare governed by ONE rule, and a place chain of any depth (a.b.c = v,list[i].f = v) is the same door. A compounds.field op= vis checked on what lands — the result ofop(§5.7) — not onv. The error is reported at the value. - A value assigned to ANY place it does not match. Every place takes the
plain
mutlocal’s rule, checked against what the place holds: a subscript (list[i] = v), a tuple position (pair.0 = v), a&mut/ownparameter or a&mutview binding, and a call answering&mut T(cell.write() = vagainstShared<T>’sT). References are transparent, so the place’s type isT, never&mut T: a bare5written through aShared<Option<i32>>is refused withExpected Option<i32>, but got i32exactly asmut x: Option<i32>; x = 5is — writeSome(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
Selfin 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.