Download

4. Type System

Primitive types#

Category Types
Boolean bool
Unsigned integers u8, u16, u32, u64
Signed integers i8, i16, i32, i64
Floats f32, f64
Text char (a Unicode scalar value), string (UTF-8)
Foreign pointer CPointer (§13)
Null null
Metatype type (compile time only, below)
flag: bool: true
byte: u8: 255                    // u8 is Int in 0..=0xFF, declared in std/core
offset: i64: -9000000000
ratio: f32: 0.5
letter: char: 'é'                // one Unicode scalar value
name: string: "héllo"            // UTF-8
missing: string | null: null

name.len()                       // 5, an i64
@size_of(u16)                    // 2, an i64
count: int: 0                    // error: there is no type `int`
  • The eight integer types are ranges of Int (Range types, below), declared in the std/core prelude: a program built without the standard library has none of them.
  • Each numeric type has one name, giving its kind and width: no int, long, float, double or byte.
  • No integer type takes the width of the target: a foreign size_t is a u64 that @c_type spells for C (§13), and a CPointer reads as an i64 or a u64.
  • A length, an index and a size are an i64 on every target: len(), byte_len(), @size_of(T).
width :: fn<T: type>(x: T) -> i64 = @size_of(T)

width(byte)                      // 1: T is u8, a value of type `type`
word :: u16                      // a local alias of a type written out
@size_of(word)                   // 2
@size_of(@type_of(offset))       // 8: the static type of offset, unevaluated
picked :: if flag { u8 } else { u16 }   // error: a local of type `type` is only a type written out

type is the metatype, whose values are Vader types, known only at compile time: in a generic head <T: type> and as the result of a reflection intrinsic such as @type_of(x). A local of type type must be an alias of a type written out (name :: <type-expr>).

Type aliases#

A type alias, Foo :: <type-expr>, is a constant whose value is a type expression, built from type names and the operators |, &, [] and fn(...) -> ... (type is not a keyword). Its name stands wherever a type is expected. Some right-hand sides declare a distinct type instead (Distinct types, below).

A generic alias puts its type parameters in angle brackets between the name and ::: Foo<T> :: <body>. There is no bracket or parenthesis form.

Mixed :: i32 | string        // alias: a constant whose value is a type
Maybe<T> :: T | null         // generic alias: type parameters after the name
Pair<A, B> :: A | B

opt: Maybe<i32>: 42

Distinct types#

Name :: <type-expr> declares a distinct type, a nominal newtype, when the right-hand side is a primitive other than void and null, an array (u32[]), a struct (Rgb) or a tuple ([i32, i32]). A union, an intersection or a function type on the right gives a transparent alias.

On the right, a bracketed list of two or more types is a tuple; with a value among its elements (["a", "b"]), it is an array constant.

Celsius :: f64            // distinct, not interchangeable with f64
Handle  :: i32            // distinct
BigInt  :: u32[]          // distinct, over an array
Color   :: Rgb            // distinct, over a struct
Coord   :: [i32, i32]     // distinct, over a tuple
Mixed   :: i32 | string   // transparent alias (union)

A distinct type is held exactly as its backing type at run time.

  • Conversions are explicit both ways and change nothing at run time: Celsius(x) wraps an f64 and f64(c) unwraps it. An implicit one is an error.
  • Structural operations forward to the backing, while trait impls stay nominal (BigInt implements Mul<u32> does not collide with the array repeat, T[] implements Mul<i64>):
    • array: indexing, the built-in array functions (.len(), .push(), ...), for x in n, and the conversion of a [...] literal;
    • struct: field access c.r and construction Color { ... }, with the backing's fields;
    • tuple: positional access p.0, construction from [a, b], and destructuring [a, b] :: p.
  • A distinct over an object is not a value of a trait it implements: passing a distinct over a struct, an array, a tuple or a function where its trait is expected (a Color for a Display) is an error, as an array element or into a union too. Its own impls are reached by a direct call (c.to_string()).
  • Arithmetic uses the distinct's own impl first, then its backing's: Instant implements Add<Duration> drives i + d, while +, -, *, / and % on a Meters :: i64 with no impl of its own are those of i64. A literal beside it is then a value of the backing: m + -1 is an i64 sum, and c + -1 with Code :: u32 is an error, as on a u32.
  • A distinct that defines the operator for another right operand does not fall back to its backing: instant + 5 is an error.
  • A distinct combines only with itself and its backing. Any operator, == included, between two distinct types that are not one another's backing is an error: Meters + Seconds, although both are i64. A Km :: Meters combines with Meters. Against its backing, a distinct compares and computes as that backing (path == "", d < i64(3)), or through its own impl (Equals, Comparable, operators) when it defines one.
  • @comptime Name :: <type> is the exception: it declares a transparent alias, usable at compile time (@size_of(Name)), never a newtype.
  • Reflection follows the backing: @size_of, @align_of and @type_kind of a distinct give those of its backing.

Default integer#

x :: 42                          // an i32: no context
y :: 2.5                         // an f64
big :: 9000000000                // error: does not fit an i32, the default
wide: i64: 9000000000            // the annotation gives the type
n + 9000000000                   // an i64, with n: i64

low: i8: -128                    // one literal, not a negation
over: u8: 300                    // error: 300 is not in u8
unsigned: u32: -1                // error: -1 is not in u32

An integer literal with no concrete context is an i32, a float literal an f64.

A literal in a concrete integer context is checked against that type at compile time, as a char literal is (§3 Char literals), and never truncates or wraps.

show :: fn(v: u8 | string) -> string = "${v}"
pick :: fn(v: i32 | i64 | string) -> string = "${v}"

show(5)                          // 5 is a u8, the union's only integer
5.show()                         // the same, as a UFCS receiver
show(300)                        // error: 300 is not in u8
pick(5)                          // 5 is an i32, the default
v: u8 | i16 | string: 5          // error: u8 or i16; write u8(5)

A literal into a union takes the default of its family (i32, f64, or char for a char literal) when the union has it, otherwise the union's only member of that family.

Implicit numeric coercion#

b: u8: 200
i: i32: 1000
w: i64: b                        // a u8 into an i64: nothing to write
n: u8: i                         // error: an i32 into a u8 takes u8(i)

b + i                            // 1200, an i32: the type that holds the other
u: u32: 7
u + i                            // error: neither u32 nor i32 holds the other

describe :: fn(x: i32) -> string = "i32"
describe :: fn(x: i64) -> string = "i64"
describe(b)                      // "i32": the tightest parameter wins

An integer type goes into any integer type that holds all its values, with no conversion written. This holds in every slot (an argument, a return value, a field, an initializer, an assignment, an array or tuple element) and into a union through such a member.

An operation or comparison between two integer types is computed in the type that holds the other; when neither holds the other, it is an error. Where paths join (the branches of a value if or match), the same rule applies.

Among overloads, the parameter that holds the argument most tightly, in the fewest bits, wins. Two parameters of as many bits tie.

bytes: u8[]: [1, 2]
ints: i32[]: bytes               // error: no widening inside an array
maybe: i32 | null: b             // a bare u8 goes into i32 | null
r: f64: i                        // error: an integer into a float takes f64(i)

The widening does not reach through a container, a function type or a union: a u8 | null is not an i32 | null. A float never widens: an f32 into an f64, and an integer into a float, take a cast.

Float bit patterns#

A cast converts the value (u64(3.7) is 3). The IEEE 754 bit pattern of a float, the same bits unconverted, is read with to_bits and from_bits:

x: f64: 3.0
bits :: x.to_bits()        // u64 0x4008000000000000, the exact IEEE bits
back :: bits.from_bits()   // f64 3.0, round-trips bit-for-bit

Both are in the prelude: f64.to_bits() -> u64 (FloatBits), u64.from_bits() -> f64 (BitsFloat), f32.to_bits() -> u32 (Float32Bits) and u32.from_bits() -> f32 (Bits32Float).

Numeric-literal context-sensitivity#

Page :: struct {
    size: u64 = 4096                    // a field default: a u64
}
larger :: fn<T: Comparable>(a: T, b: T) -> T = if a > b { a } else { b }

limit: u16: 1000                        // a typed declaration: a u16
mask: u64: ~0                           // through ~: 18446744073709551615
next :: limit + 1                       // the other operand: a u16
top :: larger(limit, 3)                 // T is u16, so 3 is a u16
size :: if flag { 0 } else { capacity } // the other branch: a u64, with flag: bool, capacity: u64
limit + 70000                           // error: 70000 is not in u16

zero :: fn<T>(x: T) -> T = 0            // error: T is not a numeric context

A free literal takes the type of the slot it lands in, with no conversion at run time. The type comes from:

  • the slot: a typed declaration, a field default, a call argument, a struct literal field, a return under a declared return type, an index (the key type of the Index impl), and the other operand of a comparison or arithmetic operation when it has a concrete numeric type;
  • the outer slot, through unary - and ~;
  • a generic call: the type substituted once the type parameters are inferred, in a plain call, a generic UFCS call and a call through a namespace import;
  • the other branch of an if, when that one has a concrete numeric type.

Type coercion via Into#

A value of type S that reaches a slot of type T it is not assignable to is converted by a call to into when S implements the std/core trait Into<T>:

Into :: trait<Target> {
    into :: fn(self) -> Target
}

Sites of implicit coercion#

  • A call argument, when the parameter is annotated.
  • A return v, or a block's trailing expression, under a declared return type.
  • The initializer of a typed declaration x: T = v.
  • A struct literal field whose declared type differs from the value's.
  • The receiver of a.f(...), when no function takes a as it is (§6 UFCS (Uniform Function Call Syntax)).

Explicit form#

In Target(value), a numeric or char target is a cast (u32(x), i64(c)); any other target, or a numeric target given a non-numeric value, calls into.

Rules#

  • The target is a struct, an enum, a primitive or a trait, never a union or a type parameter. For a trait target, Into applies only when the type does not implement the trait (T[] into Iterator<T>).
  • T implements Into<T> is an error at the impl.
  • A conversion calls one into: S into U and U into T do not take an S into a T.
  • A second impl for the same (Source, Target) is an error.
  • Overload resolution comes first: Into is considered only when no overload matches as is.

Built-in coercions#

  • T[] into Iterator<T>, through T[] implements<T> Into<Iterator<T>> (std/core).
  • T into string when T: Display, by its to_string, through T implements<T: Display> Into<string>. Interpolation ("${value}") renders through Display without Into.

Resolution order at a coercion site#

  1. S is assignable to T (a subtype, a trait S implements, a free literal taking its type): nothing converts.
  2. Otherwise, an impl S implements Into<T>, under the rules above.
  3. Otherwise, it is an error.

Integer overflow#

b: u8: 250
b + 10                           // stops the program, or 4 in a release build
b.add(10)                        // the same as b + 10
b +% 10                          // 4 in every mode
0 -% b                           // 6: -b, wrapped

LIMIT: u8: 250
@comptime NEXT :: LIMIT + 10     // error: overflow while folding a constant
LATER :: LIMIT + 10              // computed at run time: stops the program, or 4 in a release build

An integer +, -, * or unary - whose result leaves its type, at that type's own bounds, stops the program, except in a release build, where it wraps, in two's complement at the type's width (docs/TOOLING.md vader build).

An overflow in a constant the compiler folds is a compile error.

The wrapping operators +%, -% and *% (and +%=, -%=, *%=) wrap in every mode and when a constant folds, at the width of their operands, 8 and 16 bits included. They take integer operands only, and no trait overloads them. There is no wrapping negation.

A division by zero stops the program in every mode (§2 Run-time checks). A narrowing cast truncates (Casts, below), and a shift never stops the program (§3 Operators).

Range types#

A range type holds the integers of an interval. It is declared over the abstract integer Int, or written in place of a type:

Hour :: Int in 0..=23
Offset :: Int in -12..=14

Clock :: struct {
    hour: Hour
    minute: 0..<60
}
  • The bounds are integer literals, the least first, within i64 (the greatest may reach u64.max). Int in 5..=1, a bound that is not a literal, and a base other than Int are errors.
  • Int only names the base of a range: a value, a parameter or a field typed Int is an error.
  • Name :: Int in ... is a transparent name for the range; Slot :: Hour declares a distinct type (Distinct types).
  • In value position, r :: 0..=23 is a Range<i32> value, not a type.

Each machine integer is the range of its values (u8 is Int in 0..=255), and a range with the same bounds is that integer: Octet :: Int in 0..=255 is a u8 shown under its name, with the closed and checked arithmetic of a u8 (Integer overflow).

A range is stored, in a field or an array alike, in the narrowest integer that holds it, unsigned when it has no negative value: Hour is a u8, Offset an i8. Its Display, Equals and Hash are that integer's.

A literal stored in a range must be one of its values, wherever it goes: h: Hour = 30 is an error, and so is 30 -> in a match on an Hour.

A range widens to any integer type that holds all its values (Implicit numeric coercion, above). Through a function type or a union, it widens only to the integer it is stored in: a fn() -> Hour is a fn() -> u8.

next_hour :: fn(h: Hour) -> Hour = (h + 1) % 24   // h + 1 is a 1..=24

part_of_day :: fn(h: Hour) -> string {
    if h < 12 {
        return "morning"                           // h is a 0..=11 here
    }
    return "afternoon"                             // and a 12..=23 here
}

parse_hour :: fn(n: i64) -> Hour | null {
    if n in Hour {
        return n                                   // n is an Hour: nothing left to check
    }
    return null
}

Arithmetic on ranges is computed over the integers, and its type is the interval of its possible results.

  • Storing a result in a narrower range is an error: next: Hour = h + 1 and h += 1 are rejected.
  • A local takes the type of its first value: after next := h + 1, next = next % 24 is an error, since 0 is outside 1..=24. Annotate it (next: i32 = h + 1).
  • A primitive operand absorbs a range that fits in it: h * x with x: i32 is an i32.
  • A bitwise operation needs a machine integer: h & 1 is an error, u8(h) & 1 is not.

A comparison narrows a range where it holds, as is narrows a union (§3 Type tests and narrowing).

@min(T) and @max(T) are the bounds of an integer type (@max(Hour) is 23). x in T tests an integer against them and, where it holds, narrows x to T, a primitive x too, with no check that could stop the program. in, @min and @max also take char and an enum without variant data; on any other type they are errors.

Hour(x) converts an integer into the range and stops the program, in every build, when x lies outside it. A literal outside the range, a float, and a source that holds none of its values are errors. A distinct over a range converts as the range does, and back (Hour(s)) with nothing to check.

In a generic function, a T bound to a range is checked as Hour(x) checks it wherever a call or an operator gives one back: fn<T: Add>(a: T, b: T) -> T = a + b stops at Hour values 20 and 10. There, inside a type argument, a range is read as its integer (a Box<Hour> is a Box<u8>), except as an array element (Hour[] binds T to Hour).

A function generic over integers declares T: Int, which takes any integer type within i64, range or primitive, alone or beside traits (T: Int & Display). In the body, @min(T), @max(T), x in T and T(x) work as on a range type, and a value of T computes as an i64. A computed value becomes a T again through x in T or T(x):

clamp_to :: fn<T: Int>(n: i64) -> T {
    if n in T {
        return n
    }
    if n < @min(T) {
        return @min(T)
    }
    return @max(T)
}

wrap_next :: fn<T: Int>(x: T) -> T =
    T((x - @min(T) + 1) % (@max(T) - @min(T) + 1) + @min(T))

Details.

  • Operations: / truncates toward zero and % takes the sign of the dividend, as at run time. A result past 64 bits is an i64, checked like any other. A char literal stands for its code point (b - '0' past b in '0'..='9' is a 0..=9). A comparison gives a bool.
  • A primitive beside a range that holds it reads as the range of its values (b + r with b: u8 and r: 0..=300 is a 0..=555); a range that neither fits nor holds the primitive is an error. A shift's count may be a range.
  • Narrowing applies to a binding or a field chain, with the logic of !, &&, || and a guard that leaves the block. The other side of the comparison is a literal or a range value, read by its bounds: h < k with k: 1..=5 holds h within 0..=4.
  • == narrows to the compared values; != removes a value only at an end (h != 23 gives a 0..=22, h != 12 the whole Hour).
  • Ranges join at a true || (if h < 6 || h > 20 sees an Hour), at the branches of an if or a match, and at the returns of a function whose result type is inferred; a free literal branch stands for its value. A union written in the source keeps its ranges apart, and a side no value reaches is never.
  • A write to a narrowed binding, a destructuring one included, ends the narrowing and is checked against its declared range (§3 Assignment): x = 0 is legal where x == 23 holds.
  • A comparison does not narrow a machine integer, named or primitive. Where x in T fails, a range x is narrowed past the end T covers, a primitive x is not narrowed. When the type of x decides the test, it is a constant (x is still evaluated). x !in T negates it, and x in 0..<12 with literal bounds narrows the same way, though the test stays the contains of Range.
  • A narrowed value is a value of the range for what follows: past if n !in Hour { return } on an i64, n * 1000 is a 0..=23000, a := n an Hour, [n, n] an Hour[] of bytes, and n << 8 an error. Narrowed to the bounds of a machine integer, it is that integer: past n in u8, acc := n is a u8, and acc = acc * 2 stops the program once acc passes 127.
  • A comparison with @min(T) or @max(T) narrows as its literal does. The number of values of T is @max(T) - @min(T) + 1; x in char narrows nothing.
  • lo..=hi on two Hour values is a Range<Hour>, and h..=30 on an Hour a Range<0..=30>: the element is what the bounds join into.
  • Octet(x) converts as u8(x) does (Casts), with no check that stops the program. A member returning Self returns the integer (h.add(k) is a u8), and is not checked inside another type (Self | null).
  • In a fn<T: Int>, a non-integer source of T(x), a value of a non-Int type parameter included, is an error; a value of T goes where an i64 goes (i64, i64 | null), never into a narrower slot.

Strings#

greeting :: "héllo, wörld"

greeting.len()                    // 12: code points
greeting.byte_len()               // 14: bytes, since é and ö take two each
greeting[1]                       // 'é'
greeting[0..<5]                   // "héllo"
greeting == "héllo, " + "wörld"   // true: == compares the contents

for c in greeting {               // c: char, one code point at a time
    ...
}

bytes :: greeting.bytes()         // the UTF-8 bytes, a frozen u8[] view
copy :: bytes.clone()             // a u8[] of its own (std/iter), which may be written
greeting.byte_at(1)               // 195, the first byte of é
  • A string is immutable UTF-8 text, and a value: an assignment copies it, == compares the contents, + gives a new string. Nothing writes into a string (there is no IndexSet impl).
  • s.len() counts code points, in O(n); s.byte_len() counts bytes, in constant time.
  • s[i] is the code point at index i, an i64 or a free literal; an index past the last code point stops the program. s[lo..<hi] and s[lo..=hi] give the code points of the range: bounds past the end are clamped, and a range that ends before it starts gives "". Both reach index i in O(i). On an ASCII string (s.is_ascii()), s.len() and s[i] take constant time.
  • for c in s walks the code points in place (string implements Into<Iterator<char>>), and s.chars() gives them as an iterator value. The byte loop is for i in 0..<s.byte_len() { b :: s.byte_at(i) }.
  • s.bytes() is frozen (§5 Frozen values): writing it, lending it to a function that modifies it, or keeping it in a field is an error. s.byte_at(i), s.byte_slice(lo, hi) and s.codepoint_at_byte(i) (the code point that starts at a byte offset, and its length in bytes) read the string in place.
  • Nothing validates a string's bytes (bytes_to_string, read_file_string, a byte_slice that cuts a code point). Every walk over code points reads a malformed run as one U+FFFD: the longest prefix of a well-formed sequence, at least one byte, overlong forms, surrogates and code points past U+10FFFF included. So s.len(), for c in s and s[i] always agree, and a char, which never holds such a value, always encodes to UTF-8 that decodes back. The compiler reads source files the same way (§3 Source encoding).

Arrays#

xs :: [10, 20, 30, 40]
same :: xs                // the same array: an array is a reference
same[0] = 11              // xs is [11, 20, 30, 40]
copy :: xs.clone()        // a fresh array (std/iter)

h: Hour: 3
xs[h]                     // 40: any integer type indexes, a range too
xs[4]                     // stops the program: 4 is outside 0..<4

part :: xs[1..<3]         // [20, 30]: a view on xs
part[0] = 21              // xs is [11, 21, 30, 40]
part.push(50)             // part is [21, 30, 50], an array of its own
part[0] = 0               // xs is still [11, 21, 30, 40]
xs[2..<99]                // [30, 40]: the bounds are clamped
  • T[] is an array whose length is known at run time; i32[][] is an array of i32 arrays.
  • arr[r], for a range r of any integer type, is a slice of arr; its bounds, read as i64, are clamped at 0 too, and arr[r].clone() gives an independent copy.
a: i32 | string[]: 5             // an i32, or an array of strings
b: (i32 | string)[]: [1, "a"]    // an array of i32 | string

Postfix [] binds tighter than |, and parentheses group. An array type may be any member of a union (A | T[] | B).

Variance#

An array's element type is invariant wherever the array is bound, stored or returned: an i32[] into a (i32 | string)[] local is an error, at the declaration as at a reassignment.

Passed as an argument, an array may widen its element, at any depth: a Circle[] passes for a (Circle | null)[] parameter, or for a Shape[] one when Circle implements Shape. The callee's body decides, with no annotation: it may read the array, remove elements, and keep or return an element, but not replace an element, grow the array, keep it past the call, return it, or hand it to a function value. Through a method, a trait or a bound, every function the call may run is checked. Otherwise the argument is an error naming what the callee does:

total :: fn(shapes: Shape[]) -> f64 { ... }      // reads only
add_square :: fn(shapes: Shape[]) -> void {      // grows the array
    shapes.push(Square { .side = 1.0 })
}

total(circles)        // OK: a `Circle[]` is read as a `Shape[]`
add_square(circles)   // error: `add_square` may grow `shapes`

The parameter's element must also take the argument's elements as they are stored, by group:

  • scalars: integers, floats, bool, char, ranges, enums and type parameters;
  • references: structs, arrays, tuples, functions, traits, any, and unions of structs, arrays, tuples, functions and null;
  • mixed types: string, and every other union (one that holds a scalar, a string or a trait).

A distinct type is in the group of its backing. A reference element takes a reference or a mixed argument element, a mixed element only a mixed one, and a scalar element only its own type: string[] passes for (string | null)[], but i32[] for (i32 | string)[] and Circle[] for (Circle | string)[] are errors.

A fresh literal is built with the slot's element type, so xs: (i32 | string)[] = [1, 2, 3] compiles.

Array constants#

A module constant is frozen (§5 Frozen values): nothing can write it. A module keeps its state in a @singleton function (§12).

DIGITS: string[]: ["0", "1", "2"]   // frozen, built at compile time
LIMITS :: [10, 20, 30]              // same storage, inferred

@singleton
counts :: fn() -> i32[] = [0, 0, 0] // state: one instance, built on first call

bump :: fn(i: i64) -> void { counts()[i] += 1 }   // persists across calls

DIGITS.clone() is a fresh array that can be written.

A type is printed as written (i32[], Box<Cfg>), and @type_name gives the same text.

Repeat / preallocate: [lhs] * n#

[lhs] * n builds a fresh array that repeats the elements of the array literal lhs n times, the array always on the left. The result has len(lhs) * n elements, a count below 0 counting as 0.

zeros :: [0] * 3          // [0, 0, 0]
pair  :: [7, 9] * 2       // [7, 9, 7, 9]

[] * n is an empty array with room reserved for n elements, the way to preallocate (there is no with_capacity).

buf: i32[]: [] * 1024    // capacity 1024
for x in src { buf.push(x) }   // no reallocation up to 1024 elements
  • ([] * n)[0] stops the program: the reserved room becomes readable only as push and push_all grow the length. [v] * n gives n readable elements.
  • The element type comes from the left side, or from the slot when the left side is empty or a free literal ([] * n or [0] * n into an annotated T[]).
  • The left side is evaluated once, so a reference element is shared by every slot: after rows :: [[]] * 3, rows[0].push(x) grows all three. The compiler warns when that element is freshly allocated (an array or struct literal, a constructor call). filled(n, make) (std/core, in the prelude) calls make once per slot, in order, and filled_indexed(n, make) passes it each slot's index:
rows: i32[][]: filled(3, () -> [])            // 3 distinct arrays
seen :: filled(n, new_visited_set)             // `make` may be any `fn() -> T`
squares :: filled_indexed(n, (i) -> i32(i * i))   // per-slot index
grid :: 3.filled(() -> [])                     // UFCS on the count reads fine too

Mutation methods#

dst :: [1, 2]
dst.push_all([3, 4])              // [1, 2, 3, 4]
[7, 8, 9].copy_to(0, dst, 1, 2)   // [1, 7, 8, 4]: 2 elements, from index 0 to index 1
dst.copy_to(0, dst, 1, 2)         // [1, 1, 7, 4]: the two ranges may overlap
dst.remove_last()                 // 4, and dst is [1, 1, 7]
dst.clear()                       // []
dst.remove_last()                 // null: dst is empty
[9].copy_to(0, dst, 0, 1)         // stops the program: dst is too short

buf: u8[]: []
buf.push_all("hé".bytes())        // [104, 195, 169]: a string's bytes may be read
"hé".bytes().clear()              // error: a string's bytes cannot be written

Besides push(v), arrays have:

Method Effect
dst.push_all(src) Appends the elements of src, which it only reads.
src.copy_to(src_start, dst, dst_start, len) Copies len elements from src_start in src to dst_start in dst.
arr.remove_last() Removes and returns the last element.
arr.clear() Sets the length to 0 and keeps the reserved room.

Tuples#

A tuple is a sequence of fixed length whose elements may have different types. Its type is [T1, T2, ...], with two elements or more; there is no 1-tuple.

divmod :: fn(a: i32, b: i32) -> [i32, i32] { return [a / b, a % b] }

pair: [i32, string]: [42, "answer"]
println(pair.0)      // 42
println(pair.1)      // answer
  • t.0, t.1, ... access the elements, numbered in source order.
  • Elements are read-only: t.0 = x, t.0 += 1 and a destructuring target [t.0, n] = ... are errors, through a distinct over a tuple too. A struct held in an element can still be written (t.0.x = 5).
  • A [...] literal is an array or a tuple by context: without an annotation, [1, 2, 3] (one element type) is an array and [1, "x"] a tuple; with one, the annotation decides (p: [i32, string] = [1, "x"]).
  • A tuple is not an array: [i32, i32] is not assignable to i32[].
  • Element types are invariant: [i32, i32] does not pass as [i32 | string, i32], but a fresh literal is built with the slot's element types (p: [i32 | string, i32]: [1, 2]).
  • == compares the elements, not the references, each as it would alone (a struct through its Equals, a union by variant, a bounded type parameter through Equals), and != is its negation. A distinct type over a tuple compares through its backing unless it implements Equals itself.
  • A tuple is only as comparable as its elements: == is an error when an element has no working == (an array, a struct without Equals, a function or trait value, a type parameter whose bound does not name Equals), and the error names that element's type. The operands must have the same type, nested tuples included: [i32, string] == [string, i32] is an error.

Destructuring#

A local declaration (:: or :=) destructures a tuple. Patterns nest, and _ skips an element.

[k, v] := key_value()                  // mutable bindings
[k, v] :: key_value()                  // immutable bindings
[a, _, c] :: triple()                  // ignore middle slot
[[x, y], z] :: nested_pair()           // nested destructure

A match arm does not destructure ([a, b] -> is a parse error): it tests the tuple type, and its body destructures what it narrowed:

match found :: lookup(key) {
    is null          -> println("no result")
    is [i32, string] -> {
        [n, s] :: found
        println("n=${n} s=${s}")
    }
}

Array sources#

A [...] pattern also destructures an array, whose length is not checked against the pattern: elements past it are dropped, and a missing one stops the program (an index out of bounds).

parts :: "a-b-c".split("-")
[x, y] :: parts                        // x = "a", y = "b", "c" is dropped
solo  :: "a".split("-")
[p, q] :: solo                         // stops the program: index 1 is out of bounds

Spread destructuring (arrays only)#

A destructuring declaration may end with ...rest, which collects the rest of an array source into a fresh array:

arr :: [10, 20, 30, 40, 50]
[first, ...rest] :: arr                // first: i32, rest: i32[] = [20,30,40,50]
[a, b, ...tail] :: arr                 // a, b consume the head; tail = [30,40,50]
[_, _, ...short] :: arr                // wildcards combine with rest

...rest on a source that is not an array, or anywhere but in last position, is an error. A tuple source keeps its exact arity: [a, b] := pair requires a tuple of two elements.

Destructuring via Into<[...]>#

A [...] pattern destructures a value that is neither a tuple nor an array when its type implements Into<[...]> with a tuple of the pattern's arity, each name taking the matching element with its type. Without such an impl, it is an error.

[n, s] :: p                            // p implements Into<[i32, string]>: n is an i32, s a string

Destructuring assignment#

On the left of =, the same [...] shape writes targets that already exist instead of declaring names. Each element is an assignment target (a name, a field or an index) and follows every rule of a plain assignment (§3 Assignment).

a := 1
b := 2
[a, b] = [10, 20]                      // a = 10, b = 20
[a, b] = [b, a]                        // swap: a = 20, b = 10
[cfg.width, sizes[0]] = dimensions()   // fields and indices are targets too

Every element is read before any is written, which makes the swap work; the source is evaluated once.

_ skips an element: nothing is read or written for it, but it counts toward a tuple's arity. A trailing ...place writes a fresh array of the elements the other targets left, built while the elements are read.

[_, name] = lookup(id)                 // lookup runs once, its first element is dropped
[head, ...queue] = queue               // head = queue[0], queue = a copy of the rest
[x, _, ...tail] = coords               // tail = coords[2..], empty when there is none

The following are errors:

  • an element that is not an assignment target, such as a literal;
  • a tuple source whose arity differs from the number of targets;
  • a literal source of the wrong length, although other array sources are not checked;
  • a ...place on a source that is not an array, or anywhere but last;
  • a nested target ([[a, b], c] = ...);
  • a source that destructures only through Into<[...]>.

Nesting and Into belong to a destructuring declaration, where a pattern declares names.

Structs#

Point :: struct {
    x: i32
    y: i32
}

p :: Point { .x = 1, .y = 2 }

A struct is a reference type: p2 :: p names the same object as p.

  • == calls the structs' Equals impl (Equality, below), and === tests whether two names reach the same object (Identity, below).
  • The layout of a struct cannot be observed: there is no @offset_of and no unsafe cast.

A literal may list its fields in any order; their values are evaluated in the order the struct declares them. Listing a field twice is an error.

Field defaults#

A field declaration may give a default value, used when a literal omits the field:

Settings :: struct {
    name:    string
    timeout: i32  = 30
    debug:   bool = false
}

s :: Settings { .name = "loud", .debug = true }        // timeout=30
  • A default is type-checked once, at its declaration, against the field's type, before any type argument is substituted.
  • It is evaluated at each literal that omits its field. A module constant in it is copied where a write could reach it (§5 Frozen values).
  • Omitting a field that has no default, in a literal without a spread, is an error.

A struct literal inside a default fills its own omitted fields in turn. It is an error for defaults to fill one another in without end, and for a constant to read itself through the defaults its literal fills in:

A :: struct { b: B = B {} }      // error: A and B fill each other in
B :: struct { a: A = A {} }

D :: Q {}.n                      // error: D reads itself through Q's default
Q :: struct { n: i32 = D }

Spread (functional update)#

{ ...other, .field = v } copies every field of other and overrides the listed ones:

base :: P { .a = 1, .b = 2, .c = 3 }
upd  :: P { ...base, .b = 99 }                          // a=1, b=99, c=3
mix  :: P { ...base, ...other, .b = 7 }                 // 'other' wins for non-named
  • The spread source must be assignable to the literal's struct type.
  • It is evaluated once, before the values of the fields.
  • With several spreads, the last one fills every field the literal does not name.

Struct literals in if / for / match conditions#

A struct literal is not allowed at the top level of an if, for or match condition. Parentheses make it valid. Inside a bracket of the condition (call arguments, an index, a [...] literal, an interpolation, parentheses, a block), it needs none:

if Point { .x = 1, .y = 2 } == p { ... }       // error
if (Point { .x = 1, .y = 2 }) == p { ... }     // OK
if is_origin(Point { .x = 0, .y = 0 }) { ... } // OK

Unions#

Result :: string | i32 | null

show :: fn(r: Result) -> string {
    match r {
        is string -> r
        is i32    -> r.to_string()
        is null   -> "(none)"
    }
}

A union is written A | B | C, in place or named by an alias. A value that may be absent is typed T | null (Nullability, below).

A union value, like a trait value, carries the run-time type of what it holds (docs/RUNTIME.md Boxes and tags): for an enum or an integer range type, the integer that stores it; for a distinct type, its backing. A union of two variants that carry the same run-time type is an error: Direction | u8 (an enum and its backing), Fruit | Direction (two enums with one backing), Path | string (a distinct type and its backing).

The instances of a generic struct that differ only in reference-type arguments carry one type (Box<Foo> and Box<Bar> look alike), but may share a union (Type tests, below).

A union implements a trait exactly when each of its variants does.

Common-field access#

A field common to every variant of a union of structs can be read on the union directly:

Cat :: struct { name: string, age: i32 }
Dog :: struct { name: string, age: i32 }
Animal :: Cat | Dog

animal_name :: fn(a: Animal) -> string = a.name

A field is common when it, and every field before it, has the same position and the same type in every variant. The names of the earlier fields may differ:

Badge  :: struct { id: i32, label: string }
Ticket :: struct { rank: i32, label: string }
label_of :: fn(x: Badge | Ticket) -> string = x.label   // OK

Kitten :: struct { name: string, age: i32 }
Puppy  :: struct { name: string, age: i64 }
age_of :: fn(a: Kitten | Puppy) = a.age     // error: `age` is `i32` in one, `i64` in the other

Reading any other field on the union is an error, which names the first disagreement. This holds for an alias too.

Flow-narrowing on T | null#

A comparison with null, on either side, narrows as an is test does (§3 Type tests and narrowing): where x != null holds, or x == null fails, x has its type without null:

must_greet :: fn(a: Animal | null) -> string {
    if a == null {
        return "missing"
    }
    return a.name                  // `a` is an `Animal` for the rest of the block
}

Comparing with null a value that can never be null is an error, as n is null would be.

Null-coalescing ??#

left ?? fallback evaluates left once and gives its value when it is not null; otherwise it evaluates fallback. The result has the type of left without null. A left that cannot hold null is an error, a T | Error included.

The fallback is one of:

  • a value, assignable to the type of the result;
  • a divergent statement, typed never: return, return <expr>, break or continue;
  • a block { ... }, whose trailing expression or divergence gives the value.
port :: settings["port"] ?? 8080
node :: resolve(sym) ?? return null
v    :: cache[k] ?? {
    log("miss ${k}")
    continue
}

?? can appear wherever a value can. It binds looser than every other binary operator, || included (§3 Operator precedence): a ?? b + c is a ?? (b + c).

or_else in std/option takes the fallback as a function, which receives the error on a T | Error.

Enums#

Direction :: enum {
    North,
    South,
    East,
    West,
}

An enum is a closed set of named variants, each a constant of the enum's type, which may carry data (Variant data, below).

Access#

A variant is named EnumName.VariantName:

d :: Direction.North

Dot-shorthand inference#

.Variant stands for a variant of the enum the context gives:

is_north :: fn(d: Direction) -> bool = d == .North
move_towards(.South)        // parameter type is Direction

The context gives an enum in:

  • a slot typed by the enum: a declaration, an assignment, a struct field or its default, an argument, a return value or expression body, an element of an array literal in a typed slot;
  • the value of a match arm or an if branch that fills such a slot;
  • an operand of == or != whose other operand is an enum, and an element of an array literal on the right of in.

Two enums may share a variant name: the shorthand takes the enum the slot names, or, in a slot typed by a union of enums, the first of them that has the name. A shorthand is an error where the context names no enum (x :: .Red), and as an argument that several overloads accept under different enums.

The compiler warns about Enum.Variant in a slot that already names that enum, a nullable one (Direction | null) included. It does not warn where the prefix matters:

  • an unannotated local (d :: Direction.North);
  • a slot typed by a union of two enums that share the variant name;
  • any argument of an overloaded call, where the prefix can select the overload (pick(Fruit.Red)).

Variant patterns#

An arm on an enum variant is written .Variant, without is; the qualified form Direction.North does not parse in a pattern. A match on an enum must give every variant an arm, or have a _ arm.

match d {
    .North -> "north"
    .South -> "south"
    .East  -> "east"
    .West  -> "west"
}

Equality and hashing#

== and != compare enum values by variant (Equality, below). Every enum implements Equals and Hash without a written impl, and c.equals(d) is c == d. Writing an Equals or a Hash impl for an enum is an error.

Values#

Level :: enum { Low, Mid, High }   // values 0, 1, 2

@min(Level)                        // Level.Low
@max(Level)                        // Level.High
i32(Level.Mid)                     // 1
Level(2)                           // Level.High
Level(n)                           // stops the program unless n is 0, 1 or 2
Level(5)                           // error: 5 is no value of Level
Level.Mid + 1                      // error: an enum has no arithmetic
for l in Level { ... }             // Low, then Mid, then High

if n in Level {                    // n: i64
    return Level(n)                // n is a 0..=2 here
}

An enum without variant data is a named set of integers, the values of its variants. It stays nominal: no widening into an integer.

  • @min(E) and @max(E) give its variants of least and greatest value.
  • x in E tests an integer against its values (x !in E negates it), and is a constant where the type of x decides it. When the values are consecutive, it narrows x to them, as a range test would; an enum with a gap in its values narrows nothing.
  • E(x) converts as Hour(x) does (Range types, above); a non-integer x is an error.
  • for d in E walks its variants in declaration order.

Details.

  • On an enum with no variant, x in E is false, for takes no turn, and @min, @max and E(x) are errors.
  • On an enum with variant data, each of these forms is an error.
  • They work the same through a namespace or a renamed import.

Representation#

Backing type#

An enum value is held as an integer, its backing type. Without a written width, it is the narrowest integer that holds every variant value: unsigned when none is negative (u8 to u64), signed otherwise (i8 to i64). A (type) after enum writes it:

Direction :: enum(i32) { North, South, East, West }
  • The written type is one of i8 to i64 and u8 to u64, or a distinct type over one of them.
  • @size_of, a field and an array element of the enum take the width of its backing type.
  • An inferred width grows with the values. An @extern function takes only an enum whose width is written (§13 Allowed signature types).
  • An enum with variant data is sized by its number of variants instead (Variant data, below).

Variant indices#

Each variant has an integer value: 0 for the first, the previous plus one for the others, unless = value writes it:

Priority :: enum(u8) {
    Low    = 10,   // 10
    Medium,        // 11
    High   = 20,   // 20
    Critical,      // 21
}

Two variants with the same value are an error, and so is a value that does not fit the written backing type.

Memory model#

An enum value is a value, copied on assignment as a bool is.

Variant data#

An enum header may name a struct, alone (enum(Info)) or before the width of the values (enum(Info, i32)). Each variant then carries a constant value of that struct, its data, written as the struct's fields after the variant's name:

StatusInfo :: struct {
    code:   i32
    reason: string
    retry:  bool = false
}

HttpStatus :: enum(StatusInfo) {
    Ok          { .code = 200, .reason = "OK" },
    NotFound    { .code = 404, .reason = "Not Found" },
    Unavailable { .code = 503, .reason = "Service Unavailable", .retry = true },
}

line :: "HTTP/1.1 ${s.code} ${s.reason}"   // s : HttpStatus
info :: StatusInfo(s)                       // a copy of the whole value

A lone header argument is the width when it is an integer type or a distinct type over one, and the data when it is a struct. Any other argument is an error: enum(string), enum(string, i32).

  • Each block is checked as a literal of the data struct, its errors reported on the variant. A variant may omit its block when every field has a default. A block on an enum without data is an error.
  • The data holds only scalars, strings and enums; a field that holds a reference (a struct, an array) is an error.
  • Every value in the data must be computable at compile time.
  • Each variant also has an integer value, written after its block (Ok { ... } = 200) and numbered as in any enum (Variant indices, above). A written width bounds the values.
  • v.field reads a field of v's data, and Info(v) the whole value. The data is frozen, as a module constant is (§5 Frozen values): writing it or lending it to a function that modifies it is an error, and a name bound to it holds a copy. HttpStatus.code stays a variant lookup.
  • An enum value is stored in the narrowest unsigned integer that can number the variants, whatever width the header writes.
  • i32(s) gives the written value, and so does an @extern parameter of the enum, through a function value too (§13 Allowed signature types). Interpolating renders through Display, never the written value.

Display#

An enum implements Display only on request: interpolating one that does not is an error. @displayable generates an ordinary impl that renders each variant as its name; writing one as well is an error:

@displayable
Direction :: enum { North, South, East, West }

"${Direction.East}"   // "East"

For any other text, implement the trait:

Direction implements Display -> match self {
    .North -> "N"
    .South -> "S"
    .East  -> "E"
    .West  -> "W"
}
  • In a generic, a T bound to an enum renders through the enum's Display, in a generic impl too (Box<Direction>). A generic function renders a T bound to a distinct type through the distinct's Display.
  • Interpolating a union that holds an enum, or a distinct type with a Display of its own, is an error, and so is interpolating an array of such unions: narrow first. A distinct type without its own Display renders through its backing type's, inside a union too.

Pattern matching#

A match tests its scrutinee against the pattern of each arm, in order, and takes the first arm that matches (§7 match).

match value {
    is Point { x: 0, y } -> "Y axis at $y"
    is Point { x, y }    -> "($x, $y)"
    is null              -> "nothing"
    42                   -> "the answer"
    1 | 2 | 3            -> "small"
    is i32 if value > 0  -> "positive"
    is i32               -> "negative or zero"
    'A'                  -> "letter A"
    "ok"                 -> "literal ok"
    _                    -> "wildcard catch-all"
}

Header binding#

match x :: e { ... } evaluates e once and names it x in every arm and every guard. The scope of x is the match: after it, x is whatever it named before.

x is one constant name, typed as the scrutinee. Assigning it is an error, and so is any other form of header (match x := e, match e as x).

A scrutinee that is already a name needs no header: each arm narrows it in place. An arm cannot bind a name of its own (is T as x -> is an error):

match s :: shape_at(n) {        match shape {
    is Circle -> s.radius           is Circle -> shape.radius
    is Square -> s.side             is Square -> shape.side
}                               }

Type tests#

Box :: struct<T> { value: T }

kind :: fn(b: Box<Direction> | Box<u8>) -> string = match b {
    is Box<Direction> -> "a direction"   // never matches a Box<u8>
    is Box<u8>        -> "a byte"
}

holds :: fn<U>(x: i32 | string) -> bool = x is U
holds<i32>(5)                        // true
holds<string>(5)                     // false

is Type tests the type of the scrutinee and narrows it in the arm (§3 Type tests and narrowing).

  • On a generic struct, the test tells instances apart when every type argument is a primitive or an enum.
  • A test on an instance whose type argument is a reference type, or a type parameter a reference type may bind, is an error (Unions, above): test the field that holds it instead.
  • A generic function may test its own type parameters, against what the call binds them to, through generic callers too.
  • An impl member may test its own type parameters, but not those of its impl: testing one, or handing one to a function that tests it, is an error.
  • An arm whose is T can never hold is an error, as in an if.
match boxed {                        // boxed: Box<Circle> | Box<Square>
    is Box<Circle> -> ...            // error: Circle is a reference type; test boxed.value
    ...
}

Probe :: trait { probe :: fn(self) -> bool }

Box<T> implements<T> Probe {
    probe :: fn(self) -> bool = self.value is T   // error: T is a parameter of the impl
}

Literal patterns#

classify :: fn(v: i64 | string | null) -> string = match v {
    5         -> "five"              // 5 is an i64: the variant it can fill
    "ok"      -> "ok"
    null      -> "nothing"
    is i64    -> "another number"
    is string -> "another string"
}

match x {                            // x: f64
    0.0 -> "zero"                    // and -0.0: floats compare by IEEE 754
    _   -> "nonzero"
}

A literal (42, 'A', "ok", null) matches a scalar value when scrutinee == literal holds, the literal typed as the right operand of that ==:

  • a union compares by variant, and a float by IEEE 754;
  • the literal is compared with what the arms above leave of the scrutinee;
  • a literal whose variant an arm above already took ("a" after is string ->) draws a warning.
label :: fn(v: Point | string | null) -> string = match v {
    is Point  -> "a point"
    "a"       -> "the letter a"      // v is a string | null here: Point needs no Equals
    is string -> "another string"
    null      -> "nothing"
}

match b {                            // b: u8
    300 -> "big"                     // error: no u8 equals 300
    _   -> "small"
}

A literal in a struct pattern (is Holder { v: 5 }) follows the same rules against its field.

Range patterns#

'a'..='z' -> ... and 0..<10 -> ... match a char or integer scrutinee that falls in the range, ..= and ..< as in a range expression.

  • The bounds are literals, typed as the scrutinee: -1..=3 over a u8 is an error.
  • Over a distinct type, the bounds take its backing type, as < does.
  • Over a union, they take its integer or char variant, which the arm tests first. Once arms above have taken that variant, the compiler warns.
  • A scrutinee with no integer or char to order is an error.

Or-patterns#

Alternatives separated by | share one arm, which matches when any of them does. Each alternative is checked against the scrutinee, and every enum variant listed counts toward exhaustiveness. A struct pattern in an alternative may constrain a field: is A { x: 1 } | is B { x: 2 }.

is A | B -> ... is one is over the union type A | B, written without parentheses: it narrows to A | B and covers both.

Struct patterns#

The first two arms of the example in Pattern matching bind y, then x and y; the first matches only where x == 0.

match q {                                  // q: Point | Other | null
    name                            -> ... // error: the catch-all is _
    is Point { x } | is Other { x } -> ... // error: an alternative binds no field
    [a, b]                          -> ... // error: an arm does not destructure a tuple
}
  • Each name must be a field of the struct, and a literal takes the type of its field: either mismatch is an error, and so are braces after a type that is not a struct.
  • The field tests run after the type test, on the narrowed value.

Only the fields of a struct pattern bind names. The following are errors:

  • a bare identifier arm: the catch-all is _, and the header names the scrutinee;
  • a field binding inside an alternative of an or-pattern;
  • a tuple destructuring arm: test a tuple with is [T, U], and destructure it in the arm ([a, b] :: x).

Guards#

if cond after a pattern is a guard: the arm matches only when it holds. It may read the pattern's bindings and the header's name.

  • At the guard's own level, -> belongs to the arm: a bare lambda (x -> ..., (x) -> ...) is parsed only inside a bracket (xs.any_match(x -> x > 0)), its own parentheses included.
  • A struct literal can appear in a guard.

Narrowing in the arms#

Each arm narrows the scrutinee (a name, a field chain or the header name) to what its pattern matched.

  • After is X arms that constrain no field, the _ arm sees what those arms left: after is null ->, a Pet | null is a Pet.
  • Literal and range arms narrow an integer scrutinee to their values, as if h in 1..=11 would. An or-pattern narrows to the range that holds its alternatives, and _ to the values left when they form one interval. A char and a distinct type are not narrowed to a range.

Exhaustiveness#

part_of_day of Range types, written as a match, needs no _:

part_of_day :: fn(h: Hour) -> string = match h {
    0..<12  -> "morning"
    12..=23 -> "afternoon"
}

match h {                                    // error: the value 12 is not covered
    0..<12  -> "morning"
    13..=23 -> "afternoon"
}

match p {                                    // p: Point | null
    is Point { x: 0 } -> "on the axis"       // error: Point is not covered,
    null              -> "nothing"           // since the arm constrains x
}

A match must cover every value of its scrutinee; the error names the first value missing.

  • A union is covered when every variant is, or by _.
  • An integer type (a range, an integer primitive or char, a distinct type over one included) needs no _ once its unguarded literal and range arms take every value. The integer variant of a union is covered the same way. A u64 still needs _, since no literal reaches past i64.
  • Any other scrutinee (string, f64) needs _.
  • Guarded arms and arms that constrain a field cover nothing.

@partial and @unreachable lift the check (§12 @partial and @unreachable).

name :: fn(d: Direction) -> string = match d {
    .North -> "north"
    _      -> "other"                        // warning: a bare _ in a match that gives a value
}

The compiler warns about a bare _ arm in a match over a union or an enum that produces a value, a match whose every arm leaves (return, break) included. Listing every variant, or marking the match @partial, removes the warning. A statement match (_ -> {}) does not warn.

Unreachable arms#

The compiler warns about an arm whose values the arms above already take: after a _, the same variant, the same is T test, or literals and ranges that cover it. The warning applies to a @partial match too.

Type inference#

Inference is bidirectional: from the value (x :: 12 infers x: i32), and from the context (the function type a slot expects gives a lambda the types of its parameters).

There is no Hindley-Milner inference across the program: the parameters of a function declaration must be annotated (self excepted). An expression-bodied function may leave out its return type, which its expression gives; a block body without one returns nothing. The compiler warns when an exported function leaves its return type out: its public type would then change with its body.

Type arguments of a call#

first :: fn<T>(xs: T[]) -> T = xs[0]
empty_of :: fn<T>() -> T[] = []
tag :: fn<T>() -> string = "tag"

first([1, 2])                 // T = i32, from the argument
first(["a"])                  // T = string: each call binds afresh
rows: i32[]: empty_of()       // T = i32, from the slot the result fills
empty_of<string>()            // T = string, written
tag()                         // error: nothing binds T

repeat :: fn<T>(x: T) -> T[] {
    out: T[]: empty_of()      // binds empty_of's T to this body's T
    out.push(x)
    return out
}

A generic call binds its type arguments from its arguments first, then from the slot its result fills.

  • Inside a generic body, the body's own T is fixed: met in an argument or the result slot, it binds the callee's parameter to T itself.
  • Written type arguments <...> fix the callee's parameters in the order it declares them, wherever it is declared.
  • On a call with the wrong number of arguments, only the count is reported.

Generic functions as values#

id :: fn<T>(x: T) -> T = x
name_of :: fn<T: Display>(x: T) -> string = "<${x}>"
apply :: fn(f: fn(i32) -> string, x: i32) -> string = f(x)

// in a function body:
apply(name_of, 3)             // "<3>": name_of is taken at T = i32
named :: name_of<f64>         // fixed by its type argument: named(1.5) is "<1.5>"
g :: id                       // an alias: g(1) and g("a") each instantiate id
m := id                       // error: neither a function-typed slot nor a type argument

A generic function named as a value is instantiated against the function type of the slot it fills.

  • Writing fewer type arguments fixes the first parameters, and the slot binds the rest.
  • A module constant is not such an alias: it needs an annotation (h: fn(i32) -> i32: id).
  • An argument is no exception: keep(id) with keep :: fn<T>(x: T) -> T is an error.

Members with type parameters of their own#

A trait or impl member with type parameters of its own (conv :: fn<U>(self, x: U) -> string) is instantiated at each call, as a generic function is: on a concrete receiver, as a static member, through a bound in a generic function, and on a generic impl, whose own parameters the receiver binds.

Calling such a member where its impl is picked at run time is an error: on a trait value, or, inside the members of a generic impl, through a bound of that impl's type parameter.

Union parameters#

Against a union parameter, the variants the argument shares with it pair off first (T | null never binds T = null), and a variant the argument lacks binds nothing (T | null given an i32 binds T = i32).

A type parameter left alone then takes every variant left: T | null against i32 | string | null binds T = i32 | string. Several variants left on both sides pair by position.

Casts#

A conversion is written as a call to the target type:

x: i32: 42
y: i64: x                  // widening, no cast needed
z: u8: u8(x)               // narrowing cast, keeps the low bits (300 -> 44)

A conversion that can fail, such as parsing, is a function returning a union:

n: i32 | ParseError: parse_int("42")

char and integers#

A char is a Unicode scalar value: a code point from U+0000 to U+10FFFF, the surrogates D800 to DFFF excluded, held as a u32.

  • An integer conversion reads its code point (u32(c)).
  • char(x) converts an integer, checked in every build: the program stops on a value that is not a scalar value.
  • A distinct type over an integer converts as its backing type does, and one over char (Glyph :: char) is checked as char.
  • A type that implements Into<char> converts through it.
  • A UTF-16 surrogate is a code unit, an integer (is_surrogate(unit: u32) in std/string).

The following are errors:

  • a literal that is not a scalar value (char(0xD800));
  • a source type none of whose values is a scalar value;
  • a float or an enum: convert it to an integer first.
c    :: 'A'
n    :: u32(c)            // 65
back :: char(n + 1)       // 'B', checked

Generics#

Type parameters are declared <T> (never [T] or (T)) at the declaration site, and are in scope across the whole declaration.

A type parameter may declare a default after its bound: <Rhs: Numeric = Self>. Trailing arguments an impl or a bound leaves out take their defaults, a Self default being the implementing or receiver type: i32 implements Add and T: Add both read Add<Rhs = Self, Out = Self> as Add<Self, Self>. A default on a struct, function or type-alias parameter is accepted but not applied: with Pair :: struct<A, B = i32>, Pair<string> is an error.

In expression position, after a name (f, or the member in x.f, M.f), a < opens a type argument list when a matching > on the line is followed by (, {, .name (Bag<f64>.make(1)), or what ends a value: a ,, a closing bracket, the end of the line (f :: id<i32>, apply(name_of<i32>, 5)). Otherwise the < is a comparison (assert(v < 20, "msg")). In Box<Box<T>>, >> closes both levels.

A call writes explicit type arguments after the callee's name, in any call form (f<i64>(x), x.f<i64>(y), M.f<i64>(y)), and they fix the callee's parameters in declaration order.

  • Fewer than declared leaves the rest inferred (pair<i64>(5, "x") fixes A and infers B); more, or any on a function that takes none, is an error.
  • A static member takes its type's arguments on the type and its own after its name: Bag<f64>.make<i64>(1), or Bag.make<i64>(1) with those of Bag inferred.
  • A generic type written with type arguments in a struct literal, as the owner of a static member or as a conversion target (Box<i64>(x)) writes all of them: with Pair :: struct<A, B>, Pair<i32> { ... } and Pair<f64>.make(1) are errors.

Generic functions and structs:

map :: fn<T, U>(items: T[], f: fn(T) -> U) -> U[] {
    result: U[]: []
    for x in items {
        result.push(f(x))
    }
    return result
}

List :: struct<T> {
    items: T[]
    len: u32
}

A bound follows the parameter after : and names traits, or Int (§4 Range types), joined by & and all required. A union is an error as a bound, as in a trait's composition: null goes where the parameter is used, fn<T: Bump>(x: T | null), never fn<T: Bump | null>(x: T).

put :: fn<K: Hash & Equals, V>(self: MutableMap<K, V>, key: K, value: V) {
    // ...
}

A type parameter's bound applies in the generic body and at every call:

  • In a generic body, a method call or an operator on a value typed by a type parameter resolves through its bound: key.hash() under K: Hash, a < b under T: Comparable (Operator overloading).
  • At every call, wherever the callee is declared and however it is called (through UFCS, the receiver alone may bind the parameter: m.println()), each type argument must implement every trait of its parameter's bound, at the bound's arguments: a string[] (Contains<string>) does not meet C: Contains<i32>.
  • A type argument that is itself a type parameter meets a bound its own bound names.

Type parameters are opaque in their body. A generic body knows nothing of T beyond its bound: only a T fits a slot typed T, and a T fits only a slot that names it (T, T | null). Each of these is an error:

widen :: fn<T>(x: T) -> i64 {
    n: i64: x            // a T is no i64
    return n
}

narrow :: fn<T>(x: T) -> T {
    y: i64: 5
    return y             // nor is an i64 a T
}

zero :: fn<T: Add>(x: T) -> T {
    s: T: 0              // nor is a free literal
    return s.add(x)
}

So is a free literal as a T argument or operand: pick(x, 0) with pick taking two T, f(2.5) through f: fn(T) -> T, x == 0 under T: Equals.

The body of a generic function or impl binds none of its own type parameters at a call: keep("s") on a local keep :: (y: T) -> ... is an error, as is t.visit((y: Inner) -> ...) under T: Visit. A callee's own parameters are still bound (g :: identity), and in a type's impl a receiver or the type name binds the impl's parameters to the instance it names (other.put(4) on a Box<i32>, Bag.make(5)).

A generic recursion may not grow its type argument. On a cycle of calls, passing a type parameter on inside a larger type is an error: wrap<T> calling wrap([x, x], n - 1), ping<A> calling pong([a], n) when pong calls ping, [self].depth(n - 1) in an impl on T[]. These are accepted:

  • passing the parameters on unchanged (count_down(x, n - 1));
  • a call to itself at a fixed type (fixed(n, n - 1) inside fixed<T>);
  • in the impl of a generic struct, a call on a larger instance of it (a Box<T[]> inside the impl of Box<T>).

An argument is matched against a signature's free type parameters when choosing an overload or the UFCS function for a receiver (Iterator<i32> fits self: Iterator<T>). A free literal binds no type parameter: write Box<i32> { .value = 0 }, not Box { .value = 0 }.

A static function belongs to a trait (Static members), never to a type alone: there are no inherent associated functions. A factory outside a trait is a free function (new_rng(42) in random); a struct is built by its literal (MutableMap<K, V> {}).

The primitive impls are listed in §15 std/core.

Traits#

A trait is a contract of methods a type implements, called on it through UFCS. Members, in a trait as in an impl, are declared name :: fn(...), never fn name(...).

Display :: trait {
    to_string :: fn(self) -> string
}

u32 implements Display {
    to_string :: fn(self) -> string {
        // ... implementation
    }
}

print_it :: fn<T: Display>(x: T) {
    println(x.to_string())
}
  • Declaration: Name :: trait { ... }.
  • Implementation: T implements Trait { ... }, in three forms (Single-method trait sugar (SAM)). T and Trait resolve like any other name, and the impl belongs to the declarations they name: of two modules' Foo types, or Animal traits, an impl of one says nothing of the other.
  • An impl defines its trait's members and nothing else, each with its trait method's parameters and return type, Self read as the impl's type and the trait's parameters as the impl's arguments, their defaults filled in (Generics, above). Any other member or signature is an error.
  • self and Self: in a trait or an impl, a first parameter named self has the type Self with no annotation, and Self is the implementing type (Foo in Foo implements Trait { ... }, a written -> Self included). Elsewhere, Self is an error.

A union calls the members of a trait all its variants implement: u.area() on u: Sq | Circle, like k in u, u + 5 or u[k] = v, dispatches by the value's run-time type, as on a trait value. Each trait argument must be one type for all the variants, or each variant itself (Out = Self, read as the union: a + 5 on a: i32 | Money is an i32 | Money).

These are errors (narrow the union first):

  • a member that two traits offer every variant;
  • a variant that implements the trait more than once;
  • a variant that is an enum, a distinct type implementing the trait itself, or an array of either (Unions, above);
  • a member taking another Self (compare, Add<Self>), except in Equals, whose parameter checks the other operand's type;
  • a member returning Self inside another type (Self[]), unless every variant is a struct.

A generic member, or an @intrinsic array member (push), is not dispatched through a union.

Static members#

A trait member whose first parameter is not named self is static, whatever its types (combine :: fn(a: Self, b: Self) -> Self). It is called on a type: a concrete type, a type parameter whose bound names the trait, or a type reached through a namespace alias (M.Point.origin()). Type.member(args) runs the member of Type's impl.

Zero :: trait {
    zero :: fn() -> Self
}

i64 implements Zero -> 0

sum :: fn<T: Add & Zero>(xs: T[]) -> T {
    s := T.zero()
    for x in xs {
        s += x
    }
    return s
}

A value of the trait reaches only its instance members, even in a trait that also has static ones. These are errors:

  • a static member called on a value, or on the trait itself (Zero.zero());
  • an instance member called on a type;
  • a static member named without being called (f :: i64.zero).

std/core declares Zero and One (§15 std/core), and BigInt implements Zero. std/iter has sum :: fn<T: Add & Zero>(self: Iterator<T>) -> T and product :: fn<T: Mul & One>(self: Iterator<T>) -> T.

Coherence: one impl per (type, trait, arguments)#

A second impl of the same type, trait and trait arguments is an error, across the whole program, the std/core prelude included (i32 implements Display, Hash for the primitives). Impls of one trait at other arguments are allowed (Several impls of one trait).

i32 implements Display {           // error: std/core already implements
    to_string :: fn(self) -> string = "n=${self}"
}

Two generic impls of one trait on one base (Wrapper<T>, T[]) may not both cover an instance, whatever their bounds: Wrapper<T> implements<T: A> Poke beside Wrapper<T> implements<T: B> Poke is an error. The same check on a bare type-parameter receiver (T implements<T: A> ...) is planned.

An impl is declared in the module of its type or of its trait (the orphan rule). An array type belongs to the module of its element type: MonType[] to that of MonType, i32[] to std/core.

Specialization: the instance wins#

An impl on one instance of a generic base (Wrapper<MonType> implements Poke, MonType[] implements Poke) is not a second generic impl: it wins over the base's generic impl for that instance, whatever the order of declaration.

Wrapper<T> implements<T> Poke {
    poke :: fn(self) -> string = "generic"
}

Wrapper<MonType> implements Poke {      // wins for `Wrapper<MonType>` alone
    poke :: fn(self) -> string = "mon"
}
  • Every route to the impl agrees: a method call, a bounded type parameter, a trait-typed value, an instance built in a generic body (fn<T>(x: T) -> Wrapper<T> called with a MonType).
  • It replaces only the generic impls of its trait at arguments that can coincide with its own: MonType[] implements Into<Label> leaves T[] implements<T> Into<Iterator<T>> to an array of MonType. The base's other traits still reach the instance through their generic impls.

An array type takes impls on T[] and on a concrete array (i32[] implements Tag). An impl on a partly generic array (T[][], [T, i32][]) is an error.

Several impls of one trait#

Vec2 implements Mul<f64> -> Vec2 { .x = self.x * other, .y = self.y * other }
Vec2 implements Mul<Vec2> -> Vec2 { .x = self.x * other.x, .y = self.y * other.y }

v :: Vec2 { .x = 1.0, .y = 2.0 }
v * 2.0                       // Mul<f64>: (2.0, 4.0)
v * v                         // Mul<Vec2>: (1.0, 4.0)
v.mul(3.0)                    // Mul<f64>, by the argument: (3.0, 6.0)

MonType implements Into<Label> -> Label { .text = "mon ${self.id}" }
MonType implements Into<Serial> -> Serial { .n = self.id }

m :: MonType { .id = 7 }
t: Serial: m.into()           // Into<Serial>, by the type of the result
println("${m.into()}")        // error: two `into` fit, give the result a type
f :: m.into                   // error: two `into` fit

A type may implement one trait at several arguments. Each use site picks the impl:

  • an operator, by its right operand;
  • a method call, by its first argument, or, without one, by the type its result must have;
  • x in c, by the element; c[k] and c[k] = v, by the key;
  • [a, b] :: r, by the arity of the tuple an Into<[...]> builds;
  • a static member, by its argument (Vec2.make(1.5));
  • a call, a static member or an operator on a bounded T, in a generic function or a generic impl, by the bound's arguments (fn<T: Mul<f64>>(v: T) -> T = v * 2.0);
  • inside one of the impls, a member of the trait called on the impl's type, by that impl: a default method's self.tag() runs its own impl's tag.

These are errors:

  • two impls that fit a use site equally;
  • a member without an argument whose result the site leaves untyped;
  • the member taken as a value without a function type to pick it (f :: m.into; f: fn() -> Serial: m.into is fine);
  • a use that does not name the trait's arguments: a for over a type implementing Iterator twice, or converting into two Iterators without being one itself;
  • a trait value of a trait its type implements twice (v: Into<Serial>: m).

A type that is an Iterator and also converts into one is walked through its own next.

Bounded-generic impls#

An impl declares its own type parameters after implements, bounded as on a function (<T: A & B>), in scope across the whole impl, for-type and trait arguments included. Without a list (Foo<T> implements Trait<T>), T is borrowed from the struct head; with one, the impl's T is its own, its bounds holding in this impl only.

// Borrowing T from the struct head: no list after `implements`.
Countdown<T> implements Iterator<T> {
    next :: fn(self) -> [T, Continuation] { /* ... */ }
}

// Bounded blanket impl: Range<T> coerces into an Iterator when T
// satisfies both Comparable and Step.
Range<T> implements<T: Comparable & Step> Iterator<T> {
    next :: fn(self) -> [T, Continuation] { /* ... */ }
}

// Blanket on a structural source (any array, any Display-bound type).
T[] implements<T>          Into<Iterator<T>> { /* ... */ }
T   implements<T: Display> Into<string>      { /* ... */ }

Every parameter the list declares appears in the for-type: T[] implements<T, U> Tagged<T, U> and Box<T> implements<T, U> Add<U> are errors.

A bounded impl holds only for the arguments its bounds admit. With Box<T> implements<T: Display> Display, Box<Point> has no Display while Point has none: interpolating it, or passing it to a T: Display parameter, is an error naming the unmet bound.

  • An argument meets a bound as at a call (Generics); a union, when every variant does; a distinct type, through its own impl or its backing's. An array does not meet a Display bound there.
  • A member of a bounded impl is reached only through an instance its bounds admit, in every form: with Wrapper<T> implements<T: Bump> Poke, w.poke() on a Wrapper<Plain> is an error, as are for, w[i], w[i] = v and an operator through such an impl.
  • An is Trait test still matches by the type's base, whatever the bounds.

A struct's bounds hold at its literal. With Wrapper :: struct<T: Bump>, Wrapper<X> { ... } is an error when X does not meet the bound; a type written elsewhere (w: Wrapper<Plain>) is not checked. An impl borrowing the struct's parameters (Wrapper<T> implements Poke) holds only for what the struct admits.

A generic function states the bounds it relies on: fn<K>(m: MutableMap<K, V>) indexing m is an error without K: Hash & Equals. A generic struct literal without type arguments (Box { .value = p }) is typed Box, and its bounds are not checked.

Trait composition#

A trait can compose other traits, which its implementor must all satisfy: with = and no body, or with : and a body for its own methods.

// Pure alias: Numeric is Add & Sub & Mul; no own methods.
Numeric :: trait<T> = Add & Sub & Mul

// With own methods: Hashable requires Hash and Equals, plus declares its own.
Hashable :: trait<T> : Hash & Equals {
    fingerprint :: fn(self: T) -> u64
}

The composition is an &-chain in type position, with the precedence of a type intersection; a single trait makes an alias (Foo :: trait<T> = Bar). A bound T: Foo requires every composed trait.

Method dispatch on trait values#

A call on a receiver typed by a trait runs the impl of the value's run-time type, primitive and enum impls included. The receiver's trait arguments are substituted into the method's signature: with it: Iterator<i32>, it.next() returns [i32, Continuation].

report :: fn(e: Error) -> string {
    return e.message()       // dispatches on `e`'s actual struct tag
}

Single-method trait sugar (SAM)#

When a trait has exactly one required method (one without a body, Default methods aside), an impl may leave out the name :: fn(...) -> RetType line and write the body directly, with the trait's signature and parameter names (self, other, ...) in scope.

// Arrow form: single-expression body.
i32   implements Hash   -> u64(self)
i32   implements Equals -> self == other

// With generic trait args.
Tutu  implements Toto<i32, i64> -> i64(self + other)

// Block form: multi-statement body.
Point implements Hash {
    h: u64 = u64(self.x)
    h = h *% 31 +% u64(self.y)
    return h
}

// Classic form: required for traits with two or more methods.
Countdown<T> implements Iterator<T> {
    next :: fn(self) -> [T, Continuation] { ... }
}
  • An impl body is a short form when it starts with -> (the arrow form), or with a { whose first significant token does not start a member declaration (name :: fn), the block form.
  • The classic form ({ name :: fn(...) ... }) is always valid. A short form on a trait with no required method, or with several, is an error.

Default methods#

A trait method may carry a body. An impl without that method gets the body, read as its own members are (Traits); a call in it to another member (self.area()) reaches the impl's own when it defines one.

Shape :: trait {
    area     :: fn(self) -> f64
    describe :: fn(self) -> string { return "a shape of area ${self.area()}" }
}

Square implements Shape -> self.side * self.side
// Square gets `describe` from the trait.

A default body is checked once, in the trait, for every type that will implement it. Self is then any implementing type, and a type parameter of the trait has only its bounds:

  • an error in the body is reported once, at the body, even when no type implements the trait;
  • an operation on a type parameter needs its bound, as in a generic function: a + b on two T needs T: Add;
  • a test on Self or on a type parameter (self is Square, x is Box<T>) is answered for each impl, by the type it implements the trait for.

A default with an expression body may leave out its return type, as a function may (§4 Type inference): its expression gives it, to the trait and to every impl. The compiler warns when the trait is exported, since its public type would then change with the body.

Shape :: trait {
    area  :: fn(self) -> f64
    twice :: fn(self) = self.area() * 2.0   // returns f64
}

In the standard library, Async has one default method: cancel, which does nothing.

Operator overloading#

An operator on operands other than primitive numerics calls the method of a standard-library trait (equality: Equality, below):

// with Point of §4 *Structs*
Point implements Add -> Point { .x = self.x + other.x, .y = self.y + other.y }

a :: Point { .x = 1, .y = 2 }
p := a + Point { .x = 10, .y = 20 }   // a.add(...): (11, 22)
p += a                                // p = p + a: (12, 24)
p * p                                 // error: `*` is not defined for Point
p < a                                 // error: Point does not implement Comparable
Operator Trait Method
a + b (-, *, /, %) Add<Rhs = Self, Out = Self> (Sub, Mul, Div, Rem) add :: fn(self, other: Rhs) -> Out (sub, mul, div, rem)
a == b Equals equals :: fn(self, other: Self) -> bool
a < b / <= / > / >= Comparable compare :: fn(self, other: Self) -> i32
a[i] Index<I, T> at :: fn(self, i: I) -> T
a[i] = v IndexSet<I, T> set_at :: fn(self, i: I, v: T)
v in a Contains<T> contains :: fn(self, v: T) -> bool
  • Each operator trait declares only the member its operator calls, with no default method: a != b is !a.equals(b) and a <= b is a.compare(b) <= 0.
  • A compound assignment (+=, -=, *=, /=, %=) is lhs = lhs <op> rhs, dispatched as <op>, with the place lhs evaluated once. Out must be assignable to lhs.
  • A missing Equals, Comparable or Contains is an error naming that trait. Missing arithmetic, like any operands an operator does not accept (i32 && bool), is an operator not defined for those types.
// std/time: Instant implements Add<Duration, Instant> and Sub<Instant, Duration>
later :: now() + seconds(5)        // an Instant
later - now()                      // a Duration
t := later
t -= now()                         // error: a Duration does not go into an Instant

twice :: fn<T: Add>(v: T) -> T = v + v
twice(p)                           // (24, 48): through the bound
twice(3)                           // 6: the built-in +
twice("ab")                        // "abab": through string implements Add

Resolution rule for arithmetic operators (+ - * / %):

  1. Two primitive numeric operands use the built-in operator.
  2. string + string concatenates, as a.add(b) and a T: Add bound do through string implements Add.
  3. Otherwise, the impl on the left operand's type runs, the right operand picking among several (Several impls of one trait) and being assignable to its Rhs; the result has its Out type.
  4. A left operand typed by a type parameter whose bound names the operator's trait dispatches through that bound (Generics): the right operand must be a T, the result is a T, and a primitive T uses the built-in operator.
  5. A value of the operator's trait (a: Add<i32>) or a union of its implementors dispatches as its method form does (Traits), and so does in on Contains, with the union's errors: a < b on two Comparable values is an error.
  6. Any other operands are an error.

Resolution rule for ordering (< <= > >=):

  1. Primitive numerics and char use the built-in comparison, a float by IEEE 754 (every comparison with nan is false). A distinct type orders as its backing (Distinct types). string orders through its Comparable impl, by bytes: code-point order, a proper prefix first.
  2. Two struct operands call Comparable. A type parameter bounded by Comparable dispatches as in arithmetic rule 4.
  3. Any other operand is an error: bool, enums, arrays, tuples and unions have no Comparable impl.
Ring :: i32[]

Ring implements Index<i64, i32> {
    at :: fn(self, i: i64) -> i32 {
        cells :: i32[](self)
        return cells[i % cells.len()]
    }
}

r :: Ring([1, 2, 3])
r[4]                    // 2: through at
r[0] = 9                // no IndexSet: writes the backing array
r[0..<2]                // [9, 2]: a Range slices the backing

a[i], a[i] = v and in use the built-in array or range first, then their trait, for structs and primitives alike: string implements Index<i64, char>, so s[i] needs no import.

  • A type's own impl wins over the array it is backed by, trait by trait.
  • A write with neither an IndexSet impl nor an array to store into is an error.
  • Outside an array, the key has the trait's index type, for a read as for a write (m["nope"] on a MutableMap<i64, i32> is an error); a free literal key takes that type.

Details.

  • a[i()] += v() runs a and i(), reads the element, runs v(), and writes the result back to that element.
  • A distinct type over a struct indexes only through its own impls, never its struct's.
  • A bounded type parameter, a value of the trait (store: IndexSet<string, i32>) and a union of implementors (MutableMap<string, i32> | Log) read and write through at and set_at.
  • A destructuring place ([m["a"], m["b"]] = ...) is a write: it takes the value type of IndexSet, never what Index returns (V | null on a MutableMap).

Nullability#

There is no implicit null. A value that may be absent has a union type:

find_user :: fn(id: u32) -> User | null {
    // ...
}

u :: find_user(42)
match u {
    is User -> println("found ${u.name}")
    is null -> println("not found")
}

Equality#

// with Point of §4 *Structs*
Point implements Equals -> self.x == other.x && self.y == other.y

p :: Point { .x = 1, .y = 2 }
p == Point { .x = 1, .y = 2 }   // true: Point's equals
found: Point | null: null
found == p                      // false: another variant
0.0 == -0.0                     // true: IEEE 754
b == '/'                        // true for b: u8: 47: the literal takes the type of b

n == 1.5                        // error: n is an i32, 1.5 a float
Meters(5) == Seconds(5)         // error: two distinct types, though both are over i64
Type ==
Integers, bool, char By value.
Floats By IEEE 754: nan != nan, 0.0 == -0.0, in a union or a generic body too.
string By contents.
Enum By variant; every enum implements Equals and Hash.
Distinct type Its own Equals impl, else its backing's ==.
Tuple Element by element (Tuples).
Union (A | B, T | null) The same variant, then that variant's ==; null == null.
Type parameter Through Equals, which its bound must name.
Struct Its Equals impl; without one, == is an error.
Array An error: use a named comparison (below), or === for identity.
Trait value, function value An error: narrow it first (if d is Point).
  • For a type parameter, this holds when the parameter is a union variant too (T | null == T); only x == null needs no bound. x in xs needs the bound too: T[] implements Contains<T> only for T: Equals.
  • A union compares with another union or one of its members when the two sides share a variant, and each variant but null needs an == (the error names it).
  • Comparing two integer types neither of which holds the other is an error (§4 Implicit numeric coercion). A distinct type compares with its own backing on either side ("" == path with Path :: string).

For arrays of a T: Equals element, std/core names the comparisons:

a :: [1, 2, 2]
a == [1, 2, 2]                  // error: use content_equals, is_permutation_of or ===
a.content_equals([1, 2, 2])     // true: the same length, equal elements in the same order
a.is_permutation_of([2, 1, 2])  // true: the same elements in some order, each as many times
a.contains_all([2, 2, 2])       // true: every element is in a, each counting once
a.is_disjoint([3, 4])           // true: no element in common
  • is_permutation_of is quadratic.
  • An array does not meet an Equals bound: [1] in rows (rows: i32[][]) and same(a, b) with fn<T: Equals> are errors, and an array of arrays has no content_equals.
  • A distinct type over an array opts in by implementing Equals itself (Row :: i32[]); i32[](row) gives back the array for these methods.

Details.

  • A generic Equals impl failing on a bound (Box<T> implements<T: Equals> Equals on a Box<Raw>) is an error naming the type argument that lacks it.
  • A generic body compares a T: Equals with the impl of the receiver's run-time type. When the other operand holds another type (T is a union, or the values are typed Equals), equals returns false without running it, except for an enum or a distinct type, compared as their representation (Unions, above).
  • A union variant that cannot be told apart at run time (Unions, above) may sit only beside null: a type parameter, or a generic struct instance with a type argument other than a primitive or an enum (Box<Point>). A union holding such an instance beside a variant other than null does not satisfy Equals either.
  • A distinct type stands for its backing through any number of layers: two integers compare when one holds the other (s == w compares in i64, for w: i64 and s of a distinct type over Hour), and two other scalars only when they are the same primitive. h == INVALID_HANDLE with h: Handle (Handle :: CPointer) against an i64, and b == c with b: u8 and a char variable, are errors.

Identity: === and !==#

a :: Point { .x = 1, .y = 2 }
b :: Point { .x = 1, .y = 2 }
a == b                 // true: equal fields
a === b                // false: two objects
a !== b                // true
found: Point | null: a
found === a            // true: compared by the object it holds
"x" === "x"            // error: a string has no identity, compare with ==
label :: Label { .text = "x" }
a === label            // error: a Point and a Label are never the same object

=== asks whether two references are the same object, and !== is its negation.

  • Only a reference has an identity: a struct, an array, a union of references (Node | null included), a distinct type over one of these.
  • === on anything else is an error (narrow first, or use ==): a number, bool, char, string, an enum, a tuple, a function, null alone, or a type that may hold one: a value typed by a trait (i32 implements Display), any, a type parameter, Self.
  • The two operands must be able to hold the same object, sharing a variant other than null: Point | null === Label | null is an error too.
  • === goes through no trait and cannot be overloaded.