sysl

Traits

The one polymorphism mechanism — declaring, implementing, coherence, associated types, required traits, and trait objects.

sysl has one polymorphism mechanism, and it is the trait. It is nominal: a type participates in a trait only through an explicit impl Trait for Type, and never by coincidence of method names. From that one mechanism come both dispatch strategies — static, through a generic bound, and dynamic, through a trait object.

trait Greet
    name(self) -> string
    greet(self) -> string = "hello, " + self.name()

struct Cat
    tag: string

impl Greet for Cat
    name(self) -> string = self.tag

var c = Cat("ada")

print(c.greet())
hello, ada

Two things are on display. A trait’s member is either a bare signature, which an implementation must supply, or a signature with a body, which is a default every implementation inherits unless it writes its own. And an implementation’s members become the type’s own, so c.greet() is called exactly as an inherent method is.

Declaring a trait

A trait’s members are the three kinds a type’s members are, declared the same way and distinguished the same way:

memberformreached as
methodarea(self) -> intx.area()
propertysize -> intx.size, with no parentheses
a property’s setterset size(n)x.size = n
associated functionzero() -> SelfT.zero(), through the type
static propertystatic lowest -> intT.lowest, through the type and with no parentheses

A property is asked for by dropping the body from its declaration form, and supplied by an implementation writing that body:

trait Sized
    size -> int

struct Box
    w: int
    h: int

impl Sized for Box
    size -> int = self.w * self.h

var b = Box(3, 4)

print(b.size)
12

Nothing about a property’s dispatch differs from a method’s: it has a receiver, it simply never spells one, so it takes a table slot beside the methods and a bound licenses reading it exactly as one licenses calling them.

A trait may ask for the write half too, by declaring the setter beside the property (declarations). A setter is a *self method that mentions Self nowhere but its receiver, so it takes a slot of its own and changes nothing about object safety — a bound writes the property, and so does an erased object:

trait Counter
    count -> int
    set count(n)

struct Cell
    v: int

impl Counter for Cell
    count -> int = self.v
    set count(n)
        self.v = n

bump[C: Counter](c: *C)
    c.count += 1

var cell = Cell(41)

bump(&cell)

var erased: &Counter = Cell(0)

erased.count = 7

print(cell.count, erased.count)
42 7

An implementation supplies both halves, or inherits either as a default; a block supplying only the setter need not restate the property. A property reached through a bound or a table belongs to the trait, so that is where a setter for it is declared — writing one the trait does not ask for names the trait rather than the concrete type, because an inherent setter there would not license the write.

Which kind a member is has to match between the trait and the implementation, and that is a real check rather than a formality — a property and an associated function both have no receiver to compare, so size(self) -> int would otherwise quietly stand in for size -> int. A static property is a third member with no receiver, and the word is what tells it from the other two.

An implementation may take by value what the trait takes by address. A trait’s *self says what every caller has to be able to hand over — an address, so a generic body or a table may let the member write through it — and an implementation that needs less may ask for less: written self, the call hands it a copy of the receiver instead, and a val will do. It is what a handle wants, a type whose fields share their storage, since a copy of it writes the one thing the original names:

import sysl.buf.{Buf, buf}

trait Bump
    bump(*self)

struct Tally
    b: Buf[int]

impl Bump for Tally
    bump(self) = self.b.push(1)

val t = Tally(buf())

t.bump()
t.bump()

print(t.b.len())
2

That is how b[i] = x works on a val holding a Buf: IndexSet declares index_set(*self, …) and the Buf‘s implementation takes self. A generic body is read again at each instantiation, so a call there is held to the receiver of the implementation it reaches; a table slot for such a member points at an adapter that loads the value. Every other receiver has to match, the converse included — an implementation that wants an address where the trait promised only a value would be asking for something no caller agreed to give:

trait Show
    show(self)

struct P
    v: int

impl Show for P
    show(*self) = print(self.v)
method 'show' of 'impl Show for P' takes a different receiver than the trait declares

Self is the implementing type, written wherever a signature has to name it. Inside a generic impl it is the subject applied to that block’s parameters, so -> Self and -> Box[T] are the one signature conformance compares.

A trait that declares nothing is written closed by its end marker, as a struct with no fields is: the emptiness is written rather than inferred from a missing body, so a misindented member list cannot quietly become one. Such a trait is a name a bound can ask for, and an impl of it has nothing to supply:

trait Marked
end Marked

tagged[T: Marked](x: T) -> T = x

impl Marked for int

print(tagged(4))
4

Without the marker the body is still expected (indent expected). The library’s AnyBits is one, whose membership the compiler settles.

A default may assume exactly what its own trait declares

That is not a restriction bolted on; it is what a default is, since the body must serve every implementing type and the trait is all they have in common. So a default’s body is checked once, at the trait, as the generic function it is — one type parameter, Self, bounded by the trait. A default calling a member the trait does not declare is reported at the trait, on its own line, even when nothing implements the trait at all.

It may not read a field of its receiver either. A bound promises behaviour and a field is layout, so no bound could ever license one — see generics.

Every name in a default is read in the trait’s module — its signature as much as its body. A default returning Buf[int], or taking a struct the trait’s module declares, is inherited by an impl in a module that imports neither, from another package too; and a type of the same name in the implementing module never answers for one the trait’s module cannot see — that is reported once, at the trait. Self is the one name a copy takes from the impl.

What a default buys beyond convenience is that a trait can grow: adding a member with a default does not break the implementations that already exist, which is the difference between a trait a library can evolve and one frozen at its first release.

A trait whose every member has a default leaves nothing to write, so the block is optional and the opt-in is the point of writing it:

trait Loud
    volume(self) -> int = 11

struct Amp
    n: int

impl Loud for Amp

var a = Amp(1)

print(a.volume())
11

The body a program runs is a copy per implementing type, materialized under that type’s own name. That is monomorphization with Self for the parameter, so everything downstream — an ordinary call, a table slot, the escape summary — finds a function that exists and needs to know nothing about where it came from.

Replacing a default says override

An implementation may write the member itself instead of taking the trait’s body, and when it does it says so — replacing a default is the same act as replacing an implementation, and takes the same keyword:

trait Loud
    volume(self) -> int = 11

struct Amp
    n: int

impl Loud for Amp
    override volume(self) -> int = self.n

var a = Amp(4)

print(a.volume())
4

Leave it off and the member is refused, because the reader of an impl block wants to know which of its members are replacing something and which are supplying what the trait asked for — a question that otherwise means opening the trait to find out:

trait Loud
    volume(self) -> int = 11

struct Amp
    n: int

impl Loud for Amp
    volume(self) -> int = self.n

print(1)
trait 'Loud' supplies a body for method 'volume', so writing one here replaces it — say 'override volume', or leave the member out to keep the trait's

And it is refused where nothing is replaced. A member answering a bare requirement — the ordinary case, and every member of most impl blocks — supplies what the trait asked for rather than replacing a body, so the keyword would be saying something untrue:

trait Sized
    size -> int

struct Box
    w: int
    h: int

impl Sized for Box
    override size -> int = self.w * self.h

print(1)
trait 'Sized' declares property 'size' without a body, so this member supplies what the trait asked for rather than replacing anything — 'override' says a body was replaced

The rule costs almost nothing, because an implementation content with a default writes no member at all — across the whole of sysl’s own library, guides and examples, exactly two members replace one.

Conformance is explicit, always

A type that happens to have a member of the right name and shape does not satisfy a trait. There is no structural conformance, and the impl is what documents the intent:

trait Named
    label(self) -> string

struct P
    n: string

    label(self) -> string = self.n

announce[T: Named](x: T) -> string = x.label()

print(announce(P("ada")))
'announce' requires its type parameter 'T' to implement 'Named', but P does not

Nominal is the consensus among the languages sysl takes from — Swift protocols, Kotlin interfaces, and Scala traits are all nominal — and it kills accidental conformance, where a type satisfies a promise purely because two names collided.

Retrofitting is preserved. You may impl your trait for a type you do not own; you just do it explicitly rather than by implicit structural match.

Reaching a member through its trait

A member an impl gave a type is reached on a value of that type with no import of the trait. The impl travels with the type: wherever the program can see it — and a program sees every impl of the modules it is built from — x.member() finds it, as Scala 3 finds a given through its implicit scope. So a float has its sqrt and a slice its map in a file that imports nothing:

val a = [1, 2, 3]
val xs: []const int = a[0..]
val r = 2.25

print(xs.map(x -> x * 2), r.sqrt(), (-r).abs(), real.epsilon() < 1.0e-15)
[2, 4, 6] 1.5 2.25 true

What the file names answers first. The lookup has two tiers: the traits the file can name — its own module’s, the ones it imported, the ones a bound in force promised — and, only where none of those gives the type a member of that name, every trait whose impl the program can see. So a program’s own trait answers ahead of a library trait of the same member name that the file never mentioned, and an import still settles which of two traits is meant:

trait Level
    magnitude(self) -> Self

impl Level for f32
    magnitude(self) -> f32 = self + f32(1.0)

print(f32(-2.0).magnitude())
-1

sysl.math.Magnitude gives f32 a magnitude too; this file names Level and not Magnitude, so Level‘s is the one meant.

Where the file names none of several traits that each give the type the member, the call is ambiguous. With a Weigh in one module and a Measure in another, each implemented for a Box of a third, a file importing only Box is refused at Box(n).size() with

'size' on boxes.Box comes from measure.Measure and weigh.Weigh, and this file names none of them — nothing in the call says which was meant; import the one that was meant

and importing either trait is what says which. A trait private to the module that wrote it is reached by nobody outside that module, by either tier.

Generic code still needs the bound. Inside f[T](x: T) the type is nobody’s in particular, so no impl can be found for it: a T has the members its bounds promise and no others, and x.sqrt() there wants T: Float exactly as it did.

Two traits may declare a member of one name

They do not collide by existing, and a type may implement both. What separates them is the type the call goes through, so erasing to one trait or the other says which is meant:

trait Weigh
    size(self) -> int

trait Measure
    size(self) -> int

struct Box
end Box

impl Weigh for Box
    size(self) -> int = 1

impl Measure for Box
    size(self) -> int = 2

val w: &Weigh = Box()
val m: &Measure = Box()

print(w.size(), m.size())
1 2

Called on the value itself, with both traits in scope, there is nothing to read an intention off:

trait Weigh
    size(self) -> int

trait Measure
    size(self) -> int

struct Box
end Box

impl Weigh for Box
    size(self) -> int = 1

impl Measure for Box
    size(self) -> int = 2

print(Box().size())
'size' on Box comes from Weigh and Measure, and each is in scope here — nothing in the call says which was meant

Note what the refusal turns on: each is in scope here. A file importing one of the two has no ambiguity to resolve, which is the ordinary case and the reason this is rarely met.

Sharing one name costs the trait nothing else. Its other members stay the type’s, and a bound reaches every one of them, an associated function through the type’s name included. Here Level shares magnitude with the library’s sysl.math.Magnitude, which f32 already implements:

trait Level
    magnitude(self) -> Self
    half() -> Self

impl Level for f32
    magnitude(self) -> f32 = self * 2.0
    half() -> f32 = 0.5

h[S: Level]() -> S = S.half()
twice[S: Level](x: S) -> S = x.magnitude()

print(h[f32](), f32.half(), twice(f32(-3)))
0.5 0.5 -6

The same rule holds through the type’s name as on a value: an associated function both traits declare, called as f32.half() with both in scope, is refused with the message above, and a bound naming one of the two settles it.

Where it cannot be arranged away is a member taking no arguments on a type that is genuinely both, and that is why a question two traits both want is a required trait rather than a member each declares. Writer and Reader both need to answer whether a stream has gone wrong, and an open file is both — so a failed declared by each would be exactly the refusal above, on the one type that most needs to be asked. Requiring Fallible from both makes the question go away instead of moving it, and the library does that rather than declaring the member twice.

The two dispatch strategies

A trait is used two ways, and this is the pivot a programmer faces every time polymorphism comes up.

[T: Trait] — static&Trait / *Trait — dynamic
what happensone specialized copy per concrete Tone copy of the code, dispatched through a table
the calldirect, inlinableindirect, not inlined
the valueby value, no indirectionbehind a fat pointer, always
the set of typesfixed at compile timeopen
costcode sizeone indirect call
reach for it whenthe type is known — the overwhelming majoritythe collection is heterogeneous, or the boundary is a plugin

Static is the default:

trait Named
    label(self) -> string

struct Dog
    n: string

struct Cat
    n: string

impl Named for Dog
    label(self) -> string = "dog " + self.n

impl Named for Cat
    label(self) -> string = "cat " + self.n

announce[T: Named](x: T) -> string = x.label()

print(announce(Dog("rex")), announce(Cat("ada")))
dog rex cat ada

Dynamic is the escape hatch for genuine runtime heterogeneity:

trait Shape
    area(self) -> int

struct Rect
    w: int
    h: int

struct Square
    s: int

impl Shape for Rect
    area(self) -> int = self.w * self.h

impl Shape for Square
    area(self) -> int = self.s * self.s

var shapes: [2]&Shape = [Rect(2, 3), Square(4)]

print(shapes[0].area(), shapes[1].area())
6 16

Same trait, two strategies, chosen by how you spell the parameter — a bound for static, a sigil-carried trait object for dynamic. There is no dyn keyword either way, because the sigil already says everything one would.

The two are not separate worlds. A trait object satisfies a bound on the trait it dispatches through, so announce above takes a &Named as readily as it takes a Dog — the table it carries holds exactly the members the bound names:

trait Named
    label(self) -> string

struct Dog
    n: string

impl Named for Dog
    label(self) -> string = "dog " + self.n

announce[T: Named](x: T) -> string = x.label()

var d: &Named = Dog("rex")

print(announce(Dog("rex")), announce(d))
dog rex dog rex

So the table above is about what the caller holds, not about which functions it can reach. A library written against bounds is open to a program that erased, and needs no second signature written the other way. See generics for what the rule rests on — an object type is a concrete type, so nothing about monomorphization changes to allow it.

Any type may carry an impl

impl Show for int is as ordinary as impl Show for Point, and impl Show for []int is as ordinary as either. This is not a convenience: a trait that cannot cover int is a trait no library can be written against, and the library’s own Display is the first thing that needs it.

trait Double
    twice(self) -> int

impl Double for int
    twice(self) -> int = self * 2

print(5.twice(), (2 + 3).twice())
10 10

Every type has one owner key its members are filed under: a struct or an enum by the name it was declared with, everything else by its one canonical name. So impl Show for int and impl Show for i32 are the single implementation they are rather than two, and two spellings of one type collide as the duplicate they are.

An impl‘s subject is a type reference, not an identifier, so the types with no name of their own carry an implementation exactly as the named ones do:

trait Total
    total(self) -> int

impl Total for [3]int
    total(self) -> int = self[0] + self[1] + self[2]

var a: [3]int = [1, 2, 3]

print(a.total())
6

An array’s length is part of its type, so [2]int and [3]int are two types and may implement the same trait differently.

Two subjects are refused, each because an implementation for it would be about nothing:

  • a memory mode — *Point, &Point. A mode is a way of holding a Point rather than a type beside it, and a member call already sees through one level of * or & to find the receiver’s members, so an impl for the mode would register members nothing could reach;
  • a trait object — *Show. An impl says how one particular type behaves, and which type it holds is precisely what an erased value has forgotten.
struct P
    v: int

trait Show2
    show2(self) -> int

impl Show2 for *P
    show2(self) -> int = 1

print(1)
'*P' is a way of holding a P rather than a type of its own — write the 'impl' for P

A compiler-provided member is out of reach for the same reason a field is. len on a slice or an array, and bytes on a string, are reached ahead of the member table rather than through it, so an impl declaring one would register a member no reader could find.

Where an impl may live

An impl is unnamed — nothing at a use site says which one to apply — so resolving T: Show or 5.show() means searching for an implementation. Once a program is more than one module, that search needs a bound, or it would range over every module in the program, which is exactly the property that makes separate compilation impossible.

An impl Trait[A…] for Type may appear only in the module that declares Trait, or in one that declares a type the block names — in Type or among A…. Resolving a bound therefore inspects only the modules a use site already depends on in order to write the trait and the type down. No global search, and no dependency edge the source does not show.

This is Rust’s orphan rule, and it costs nothing:

  • retrofitting still works — impl MyTrait for TheirType lives with MyTrait, and the trait’s module licenses it;
  • impl Show for int still works — a built-in has no module of its own, so its owner key belongs to the library, and every impl on one is licensed by its trait’s module instead;
  • a composed type is the module’s when anything named in it is — override impl Display for []Point is licensed by Point, while a block for []int names nothing outside the library and has no home. (It says override because the library implements Display for every slice; the two rules are separate, and a slice of your own struct needs both — coherence to have a home, and override to outrank the block already covering it);
  • a type parameter is not a local type, so impl[T: Display] Display for []T is refused however its bound is written. Making every printable slice printable is the library’s job;
  • a local type in the trait’s arguments licenses the block too, which is what puts a scalar on the left of an operator. impl Mul[Point, Point] for real is written by the module that declares Point — real is the library’s and Mul is the library’s, and the argument list is what makes the block that module’s business:
struct Point
    x: real
    y: real

impl Mul[real] for Point
    mul(self, k: real) -> Point = Point(self.x * k, self.y * k)

impl Mul[Point, Point] for real
    mul(self, p: Point) -> Point = Point(p.x * self, p.y * self)

val p = Point(1.0, 2.0)
val a = p * 2.0
val b = 2.0 * p

print(a.x, a.y, b.x, b.y)
2 4 2 4

What the rule forbids is the case with no home: a foreign trait implemented for a foreign type, where two unrelated modules could each supply a different implementation and no rule picks one. So impl Mul[real, real] for real is refused in any module but the library’s, naming nothing of its own in either place:

impl Mul[real, real] for real
    mul(self, k: real) -> real = self

print(1)
an 'impl' may be written only in the module that declares the trait or in one that declares a type the block names, and 'sysl.Mul' is the library's while nothing this block names is declared outside the library — so this one has no home

The built-in keeps everything it had. real is a member of Mul whatever anybody writes, and only the argument list tells the two apart — so 2.0 * 3.0 is still the machine’s multiply, and the block above reaches real only at the pair it was written for.

An impl is part of its module’s public surface. Adding, removing, or changing one is an interface change visible to everything downstream — the same reasoning that puts implicit-resolution schemes out of scope. See modules.

An impl covers a generic type as a whole

A block may declare type parameters of its own, written directly after the keyword:

struct Box[T]
    v: T

trait Show
    show(self) -> string

impl[T: Display] Show for Box[T]
    show(self) -> string = "box of " + str(self.v)

var b = Box(41)

print(b.show())
box of 41

That is one implementation for every Box, and its members are monomorphized per receiver exactly as a generic type’s own members are.

Its subject must be the type applied to the block’s parameters and nothing else — each argument one parameter, each parameter used once, all of them spoken for:

struct Box[T]
    v: T

trait Show2
    show2(self) -> int

impl Show2 for Box[int]
    show2(self) -> int = 1

print(1)
'Box' is generic, so an 'impl' for it covers every instantiation at once — write 'impl[T] Show2 for Box[T]'

A generic type has one key for all of its instantiations, so an implementation for some of them would be a second implementation for a key that holds one. Overlapping implementations, and the specialization rule that would be needed to pick between them, are deliberately not in the language.

The parameters are matched to the arguments by position in the subject, not by the order they were declared in, so impl[X, Y] Show for Pair[Y, X] reads as it looks.

Conditional conformance

A bound on the block is what makes the conformance conditional. In the Box example above, a Box[T] implements Show precisely when T implements Display — so Box[int] does and a Box of something unprintable does not.

That question is asked one step in and composes: under impl[T: Show] Show for Box[T], a Box[Box[int]] conforms exactly when Box[int] does, which is what makes a conditional implementation usable on nested types at all.

struct Box[T]
    v: T

trait Show
    show(self) -> string

impl Show for int
    show(self) -> string = "i"

impl[T: Show] Show for Box[T]
    show(self) -> string = "b" + self.v.show()

var b = Box(Box(1))

print(b.show())
bbi

Everything that asks whether a type conforms asks it the same way — a generic function’s bound, an erasure to a trait object, print reaching for a Display, a call of the member itself — so an instantiation that fails the condition is refused at each of them while its siblings are not:

struct Box[T]
    v: T

trait Show
    show(self) -> string

trait Mark
    m(self) -> int

impl[T: Mark] Show for Box[T]
    show(self) -> string = "b"

struct N
    n: int

print(Box(N(1)).show())
'show' is a member the 'impl' of 'Show' gives 'Box[N]' only where its condition holds, and it asks 'Mark' of N, which does not implement it

What the bounds buy beyond deciding conformance is that the members become checkable at their definition: a block states what it assumes, so its bodies are walked once against those bounds alone, and a method calling something no bound licenses is reported on its own line with nothing instantiated.

A shape is covered the same way

A composed type has no name to be generic over, but it has a shape, and a block with type parameters may match that instead:

trait Count
    count(self) -> usize

impl[T] Count for []T
    count(self) -> usize = self.len

var xs: []int = [1, 2, 3, 4]

print(xs.count())
4

Everything above holds unchanged: the subject is the shape applied to the block’s parameters and nothing else, so impl[T] Count for [][]T is refused because the element is a shape rather than a parameter, and so is a fixed element:

trait Count
    count(self) -> int

impl[T] Count for []int
    count(self) -> int = 1

print(1)
'int' fixes the element type, and an 'impl' with type parameters covers every slice — write one of the block's own parameters here

Two things are the shape’s own. A composed type is filed under the whole of itself — []int, not [] — so a shape needs a key the types it covers do not have, and dropping the arguments is what makes one; a lookup finding nothing under the type’s own key falls back to it. And because an array’s length is not something a parameter can stand for, the length stays part of the shape: [2]T and [3]T are two shapes, each covering every element type at its own length.

A string is not covered by []T. It is a view of bytes that are valid UTF-8, and that invariant is the whole difference between it and a []u8 — a block written for every slice has said nothing about it. "hi".bytes is a []const u8 and is covered, which is the next rule.

Both views of a slice are one shape

[]T and []const T are one type with a bit rather than two types (arrays.md), and they share the one shape. A block written for either subject therefore reaches a receiver of either view:

trait Count
    count(self) -> usize

impl[T] Count for []T
    count(self) -> usize = self.len

var xs = [1, 2, 3]
val view: []const int = xs[..]

print(view.count(), "hi".bytes.count())
3 2

The block is written against what a slice is — a pointer and a count of T — and whether this one may be written through is no part of that.

What the covering does not do is lend a member a licence its receiver never had. The block is made real once per view, so the instance a read-only receiver reaches has a self it may not write through. A member that writes is refused, in its own body:

trait Bump
    bump(self)

impl[T] Bump for []T
    bump(self)
        self[0] = self[0]

var xs = [1, 2, 3]
val view: []const int = xs[..]

view.bump()
made real at the read-only view because that is what the receiver was

The refusal lands on the line that writes rather than on the call, which is where a reader can act on it. It has to say more than the write, though: the block’s own file wrote []T and never typed a const, so the message names the receiver that chose this instance. bump is callable as it always was — on a []int:

trait Bump
    bump(self)

impl[T] Bump for []T
    bump(self)
        self[0] = 9

var xs = [1, 2, 3]

xs[..].bump()

print(xs[0])
9

Write []const T as the subject where the members only read. Nothing forces it and both forms reach both views, but the subject is where a reader looks to find out what a block does with what it is given, and a promise made there is one nobody has to discover from a refusal.

A shape and a written-out type overlap, and an unmarked overlap is refused. Both blocks would say how a []int renders, so whichever is written second is refused and the diagnostic names the one already there:

trait Show2
    show2(self) -> int

impl Show2 for []int
    show2(self) -> int = 1

impl[T] Show2 for []T
    show2(self) -> int = 2

print(1)
'[]int' already implements 'Show2', and this 'impl' would implement it for every slice — including that one

That is the default and it is worth keeping: two blocks that overlap are usually a mistake — a duplicate written by accident, or one put in the wrong module — and refusing them is how that gets found.

override — when the overlap is deliberate

An implementation may say override, and then it wins:

trait Show2
    show2(self) -> int

impl[T] Show2 for []T
    show2(self) -> int = 2

override impl Show2 for []int
    show2(self) -> int = 1

var xs: []int = [7, 8]
var ss: []string = ["a"]

print(xs.show2(), ss.show2())
1 2

The keyword goes on the overriding side, not the overridden one. That is the whole of the design, and it is the opposite of C#’s virtual/override pair and of Rust’s unstable default: both of those make the general implementation grant permission in advance, and a library author cannot know which of their implementations somebody will need to replace. Intent is something the writer of the override has and the writer of the original does not.

It grants no permission, so what it buys is the diagnostic. An unmarked second implementation is still refused exactly as above.

The overriding side is always a type written out in full. A shape is one key and so is a generic type’s name, and the blocks that would sit under those are already refused — impl[T] Show2 for [][]T matches a shape’s argument by its shape, and impl Show2 for Box[int] fixes one instantiation of a block covering every instantiation. So there is nothing below either of them to be more specific than, and marking one says so:

trait Show2
    show2(self) -> int

override impl[T] Show2 for []T
    show2(self) -> int = 2

print(1)
'override' says this block replaces a more general one, and '[]T' is the general kind — an implementation for a shape or for a generic type covers every type it matches at once, so there is nothing below it. The override is written on the block for one type spelled out in full

An override that overrides nothing is refused, which is the check in the other direction and the one that earns its keep later: a library drops or narrows the implementation a program was overriding, and without this the override silently becomes the only one while still claiming to replace something.

trait Show2
    show2(self) -> int

override impl Show2 for []int
    show2(self) -> int = 1

print(1)
'[]int' says 'override', but nothing else implements 'Show2' for it — an override replaces an implementation that covers the type more generally, and there is none to replace

What keeps this sound is coherence rather than the keyword. The hazard a rule like this usually brings is two method tables for one type — a []Point erased to a *Show2 picking one implementation at one site and the other elsewhere. An impl may live only in the module declaring the trait or in one declaring a type named in the subject, so a program cannot write impl[T] Display for []T at all — []T names no type of its own. The only override anybody can write across a module boundary is one that names their own type, so there is exactly one per type and it lives with that type; and any site that can write []Point down already depends on the module declaring Point. One type, one table.

The cost that remains, stated plainly: a library can no longer rely on its own implementations. What override buys is that every such site is greppable rather than invisible.

The compiler writes four of them

Four traits are pure structure. Eq on a product is its fields compared one by one; Ord is those comparisons in declaration order; Hash is the fields mixed; Display is the name and the fields. Writing them out is the most mechanical code a program contains, and it is code that goes wrong quietly — a field added to a struct and forgotten in its eq is a comparison that silently stops looking at it.

A derives clause on a struct or an enum asks for them:

struct Size derives Eq, Ord, Hash, Display
    w: int
    h: int

val a = Size(3, 4)

print(a == Size(3, 4), a < Size(3, 5), a)
true true Size(3, 4)

The clause goes after the name and its type parameters, before the body. What it produces is an ordinary impl block — the one a person would have typed — so a derived implementation is found, checked, dispatched and erased exactly as a written one is, and a field that cannot do the work says so in the ordinary words.

Each of the four is written by the library, as the trait’s own derive member — the same mechanism a trait of a program’s own uses, so nothing about the four is special to the compiler except what the clause refuses up front. Display‘s writes the name and each part straight through to wherever the rendering is going; where a width was asked for, it renders into a Counting sink first, learns how wide the value came out, pads once and renders for real — so a derived rendering builds no string and an ordinary print of one costs a single pass. A shape with no parts — a fieldless struct, an enum whose variants carry nothing — renders as its name.

The four are the ones the library already provides structurally for every tuple, and a derived block walks a named product by the same rules a tuple’s is walked by — so a Size and a (int, int) compare and order alike, and render alike but for the name in front. Any other trait is derived by its own derive members, below.

A trait says how it is derived

A trait may carry a derive member: derive[T] name(self: T, …), with a body written over T‘s make-up using reflection — T::Kind, for const f in T::Fields, self.(f). A derives clause naming the trait instantiates each derive member with T standing for the type, as the impl block a person would have written:

@annotation
struct rename
    name: string

trait Json
    encode(self, w: *Writer)

    derive[T] encode(self: T, w: *Writer)
        if const T::Kind == Kind.Struct
            w.write("{".bytes)

            for const f in T::Fields
                if f.index > 0 then w.write(",".bytes)

                val key = f::Annotation[rename].map(r -> r.name).unwrap_or(f.name)

                w.write(s"\"$key\":".bytes)
                self.(f).encode(w)

            w.write("}".bytes)
        else
            @assert(false, "Json derives for a struct")

impl Json for int
    encode(self, w: *Writer) = w.write(s"$self".bytes)

impl Json for string
    encode(self, w: *Writer) = w.write(s"\"$self\"".bytes)

struct User derives Json
    @rename("id") user_id: int
    name: string

User(7, "ann").encode(stdout())
print("")
{"id":7,"name":"ann"}

User gets an impl Json for User whose body is two unrolled field writes. The body is read in the trait’s file, under the trait’s imports, and it is a template rather than a default: a block written by hand inherits nothing from it. A generic type derives at each instance (struct Box[T] derives Json asks T: Json of each), and a data enum is walked with for const c in T::Variants.

The copy is checked once per field, and refused at the derives: a field whose type cannot do what the body asks is the deriving type’s problem, said where the type asked, naming the field. A call in the body of a member the trait declares means that trait’s member, as a bound T: Json would make it: a field whose type has a member of the same name from some other trait is still refused as not implementing Json, and is never handed to the other trait’s member. A member of a trait the derived one requires is reached the same way, through the trait that declares it.

trait Json
    to_json(self) -> string

    derive[T] to_json(self: T) -> string
        var out = ""

        for const f in T::Fields
            out = out + self.(f).to_json()

        out

impl Json for int
    to_json(self) -> string = s"$self"

struct Pin
    on: bool

struct Lamp derives Json
    level: int
    pin: Pin

print(Lamp(1, Pin(true)).to_json())
'Pin' does not implement 'Json': method 'to_json' is missing (in the copy for field 'Lamp.pin')

A derivation reaches the type’s private fields. self.(f) on a private field is otherwise refused outside the type’s own file; naming the trait in derives is the type’s author opting in.

A trait with no derive member cannot be derived, and is implemented with an impl block:

trait Show
    show(self) -> string

struct P derives Show
    x: int
'Show' is not a trait the compiler knows how to write, and it carries no 'derive' member

Order is declaration order

Ord is lexicographic, first field first — the only ordering a named product has a claim to, and the one a tuple already has. That makes the order fields are written in part of what the type promises, which it already was for layout: a struct’s fields are laid out in the order they are written.

struct Version derives Ord, Display
    major: int
    minor: int

print(Version(1, 9) < Version(2, 0), Version(2, 0) < Version(1, 9))
true false

So a type that orders by one of its fields should say so rather than derive. A block written out is not a failure to use the clause; it is the case the clause does not cover.

A generic type derives conditionally

Every type parameter gains the derived trait as a bound, which is exactly conditional conformance written for you: a Box[int] is Eq and a Box of something unequatable is not, and neither needs saying.

struct Box[T] derives Eq, Display
    v: T

print(Box(1) == Box(1), Box(1) == Box(2), Box("x"))
true false Box(x)

A const value parameter gains nothing — it is not a type and has no membership to ask for — and the type’s own bounds are kept, since struct Sorted[T: Ord] is only a type at all where T is Ord.

An enum takes the clause too

A variant renders under its own name, which is how it is written. Comparison is by variant first, in declaration order, and then field by field within the variant.

enum Shape derives Eq, Ord, Display
    Circle(r: int)
    Rect(w: int, h: int)
    Empty

print(Circle(2), Rect(3, 4), Empty)
print(Circle(9) < Rect(0, 0), Rect(1, 5) < Rect(1, 6), Circle(1) == Circle(1))
Circle(2) Rect(3, 4) Empty
true true true

The hash mixes the variant before the payload, so two variants carrying equal payloads are not one key — which a table holding both would otherwise collide on at every insert.

A simple enum — one where no variant carries anything — is already Eq by rule, because its value is its discriminant. The clause says so rather than letting the block be refused further in:

enum Colour derives Eq
    Red
    Green
a simple enum is already 'Eq'

Its Ord is its discriminants’ order, so a simple enum still has three worth deriving:

enum Colour derives Ord, Hash, Display
    Red
    Green
    Blue

print(Red < Green, Blue < Green, Green)
true false Green

What the clause deliberately cannot say

It takes no override. Nothing in the library covers a type you declared, so there is no more general block for a derived one to outrank; where one is ever needed, an override impl is written by hand and the two forms do not have to compose.

And derivation is all or nothing per trait. There is no writing the block and then replacing one method of it, and a derived block beside a hand-written one for the same trait is the duplicate implementation it looks like.

Nor does it reach a type whose fields it cannot see. An opaque struct with no body is C’s incomplete type — the storage belongs to whoever allocated it and nothing here knows its shape — so a derived Eq over no fields at all would answer true for every pair with nothing to say it had:

opaque struct Handle derives Eq
opaque and declares no fields

That is the only place visibility comes into it, and it is about a layout that is absent rather than one that is hidden. The clause is part of the declaration, so the block it writes is in the module that declares the type: a private field is walked, because the block is written where the type is.

Every type has a Debug

Debug is the library’s other rendering trait, shaped exactly as Display is — a sink and the field the whole value occupies — and it is the one exception to conformance being explicit: every type has one, and none has to be written. The compiler supplies a Debug for every struct and enum that does not write its own, the way a derives clause would but without being asked: the name, then each field by name, each shown through its own Debug, so a string inside a structure is quoted and escaped. An enum’s variant is shown under the enum’s name.

struct Point
    x: int
    y: int

enum Color
    Red
    Green

struct Label
    text: string
    at: Point
    color: Color

print(Label("a \"b\"", Point(1, 2), Red))
Label(text: "a \"b\"", at: Point(x: 1, y: 2), color: Color.Red)

The library writes the rest: the scalars (a char in single quotes, a string in double), tuples, arrays, slices, Buf, Option and Result, each shown as its Display shows it but with the parts through their Debug. So print of a value with neither block written still prints — print, str and an interpolation hole reach Display where a value has one and Debug where it does not, and f"${x}%?" and debug(x) always reach Debug (strings). assert_eq reports both sides through it, so got "1", want 1 cannot read as equal.

An impl Debug of your own replaces the supplied one — to hide a field, or to abbreviate a large buffer — and a structure holding the type shows it through that block too:

struct Secret
    key: string

impl Debug for Secret
    debug(self, out: *Writer, fmt: FormatSpec) = display_pad("Secret(***)".bytes, out, fmt)

struct Login
    user: string
    secret: Secret

print(Login("ed", Secret("hunter2")))
Login(user: "ed", secret: Secret(***))

A generic type’s supplied block is conditional, as a derived one is, and every type meets the condition, so a T: Debug bound costs a caller nothing:

show[T: Debug](x: T) = print(f"${x}%?")

struct Box[T]
    v: T

show(Box("in"))
show(3)
Box(v: "in")
3

A type’s own debug is untouched by the supplied one. A field, a variant or a member named debug keeps the name on the type, and %?, debug(x) and a T: Debug bound still reach the supplied rendering:

struct Options
    title: string
    debug: bool

impl Options
    describe(self) -> string = if self.debug then "verbose" else "quiet"

var o = Options("run", false)
o.debug = true
print(o.debug, o.describe())
print(f"${o}%?")
true verbose
Options(title: "run", debug: true)

An impl Debug written for such a type is refused, as any trait’s member is that shares a field’s name:

struct Flags
    debug: bool

impl Debug for Flags
    debug(self, out: *Writer, fmt: FormatSpec) = display_pad("Flags".bytes, out, fmt)
type 'Flags' has both a field and a member named 'debug'

What is shown by rule

A few values have no block that could be written for them, and are shown by the compiler’s own rule:

  • a raw pointer *T shows its address, in lower-case hex with no padding, the null one as 0x0. It is not followed: what it points at may be gone. This is a split, and it is deliberate: a member call sees through one * (above), so in generic code a T: Debug instantiated at *P reaches P‘s Debug — the bound is met by what the pointer points at — while print(p), f"${p}%?" and a field of type *P show the address;
  • a reference &T is followed — it is a value — and shows as its referent;
  • a callable shows as <fn>, a weak T as weak(…) without following it, unit as () and a task as <task>;
  • device memory is shown by its type and never read — a volatile field, or an array or a view of them, inside a structure shows as <volatile u32> and the like, since reading a register is an act with consequences;
  • an opaque struct gets no supplied Debug — its layout is its own module’s — and is held through a pointer, which shows its address; one shown by value where its layout is private shows as its type in angle brackets, as does any value whose type has no Debug at all;
  • a trait object shows its concrete value: every object’s table carries a Debug slot after its own members, whether or not the trait requires Debug, so print of a *Shape reaches the Debug of the Rect behind it. Two kinds of table carry none: a callable’s, shown as <fn>, and a Writer‘s, shown as <writer> — a sink is where a rendering goes rather than a value to show, and a slot in every writer’s table would put a Debug of every sink into every program that writes to one. The slot costs one word per table, and keeps the concrete type’s Debug in the program wherever the table is.

A sink that should show itself says so in its trait: a trait requiring Debug beside Writer puts the slot back, so its objects print the concrete sink while a bare &Writer stays <writer>. A concrete writer, never erased, prints through its Debug like any other value.

struct Tally
    n: usize

impl Fallible for Tally

impl Writer for Tally
    write(*self, bytes: []const u8)
        self.n = self.n + bytes.len

trait LoggedWriter: Writer + Debug

impl LoggedWriter for Tally

val w: &Writer = Tally(1)
val l: &LoggedWriter = Tally(2)

print(w, l, Tally(3))
<writer> Tally(n: 2) Tally(n: 3)

Printing a placeholder directly is warned about, and still builds: a value whose whole printed form is <writer>, <fn>, <task> or a <Type> with no Debug, handed straight to print, str, an interpolation or format hole, debug(x), or an argument meeting a T: Debug bound such as assert_eq‘s, was almost certainly meant to show something. The print(w) above says:

warning: printing 'w' shows only '<writer>': a Writer object carries no Debug; print the concrete
sink, or ask for 'Writer + Debug'

A placeholder inside a structure — a field, an element, an option’s payload — is part of a larger rendering and is never warned about, and an object whose trait asks for Debug has nothing to warn about. As with the compiler’s other warning, there is no switch to silence it: the fix the sentence names is how it goes away.

A rendering that nests deeper than thirty-two levels writes … and stops. Following references is what makes a list or a tree show as one, and it is also what would make a structure that refers back to itself render for ever; the limit is what makes every value safe to show.

struct Node
    v: int
    next: Option[&Node]

val list: &Node = Node(1, Some(Node(2, None)))

print(list)
Node(v: 1, next: Some(Node(v: 2, next: None)))

A trait may take type parameters

A trait declares parameters in the same bracketed list every other generic declaration writes, and an implementation says which arguments it supplies:

trait Sink[T]
    put(*self, x: T) -> int

struct Buffer
    n: int

impl Sink[int] for Buffer
    put(*self, x: int) -> int
        self.n += x

        self.n

var b = Buffer(0)

print(b.put(3), b.put(4))
3 7

Sink is not one promise but a family of them: Sink[int] and Sink[string] say different things, and which one a Buffer makes is the implementation’s to state. That is what a trait needs before it can describe a relation between two types rather than a property of one — what a sink accepts, what a conversion converts from, what an iterator yields.

The arguments are written in the same place in all three positions a trait is named, and mean the same thing in each:

positionwritten
a boundf[X: Sink[int]](x: X), and the body’s x.put(…) then takes an int
an implementationimpl Sink[int] for Buffer
a trait object&Sink[int]

A trait’s own parameters carry bounds too — trait Get[T: Show] — and everything applying the trait supplies them, exactly as everything applying a bounded struct does.

A parameter may carry a default, and Self is the case the feature exists for: trait Scale[R = Self] is the operand type, usually the implementing type, so impl Scale for P is the impl Scale[P] for P it reads as and [T: Scale] asks for Scale[T]. See generics for how defaults are filled.

One implementation per argument list

A type may implement a trait once at each argument list, and the argument list is what tells two implementations apart:

trait Sink[T]
    put(self, x: T) -> string

struct Buf
    tag: string

impl Sink[int] for Buf
    put(self, x: int) -> string = self.tag + " int"

impl Sink[string] for Buf
    put(self, x: string) -> string = self.tag + " string"

var b = Buf("b")

print(b.put(1), b.put("x"))
b int b string

A second impl Sink[int] for Buf beside those is refused — that one is already there.

This is the one place the “a trait’s members become the type’s, and a type’s members are one namespace” rule is qualified, and the qualification is narrow. A Celsius with both blocks has two members called from, and what says which a use means is the argument list, which every way of reaching one already carries: an operator carries its pair of operands, a bound names the arguments, a trait object is formed at written arguments, and a named call passes values whose types are the arguments.

The resolution is determined, not preferred. Nothing ranks two candidates: a call is answered by the one implementation whose parameters are the types the arguments have, and a call answering to none of them or to more than one is reported rather than resolved. So c.mul(2) where the candidates take a Complex and a real is refused — an integer literal is neither, and picking the nearest would be the specialization rule this language does not have.

Two limits fall out of “several implementations are told apart inside one namespace”:

  • a property has no arguments, so two implementations both supplying one leave nothing to select with, and reading it is refused;
  • a shape and a type of that shape are filed under two different owner keys, and a member lookup takes one or the other and never both — so a second implementation split across that boundary would be one no call could reach.

A generic block may write its own parameter as a trait argument — impl[T] Index[usize, T] for Buf[T] says a Buf[int] implements Index[usize, int] and nothing else. That is what lets a container carry the type of what it holds in the trait it implements, without reaching for the associated type below — and it is the right tool when the type is one the container’s own parameters already name. The block’s parameters are exactly the arguments of the type it is written for, so an argument built out of them says one thing per instantiation and the subject settles which — including where the argument is the subject itself, which is how an operator carries a result that is not its operands’ type.

What a generic block may not write is an argument fixed to one instantiation of its own subject. Where the trait’s default names the type being asked about, such an argument coincides with the defaulted block at that one instantiation and differs from it at every other — which is a choice between implementations rather than a lookup:

struct Box[T]
    v: T

impl[T] Mul[Box[int]] for Box[T]
    mul(self, rhs: Box[int]) -> Box[T] = self

print(1)
whose arguments default names the type it is written for

A trait may declare an associated type

A trait parameter is written where the trait is applied. An associated type is written by the implementation, and everything else reads it back off the type:

trait Render
    render(self) -> string

trait Seq
    type Item: Render
    head(self) -> Self::Item

struct Words
    first: string

impl Render for string
    render(self) -> string = self

impl Seq for Words
    type Item = string
    head(self) -> Self::Item = self.first

show[S: Seq](s: S) -> string = s.head().render()

print(show(Words("hi")))
hi

type Item: Render declares the parameter and what the type filling it must implement. type Item = string fills it. Self::Item is the projection — the same :: that reads int::Max in expression position, reaching type position — and show‘s body may call render on what it gets because that is what the trait asked of Item, and nothing more.

That is the whole difference from trait Sink[T], and it is one difference with three consequences:

a trait parameteran associated type
who writes the argumentwhoever names the traitthe implementation
how many per typeone implementation per argument listone, and the subject settles it
how a use names itSink[int], everywhere the trait is namedT::Item, off the type

A trait may declare both. The parameters go in the bracketed list and the associated types among the members, and neither is in the other’s way.

The projection normalizes as soon as its subject is concrete

Self::Item inside a generic body is abstract — it stands for a type nobody has chosen yet, and is licensed to do exactly what the trait’s bound on it promises. Off a type that is actually known it is the type the implementation chose, and may be written wherever a type may:

trait Render
    render(self) -> string

trait Seq
    type Item: Render
    head(self) -> Self::Item

struct Nums
    first: int

impl Render for int
    render(self) -> string = "an int"

impl Seq for Nums
    type Item = int
    head(self) -> Self::Item = self.first

val n: Nums::Item = 41

print(n, n.render())
41 an int

So there is no hiding, and that is deliberate. Swift’s some View conceals the type because Swift gives you no way to write it down; Nums::Item is a way to write it down, so concealment would buy a restriction rather than an abstraction. What a generic caller sees is still only the bound — that is where the abstraction pays, and it is delivered by the projection being abstract there.

A generic block answers with one type per instantiation

The block’s own parameters may stand in the associated type, exactly as they may stand in a trait argument. Each instantiation of the subject then settles it:

trait Render
    render(self) -> string

trait Seq
    type Item: Render
    head(self) -> Self::Item

struct Cell[T]
    v: T

impl[T: Render] Render for Cell[T]
    render(self) -> string = "[" + self.v.render() + "]"

impl Render for int
    render(self) -> string = "i"

impl Render for string
    render(self) -> string = self

struct Box[T]
    v: T

impl[T: Render] Seq for Box[T]
    type Item = Cell[T]
    head(self) -> Self::Item = Cell(self.v)

show[S: Seq](s: S) -> string = s.head().render()

print(show(Box(3)), show(Box("x")))
[i] [x]

Box[int]::Item is Cell[int] and Box[string]::Item is Cell[string]. Monomorphization keeps the concrete type all the way down, so none of this costs anything at run time — there is no box per value and no indirect call.

A default’s callable may name the projection

A default body is checked once at the trait, where Self is a parameter, and copied per implementing type, where Self is that type. A bare arrow is sugar for a bound rather than a parameter type, so a default taking one over Self::Item puts the projection somewhere neither half is written out — and each copy binds it to what its own subject chose:

trait Mapper
    type Item

    first(self) -> Self::Item
    over[N](self, f: Self::Item -> N) -> N = f(self.first())

struct One
    v: int

impl Mapper for One
    type Item = int
    first(self) -> int = self.v

struct Word
    w: string

impl Mapper for Word
    type Item = string
    first(self) -> string = self.w

print(One(20).over(n -> n + 1))
print(Word("hello").over(s -> s.len))
21
5

n is an int and s is a string, from one body that names neither. The arrow costs nothing at the call — it monomorphizes, so there is no closure on the heap — and it is the member’s own type parameter N that keeps over out of the table rather than anything about the projection.

some Trait — the result read off the body

The type an implementation chooses is often one nobody wants to write. A tree of nested containers is the case the feature exists for: the type is enormous, it is the implementation’s business, and it changes whenever the body does. some Trait in an impl member’s result says “the associated type is whatever this body produced, and it implements this”:

trait Render
    render(self) -> string

trait View
    type Body: Render
    body -> Self::Body

struct Text
    s: string

impl Render for Text
    render(self) -> string = self.s

struct Counter
    n: int

impl View for Counter
    body -> some Render = Text("count")

draw[V: View](v: V) -> string = v.body.render()

print(draw(Counter(7)))
count

No type Body = … line is written, and none is wanted: the body is the answer. Counter::Body is Text from that point on, so the two spellings are the same feature — one writes the type and the other reads it off the body.

some is a contextual word, special only in front of a bound in a result position. A program may still name a variable, a parameter or a function some.

Every path out of the member must produce one type. Two branches yielding different concrete types are an error, not a silent widening to the bound — the bound is what a caller may rely on, and it is not what the member returns. No rule of its own is needed for this: an if used as a value already has to agree with itself, and that is the rule that speaks.

trait Render
    render(self) -> string

trait Seq
    type Item: Render
    head(self) -> Self::Item

struct A
    n: int

struct B
    n: int

impl Render for A
    render(self) -> string = "a"

impl Render for B
    render(self) -> string = "b"

struct Box
    flag: bool

impl Seq for Box
    head(self) -> some Render = if self.flag then A(1) else B(2)

print(1)
if branches have different types: A and B

And the type may be one nobody else can name. A private struct supplied through a some result works and is genuinely opaque outside the file that declared it — a caller reaching it through a bound does what the bound allows and has no spelling for the type at all, which is what Swift’s some View buys by concealment and this gets from ordinary visibility.

It stands in that one position and nowhere else. A free function has no trait to settle anything for, so the reason to write one is missing and the refusal says where it belongs:

trait Render
    render(self) -> string

f(x: int) -> some Render = x

print(1)
'some Render' says the type is read off the body of a member that supplies a trait's associated type, so it stands only in an 'impl' block

The promise is checked against what the body actually produced:

trait Render
    render(self) -> string

trait Seq
    type Item
    head(self) -> Self::Item

struct Box
    v: int

impl Seq for Box
    head(self) -> some Render = self.v

print(1)
'head' promises 'some Render' and its body yields int, which does not implement 'Render'

What the implementation is held to

A written binding is held to the trait’s bound in the same way:

trait Render
    render(self) -> string

trait Seq
    type Item: Render
    head(self) -> Self::Item

struct Box
    v: int

impl Seq for Box
    type Item = int
    head(self) -> Self::Item = self.v

print(1)
trait 'Seq' asks that its associated type 'Item' implement 'Render', and int does not

And an implementation that supplies none has not implemented the trait, exactly as a missing method has not:

trait Seq
    type Item
    head(self) -> Self::Item

struct Box
    v: int

impl Seq for Box
    head(self) -> Self::Item = self.v

print(1)
'Box' does not implement 'Seq': the associated type 'Item' is missing

Self inside such a bound is the associated type, not the type implementing the trait. A bound asks something of the thing it is written on, and what type W: Add is written on is W — so Add‘s defaults, which are Self, are filled from W. type W: Add on an implementation supplying type W = u32 therefore asks whether u32 is Add[u32, u32], which it is, and never whether it can be added to the implementing type:

trait Holder
    type W: Add
    one(self) -> Self::W

struct N
    v: u32

impl Holder for N
    type W = u32
    one(self) -> u32 = self.v

sum[H: Holder](h: H) -> H::W = h.one() + h.one()

print(sum(N(21)))
42

It is the same reading Self has in a bound anywhere else, and it is stated here because the associated type is the one place where two types are in play and either would be a plausible answer.

And the bound is answered once every impl in the program is registered, so the block making the supplied type a member of it may be written below the block that chose it — the same rule a required trait follows, and for the same reason.

One name per type, because a projection does not name its trait

Box::Item says which type and which name, and never which trait. So a type may implement at most one trait declaring an associated type of any one name, and the second block is where that is said:

trait A
    type Item
    a(self) -> Self::Item

trait B
    type Item
    b(self) -> Self::Item

struct Box
    v: int

impl A for Box
    type Item = int
    a(self) -> Self::Item = self.v

impl B for Box
    type Item = bool
    b(self) -> Self::Item = true

print(1)
one type cannot have two of one name

Rust spells the qualified form <T as A>::Item; there is no such spelling here, and the collision is refused rather than left to be disambiguated at every use.

Declaring one spends the trait’s erasability, unless the object says which type it is

An erased value has forgotten which type it is, and an associated type is a function of exactly that — so the slot would have a different signature for every implementing type. A bare &Seq is therefore not something that can be formed, and the refusal names the associated type rather than leaving it to be discovered as a Self:

trait Seq
    type Item
    head(self) -> Self::Item

look(s: &Seq) -> int = 0

print(1)
'Seq' declares the associated type 'Item', whose meaning is the implementing type's — an erased value has forgotten which type that is, so there is no '&Seq' to form

A bound keeps the type, and so keeps the answer. [S: Seq] is what such a trait is for, which is the same place the operator catalog ends up and for a related reason. A trait that requires one is unerasable too, and the diagnostic names the trait the associated type came from.

An object may fix the associated type

The other answer is to write the type down. &Seq[Item = int] is a value of some forgotten type whose Item is known to be int — so every slot has one signature again, and there is a table to point at:

trait Seq
    type Item
    head(self) -> Self::Item

struct Box
    v: int

impl Seq for Box
    type Item = int
    head(self) -> Self::Item = self.v

show(s: &Seq[Item = int]) -> unit
    print(s.head())

show(Box(7))
7

Where a trait has no parameters of its own and exactly one associated type, the name may be left off. &Seq[int] and &Seq[Item = int] are then the same type — not two types with a conversion between them — so a function declared with either takes what the other made:

trait Seq
    type Item
    head(self) -> Self::Item

struct Box
    v: int

impl Seq for Box
    type Item = int
    head(self) -> Self::Item = self.v

named(s: &Seq[Item = int]) -> int = s.head()
bare(s: &Seq[int]) -> int = named(s)

print(bare(Box(4)))
4

The short form stops exactly where it could be read two ways. A trait with parameters and an associated type takes its arguments in order, at the front, and names the associated type beside them — &Keyed[int, Item = string] — because a bare argument there means one of the trait’s own:

trait Keyed[K]
    type Item
    at(self, k: K) -> Self::Item

struct Row
    v: int

impl Keyed[int] for Row
    type Item = int
    at(self, k: int) -> Self::Item = self.v + k

show(s: &Keyed[int, Item = int]) -> unit
    print(s.at(2))

show(Row(5))
7

The value put into the object has to have chosen the same type. That is what makes the binding sound rather than a promise: every slot’s signature was read under the object’s answer, so a value whose own implementation chose otherwise would be called through a table promising the wrong types. It is refused at the erasure, which is the one place both are known:

trait Seq
    type Item
    head(self) -> Self::Item

struct Box
    v: int

impl Seq for Box
    type Item = int
    head(self) -> Self::Item = self.v

show(s: &Seq[Item = string]) -> unit
    print(s.head())

show(Box(7))
a &Seq[Item = string] says 'Item' is string, and Box supplies int for it — an object fixes the associated type, so only a type that chose the same one goes into it

A binding is held to whatever the trait asked of the associated type, exactly as an implementation is; and a required trait’s associated type is bound in the same brackets, under the name it was declared with, since its members are slots in the same table.

A bound has no such spelling. [S: Seq[Item = int]] is not written, and nothing bounds a projection either — so a generic body may call what a bound licenses and may not assume anything about what comes back beyond what the trait itself promised of the type. Where a body needs more, the promise belongs on the trait’s own declaration: type Item: Display.

A trait may require another trait

trait Word: Add + BitXor — written after the name, with the same : and the same + a bound uses, because it asks the same thing of the implementing type.

trait Named
    label(self) -> string

trait Greet: Named
    greet(self) -> string = "hello, " + self.label()

struct P
    n: string

impl Named for P
    label(self) -> string = self.n

impl Greet for P

var p = P("ada")

print(p.greet())
hello, ada

A required trait is a promise the trait makes rather than one each declaration repeats. [T: Greet] then licenses label; a default body in Greet may use it, since what a default may assume is exactly what its trait promises; and a &Greet object carries the required trait’s members in its table.

The requirement is checked at the impl, not at the bound, and the diagnostic belongs on the declaration that cannot keep its word:

trait Named
    label(self) -> string

trait Greet: Named
    greet(self) -> string = "hi"

struct P
    n: string

impl Greet for P

print(1)
'Greet' requires 'Named', so 'P' has to implement that too — write 'impl Named for P'

Checking at the impl is also what keeps conformance a plain lookup: by the time anything asks whether a type implements a required trait, an implementation of it is already registered. The question is held until every impl has been seen, since the block supplying a required trait may be written below the one that needs it.

The table carries the required trait’s slots

A trait’s members are the required traits’ members, depth-first with each trait taken once, followed by its own. Both the table and the call sites indexing into it are laid out from that one list, so a required trait’s method is one indirect call, exactly like the trait’s own. The alternative — a word in the table pointing at the required trait’s own table — costs a second load on every such call and buys one thing sysl does not have: an upcast.

So a &Sub cannot become a &Super, and that is the price of the choice rather than an oversight. Nothing is unwritable for want of it: what a program does with a required trait is call its members, which works.

The diamond needs no rule of its own. D: A + C with both A: B and C: B carries B‘s members once, because the walk takes each trait the first time it reaches it. What is refused is two traits in one closure declaring a member of the same name — and the reason is the table, not the namespace. Two unrelated traits may each name a member of one type, because a call says which by naming the trait; two traits inside one requirement closure are laid out as one table, and a call through a &Sub has already forgotten everything that could have said which slot it meant.

trait L
    len2(self) -> int

trait R
    len2(self) -> int

trait Both: L + R
    both(self) -> int

print(1)
'R' and 'L' both declare 'len2', and a trait's members become the implementing type's — so 'Both' cannot require both

A trait may not require itself, directly or around a cycle:

trait Loop: Loop
    step(self) -> int

print(1)
trait 'Loop' requires itself, through Loop -> Loop

Two rules close the section. A trait may not require one that reaches less far than it does, since implementing the trait means implementing the required one — a requirement the implementer cannot name leaves the trait unimplementable from outside. And Self in a requirement’s arguments is the type implementing the requiring trait, so trait Vector: Scale[Self] asks that whatever implements Vector can be scaled by its own type — which is the same requirement trait Vector: Scale writes when Scale defaults its parameter to Self.

Trait objects

A trait object is a fat pointer — two words, the method table for the type it forgot and the value itself:

{ ptr vtable, ptr data }

The sigil says who owns the second word, and nothing else changes between the two:

writtenthe data word iswho frees it
*Traitthe value’s own addressnobody — raw and unmanaged, like every *T
&Traitthe reference-counted box the value sits inARC, exactly as for the &T it was erased from

*T and &T on a concrete type are thin pointers; on a trait they are fat. The trait-ness makes them fat, and that is why there is no dyn.

The table is per (trait, type, sigil), two flavours rather than one because the data word means different things: an entry has to reach a receiver, and from a box that is one step further in than from a bare value. Where the data word already is the receiver an implementation declared, the entry names that implementation itself; otherwise it names a small adapter that steps over the box header, loads the value, or both. So the common case costs one indirect call and nothing else.

A table is in the output only where a function the program reaches erases a value into it, and what it points at comes with it. The standard library’s own tables follow the same rule: a body of the library that makes an object, reached from nowhere, keeps neither the table nor the functions it names, so a board image that never makes a Buf carries no malloc on account of Buf‘s Writer table. Erasing a library type in the program’s own code keeps that type’s table:

main()
    var c = Counting(0)
    val w: *Writer = &c

    w.write("abc".bytes)
    print(c.n)
3

Object safety

Erasure forgets the type, so a member may promise nothing that depends on knowing it. A trait may be made into an object when it declares no associated type — the meaning of one is the forgotten type — and when every member:

  • has a receiver. An associated function has nothing to dispatch on. A property does have one — by value, and unwritten — so a trait asking for a property is as safe to erase as one asking for a method.
  • mentions Self nowhere but that receiver. A second Self would have to be the same forgotten type as the first, which is exactly the fact an object no longer carries, and a Self result has no size to hand back.
  • does not take &self, for a *Trait only. &self asks for its receiver inside a box, and a raw object points straight at a value. A &Trait carries one, so it accepts such a member.
  • takes no .... A call to a variadic names the callee’s whole function type, because that is how it says where the declared parameters stop and the tail begins, and a slot in a table is one word and names none.
trait Scale
    scale(self, k: int) -> Self

struct P
    v: int

impl Scale for P
    scale(self, k: int) -> P = P(self.v * k)

grow(s: &Scale) -> int = 0

print(1)
'scale' of 'Scale' mentions 'Self' away from its receiver, and an erased value has forgotten which type that is — so there is no '&Scale' to form

The middle rule excludes every trait in the operator catalog — add(self, rhs: Self) -> Self first among them — and that is the right answer rather than a limitation: those traits describe an operator over two values of one type, which is a question about types known at compile time. They are for bounds.

A trait that requires an unerasable one is unerasable itself, and the diagnostic names the trait the offending member came from. A built-in that satisfies a requirement by the compiler’s rule cannot be erased through it either — a table holds function pointers, and a scalar’s add is an instruction.

What that bites is the operator catalog at written arguments, and only that. Add[int, int] declares add(self, rhs: int) -> int — no Self anywhere — so it is a formable object type, and an int belongs to it by the compiler’s rule; &Add[int, int] = 3 is therefore refused, and the diagnostic says why rather than reporting a plain mismatch.

Display and Hash are not among them: every built-in reaches both through an impl, so a *Display carries an int, a u256, a string or a float alike, a &Hash carries anything that hashes, and a heterogeneous array of either is ordinary code. The rest of the catalog — Eq, Ord, Bits, Signed — names Self away from the receiver, so object safety refuses the type before a value gets that far.

A member with its own type parameters leaves the table, not the trait

The four rules above decide whether a trait has an object at all. A member that declares type parameters of its own is a different case and costs less: it is not a function until a call names its types, so no slot can point at it — and nothing about the other members changes. The object forms, and dispatches them:

trait Applies
    tag(self) -> int
    apply[U](self, f: &Fn(int) -> U) -> U

struct N
    v: int

impl Applies for N
    tag(self) -> int = self.v
    apply[U](self, f: &Fn(int) -> U) -> U = f(self.v)

reach[T: Applies](x: T) -> string = x.apply(n -> s"<${n}>")

val o: &Applies = N(3)

print(o.tag())
print(N(3).apply(n -> s"<${n}>"))
print(reach(N(4)))
3
<3>
<4>

Three routes to that member and only one of them is closed. On the value it is an ordinary call; a bound solves the member’s own parameters at the instantiation, which is what reach is; and the object is the one that cannot, because a table is what it dispatches through:

trait Applies
    tag(self) -> int
    apply[U](self, f: &Fn(int) -> U) -> U

struct N
    v: int

impl Applies for N
    tag(self) -> int = self.v
    apply[U](self, f: &Fn(int) -> U) -> U = f(self.v)

val o: &Applies = N(3)

print(o.apply(n -> s"<${n}>"))
'apply' of 'Applies' declares type parameters of its own, so it is not a function until a call names them

What follows for bounds is the one consequence worth carrying: an object does not satisfy a bound on such a trait, because a bound promises every member and the object reaches only its table (generics). That is the exception to a rule which is otherwise total, and it is stated there.

This is what lets a trait carry a map. A member choosing its own result type is the whole reason sysl.seq can exist — map[U] names U at the call and nowhere else — and the price is a member no object dispatches, paid by traits that declare one and by nothing else.

A bare-arrow callable is the same case wearing different syntax, and it is the one a trait meets most often. f: T -> U is sugar for a type parameter bounded by Fn(T) -> U (types), so a member written that way declares one whether or not its author was thinking about type parameters — and loses its slot for it. That is the trade sysl.seq takes on purpose: nothing is boxed at any call into it, and &Sequence[int] dispatches none of its members. The boxed spelling keeps the slot, which is what a trait meant to be erased writes.

Forming and using one

Erasure is a coercion, applied wherever a trait-object type is expected: at an argument, a declared variable, an assignment, a returned value, an array element, a struct field. &r erases to *Shape; a &Rect erases to &Shape; and a plain Rect(3, 4) where a &Shape is expected is boxed and then erased, which is the ordinary “write the construction and it is allocated” rule with one more step.

A *Trait will not take a bare value. A raw pointer needs an address, and taking one silently is how a program acquires a dangling pointer without a line to point at:

trait Shape
    area(self) -> int

struct Rect
    w: int

impl Shape for Rect
    area(self) -> int = self.w

var s: *Shape = Rect(2)

print(s.area())
a *Shape points at a value, so it needs an address — write '&' in front of the Rect to take one

And writing it is all it asks: &Rect(2) gives the value a hidden local of that scope and hands back its address, so the fix the diagnostic names is one character rather than a second declaration. What stays refused is the silent case, which is the one above — see Addressing a value.

Because the coercion applies per branch, an if or a match whose arms are different concrete types meets at one trait object, which is the point of having them:

trait Shape
    area(self) -> int

struct Rect
    w: int
    h: int

struct Square
    s: int

impl Shape for Rect
    area(self) -> int = self.w * self.h

impl Shape for Square
    area(self) -> int = self.s * self.s

var wide = true
var s: &Shape = if wide then Rect(2, 3) else Square(4)

print(s.area())
6

What an object offers is the trait’s members and nothing else: no dereference, no fields, no comparison.

trait Shape
    area(self) -> int

struct Rect
    w: int
    h: int

impl Shape for Rect
    area(self) -> int = self.w * self.h

var s: &Shape = Rect(2, 3)

print(s.w)
a &Shape has no fields, and trait 'Shape' declares no 'w'

A call is checked against the trait’s signature, which stands in for every implementation because conformance is exact.

An object keeps one trait and what that trait requires. A bound may name several traits because a bound is a list; a trait-object type names one because it is a type. So a value implementing Shape and Display keeps only the first when it becomes a &Shape — unless Shape requires Display, which is what makes the difference between the object being printable and not. A multi-trait object type (&(Shape + Display)) would be a second way to say the same thing and a worse one, since it puts at every use a fact that belongs on the trait.

The two sigils do not convert

*Trait and &Trait are two types and neither is accepted for the other.

In the direction that would matter — lending a counted object to something that only wants to ask it questions — this is sharper than it is for plain references, which have a spelling for it: &*r is the address of the place *r, so a &T reaches a function written against *T with the crossing into the unsafe tier written down at the call. An object has no dereference, so &*o says nothing, and a function that only reads a shape has to exist once per sigil. That is recorded as a gap rather than settled; what is missing is a spelling, and any spelling must keep the crossing greppable.

The other direction stays refused for a stronger reason: a raw object points at a value with no count to take a share of, so accepting one where a counted object is wanted would be inventing ownership.

An object cannot be erased a second time

trait Shape: Display puts Display‘s slots in a &Shape‘s table, and a [T: Display] bound is satisfied by the object — but a &Display is not, and the difference is worth being exact about. A bound asks what may be called through the value, and the table answers it. Forming an object asks what may be assembled from the value’s type, and a table is laid out from a type’s implementations, which an object has none of.

trait Shape: Display
    area(self) -> int

struct Rect
    w: int
    h: int

impl Shape for Rect
    area(self) -> int = self.w * self.h

impl Display for Rect
    display(self, out: *Writer, fmt: FormatSpec) = display_pad("a rect".bytes, out, fmt)

var o: &Shape = Rect(3, 4)
var d: &Display = o
a &Shape has forgotten which type it holds, so there is nothing for a &sysl.Display to be built from

Display‘s members really are in that table — they are laid out inline, so that a required trait’s method stays the one indirect call the trait’s own methods are. What is missing is a name for that run of slots. This is the upcast flattening gives up, and it is the reason it is worth giving up: every call through a &Shape costs one indirection instead of two.

There is no way back to the type

An object cannot be asked what it forgot. There is no cast and no test, and the spelling a reader reaches for first is not one either — a type is not a pattern, so s is Rect reads Rect as an ordinary binding, which matches anything:

trait Shape
    area(self) -> int

struct Rect
    w: int
    h: int

impl Shape for Rect
    area(self) -> int = self.w * self.h

var s: &Shape = Rect(2, 3)

if s is Rect then print("yes") else print("no")
this pattern matches every &Shape, so the test is always true — take the value apart with 'match', or bind it with 'var'

This is a decision, not an omission. A downcast is the one operation that makes erasure a lie: every other rule here says an object offers the trait’s members and nothing else, and a type test would say that it also secretly offers its identity, which is what the table pointer is and what the type deliberately stops promising. Languages that offer it need a whole parallel mechanism to do so, and that mechanism is the honest price rather than a small addition to this one.

The identity is readable, and it is not a way back

o::Id answers which type is inside an object, as a usize. It is the one fact about the forgotten type that an object still carries, and it carries it because the method table it points at begins with it:

trait Shape
    area(self) -> int

struct Rect
    w: int

struct Sq
    s: int

impl Shape for Rect
    area(self) -> int = self.w

impl Shape for Sq
    area(self) -> int = self.s

var a: *Shape = &Rect(2)

print(a::Id == Rect::Id, a::Id == Sq::Id)
true false

This is not the downcast the section above refuses, and the difference is the whole of why it is here. The id compares — two values with the same one hold the same type — and there is nothing else to do with it: no map from an id back to a type, no test that changes what an object offers, and no Any. What it makes possible is the two things a catalogue of erased values actually wants, and neither is a cast: a key, for asking whether the node here is the same kind as the node that was here before, and the first half of a memo table’s key.

So the cost the decision used to carry is paid. A program counting the circles in a catalogue declared a kind property that every implementation answered with a constant — a hand-maintained copy of exactly the fact the object’s first word already is — and it can ask the word instead:

trait Shape
    area(self) -> int

struct Rect
    w: int

struct Sq
    s: int

impl Shape for Rect
    area(self) -> int = self.w

impl Shape for Sq
    area(self) -> int = self.s

var xs: [3]*Shape = [&Rect(1), &Sq(2), &Rect(3)]
var rects = 0

for x in xs
    if x::Id == Rect::Id then rects += 1

print(rects)
2

T::Id is how a type that is known asks, and ::Id on a value is admitted only where the value is erased — which is the case that says something the static type does not:

struct Rect
    w: int

var r = Rect(2)

print(r::Id)
'::Id' on a value reads the identity an erased value carries

What it guarantees is that equal ids mean the same type, and nothing else. It is not stable across releases — it is derived from the type’s name, and the naming is free to change — it is not a number to write down anywhere, and two compilations of one program agree only because they compute the same thing from the same name. A generic body asks through its parameter, T::Id, which is answered once per instantiation.

A method may promise to borrow

A call through a trait object is opaque: which body it reaches is settled at run time, so escape analysis has to assume the worst of every argument. A view of an array in your own frame passed through one could be kept by whatever is behind it, so the array goes on the heap.

@borrows is how a trait says otherwise — and the compiler holds every implementation to it, so the promise is checked rather than trusted:

trait Sink
    @borrows(bytes)
    put(*self, bytes: []const u8)

struct Counter
    n: usize

impl Sink for Counter
    put(*self, bytes: []const u8)
        self.n += bytes.len

var c: Counter
var s: *Sink = &c
var buf: [8]u8

buf[0] = 7u8
s.put(buf[0..<4])

print(c.n)
4

Without the annotation that program is identical and buf is allocated. With it, the frame keeps its storage — which is what makes rendering allocation-free, since a renderer writes into a buffer on its own stack and passes a slice of it. sysl.Writer declares exactly this, and is an ordinary user of the feature rather than a case the compiler knows about.

An implementation that keeps what it was lent is refused, by the name the trait gave the parameter:

trait Sink
    @borrows(bytes)
    put(*self, bytes: []const u8)

struct Bad
    held: []const u8

impl Sink for Bad
    put(*self, bytes: []const u8)
        self.held = bytes

var b: Bad
var s: *Sink = &b
var buf: [8]u8

s.put(buf[0..<4])
keeps what it is passed as 'bytes', but 'Sink' declares that parameter borrowed

Reading out of the view is not keeping it, which is the distinction the check draws and the reason it is a question about what the body does rather than about how it is written.

It names parameters rather than being a flag, because a method may take a buffer it borrows and something it legitimately retains. A name that is not a parameter is refused: a misspelling would promise nothing and say nothing, which reads exactly like a rule being enforced.

It says nothing above a free function and is refused there. That body is right in front of the compiler and the analysis reads the answer out of it — a written promise would restate what is already known and go stale the moment the body changed.

@borrows(xs)
take(xs: []const u8) -> usize = xs.len

print(take([1u8][..]))
'@borrows' is about a call whose body the compiler cannot see

Reaching a trait’s members without a value

A trait may declare a member with no receiver — an associated function — reached through the type rather than through a value of one:

trait Origin
    origin() -> Self

impl Origin for int
    origin() -> int = 0

start[T: Origin]() -> T = T.origin()

var n: int = start()

print(n)
0

T.bits() inside a generic body, Self.bits() inside a member’s, and u32.bits() from anywhere are the same member reached through three spellings of its type.

A built-in may carry one. Through a bound the name is the parameter, which every type has whether or not it has one of its own — so impl Word for u32 may declare bits(), and impl Float for real declares the epsilon and the two values no literal spells. What stays refused is the case the rule was really about: a composed type has no name at all, so an impl for []int still refuses a member with no receiver.

The library’s own pair is Zero and One, which are this feature at its smallest: one member each, no receiver, and nothing else a generic body could use to write down the value it has to start an accumulation from. They are in the standard module, so T.zero() under a [T: Add + Zero] needs no import — and a T that is itself generic brings its own arguments, so the answer at T = Complex[real] is a Complex[real] with nothing written at the call.

They are also where a receiverless member is compiler-provided rather than written, which is the half a built-in could not have before. Every integer type is a member of both, and there is no block for it anywhere: the iN/uN families are open, so a membership over them has to be a rule, and a rule about a member with no receiver has to say what it lowers to rather than reading it off a value. T.zero() at an integer is the literal, so a bounded accumulator costs no call. Expressions has the rule and the two types it stops at.

It may carry a default, exactly as a member with a receiver may, and it is inherited and replaced by the same rules: an implementation that leaves the member out gets the trait’s body, and one that wants its own says override. That is what lets a trait made entirely of receiverless members grow — sysl.crypto‘s internal Compression gained a member saying which end of a word goes first, and the three compressions for which the answer was already the common one needed no edit.

trait Sized
    width() -> usize
    doubled() -> usize = Self.width() * 2

struct Word
end Word

impl Sized for Word
    width() -> usize = 4

show[T: Sized]() = print(T.width(), T.doubled())

show[Word]()
4 8

The one member that may not carry a default is a static property, and its reason is its own rather than anything about receivers: it has no parameter list and no receiver, so there is nothing available to vary what it would hand every implementation.

trait Maker
    static size -> int = 1
'Maker.size' is a property of the type, so a default body would give every implementation the same value — drop the body and let each one supply it

It is static dispatch only, and nothing was added to keep it that way. Object safety already excludes a member with no receiver, because a table slot is selected by the receiver and there is nothing here to select with. A trait declaring one is usable as a bound and not as an object, exactly as one that mentions Self twice already is.

One gap the mechanism makes visible: a routine can be entirely about a type and mention it nowhere in its signature, and such a function cannot be called, because inference reads the binding and a call cannot write its type arguments. describe[T: Word]() -> string is well-formed and unreachable. Taking a value of T is the workaround.


Next: generics.

Search

Esc
↑↓ to navigate ↵ to open Esc to close