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 thestd/coreprelude: 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,doubleorbyte. - No integer type takes the width of the target: a foreign
size_tis au64that@c_typespells for C (§13), and aCPointerreads as ani64or au64. - A length, an index and a size are an
i64on 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 anf64andf64(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.rand constructionColor { ... }, with the backing's fields; - tuple: positional access
p.0, construction from[a, b], and destructuring[a, b] :: p.
- array: indexing, the built-in array functions (
- 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
Colorfor aDisplay) 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>drivesi + d, while+,-,*,/and%on aMeters :: i64with no impl of its own are those ofi64. A literal beside it is then a value of the backing:m + -1is ani64sum, andc + -1withCode :: u32is an error, as on au32. - A distinct that defines the operator for another right operand does not fall back to its backing:
instant + 5is 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 arei64. AKm :: Meterscombines withMeters. 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_ofand@type_kindof 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
returnunder a declared return type, an index (the key type of theIndeximpl), 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 takesaas 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,
Intoapplies only when the type does not implement the trait (T[]intoIterator<T>). T implements Into<T>is an error at the impl.- A conversion calls one
into:SintoUandUintoTdo not take anSinto aT. - A second impl for the same
(Source, Target)is an error. - Overload resolution comes first:
Intois considered only when no overload matches as is.
Built-in coercions#
T[]intoIterator<T>, throughT[] implements<T> Into<Iterator<T>>(std/core).TintostringwhenT: Display, by itsto_string, throughT implements<T: Display> Into<string>. Interpolation ("${value}") renders throughDisplaywithoutInto.
Resolution order at a coercion site#
Sis assignable toT(a subtype, a traitSimplements, a free literal taking its type): nothing converts.- Otherwise, an impl
S implements Into<T>, under the rules above. - 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 reachu64.max).Int in 5..=1, a bound that is not a literal, and a base other thanIntare errors. Intonly names the base of a range: a value, a parameter or a field typedIntis an error.Name :: Int in ...is a transparent name for the range;Slot :: Hourdeclares a distinct type (Distinct types).- In value position,
r :: 0..=23is aRange<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 + 1andh += 1are rejected. - A local takes the type of its first value: after
next := h + 1,next = next % 24is an error, since0is outside1..=24. Annotate it (next: i32 = h + 1). - A primitive operand absorbs a range that fits in it:
h * xwithx: i32is ani32. - A bitwise operation needs a machine integer:
h & 1is an error,u8(h) & 1is 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 ani64, checked like any other. Acharliteral stands for its code point (b - '0'pastb in '0'..='9'is a0..=9). A comparison gives abool. - A primitive beside a range that holds it reads as the range of its values (
b + rwithb: u8andr: 0..=300is a0..=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 < kwithk: 1..=5holdshwithin0..=4. ==narrows to the compared values;!=removes a value only at an end (h != 23gives a0..=22,h != 12the wholeHour).- Ranges join at a true
||(if h < 6 || h > 20sees anHour), at the branches of anifor amatch, and at thereturns 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 isnever. - A write to a narrowed binding, a destructuring one included, ends the narrowing and is checked against its declared range (§3 Assignment):
x = 0is legal wherex == 23holds. - A comparison does not narrow a machine integer, named or primitive. Where
x in Tfails, a rangexis narrowed past the endTcovers, a primitivexis not narrowed. When the type ofxdecides the test, it is a constant (xis still evaluated).x !in Tnegates it, andx in 0..<12with literal bounds narrows the same way, though the test stays thecontainsofRange. - A narrowed value is a value of the range for what follows: past
if n !in Hour { return }on ani64,n * 1000is a0..=23000,a := nanHour,[n, n]anHour[]of bytes, andn << 8an error. Narrowed to the bounds of a machine integer, it is that integer: pastn in u8,acc := nis au8, andacc = acc * 2stops the program onceaccpasses127. - A comparison with
@min(T)or@max(T)narrows as its literal does. The number of values ofTis@max(T) - @min(T) + 1;x in charnarrows nothing. lo..=hion twoHourvalues is aRange<Hour>, andh..=30on anHouraRange<0..=30>: the element is what the bounds join into.Octet(x)converts asu8(x)does (Casts), with no check that stops the program. A member returningSelfreturns the integer (h.add(k)is au8), and is not checked inside another type (Self | null).- In a
fn<T: Int>, a non-integer source ofT(x), a value of a non-Inttype parameter included, is an error; a value ofTgoes where ani64goes (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 noIndexSetimpl). s.len()counts code points, in O(n);s.byte_len()counts bytes, in constant time.s[i]is the code point at indexi, ani64or a free literal; an index past the last code point stops the program.s[lo..<hi]ands[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 indexiin O(i). On an ASCII string (s.is_ascii()),s.len()ands[i]take constant time.for c in swalks the code points in place (string implements Into<Iterator<char>>), ands.chars()gives them as an iterator value. The byte loop isfor 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)ands.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, abyte_slicethat cuts a code point). Every walk over code points reads a malformed run as oneU+FFFD: the longest prefix of a well-formed sequence, at least one byte, overlong forms, surrogates and code points past U+10FFFF included. Sos.len(),for c in sands[i]always agree, and achar, 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 ofi32arrays.arr[r], for a rangerof any integer type, is a slice ofarr; its bounds, read asi64, are clamped at 0 too, andarr[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 andnull; - mixed types:
string, and every other union (one that holds a scalar, astringor 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 aspushandpush_allgrow the length.[v] * ngivesnreadable elements.- The element type comes from the left side, or from the slot when the left side is empty or a free literal (
[] * nor[0] * ninto an annotatedT[]). - 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) callsmakeonce per slot, in order, andfilled_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 += 1and 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 toi32[]. - 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 itsEquals, a union by variant, a bounded type parameter throughEquals), and!=is its negation. A distinct type over a tuple compares through its backing unless it implementsEqualsitself.- A tuple is only as comparable as its elements:
==is an error when an element has no working==(an array, a struct withoutEquals, a function or trait value, a type parameter whose bound does not nameEquals), 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
...placeon 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'Equalsimpl (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_ofand 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>,breakorcontinue; - 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
returnvalue or expression body, an element of an array literal in a typed slot; - the value of a
matcharm or anifbranch 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 ofin.
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 Etests an integer against its values (x !in Enegates it), and is a constant where the type ofxdecides it. When the values are consecutive, it narrowsxto them, as a range test would; an enum with a gap in its values narrows nothing.E(x)converts asHour(x)does (Range types, above); a non-integerxis an error.for d in Ewalks its variants in declaration order.
Details.
- On an enum with no variant,
x in Eisfalse,fortakes no turn, and@min,@maxandE(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
i8toi64andu8tou64, 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
@externfunction 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.fieldreads a field ofv's data, andInfo(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.codestays 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@externparameter of the enum, through a function value too (§13 Allowed signature types). Interpolating renders throughDisplay, 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
Tbound to an enum renders through the enum'sDisplay, in a generic impl too (Box<Direction>). A generic function renders aTbound to a distinct type through the distinct'sDisplay. - Interpolating a union that holds an enum, or a distinct type with a
Displayof its own, is an error, and so is interpolating an array of such unions: narrow first. A distinct type without its ownDisplayrenders 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 Tcan never hold is an error, as in anif.
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"afteris 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..=3over au8is an error. - Over a distinct type, the bounds take its backing type, as
<does. - Over a union, they take its integer or
charvariant, which the arm tests first. Once arms above have taken that variant, the compiler warns. - A scrutinee with no integer or
charto 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 Xarms that constrain no field, the_arm sees what those arms left: afteris null ->, aPet | nullis aPet. - Literal and range arms narrow an integer scrutinee to their values, as
if h in 1..=11would. An or-pattern narrows to the range that holds its alternatives, and_to the values left when they form one interval. Acharand 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. Au64still needs_, since no literal reaches pasti64. - 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
Tis fixed: met in an argument or the result slot, it binds the callee's parameter toTitself. - 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)withkeep :: fn<T>(x: T) -> Tis 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 aschar. - A type that implements
Into<char>converts through it. - A UTF-16 surrogate is a code unit, an integer (
is_surrogate(unit: u32)instd/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")fixesAand infersB); 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), orBag.make<i64>(1)with those ofBaginferred. - 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: withPair :: struct<A, B>,Pair<i32> { ... }andPair<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()underK: Hash,a < bunderT: 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: astring[](Contains<string>) does not meetC: 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)insidefixed<T>); - in the impl of a generic struct, a call on a larger instance of it (a
Box<T[]>inside the impl ofBox<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)).TandTraitresolve like any other name, and the impl belongs to the declarations they name: of two modules'Footypes, orAnimaltraits, 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,
Selfread 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. selfandSelf: in a trait or an impl, a first parameter namedselfhas the typeSelfwith no annotation, andSelfis the implementing type (FooinFoo implements Trait { ... }, a written-> Selfincluded). Elsewhere,Selfis 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 inEquals, whose parameter checks the other operand's type; - a member returning
Selfinside 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 aMonType). - It replaces only the generic impls of its trait at arguments that can coincide with its own:
MonType[] implements Into<Label>leavesT[] implements<T> Into<Iterator<T>>to an array ofMonType. 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]andc[k] = v, by the key;[a, b] :: r, by the arity of the tuple anInto<[...]>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'stag.
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.intois fine); - a use that does not name the trait's arguments: a
forover a type implementingIteratortwice, or converting into twoIterators 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
Displaybound 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 aWrapper<Plain>is an error, as arefor,w[i],w[i] = vand an operator through such an impl. - An
is Traittest 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 + bon twoTneedsT: Add; - a test on
Selfor 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 != bis!a.equals(b)anda <= bisa.compare(b) <= 0. - A compound assignment (
+=,-=,*=,/=,%=) islhs = lhs <op> rhs, dispatched as<op>, with the placelhsevaluated once.Outmust be assignable tolhs. - A missing
Equals,ComparableorContainsis 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 (+ - * / %):
- Two primitive numeric operands use the built-in operator.
string + stringconcatenates, asa.add(b)and aT: Addbound do throughstring implements Add.- 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 itsOuttype. - 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 aT, and a primitiveTuses the built-in operator. - 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 doesinonContains, with the union's errors:a < bon twoComparablevalues is an error. - Any other operands are an error.
Resolution rule for ordering (< <= > >=):
- Primitive numerics and
charuse the built-in comparison, a float by IEEE 754 (every comparison withnanis false). A distinct type orders as its backing (Distinct types).stringorders through itsComparableimpl, by bytes: code-point order, a proper prefix first. - Two struct operands call
Comparable. A type parameter bounded byComparabledispatches as in arithmetic rule 4. - Any other operand is an error:
bool, enums, arrays, tuples and unions have noComparableimpl.
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
IndexSetimpl 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 aMutableMap<i64, i32>is an error); a free literal key takes that type.
Details.
a[i()] += v()runsaandi(), reads the element, runsv(), 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 throughatandset_at. - A destructuring place (
[m["a"], m["b"]] = ...) is a write: it takes the value type ofIndexSet, never whatIndexreturns (V | nullon aMutableMap).
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); onlyx == nullneeds no bound.x in xsneeds the bound too:T[]implementsContains<T>only forT: Equals. - A union compares with another union or one of its members when the two sides share a variant, and each variant but
nullneeds 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 (
"" == pathwithPath :: 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_ofis quadratic.- An array does not meet an
Equalsbound:[1] in rows(rows: i32[][]) andsame(a, b)withfn<T: Equals>are errors, and an array of arrays has nocontent_equals. - A distinct type over an array opts in by implementing
Equalsitself (Row :: i32[]);i32[](row)gives back the array for these methods.
Details.
- A generic
Equalsimpl failing on a bound (Box<T> implements<T: Equals> Equalson aBox<Raw>) is an error naming the type argument that lacks it. - A generic body compares a
T: Equalswith the impl of the receiver's run-time type. When the other operand holds another type (Tis a union, or the values are typedEquals),equalsreturnsfalsewithout 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 thannulldoes not satisfyEqualseither. - A distinct type stands for its backing through any number of layers: two integers compare when one holds the other (
s == wcompares ini64, forw: i64andsof a distinct type overHour), and two other scalars only when they are the same primitive.h == INVALID_HANDLEwithh: Handle(Handle :: CPointer) against ani64, andb == cwithb: u8and acharvariable, 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 | nullincluded), 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,nullalone, or a type that may hold one: a value typed by a trait (i32implementsDisplay),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 | nullis an error too. ===goes through no trait and cannot be overloaded.