#Type System
Status: language specification draft.
This chapter defines types, assignability, value types and writable places, generics, and variance.
- types.statichd-lang is statically typed: every expression has a compile-time type.
- types.no-executeA program with a type error must not execute.
- types.local-inferenceLocal inference removes redundant annotations; public and aggregate boundaries remain explicit.
fn demo(x: i32) -> i32:
x + 1 # every expression has a compile-time typeSee also: Type Inference Boundaries.
#Type Forms
- types.forms.with-opaque-resultsThe type forms are:
| Form | Spelling |
|---|---|
| Primitive types | |
| Nominal struct types and enums | |
| Tuples | (A, B), or ending in a rest element, (A, List[T]...) |
| Lists and maps | List[T], Map[K, V] |
| Optional types | T? |
| Function types | fn(...) -> T, sugar for Fn[(...), T, $()] |
| Suspending function types | fn!(...) -> T, sugar for SuspendFn[(...), T, $()] |
| Requirement-bearing function types | ending in $ Row |
| Generic instantiations | |
| Associated type projections | such as T::Item |
| Trait value types | |
| Transparent aliases and nominal newtypes | |
| Reference types | *T, *mut T, &T, &mut T |
| Opaque function result types | impl Trait[Args] |
- types.suspend
Suspend[T]is the dynamic one-shot computation protocol.
fn demo() -> (List[i32], string?):
([+1, +2], .Some("a")) # a tuple of a list and an optionalSee also: Requirements and Suspension.
#The never Type
- types.never
neveris the uninhabited bottom type. - types.never.assignable
neveris assignable to every type, and no ordinary value is assignable to it. - types.never.abruptAn expression that completes abruptly has type
neveron that control-flow path. - types.never.abrupt-formsThese expressions complete abruptly: an unconditional
return,break, orcontinue, propagation that exits the current body, and a call topanic. - types.never.formsExactly six forms have type
never:return,break,continue, a call topanic, and any other call whose result type isnever. The sixth is an infinite loop that nobreaktargets. - types.never.fitsEach of them therefore fits any expected type, as in a match arm
.None => continueor a branchelse: break.
fn total(entries: List[i32?]) -> i32:
let sum = 0
for entry in entries:
amount := match entry:
.Some(value) => value
.None => continue
sum = sum + amount
sumNote. This explains why every reachable branch of a value-producing
iformatchmay end in one of these forms. It changes no program's validity.
#Primitive Types
types.prim.setThe primitive types are:
| Category | Types |
|---|---|
| Boolean | bool |
| Signed integers | i8, i16, i32, i64 |
| Unsigned integers | u8, u16, u32, u64, usize |
| Floating point | f32, f64 |
| Text | char, string |
- types.prim.no-aliasesThere are no core aliases named
int,uint, orfloat. - types.prim.decimal
decimalmay be a standard-library type but is not primitive. - types.prim.no-bytehd-lang has no primitive
byteorbytestype and no byte literal syntax. - types.prim.by-valuePrimitive values are passed and returned by value.
- types.prim.no-mutPrimitive types do not accept the
mutmodifier. - types.prim.no-mut.errorA primitive type written with
mut, as inlet n: mut i32 = 0, is an error. Error:mut-on-primitive.
fn invalid(scale: mut f64) -> void: # error: mut-on-primitive
let n: mut i32 = 0 # error: mut-on-primitiveWhy. Primitive types do not expose mutable reference state.
- types.prim.no-mut.selfA
mut selfreceiver in an impl whoseSelfis primitive is valid: it is not a primitive type written withmut.
trait Counter:
fn reset(mut self) -> void
impl Counter for i32:
fn reset(mut self) -> void: pass # validWhy. An impl repeats its trait method's signature. A trait with a
mut selfmethod must therefore stay implementable for a primitiveSelf.
- types.prim.no-mut.self-typeIn such a method the
mutis dropped, because a primitive has nomutform:selfhas the plain typeSelf, as ini32. - types.prim.no-mut.self-callA call of such a method therefore needs no writable place as its receiver.
trait Step:
fn next(mut self) -> i32
impl Step for i32:
fn next(mut self) -> i32: self + 1 # valid: self is i32
fn advance(start: i32) -> i32: start.next() # valid#The usize Type
usize is the type of sizes: lengths, indices, counts, and byte offsets:
fn last(items: List[string]) -> string:
items[items.len() - 1] # in a debug or test build, panics with integer-overflow when items is empty
fn middle(items: List[i32]) -> usize:
items.len() / 2- types.usize.primitive
usizeis a primitive unsigned integer type. It is distinct fromu32and from every other numeric type. - types.usize.width
usizeis an unsigned integer with the target's pointer width. On the Wasm32 target it is 32 bits. - types.usize.behaviorIts range, overflow, wrapping, casts, and literal range are those of an unsigned integer of that width, so on Wasm32 they are those of
u32. - types.usize.prelude
usizeis a prelude name, so every module may write it without ause. - types.usize.sizesEvery length, index, count, and byte offset that the language defines has type
usize. - types.usize.convertA
usizeand au32convert only by a written cast, asusize(limit)oru32(size), bytypes.num.no-implicit. A value of one where the other is expected is an error. Error:type-mismatch.
fn take_count(count: u32) -> u32:
count
fn total(items: List[i32], limit: u32) -> usize:
limit + items.len() # error: type-mismatch
fn counted(items: List[i32]) -> u32:
take_count(items.len()) # error: type-mismatch
fn fixed(items: List[i32], limit: u32) -> usize:
usize(limit) + items.len()Note. On Wasm32 a
usizehas the representation of au32, a Wasmi32. That is a fact about representation: the two stay distinct types.
Note. A size is never negative, so
len() - 1on an empty collection is below the range ofusize. In a debug or test build it panics withinteger-overflowbytypes.arith.checked, as in Rust. A release build wraps bytypes.arith.release.
Why. A size cannot be negative, so an unsigned type rejects a negative index when the program is checked rather than when it runs. A size has its own type, as in Rust's
usizeand Go'sint. So a size never mixes silently with a 32-bit value that means something else.
See also: Numeric Conversions, Numeric Casts.
#void And The Empty Tuple
- types.void
voidis an alias for the empty tuple type(), so the two spellings name one type and are interchangeable. - types.unitThe empty tuple value is
(). - types.void.roleA function that produces no useful value returns
(), and its result type is usually spelled-> void. - types.unit.role
()is a tuple value and tuple type.
fn log_start() -> void:
pass
fn same() -> ():
log_start() # valid: void and () are one typeNote. The type has one value,
(). Avoidresult, a statement result, andpassall produce it, as Swift'stypealias Void = ()does.
#Strings
A string is an immutable sequence of bytes that is always valid UTF-8. Its
length and its index count bytes:
fn initial(name: string) -> u8:
name[0]
fn size(name: string) -> usize:
name.len() # 6 for "héllo"- types.string.utf8-bytesA
stringis a sequence of bytes whose contents cannot be mutated. - types.string.valid-utf8Every
stringvalue is valid UTF-8. - types.string.literal-utf8The value of a string literal is the UTF-8 encoding of its Unicode scalar values.
- types.string.concat-bytesConcatenation joins the bytes of its operands, so its result is valid UTF-8.
- types.string.from-bytesA conversion from arbitrary bytes to
stringmust check that they are valid UTF-8, and must fail when they are not. - types.string.boundaryA scalar boundary of a string is a byte offset from
0to its length. It does not fall inside the encoding of a scalar value. - types.string.split-scalarAn operation given a byte offset that is not a scalar boundary is a checked runtime panic.
- types.string.no-normalizationSource text and runtime operations do not perform Unicode normalization.
- types.string.compare-bytesEquality and ordering compare bytes in sequence.
- types.string.encodingsCanonically equivalent but differently encoded scalar sequences are distinct.
- types.string.len-bytes
string.len()is the number of bytes, and takes constant time. - types.string.graphemesLibraries may provide grapheme traversal.
- types.string.host-bytesAt every Wasm host boundary, a string crosses as its UTF-8 bytes with no conversion, including embedded U+0000 scalar values.
Note. For valid UTF-8, the order of bytes is the order of scalar values, so comparing bytes sorts strings by scalar value.
Why. This is Go's layout with the validity guarantee of Rust and Swift. Length and indexing take constant time, and a host string crosses without conversion. Because
lenands[i]count bytes, a string is notIterable. A loop over characters is written out, so no code silently treats a string as characters.
See also: String Indexing, Ordering, Iteration Protocols, String Methods.
#Literal Types
An unsuffixed numeric literal takes its type inside the expression that holds it. It comes from an expected type, from the typed parts of that expression, or else from its own form, as Open Literal Width states:
x := 1 # usize
y := -1 # i32#Integer Literals
- types.literal.int-localAn integer literal in any supported radix with no expected type takes its width from its expression, by
types.literal.local.class.meet. Otherwise it takes its default type. Both hold in every value range. - types.literal.int-no-widenAn integer literal does not automatically choose a wider type.
- types.literal.int-rangeWhen an integer literal has an expected integer type, the compiler checks the literal against that type's range. A literal outside it is an error. Error:
integer-literal-range.
let small: i8 = 1 # valid
let bad: u8 = 300 # error: integer-literal-range
let ratio: f64 = 1 # error: type-mismatch- types.literal.int-diagnosticThe diagnostic must identify the literal, the target range, and an appropriate wider type when one exists.
- types.literal.int-not-floatAn integer literal never takes a floating-point type. An integer literal whose expected type is
f32orf64is an error; write1.0. Error:type-mismatch. - types.literal.unary-plusAn expected numeric type passes through unary
+to a numeric literal, solet positive: u8 = +1checks the literal against theu8range.
#Negated Integer Literals
- types.literal.negationUnder an expected signed integer type, unary
-applied directly to an integer literal is range-checked as a unit. The check considers the negated mathematical value. - types.literal.negation.minimumThis makes
let minimum: i8 = -128valid even though positive128does not fit ini8. - types.literal.negation.unsignedA negated literal for an unsigned expected type is an error. Error:
type-mismatch. - types.literal.negation.below-minimumValues below the signed minimum remain errors.
fn demo() -> i8:
-128 # valid: checked as a unit against i8#Floating-Point Literals
- types.literal.float-localA floating-point literal with no expected type takes its width from its expression, by
types.literal.local.class.meet, or elsef64. It must be representable as a finite value of the type it takes. - types.literal.float-expectedWhen an expected
f32orf64type is available, the literal is converted directly to that type. - types.literal.float-finiteThe converted literal must be representable as a finite value under that type's IEEE 754 rounding rules. Error:
float-literal-range.
fn demo() -> f64:
1.5#Open Literal Width
A literal's width is settled within the one expression that holds it. An expected type decides it; otherwise the typed parts of the expression do; otherwise the literal's own form does:
fn biggest[T < Ord](left: T, right: T) -> T:
match left.cmp(right):
.Less => right
_ => left
fn total(price: i64, items: List[string]) -> i64:
let count = 0 # usize: nothing else decides it
let delta = -1 # i32: a signed literal
let mixed = [1, -2] # List[i32]: a signed member
let last = biggest(0, items.len()) # 0 is a usize, as items.len() is
price + 5 # 5 is an i64, as price is- types.literal.local.expectedAn unsuffixed numeric literal with an expected type takes that type, by Integer Literals and Floating-Point Literals.
- types.literal.local.signedA signed literal is an integer literal written directly after unary
-or+, as in-1or+5. - types.literal.local.classUnsuffixed literals with no expected type that meet one another, directly or through a type not yet known, form one literal class.
- types.literal.local.defaultA literal class's default type is
i32for integers when a member is a signed literal, andusizeotherwise. For floating-point literals it isf64. - types.literal.local.class.meetWhen a literal class meets a type in one of the forms below, every literal of the class takes that type.
- types.literal.local.class.openA literal class that meets no type by the end of its statement takes its default type.
- types.literal.local.class.onceA literal class's type is decided once, when it meets a type or takes its default. A statement that fails to check is an error and is not checked again with other widths.
- types.literal.local.statementNothing crosses a statement. A binding takes the type of its initializer, and no later statement changes it.
- types.literal.local.blockA block of two or more statements inside an expression is not part of it. A branch, arm, or closure body that is one expression is part of it.
fn take(n: i32) -> i32:
n
fn run(items: List[string]) -> i32:
let i = 0 # a usize: a later use does not change it
if i < items.len():
return 0
take(i) # error: type-mismatchTwo literal classes in one statement take their widths apart:
fn pick[T](first: T, second: T) -> T:
first
fn widths(big: i64, small: i32) -> (i64, i32):
(pick(1, big), pick(2, small)) # 1 is an i64, and 2 is an i32The expression forms in which literals meet typed parts:
| Rule | Form | Example | The width comes from |
|---|---|---|---|
| types.literal.local.form.operandOperands | an operand of an arithmetic or comparison operator, on either side | 5 + price, identity(0) + price |
the other operand |
| types.literal.local.form.argumentCall arguments | the arguments of one call | biggest(0, items.len()) |
the parameter types, and the other arguments that solve a type parameter |
| types.literal.local.form.receiverReceivers | the receiver of a method call | [1, 2].iter().all(fn(n: i32) -> bool: n > 0) |
the typed values the call meets, such as a closure parameter type |
| types.literal.local.form.elementCollections | the elements of one list, map, tuple, or struct literal | [small, 1] |
the typed elements |
| types.literal.local.form.armArms | the branches of one if and the arms of one match |
if c: 1 else: count |
the typed branches |
| types.literal.local.form.closure-returnClosure returns | the return paths of a closure with no written result type | return value and a final 0 |
the typed return paths |
| types.literal.local.form.loop-elseLoop values | the else value and the break values of one while or for |
break total and else: 0 |
the typed values |
| types.literal.local.form.pipePipes | the value of a pipe and its step | 3 |> twice |
the step's parameter type |
| types.literal.local.form.factTyped facts | the arguments of a typed fact | @range(0, 100, "percent") on an i32 field |
the annotated field or parameter type |
| types.literal.local.form.indexIndexes and ranges | an index, a slice bound, or a range bound | xs[0], 0..items.len() |
usize, or the other bound |
| types.literal.local.form.interpolationInterpolations | an expression inside ${...} |
"${price + 1}" |
the expression's own typed parts |
| types.literal.local.form.expectedExpected types | a default argument, a returned value, an assigned value, or a typed field | fn pay(amount: i64 = 5) |
the expected type |
- types.literal.local.form.closure-return.statementsThe return paths of one closure, and the values of one loop, join although they are in different statements of its body. A value that leaves its own statement takes no part.
- types.literal.local.form.function-return.statementsThe return paths of a non-public function whose result type is omitted join the same way, across the statements of its body.
- types.literal.local.form.join-openA literal class that reaches such a join stays open until the join, and takes its default type there when the join meets no type.
- types.literal.local.erased.reference-valuesA literal addressed and converted to a trait value reference has its class's default type. So
let x: &dyn Any = &42refers to ausize. - types.literal.local.instantiationTwo or more instantiations of one generic trait may fit a call only because a literal argument could take several widths. Then the literal keeps its default type. With no instantiation for that type, the call is an error. Error:
type-mismatch.
use std.ops.Add
struct Money:
cents: i64
impl Add[i32] for Money:
type Out = Money
fn add(self, rhs: i32) -> Money:
Money { cents: self.cents + i64(rhs) }
impl Add[i64] for Money:
type Out = Money
fn add(self, rhs: i64) -> Money:
Money { cents: self.cents + rhs }
fn charge(price: Money) -> Money:
let after = price.add(-5) # valid: the signed literal is an i32
price.add(5) # error: type-mismatch- types.literal.local.hintWhen a binding whose type is a literal's default type meets another type, the diagnostic should point at that literal and suggest the fix there.
Note. The suggested fix is a sign for
i32, aslet total = +0or{"tea": +3}, and an annotation for another width, aslet total: i64 = 0.
Note. With no annotation,
let balance = 100followed bybalance = balance - 150is ausizesubtraction below zero, which panics withinteger-overflowin a debug or test build. Write+100for a value that may go negative. The panic report says that the type is a literal's default type, byflow.panic.report.fallback.
Why. An unannotated integer usually counts or indexes, so
usizeletslet i = 0meetlen()with no annotation. A written sign is the program's own statement that the value may be negative. Settling each literal within its expression keeps every type visible where it is written, and an error never blames a distant line.
Note.
let mut i = 0gets the warning oftypes.bind.let-mut-primitive-lintunless&mut iis taken: a number is primitive at every width.
Note. A type that only an implementation names is not one the literals meet. So
10.halve(), with onlyimpl Halve for i64, is an error: writei64(10).halve(), or give its binding an annotation.
See also: Binary Numeric Operators,
Inference From Several Arguments,
Binding Forms, Range Expressions,
For Loops,
Let Patterns, and
the REPL.
Ordering covers comparisons that a usize
default makes always true.
#Duration Literals
A duration literal has the standard std.time.Duration type without importing its unit function.
fn delay() -> void:
_ := 500ms
_ := 30s- types.literal.duration.fixedA duration literal has type
std.time.Duration, independent of its expected type and names in scope. - types.literal.duration.unitsUnits
ms,s,min, andhmultiply their integer counts by 1, 1000, 60000, and 3600000 milliseconds, respectively. - types.literal.duration.rangeA duration literal must represent between zero and 9223372036854775807 milliseconds, inclusive. Error:
integer-literal-range. - types.literal.duration.callA duration literal evaluates as the corresponding qualified
std.timeunit function applied to its integer count. - types.literal.duration.negativeUnary
-does not apply to a duration literal. Error:type-mismatch.
The functions and named constructors remain available; a duration literal is the preferred spelling for a literal duration.
#Suffixed Literals
A primitive suffix fixes a numeric literal's type. A user suffix has the type that its suffix function returns:
use std.ops.num_suffix
struct Millis:
count: i64
@num_suffix
fn ticks(count: i64) -> Millis:
Millis { count: count }
fn delay() -> Millis:
250ticks # ticks(250): Millis- types.literal.user-call-resultA user-suffixed literal or prefixed string has the result type of its literal function.
- types.literal.suffixed.inThe numeric literal is checked with the suffix function's parameter type as its expected type, by the rules above. So a suffix whose parameter is
i8range-checks300b. - types.literal.primitive.fixedA primitive-suffixed literal has the suffix's primitive type, independent of its expected type or literal class.
- types.literal.primitive.kindAn integer literal requires an integer primitive suffix, and a floating-point literal requires a floating-point primitive suffix. Error:
type-mismatch. - types.literal.primitive.integer-rangeA primitive-suffixed integer literal must fit its suffixed type's range. Error:
integer-literal-range. - types.literal.primitive.float-rangeA primitive-suffixed floating-point literal must be finite under its suffixed type's rounding rules. Error:
float-literal-range. - types.literal.primitive.negationUnary
-directly on a primitive-suffixed signed integer literal checks the negated value, so-128i8is valid.
use std.ops.num_suffix
@num_suffix
fn secs(count: i64) -> i64: count * 1000
fn wait() -> void:
half := 1.5secs # error: type-mismatch
passNote. Argument checking rejects a literal of the wrong kind, so
1.5sis invalid whenstakes ani64; write1500ms.
Note.
-5sis the ordinary negation-(5s): unary-applies to the result ofs(5), so a non-primitive result type needsNeg. The minus is not part of the literal, so-128bchecks128against ani8parameter and is out of range; writeb(-128).
See also: Literal Suffixes.
#Prefixed Strings
A prefixed string has the type that its prefix function returns, and each interpolated value is checked like an argument:
use std.ops.{Template, str_prefix}
@str_prefix
fn ids(t: Template[i32]) -> string:
"ids"
fn find(name: string, big: i64) -> string:
first := ids"id = $name" # error: type-mismatch
ids"id = $big" # error: implicit-narrowing- types.literal.prefixed.valuesEach interpolated expression is checked with the
Tof the prefix function'sTemplate[T]parameter as its expected type, as an argument is. It converts toTby the same rules.
Note. So the argument errors apply at each value: a
stringwhereTisi32istype-mismatch, and ani64there isimplicit-narrowing. WhenTis the trait value typeDisplay, every value whose type implementsDisplayconverts, as Dynamic Trait Values specifies.
See also: Prefixed Strings.
#Other Literals
- types.literal.bool
trueandfalsehave typebool. - types.literal.stringA string literal has type
string, and a character literal has typechar. - types.literal.no-absentThere is no literal for an absent optional; it is the enum variant
.None.
fn demo() -> (bool, string, char):
(true, "a", 'b')See also: Optional Types.
#Nominal And Structural Types
Each struct and enum declaration introduces a distinct nominal type:
struct UserId:
value: string
struct PostId:
value: stringUserId and PostId are distinct even though their field shapes match.
- types.nominal.distinctEach
structandenumdeclaration introduces a distinct nominal type. - types.nominal.shapeMatching fields or variants do not make two nominal types interchangeable.
#Tuple Types
A tuple is an immutable value without identity, so it implements AnyVal,
as types.sealed.anyval-members states. No
element of it is a place, as
expr.place.tuple-element
states.
- types.tuple.structuralTuple types are structural.
- types.tuple.sameTwo tuples have the same type when they have the same arity and pairwise-equal element types.
- types.tuple.one-elementOne-element tuples require a trailing comma.
- types.tuple.empty
()is the empty tuple.
fn swap(pair: (i32, string)) -> (string, i32):
(pair._1, pair._0) # (i32, string) is the same type wherever it is writtenSee also: types.value.no-mut.written,
which rejects a tuple type written with mut.
#Rest Elements
A tuple type may end in a rest element List[T]..., which stands for
any number of trailing T values:
fn tail(values: (string, List[i32]...)) -> usize:
values._1.len()
fn main() -> usize:
tail(("a", 1, 2, 3))Open in playground- types.tuple.rest.formA tuple type may end in one rest element, a type followed by
..., as in(i32, i32, List[i32]...). Its other elements are its fixed elements. - types.tuple.rest.listA rest element whose type is not
List[T], as in(i32, i32...), is an error. Error:type-mismatch. - types.tuple.rest.valueA value of such a tuple holds its fixed elements, then one
List[T]that holds the trailing values in order. Selection reads that list like any other element, sovalues._1above is aList[i32]. - types.tuple.rest.sameTwo tuple types are the same type only when both or neither end in a rest element, so
(i32, List[i32]...)and(i32, List[i32])differ. With rest elements, the fixed elements and the rest elements must each be pairwise equal. - types.tuple.rest.assignNo conversion adds, removes, or changes a rest element: a tuple type with a rest element converts only to an identical one. Error:
type-mismatch.
fn plain(values: (i32, List[i32]...)) -> (i32, List[i32]):
values # error: type-mismatchWhy. The rest element lets a function's inputs tuple say that it takes varargs, so
fn(i32, List[i32]...)andfn(i32, List[i32])are different types. Only aListtail is allowed: there is no tuple concatenation in types.
See also: Vararg Inputs, Tuple Rest Elements.
#Function Type Identity
- types.fn.constructorA function type is an application of the standard constructor
FnorSuspendFn, and thefn(...) -> Tspelling is exact sugar for it. - types.fn.sameTwo function types are the same type when they apply the same constructor to the same inputs. The output and the normalized requirement row must also be the same.
- types.fn.declared-varianceFunction types convert only by the declared variance of their constructor, as Readonly Outer Views states.
fn apply(f: fn(i32) -> i32, x: i32) -> i32:
f(x)See also: Function Type Constructors, Variance.
#Transparent Aliases And Newtypes
A transparent alias introduces another name for the same type:
type UserName = string- types.alias.sameA transparent alias introduces another name for the same type.
- types.alias.identical
UserNameandstringare identical for assignability, method lookup, trait conformance, and runtime representation. - types.alias.rowA transparent alias may also name a requirement row, as Row Aliases defines.
- types.alias.cycleAn alias that expands to itself, directly or through other aliases, is an error. Error:
alias-cycle. - types.alias.cycle.reportedThe error is reported once per cycle, on the declaration of the cycle that comes first in the source.
- types.alias.target-unknownA name on an alias's right side that resolves to nothing is an error at the alias declaration, whether or not anything uses the alias. Error:
unknown-type. - types.alias.target-unknown.keyA key after
$on a row alias's right side that resolves to no trait is likewise an error at the alias declaration. Error:unknown-trait.
type Left = List[Right] # error: alias-cycle
type Right = Left?type Owner = Account # error: unknown-type
type AppRow = $ Ledger # error: unknown-traitWhy. A transparent alias is replaced by its right side, so a cycle never ends. A recursive type needs a
structorenumdeclaration.
#Newtypes
A parenthesized type declaration creates a nominal single-field newtype:
type Mile(i32)- types.newtype.declA parenthesized type declaration creates a nominal single-field newtype.
- types.newtype.distinct
Mileis distinct fromi32. - types.newtype.constructConstructor syntax creates the newtype, and the base type constructor unwraps it:
m := Mile(10)
n := i32(m)- types.newtype.no-implicitNo implicit conversion exists in either direction.
- types.newtype.no-inheritA newtype does not inherit trait implementations from its underlying type.
- types.newtype.map-keyIn particular, a newtype is not a valid map key until it explicitly implements or derives both
EqandHash. - types.newtype.construct-refConstructing a newtype over an
AnyRefbase creates no new object: the newtype value wraps the base value. - types.newtype.construct-valueA newtype construction over an
AnyValbase, such asMile(10), is a value, as its base is.
See also: Map Key Types.
#Optional Types
The prelude declares the ordinary generic enum Option:
enum Option[T]:
Some(value: T)
None- types.option.declThe prelude declares the ordinary generic enum
Option. - types.option.sugar
T?is exact sugar forOption[T]: the two spellings denote the same type everywhere a type may appear, including implementation targets. - types.option.distinct
TandT?are different types; a non-optional type never contains an absent value. - types.option.prelude-name
Optionis the only prelude name this adds;SomeandNoneare variants, not prelude names. - types.option.bare-variantA bare
NoneorSome(value)expression therefore resolves like any other identifier. It is an error unless a declaration in scope supplies that name. Error:unknown-name.
#Optional Values
- types.option.noneThe absent value is written
.Nonewhere an optional type is expected, orOption.None. - types.option.someA present value is written
.Some(value)orOption.Some(value). - types.option.constructionThese spellings follow the ordinary enum construction rules.
- types.option.none-contextIn particular,
.Nonewithout an expected optional type is an error. Error:missing-contextual-enum-type. - types.option.patternsOptionals are matched with ordinary enum patterns,
.Some(pattern),.None, and theirOption.-qualified forms.
let value: i32? = None # error: unknown-name
missing := .None # error: missing-contextual-enum-typeSee also: Enum Declarations, Match Expressions.
#Implicit Wrapping And Nesting
- types.option.wrapA value of
Tis also implicitly accepted whereT?is expected, constructing.Some(value). - types.option.wrap.sitesThe implicit wrap applies to assignments, arguments, and return values.
- types.option.wrap.onceThe source expression is evaluated once.
- types.option.wrap.one-layerThe implicit wrap adds one layer only.
- types.option.nestOptionality may nest:
T??isOption[Option[T]]. It preserves the distinction between an absent outer value and a present outer value containing an absent inner value. - types.option.nest.acceptA
T?value is accepted whereT??is expected, but a plainTis not. - types.option.nest.explicitThe inner layer needs an explicit
.Some(...), as inlet nested: i32?? = .Some(1). - types.option.nest.none
.Nonewith an expectedT??is the outer absent value, and.Some(.None)is a present outer value holding an absent inner value.
fn demo(flag: bool) -> i32?:
if flag: +1 else: .None#Representation And Propagation
- types.option.reprThe representation of
Option[T]is an implementation detail; for example, an implementation may represent.Noneas a null reference. - types.option.propagatePostfix
?on an optional expression either produces its contained value or returns.Nonefrom the nearest function. - types.option.propagate.resultThat function must itself return a compatible optional type.
- types.option.propagate.one-layerPostfix
?removes and propagates one optional layer at a time. - types.option.propagate.permissionA present value keeps the optional's declared contained type
T, including*mut UwhenT = *mut U; unwrapping does not weaken that generic argument.
fn first(items: List[i32]) -> i32?:
if items.len() == 0: .None
else: .Some(items[0])
fn demo(items: List[i32]) -> i32?:
first(items)? # the value, or .None returns at once#Optional Identity And Variance
- types.option.ordinaryOptionals follow the ordinary enum rules in every other respect.
- types.option.any.reference-valuesA reference to an optional value may be erased to
&dyn Anylike a reference to any other enum value. - types.option.valueAn optional is an enum, so it is a value without identity and implements
AnyVal, whatever its payload type. - types.option.identity-error
iswith an optional operand is an error, as for every enum. Error:identity-requires-references. - types.option.invariant
Optiondeclares its parameter unmarked, so an optional is invariant in its contained type, as every unmarked parameter is. - types.option.invariant.result
Result[T, E]likewise declares both parameters unmarked, so it is invariant in bothTandE.
struct User:
name: string
fn view(user: (*mut User)?) -> (*User)?:
user # error: type-mismatch
fn settle(result: Result[*mut User, string]) -> Result[*User, string]:
result # error: type-mismatch
struct Maybe[+T]:
value: T? # error: invalid-varianceWhy. The standard methods on optionals and results take
Tas a parameter, asunwrap_or(self, fallback: T)does. That is a negative position on the public surface, so a covariantTwould fail its own polarity check.
See also: Unary And Binary Operators, Variance, and the stdlib chapters Option and Result.
#Result Types
Recoverable errors use the prelude enum Result.
- types.result.declRecoverable errors use the prelude enum
enum Result[T, E]: Ok(value: T); Err(error: E). - types.result.prelude-name
Resultis a prelude name;OkandErrare its variants, not prelude names. - types.result.constructionValues follow the ordinary enum construction rules:
.Ok(value)and.Err(error)where aResulttype is expected, orResult.Ok(value)andResult.Err(error). - types.result.ok-contextIn particular,
.Ok(value)without an expectedResulttype is an error. Error:missing-contextual-enum-type. - types.result.bare-variantA bare
Ok(value)orErr(error)expression resolves like any other identifier. It is an error unless a declaration in scope supplies that name. Error:unknown-name. - types.result.patternsResults are matched with the patterns
.Ok(pattern),.Err(pattern), and theirResult.-qualified forms.
fn parse() -> Result[i32, string]:
Ok(1) # error: unknown-nameSee also: Enum Declarations, Match Expressions.
#Void Results
- types.result.unit-okWhen
Tisvoid, the success value is(), so the constructor is written.Ok(())orResult.Ok(()). - types.result.no-ok-omitNo shorthand omits that
(), so.Ok()passes no argument forvalue. Error:argument-count. - types.result.no-ok-pass
.Ok(pass)is not the source spelling for this case. - types.result.unit-patternA match names that success with the unit pattern, as in
.Ok(()) => ..., so construction and matching read alike.
fn save(ready: bool) -> Result[void, string]:
if ready:
return .Ok(())
.Ok() # error: argument-count#Result Propagation
- types.result.propagatePostfix
?onResult[T, E]either produces the success value or immediately returns the error from the nearest function. - types.result.propagate.enclosingThe enclosing function must return a
Result[U, F]. - types.result.propagate.one-stepThe error reaches
Fin one step: by one rule of Assignability And Coercion, or otherwise by one call ofF'sFrom[E]implementation. - types.result.propagate.not-bothThe step is never both, and conversions are never chained.
- types.result.err-valueReturning an
.Errvalue without?does not itself alter control flow, and it never calls a conversion. - types.result.propagate.permissionThe success value retains its declared generic type
T, including*mut UwhenT = *mut U. - types.result.no-effectsThe core language does not use effect syntax for recoverable errors.
fn get(items: List[i32]) -> Result[i32, string]:
if items.len() == 0: .Err("empty")
else: .Ok(items[0])
fn demo(items: List[i32]) -> Result[i32, string]:
.Ok(get(items)?) # the value, or the error returns at onceSee also: Propagation, which defines the rule and its diagnostic, and Conversion Trait.
#Numeric Conversions
No numeric value changes type implicitly. Every change of width is a written cast, and a literal takes the type it needs:
fn total(small: i16, large: i64) -> i64:
let wide: i64 = small # error: type-mismatch
i64(small) + large + 1The numeric types form three families, each ordered from narrower to wider:
i8, i16, i32, i64
u8, u16, u32, usize, u64
f32, f64
- types.num.familiesThe signed integers, the unsigned integers, and the floating-point types are the three numeric families above, each ordered by width.
- types.num.no-implicitNo numeric type converts implicitly to another numeric type. Every change of numeric type is an explicit cast, as in
i64(small)orf64(ratio). - types.num.no-implicit.widerSo a value where a wider type of its family is expected is an error. Examples are an
i16passed to ani64parameter and anf32assigned to anf64. Error:type-mismatch. - types.num.no-implicit.fixThat diagnostic should offer a fix-it that writes the conversion around the value, as in
i64(small). - types.num.narrowingA value where a narrower type of its family is expected is an error. Error:
implicit-narrowing. - types.num.usize-width
usizeis the unsigned type of the target's width, bytypes.usize.width. On Wasm32 it is wider thanu16, narrower thanu64, and as wide asu32. - types.num.same-widthA value where another type of its family with the same width is expected is an error, as a
u32passed to ausizeparameter. Error:type-mismatch. - types.num.no-sign-changeThere is no implicit conversion between signed and unsigned integers.
- types.num.no-int-floatThere is no implicit integer-to-floating or floating-to-integer conversion in the current core.
- types.num.literal-kindA numeric literal is not a conversion: it takes an expected type of its own kind, as Literal Types states. So
let x: i64 = 300is valid, andlet y: f64 = 1is an error. Error:type-mismatch.
Why. Go, Rust, and Swift have no implicit numeric conversion, and Kotlin's mixed operators are one overload per pair of types. With none, an operator is one trait method on one type, and every width change is visible where it happens.
#Binary Numeric Operators
- types.num.binary.literal-joinIn a binary arithmetic or comparison expression, an operand built only of unsuffixed literals takes the other operand's type, on either side, by
types.literal.local.form.operand. - types.num.binary.literal-leftSo an unsuffixed integer literal on the left takes the right operand's integer type.
0xFFFF_FFFF_FFFF_FFFF - countwith au64countis valid, and1 < countcompares twou64values. - types.num.binary.same-typeOtherwise both operands must have one type, and the result has that type, so
f32 op f32producesf32. Two types of one family are an error, assmall + largewith ani16and ani64, or anf32and anf64operand. Error:type-mismatch. - types.num.binary.no-mixSigned and unsigned integers do not mix implicitly, and integers do not mix implicitly with floating-point values. A signed and an unsigned operand are an error. Error:
mixed-signedness. - types.num.binary.castThe user must cast one operand explicitly in those cases.
fn add(small: i16, large: i64, ratio: f32, scale: f64) -> f64:
let count: i64 = small + large # error: type-mismatch
let mixed: f64 = ratio * scale # error: type-mismatch
let next = small + 1 # the literal is an i16
let rest = 100 - small # so is this one, on the left
f64(ratio) * scale#Numeric Casts
Explicit numeric conversion uses constructor-style casts:
let wide: i64 = 9000
narrow := i16(wide)- types.cast.syntaxExplicit numeric conversion uses constructor-style casts.
- types.cast.wrapAn integer-to-integer cast wraps at run time, as Go conversions and Rust
asdo. The result keeps the low bits of the source value's two's-complement form, read in the target type. It never panics. - types.cast.wrap.exampleSo
u8(x)withx = 300gives 44, andi8(x)withx = 200gives -56. - types.cast.literal-rangeA cast's argument may be an integer literal, alone or under unary
-or+. Then the literal is checked with the target type as its expected type, by Integer Literals. An out-of-range literal is an error, as Go reports a constant overflow. Error:integer-literal-range. - types.cast.fallibleLibraries may provide separate fallible conversion functions returning
Result.
fn narrow(wide: i64) -> u8:
u8(wide) # wraps: 300 gives 44
fn fixed() -> u8:
u8(300) # error: integer-literal-rangeThe core numeric cast rules are:
| Cast | Behavior |
|---|---|
| types.cast.int-int-wrapInteger to integer | wraps to the target width, by types.cast.wrap |
| types.cast.int-floatInteger to floating point | rounds to the nearest representable IEEE 754 value using ties-to-even |
types.cast.f32-f64f32 to f64 |
is exact |
types.cast.f64-f32f64 to f32 |
uses IEEE 754 ties-to-even rounding |
| types.cast.float-int-saturateFloating point to integer | truncates toward zero, then saturates, by types.cast.saturate |
- types.cast.saturateA floating-to-integer cast saturates, as Rust
asdoes. A value below the target's minimum, negative infinity included, gives the minimum. A value above its maximum, positive infinity included, gives the maximum. NaN gives 0. - types.cast.saturate.exampleSo
i8(x)withx = 300.0gives 127,u8(x)withx = -1.5gives 0, andi32(x)gives 0 whenxis NaN. - types.cast.no-panicNo numeric cast panics.
- types.cast.precisionAn explicit numeric cast may lose precision according to these rules.
- types.cast.exactLibraries may expose exact or fallible conversions when loss must be rejected.
- types.cast.nonnumericConstructor-style calls involving nonnumeric types are not numeric casts: they must resolve to a nominal newtype constructor, enum constructor, or ordinary function.
Why. Every numeric cast is total, as in Rust since 1.45, so a cast never needs a panic path. Code that must reject an out-of-range value uses a fallible library conversion.
#Integer Arithmetic
A program is built under one of three build profiles: debug, release, or test. The hd command selects the profile.
- types.arith.checkedIn a debug or test build, an integer overflow or underflow of
+,-,*,**, or unary-panics. Panic:integer-overflow. - types.arith.releaseIn a release build, those operations wrap to the type's width as two's complement, as in Rust.
- types.arith.failureIn a debug or test build, an invalid shift panics. An explicit wrapping or fallible library operation is the way to ask for other behavior.
- types.arith.alwaysDivision and remainder by zero,
MIN / -1, an out-of-bounds index, and the rules of an explicit conversion behave the same in every build. So do thechecked_*,wrapping_*, andsaturating_*library operations. - types.arith.divisionInteger division truncates toward zero.
- types.arith.remainderInteger remainder has the sign of the dividend.
- types.arith.shift-countA shift count is invalid when it is negative or not smaller than the bit width of the shifted value.
- types.arith.shift-count.debugIn a debug or test build, an invalid shift count panics. Panic:
invalid-shift. - types.arith.shift-count.releaseIn a release build, an invalid shift count is masked to the bit width of the shifted value. So
x << nshifts byn % bits, as in Rust. - types.arith.shift-rightRight shift of a signed integer is arithmetic and sign-extending.
- types.arith.min-divisionFor every signed width,
MIN / -1panics withinteger-overflowin every build. - types.arith.min-remainderFor every signed width,
MIN % -1produces zero.
let result: i32 = 2147483647 + 1 # panics in a debug or test build, gives -2147483648 in a release build
fn overflow() -> i32:
let minimum: i32 = -2147483648
minimum / -1 # panics in every buildWhy. This is Rust's model: overflow checks help while a program is written and tested, and cost speed once it ships.
hd testalways uses a checked build, so a property test finds an overflow even when the program ships as a release build.
See also: Runtime Panics.
#Numeric Display
- types.display.int
Displayformats integers in base ten. - types.display.float
Displayformats floating values with the shortest round-trip decimal digits. When two shortest strings are equally close to the value, it writes the one with an even last digit. - types.display.widthThe digits round-trip at the value's own width, so an
f32displays itsf32digits even when formatted through generic code. - types.display.notationFinite floats use fixed notation when the normalized decimal exponent is in
[-6, 21)and lowercase scientific notation otherwise. - types.display.exponentScientific exponents always include
+or-and no leading zeroes. - types.display.fixedFixed notation always contains a decimal point and at least one fractional digit, so
1.0remains visibly floating. - types.display.specialNegative zero is
-0.0; infinities areinfand-inf; every NaN isNaN. - types.display.nan-canonicalBefore hashing, boundary serialization, or
Display, every NaN is replaced with the one canonical quiet-NaN value for its width. - types.display.nan-compareNaN comparison continues to follow IEEE 754.
fn demo() -> string:
"${1.5}" # "1.5": shortest round-trip digitsNote. Boundary serialization writes a float as its bit pattern, after this replacement, by
module.boundary.float-bits.
#Assignability And Coercion
types.assignAn expression of type S is assignable to a location of type T when at least one of these rules applies:
- types.assign.identical
SandTare identical after expanding transparent aliases. - types.assign.weaken
Sismut Suspend[U]andTis its readonly formSuspend[U], orSis a writable reference andTthe read-only one of its kind, as*mut Uand*U, byref.convert.weaken. - types.assign.varianceA declared generic variance conversion permits the readonly outer type to change its type arguments.
- types.assign.optionalA value of
Tis injected intoT?. The injection adds one layer only, so aTis not injected intoT??. - types.assign.row-subsumption
SandTare function types, andT's row entails every key ofS's row. ThenSwithT's row is assignable toT, as Row Subsumption states. - types.assign.never
Sisnever, astypes.never.assignablestates. - types.assign.trait-value.ref
Tis a trait value reference, such as&dyn Traitor*mut dyn Trait, andSconverts to it by Trait Value References.
trait Counter:
fn bump(&mut self) -> void
struct Clicks:
value: i32
impl Counter for Clicks:
fn bump(&mut self) -> void:
self.value = self.value + 1
fn edit() -> void:
let mut clicks = Clicks { value: 0 }
let edit: &mut dyn Counter = &mut clicks
edit.bump()No rule converts one numeric type to another, as Numeric Conversions states.
See also: Inspectable Types.
#Assignment And Binding Types
- types.assign.no-subtypingNo inheritance or structural record subtyping exists.
- types.assign.binding-typeAssignment never changes the declared or inferred type of a binding.
- types.assign.let-declaredIn particular, later assignment to a
letbinding must remain assignable to the type established at its declaration. A literal initializer's width is fixed there, bytypes.literal.local.statement.
fn demo() -> i32:
let x = +1
x = +2 # still i32: assignment never changes the binding's type
x#Access Permission
A *mut T or &mut T reference and a mut Suspend[T] permit writes. A
*T, a &T, and a readonly Suspend[T] do not, and never gain that
permission.
- types.mut.weakenA
mut Suspend[T]may be used as aSuspend[T], a*mut Tas a*T, and a&mut Tas a&T. - types.mut.no-upgradeA
Suspend[T]must never be upgraded tomut Suspend[T], nor a*Tor&Tto*mut Tor&mut T. An upgrade is an error. Error:mutable-upgrade. - types.mut.no-upgrade.inferenceThe error includes an upgrade that generic inference would produce. Given
keep[T](value: T) -> T, bindingkeep(user)withuser: *Userto a*mut Userdeclaration is rejected rather than inferringTas*mut User. - types.mut.argumentA readonly
Suspend[T]argument cannot satisfy amut Suspend[T]parameter. Error:readonly-argument-to-mutable-parameter. - types.mut.argument.inferBefore reporting this error for a generic mutable parameter, inference solves its type variables from the argument's readonly shape.
Why. Solving the generic variables keeps the diagnostic concrete without granting mutable access to the readonly argument.
struct User:
name: string
fn keep[T](value: T) -> T:
value
fn invalid(user: *User) -> void:
let mutable: *mut User = user # error: mutable-upgrade
let kept: *mut User = keep(user) # error: mutable-upgradeSee also: Conversions.
#Value Types
A value type has values without identity: binding a value copies it, and nothing shares it. A struct type is a value type.
struct Point:
x: i32
y: i32
fn moved(p: Point) -> Point:
let mut q = p # q is a copy of p
q.x += 1 # p is unchanged
q- types.value.structA struct type is a value type. Each struct declaration introduces a distinct nominal type, as an enum declaration does.
- types.value.copyBinding, assigning, passing, and returning a struct value copies it. A later change to the copy leaves the source unchanged, and the reverse.
- types.value.storedA struct value stored in a struct field, a list element, a map value, a tuple element, or an enum payload is a copy, and reading it out yields a copy.
- types.value.no-viewA struct type has no readonly or
mutview, so a struct literal or copy-update has the plain struct type. - types.value.no-mut.writtenA type written with
mut, as inmut Point,mut List[i32],mut (i32, i32), ormut Tfor a type parameterT, is an error unless it is aSuspend[T]. Error:mut-on-struct. - types.value.field-typeA field read through a struct value has the field's declared type, whatever holds the struct.
- types.value.anyvalA struct type implements
AnyVal, soiswith a struct operand is an error. Error:identity-requires-references.
struct Point:
x: i32
y: i32
fn nudge(p: mut Point) -> void: # error: mut-on-struct
pass
fn reset[T](value: mut T) -> void: # error: mut-on-struct
pass
fn invalid() -> i32:
let p: mut (i32, i32) = (1, 2) # error: mut-on-struct
p._0
fn same(a: Point, b: Point) -> bool:
a is b # error: identity-requires-referencesNote. A primitive type written with
mutis the error oftypes.prim.no-mut.errorinstead.
Why. A struct value has no identity, so a
mutview of one would name no shared object, andiswould have nothing to compare.
#Writable Places
A field of a struct value changes in place only through a writable place.
- types.value.writable-placesA place is writable when it is a
let mutbinding, local or top-level, amutparameter, ormut self. A value reached through a&mutreference is writable too. So are a struct field of a writable place, an element of a list in a writable place, and a value of a map in a writable place. - types.value.let-mut
let mut name = valuewith a struct value is valid, with or without a struct annotation, and makesnamewritable. - types.value.field-writeAssigning a struct field, with
=or a compound assignment, requires the struct to be in a writable place. - types.value.write-onceThe assignment changes that place alone, since no other place shares its value.
- types.value.reassign-bindingA plain
letbinding of a struct and amutparameter may be reassigned as a whole. A:=binding and a parameter that is notmutnever change.
A field write through a struct that is not in a writable place is an error, by what holds the struct:
| Rule | The struct is held by | Error |
|---|---|---|
| types.value.write.shortShort binding | a := or using binding |
non-reassignable-binding |
| types.value.write.plain-paramParameter | a value parameter or self that is not mut |
non-reassignable-parameter-binding |
types.value.write.letPlain let |
a let binding without mut |
not-writable |
| types.value.write.other-holderOther value | a call result, an element of a list or a value of a map that is no writable place, an unwrapped value, or a pattern binding | not-writable |
| types.value.write.shared-refRead-only reference | a value reached through a &T reference |
not-writable |
struct Point:
x: i32
y: i32
fn shift(p: Point) -> void:
p.x += 1 # error: non-reassignable-parameter-binding
fn demo() -> void:
origin := Point { x: 0, y: 0 }
origin.x = 1 # error: non-reassignable-binding
let start = origin
start.x = 2 # error: not-writable
start = Point { x: 2, y: 0 } # valid: reassigned as a whole
let mut cur = start
cur.x = 3 # valid: cur is writable
fn shifted(mut p: Point) -> Point:
p.x += 1 # valid: a mut parameter is writable
pEvery other write to a place that is not writable has one code:
- types.path.store.not-writableA write to a place that is not writable is an error. A write is an assignment or compound assignment, an element store, or a call of a
&mut selfmethod on the place. A field write takes the code that the table above gives. A call of amut selfmethod on a readonlySuspend[T]is the same error. Error:not-writable.
struct Child:
name: string
impl Child:
fn rename(&mut self, name: string) -> void:
self.name = name
struct Parent:
child: *Child
fn invalid(parent: &mut Parent, child: &Child) -> void:
parent.child.name = "new" # error: not-writable
child.name = "new" # error: not-writable
parent.child.rename("new") # error: not-writableNote. A struct's
mut selfmethod takes its own copy of the receiver, byfn.method.mut-self-struct, so a call of one needs no writable place.
See also: Struct Declarations, Places, Writing Through A Reference.
#Binding Forms
A binding's form decides whether its name may be rebound and whether its value may change in place.
- types.view.non-reassignableA binding is non-reassignable when its name cannot be rebound, as a
:=binding and a parameter that is notmutcannot. - types.bind.let-rebindA
letbinding withoutmutmay still be reassigned as a whole; only a change in place through it is rejected.
fn rename(other: User) -> void:
let user = User { name: "Ada" } # User
user = other # valid: reassignmentfn invalid() -> void:
let user = User { name: "Ada" }
user.name = "Grace" # error: not-writable- types.bind.let-mut-primitive-local
let muton a local whose type is primitive, as inlet mut n = 0, is valid, since&mut nneeds a writable place. - types.bind.let-mut-primitive-lintWhen no
&mutof such a local is taken, its redundantmutproduces a lint finding with a removal fix-it. Warning:redundant-let-mut. - types.bind.let-mut-primitive-top
let muton a top-level binding whose type is primitive is an error. Error:mut-on-primitive. - types.bind.let-mut-primitive.hintThe diagnostic says that a plain
letis already reassignable. - types.bind.let-mut-tuple-valid
let muton a name whose type is a tuple, as inlet mut pair = (1, 2), is valid.
fn count_up() -> void:
let mut count = 0 # warning: redundant-let-mut
let mut n: i32 = 0 # warning: redundant-let-mut
let mut pair = (1, 2) # valid: a tuple- types.bind.let-pattern-mutA name written
mutin aletpattern is a writable binding, as in amatcharm; a name withoutmutis not. Inlet (mut log, db) = pair,logis writable anddbis not. - types.bind.let-pattern-mut.data-fieldThe same holds in a struct or variant pattern. In
let Profile { mut tags, name } = profile,tagsis a writable copy of the field.
struct User:
name: string
fn pair() -> (User, User):
(User { name: "Ada" }, User { name: "Grace" })
fn edit() -> void:
let (mut first, second) = pair() # first is writable, second is not
first.name = "Lin"
second.name = "Kay" # error: not-writablestruct Profile:
name: string
tags: List[string]
fn tag(profile: Profile) -> string:
let Profile { mut tags, name } = profile # tags is a writable copy
tags.push(name)
nameNote. No binding form is non-reassignable and mutable at once: a local that is changed in place but never reassigned is written with
let mut.
Why.
let mutsays at the declaration which locals change in place.
- types.bind.neverA binding without a type annotation whose initializer has type
neveris an error. Error:uninhabited-binding.
fn first_even(values: List[i32]) -> i32:
found := panic("no even value") # error: uninhabited-binding
foundWhy. No value of type
neverexists, so the binding could never be read. The initializer alone, withoutfound :=, says the same thing.
#Collection Values
List[T] and Map[K, V] are values with copy-on-write: a copy shares the
collection's elements until one side changes.
fn tagged(names: List[string]) -> List[string]:
let mut copy = names # a copy of names
copy.push("new") # names is unchanged
copy- types.coll.value
List[T]andMap[K, V]are value types. Binding, assigning, passing, and returning a list or a map copies it, and so does storing one in a field, an element, or a payload. A later change to one copy leaves every other copy unchanged. - types.coll.copy-on-writeA copy shares its source's elements until one of the two changes. That change first copies the changed side's elements, once.
- types.coll.no-identityA list or a map has no identity.
ListandMapimplementAnyVal, soiswith one is an error. Error:identity-requires-references. - types.coll.let-mut
let mut name = valuewith a list or map value is valid, with no annotation or aList[T]orMap[K, V]one, and makesnamewritable. - types.coll.writableChanging a list or a map needs a writable place, as a struct field write does (
types.value.writable-places). A change is a call of a changing method such aspush, or an element store. - types.coll.writable.callA
&mut selfmethod such aspush, called on a list or a map that is no writable place, is an error. Amut selfmethod needs no writable place, since it changes its own copy (fn.method.mut-self-struct). The fix-it makes the binding writable. Error:not-writable. - types.coll.writable.storeAn element store into such a list or map, or a field write through its element, is an error with the same fix-it. Error:
not-writable. - types.coll.element-placeAn element of a list in a writable place is a writable place. So
xs[i] = v,xs[i].x = v, andxs[i].push(v)changexsin place. - types.coll.nestedA change through a path, such as
m[k].push(x),team.members.push(x), orxs[i].name = n, changes the inner value where it is stored. Amut selfmethod called on such a path changes only its own copy (fn.method.mut-self-struct), so the inner value stays as it was. - types.coll.sharedA list or a map that two places share is written
*mut List[T]or*mut Map[K, V], or&mut List[T]or&mut Map[K, V]when lent to one call. A change through the reference changes the place it refers to.
fn names() -> usize:
found := ["ada"]
found.push("grace") # error: not-writable
found.len()
fn first() -> i32:
let scores = [+1, 2]
scores[0] = 5 # error: not-writable
scores[0]
fn same(left: List[i32], right: List[i32]) -> bool:
left is right # error: identity-requires-referencesWhy. A list that every binding shares lets a callee change its caller's list with no mark at the call. A value changes only where its place is written, and sharing is written
*mutor&mut. Writing through the path keeps building a map of lists linear: each push copies nothing.
#Map Values
A value of a map is a place, as an element of a list is.
- types.coll.map-value
m[k] = vandm[k] op= vreplace a map value. A changing method called on one, such asm[k].push(x), changes it in place. - types.coll.map-value.placeA value of a map in a writable place is a writable place. So
m[k].x = vandm[k].x += 1change the value in place, asxs[i].x = vchanges a list element. - types.coll.map-value.missingA field write or a changing method call through
m[k], when the map holds no equal key, is a checked runtime panic, as a read ofm[k]is. Panic:index-out-of-bounds. - types.coll.map-value.reference
&m[k]and&mut m[k]are element references, which live for one call, as Element References states.
struct Stock:
count: i32
fn restock(shelves: Map[string, Stock]) -> Map[string, Stock]:
let mut copy = shelves
copy["apple"].count = 5 # changes the value in place
copy
fn count(shelves: Map[string, Stock]) -> i32:
found := &shelves["apple"] # error: element-reference-outlives-call
found.countSee also: Snapshot Iteration, Writable Places.
#Generics
This section defines generic parameters, bounds, arguments, and their runtime representation.
- types.generic.completeGeneric type and function parameters denote complete types.
- types.generic.namingBy convention, generic parameters, including row parameters, use uppercase names such as
T,U,K,V, andR. - types.generic.kindThe convention is style only: a parameter's kind comes from its declaration and use, never from the case of its name.
fn first[T](items: List[T]) -> T?:
if items.len() == 0: .None
else: .Some(items[0])#Bound Order
A bound may name a later parameter of its list, and two bounds may name each other:
trait Source[T]:
fn take(self) -> T
trait Sink[T]:
fn give(self, value: T) -> void
struct Tank:
level: i32
struct Pipe:
width: i32
struct Hose:
length: i32
impl Source[Pipe] for Tank:
fn take(self) -> Pipe: Pipe { width: self.level }
impl Source[Hose] for Tank:
fn take(self) -> Hose: Hose { length: self.level }
impl Sink[Tank] for Pipe:
fn give(self, value: Tank) -> void: pass
impl Sink[Tank] for Hose:
fn give(self, value: Tank) -> void: pass
fn wire[S < Source[U], U < Sink[S]](s: S, u: U) -> void:
u.give(s)
fn connect(tank: Tank, pipe: Pipe, hose: Hose) -> void:
wire(tank, pipe)
wire(tank, hose)- types.generic.bound.any-orderIn a generic declaration of any kind, a bound may name any parameter of the same list, earlier or later.
- types.generic.bound.mutualBounds may name each other, as
S < Source[U]andU < Sink[S]do above. Such mutual references are not a circularity error. - types.generic.bound.defaults-apartDefaults keep their own order: they are trailing, as
types.generic.default.orderstates, and see only earlier parameters, astypes.generic.default.laterstates.
Why. A bound is a trait constraint, and hd has no subtyping, so bounds need no order, as Rust where-clauses need none. An associated type allows one pairing per type. A generic trait parameter with mutual bounds allows many, as
Tankpairs with bothPipeandHose.
See also: Function And Closure Scopes.
#Generic Arguments
- types.generic.inferGeneric arguments are inferred at call sites when unambiguous.
- types.generic.explicitCallers may supply the complete generic argument list explicitly.
- types.generic.short-listAn explicit list in an expression may omit trailing slots. Each omitted slot is inferred as a
_slot is, then defaulted. - types.generic.placeholder-slotIn a generic function's or method's explicit list,
_may occupy any slot and requests inference for that argument. - types.generic.placeholder.not-type
_is not itself a type. Its use in an ordinary type application is an error. Error:syntax-error. - types.generic.too-longA type-argument list with more positional arguments than its declaration has generic parameters is an error, in an expression and in a written type alike. Error:
argument-count.
fn count(names: List[string, i32]) -> i32: # error: argument-count
0#Inference From Several Arguments
When a call solves one type parameter from several arguments, the
arguments' types may differ only in an outer mut:
- types.generic.infer.joinWhen generic call inference solves one type parameter from several arguments, the only conversion between their types is permission weakening. A
mut Suspend[X]and aSuspend[X]meet atSuspend[X]. - types.generic.infer.join.no-trait-valueA trait-value conversion never applies, as
types.lct.no-trait-valuestates for the least common type. Socmp(user, label)with aUserand aDisplayargument is an error. Error:no-common-type. - types.generic.infer.join.no-supertrait-wideningA supertrait widening never applies either, so two arguments of two child traits of one supertrait are an error. Error:
no-common-type. - types.generic.infer.join.other-conflictAny other conflict between the arguments' types is an error. Examples are
choose(1, true),max(small, large)with ani32and ani64, a*mut Userand a*User, and aTand aT?. Error:type-mismatch. The caller writes a cast, as inmax(i64(small), large). - types.generic.infer.join.literalAn integer literal argument is not a conversion: it takes the type solved from the other arguments as its expected type, in any position. So
pick(1, large)with ani64largesolvesT = i64. - types.generic.infer.join.explicit.reference-valuesAn explicit type argument, as in
cmp::[*dyn Display](&user, label), is an expected type for each argument, which then converts by Assignability And Coercion, astypes.lct.expected-trait.reference-valuesallows. - types.generic.infer.join.not-lctThis join is narrower than the least common type, and is not one of that section's constructs.
fn max[T < Ord](left: T, right: T) -> T:
match left.cmp(right):
.Less => right
_ => left
fn cmp[T < Display](left: T, right: T) -> bool:
"$left" == "$right"
fn widest(small: i32, large: i64) -> i64:
max(small, large) # error: type-mismatch
fn same(user: User, label: *dyn Display) -> bool:
cmp(user, label) # error: no-common-typestruct User:
name: string
fn pick[T](left: T, right: T) -> T:
left
fn either(large: i64) -> i64:
pick(1, large) # T is i64; the literal 1 is an i64
fn widest(small: i32, large: i64) -> i64:
max(i64(small), large)
fn same(user: User, label: *dyn Display) -> bool:
cmp::[*dyn Display](&user, label)Why. A call's arguments are not a list literal. A reader expects
Tto be the type written at the call, as Rust and Go do. So a mixed numeric call names its cast, and a trait value is asked for by name.
#Inference Through A Bound
A call solves a type parameter that only a bound names from the bounded argument's implementation of that bound's trait:
trait Source[T]:
fn take(self) -> T
struct Label:
text: string
impl Source[string] for Label:
fn take(self) -> string:
self.text
fn read[U, S < Source[U]](source: S) -> U:
source.take()
fn name(label: Label) -> string:
text := read(label) # S is Label, then U is string
text- types.generic.infer.boundA type parameter of a call is named only in bounds when no parameter type names it but a bound of another type parameter does. One case is
Uinread[U, S < Source[U]](source: S). - types.generic.infer.bound.afterCall inference solves a parameter named only in bounds after the other parameters.
- types.generic.infer.bound.oneOnce the bounded parameter is known, if its type implements the bound's trait for exactly one instantiation, that instantiation solves the parameter.
- types.generic.infer.bound.noneIf the type implements the bound's trait for no instantiation, the call is an error. Error:
unsatisfied-trait-bound. - types.generic.infer.bound.ambiguousIf the type implements the bound's trait for several instantiations, this step leaves the parameter unsolved.
- types.generic.infer.bound.defaultA parameter named only in bounds and left unsolved takes its type-argument default, as
types.generic.default.fillgives, and the bound is then checked. - types.generic.infer.bound.no-defaultA parameter named only in bounds, left unsolved without a default, is an error, and an explicit type argument resolves it.
- types.generic.infer.bound.no-default.ambiguousWhen a bound of the parameter allows several instantiations, the error is
ambiguous-type, bytypes.infer.ambiguous.code. Error:ambiguous-type. - types.generic.infer.bound.no-default.disagreeWhen each bound allows exactly one instantiation and they differ, no solution exists. Error:
cannot-infer-type. - types.generic.infer.bound.several-boundsWhen several bounds name one parameter named only in bounds, this step solves it only once every parameter those bounds constrain is known.
- types.generic.infer.bound.agreeIt then solves the parameter only if each of those bounds allows exactly one instantiation and they all allow the same one.
- types.generic.infer.bound.disagreeOtherwise, when bounds allow different instantiations or one allows several, the parameter is left unsolved, as for one bound.
- types.generic.infer.bound.fixed-pointThe step repeats until it solves no further parameter, so a chain of such bounds is solved in dependency order.
- types.generic.infer.bound.precedenceAn explicit type argument and an expected type take precedence: this step never solves a parameter that either of them solves.
- types.generic.infer.bound.placeholderAn explicit list may mix written arguments with
_slots, as inread::[string, _](both); each_slot is inferred by the steps above. - types.generic.infer.bound.call-siteThis step is call-site inference only. A declaration's generic parameters and signature are never inferred, as
types.infer.explicitrequires.
struct Both:
count: i32
impl Source[i32] for Both:
fn take(self) -> i32:
self.count
impl Source[string] for Both:
fn take(self) -> string:
"both"
struct Plain:
count: i32
fn several(both: Both) -> void:
value := read(both) # error: ambiguous-type
fn none(plain: Plain) -> void:
value := read(plain) # error: unsatisfied-trait-boundA written argument or an expected type picks one of Both's
instantiations, and a chain of bounds is solved outermost first:
fn label(both: Both) -> string:
text := read::[string, _](both) # U is string; S is Both
text
fn count(both: Both) -> i32:
let total: i32 = read(both) # U is i32 from the expected type
total
fn first_item[T, Inner < Iterable[T], Outer < Iterable[Inner]](groups: Outer) -> T?:
for group in groups:
for item in group:
return .Some(item)
.None
fn first_name(teams: List[List[string]]) -> string?:
found := first_item(teams) # Outer, then Inner is List[string], then T is string
foundA default fills a parameter named only in bounds that several
instantiations leave open. The bound of S names the later U, so only U needs a default:
fn read_or_text[S < Source[U], U = string](source: S) -> U:
source.take()
fn label_of(both: Both) -> string:
text := read_or_text(both) # S is Both; U is open, so U is string
text
fn label_of_both(both: Both) -> string:
text := read_or_text::[Both](both) # the list binds S; U is string
textTwo bounds that name one parameter must agree on it:
struct Note:
text: string
impl Source[string] for Note:
fn take(self) -> string:
self.text
fn joined[U, A < Source[U], B < Source[U]](first: A, second: B) -> List[U]:
[first.take(), second.take()]
fn titles(label: Label, note: Note) -> List[string]:
found := joined(label, note) # both bounds allow only U = string
found
fn mixed(label: Label, both: Both) -> void:
items := joined(label, both) # error: ambiguous-typeNote. The std adapter
zip[U, I < Iterable[U]](self, other: I)(More Adapters) relies on this step. Withnames: List[string],xs.iter().zip(names)solvesIfrom the argument, thenU = string.
Why. When the argument's type has one implementation, the trait argument is a fact of that type, as Rust's associated
IntoIterator::Itemis. Writing it at every call adds no information. Several implementations are a real choice, so the caller names it, unless the declaration names it with a default. Bounds that disagree are a choice too, and inference does not pick one.
See also: Explicit Type Arguments, Instantiations Of One Generic Trait.
#Type-Argument Defaults
A generic parameter may declare a type-argument default, written with
= after its bound. It fills the parameter when a use site leaves it
unsolved or a written type omits it:
struct AppError:
message: string
type Outcome[T, E = AppError] = Result[T, E]
fn load(path: string) -> Outcome[string]:
.Ok(path)
fn widen[T = i64](value: T) -> T:
value
fn small() -> i32:
narrow := widen(3)
narrow- types.generic.default.formA generic parameter of a function, method, struct, enum, trait, or
typedeclaration may declare a default. - types.generic.default.orderAfter the first parameter with a default, every later parameter of the same list must have one. A later parameter without one is an error. Error:
default-order. - types.generic.default.scopeA default may name earlier parameters of the same list, the parameters of an enclosing declaration, and
SelfwhereSelfis in scope. - types.generic.default.laterA default that names its own parameter or a later one is an error. Error:
binding-not-yet-visible. - types.generic.default.kindA default has its parameter's kind: a type for a type parameter, a row for a row parameter. A default of the other kind is an error. Error:
generic-kind-mismatch. - types.generic.default.boundA default must satisfy its parameter's bounds for every instantiation of the earlier parameters. A default that does not is an error. Error:
unsatisfied-trait-bound. - types.generic.default.checked-onceThat check is made once, at the declaration.
How a use site treats a defaulted parameter:
| Written | The parameter is |
|---|---|
| no list, in an expression | inferred; the default when inference leaves it unsolved |
_ in its slot |
inferred; the default when inference leaves it unsolved |
| its slot omitted from an explicit list in an expression | inferred; the default when inference leaves it unsolved |
| its slot omitted, or no list, in a written type | the default |
- types.generic.default.after-inferenceAt a use site that infers, the use site first solves its parameters as it would without defaults. That holds for a call, a function value, and a struct literal.
- types.generic.default.fillThen each parameter left unsolved that has a default takes it, in declaration order, with the earlier arguments substituted. The bounds are checked last.
- types.generic.default.argument-winsA default never replaces a solution, so an argument or expected type that solves the parameter wins:
widen(3)above hasT = i32. - types.generic.default.unsolvedA parameter left unsolved that has no default stays an error. Error:
cannot-infer-type. - types.generic.default.writtenA written type, such as an annotation, a signature, a field, a bound, or an implementation header, infers nothing. An omitted trailing slot there takes its default.
- types.generic.default.bareA name written in a type without a list, or with bindings only, omits every positional slot. So
impl Same for Moneyis valid fortrait Same[Other = Self]. - types.generic.default.written-missingA written type that omits a slot without a default is an error. Error:
partial-generic-arguments.
fn swap[A = B, B = i32](value: B) -> A: # error: binding-not-yet-visible
panic("unreachable")
fn pick[F = i32, T](value: T) -> T: # error: default-order
value
fn size(counts: Map[string]) -> i32: # error: partial-generic-arguments
0Why. Defaults apply only after inference, as in C++ and TypeScript. So adding a default never changes a program that compiled without it; it only fills a parameter that used to be an error. Rust's inference fallback could not keep that property. A default is written at the declaration, so declarations stay fully written; only use sites apply it.
See also: Type-Argument Default Syntax, Explicit Type Arguments, Method Generic Parameters.
#Generic Representation
- types.generic.unobservableThe runtime representation of generic code is not observable.
- types.generic.sharingA program cannot distinguish an implementation that shares one body among instantiations from one that specializes each instantiation, except through these rules of this chapter:
- a type parameter has runtime type identity only through a
T < Inspectablebound; ison a type parameter requiresT < AnyRef;- variance conversions must be representation-preserving.
- a type parameter has runtime type identity only through a
- types.generic.interfacesPackage interfaces therefore carry the bodies of generic functions needed by downstream compilation.
- types.generic.instantiation-depthAn implementation may limit how deeply generic instantiations nest, where each instantiation is reached from the one that calls it. The limit is implementation-defined.
- types.generic.instantiation-depth.errorA program whose instantiations exceed that limit is an error, reported on the call that would exceed it. Error:
instantiation-too-deep. - types.generic.call-cycle.stepA call passes type parameter
Pof its body to parameterQof its callee when its type argument forQholdsP. - types.generic.call-cycleA generic call cycle is a chain of such steps that leads a type parameter back to itself.
- types.generic.call-cycle.growsA step grows when its type argument is larger than
P, such asList[P],(P, P), orP?. - types.generic.polymorphic-recursionA generic call cycle with a step that grows is an error, reported on that step's call. Neither the instantiation limit nor how bodies are shared changes this. Error:
instantiation-too-deep. - types.generic.call-cycle.fixed-calleeOnly a call whose callee is fixed where it is written makes a step. That is a call of a function, a local function, a method, or a trait method whose implementation the receiver's type selects. A call through a bound or a trait value makes none.
- types.generic.call-cycle.impl-paramsEach method of an implementation has its own copy of the implementation's type parameters.
- types.generic.call-cycle.projectionA type argument does not hold a type parameter that occurs only inside a projection, such as
P.Item. - types.limit.type-sizeAn implementation may limit the size of one type, counted in the types it is built from. The limit is implementation-defined.
- types.limit.type-size.errorA type over that limit, whether written or inferred, is an error. Error:
type-too-large.
fn first[T](items: List[T]) -> T?:
if items.len() == 0: .None
else: .Some(items[0]) # shared or specialized: unobservable either wayfn nest[T](value: T, depth: i32) -> i32:
if depth == 0: return depth
nest((value, value), depth - 1) # error: instantiation-too-deepWhy. Each instantiation chain then ends, so an implementation may compile generic code per concrete type with no fallback path. The check reads only the calls, as Go's instantiation cycle check does, so whether a program compiles never depends on how bodies are laid out. The fix is a trait value or a non-generic helper.
Note. A chain that grows through a call with no fixed callee, such as a call through a bound, still meets the instantiation limit.
See also: Implementation Model, which describes the reference strategy.
#Identity On Type Parameters
- types.generic.identityIdentity comparison
ison a type parameter is permitted only with the sealedT < AnyRefbound. Without it, the comparison is an error. Error:identity-requires-references. - types.generic.identity.primitiveAn unconstrained type parameter may be a value type after substitution, such as a primitive or an enum, and therefore cannot be used with
is.
fn same[T < AnyRef](a: T, b: T) -> bool:
a is b#Variance
Generic type declarations mark covariance with +T, contravariance with -T,
and invariance by leaving T unmarked:
struct Box[+T]:
value: T
struct Consumer[-T]:
consume: fn(T) -> void
struct Cell[T]:
value: T- types.variance.markersGeneric type declarations mark covariance with
+T, contravariance with-T, and invariance by leavingTunmarked. - types.variance.verifiedThe compiler verifies each declared parameter against its use on the type's surface.
- types.variance.surfaceThat surface includes struct fields, enum shared data and variant payloads, and trait method signatures. It also includes the signature of every inherent method of the nominal type that has a
selfreceiver, private methods included. - types.variance.trait-implA separate trait implementation does not alter the nominal type declaration's variance; its own instantiated signatures must still type-check.
- types.variance.trait-paramsA trait's generic parameters are invariant. A variance marker on one is an error. Error:
invalid-variance.
struct Box[+T]:
value: T
impl[T] Box[T]:
fn replace(self, next: T) -> void: # error: invalid-variance
passWhy. Privacy in hd is module-wide. So any code in the declaring module can convert a value by variance and then call a private method on it. So a private method's signature counts toward variance as a public one's does.
A mut self, &self, or &mut self method is on that surface too:
- types.variance.surface.mut-selfAn inherent method with a
mut self,&self, or&mut selfreceiver counts toward declared variance, as a method with aselfreceiver does.
struct Box[+T]:
value: T
impl[U] Box[U]:
pub fn set(&mut self, value: U) -> void: # error: invalid-variance
self.value = valueAn associated function with no receiver is not on that surface:
- types.variance.surface.no-receiverAn inherent associated function with no
selfreceiver does not count toward declared variance. Only instance methods do, whatever their receiver.
struct Box[+T]:
value: T
impl[U] Box[U]:
pub fn new(value: U) -> Box[U]:
Box { value: value }#Polarity
types.polarityPolarity is computed as follows:
- types.polarity.positiveA returned value and an ordinary field are positive positions.
- types.polarity.negativeA function or method parameter is a negative position.
- types.polarity.functionEntering a function parameter reverses polarity, while entering a function result preserves it.
- types.polarity.genericApplying a covariant generic argument preserves polarity, a contravariant argument reverses it, and an invariant argument makes the occurrence invariant.
- types.polarity.mutOccurrence beneath
&mutis invariant, and an embedded field's type counts as beneath&mut. - types.polarity.bothA parameter used in both positive and negative positions must be invariant.
Why. The storage beneath
&mutcan be both read and written. Access through an embedded field follows its container.
- types.variance.covariant-checkA declared
+Tis rejected if any occurrence is negative or invariant. Error:invalid-variance. - types.variance.contravariant-checkA declared
-Tis rejected if any occurrence is positive or invariant. Error:invalid-variance. - types.variance.unmarkedAn unmarked invariant parameter may occur in any position.
struct Box[+T]:
value: TSee also: Data Embedding.
#Variance On A Non-Bare Target
An inherent implementation whose target is not the bare declared
parameters checks its methods by the variance each impl parameter has
in the target:
struct Box[+T]:
value: T
struct Consumer[-T]:
consume: fn(T) -> void
impl[U] Box[Consumer[U]]:
pub fn feed(self, value: U) -> void: # U is contravariant in the target
pass- types.variance.target.derivedFor an inherent
impl[U, ...] G[A1, ..., An], eachimplparameterUhas a derived variance: its variance in the target type, which the Polarity rules compute. - types.variance.target.composeA covariant parameter of
GkeepsU's variance in its argument, a contravariant one reverses it, and an unmarked one makes it invariant. - types.variance.target.bothA
Uthat the target places both covariantly and contravariantly, as inimpl[U] Pair[U, U]forPair[+A, -B], is invariant. - types.variance.target.checkEach method signature of that implementation is checked as if
Uwere declared with its derived variance: covariantUbytypes.variance.covariant-check, contravariantUbytypes.variance.contravariant-check. Error:invalid-variance. - types.variance.target.invariantAn invariant
U, as inimpl[U] Box[U?], may occur in any position, astypes.variance.unmarkedallows.
impl[U] Box[Consumer[U]]:
pub fn make(self, factory: fn() -> U) -> U: # error: invalid-variance
factory()Note. For a bare target, as in
impl[U] Box[U], the derived variance isT's declared marker, so the check is the one Polarity states.
Why. A variance conversion changes a target's arguments only as
G's markers allow. SoUcan change only as its place in the target lets it, and an invariant place fixes it. Kotlin and Scala compose variance the same way.
See also: Polarity, types.option.invariant.
#Readonly Outer Views
- types.variance.readonly-onlyVariance conversion applies only to a readonly outer type: one not written
*mut,&mut, ormut, such asG[T],*G[T], or&G[T]. - types.variance.mut-invariantEvery writable reference,
*mut G[T]or&mut G[T], and everymut Suspend[T], is invariant in all generic arguments. - types.variance.listThe built-in
Listdeclares a covariant element parameter. - types.variance.map
Map[K, V]is invariant inKand covariant inV. - types.variance.functionThe function type constructors
FnandSuspendFnare contravariant in each input element, covariant in the output, and invariant in the requirement row.
Why. A writable reference may replace stored values. Map keys are both accepted for lookup and exposed during traversal. Converting a function's row would change which providers the call passes, so the row stays invariant under variance. A function value itself may still widen its row by row subsumption, which may adapt the value.
struct User:
name: string
struct Box[+T]:
value: T
fn demo(box: *Box[*mut User]) -> *Box[*User]:
box # a readonly outer type converts covariantly#Representation-Preserving Variance
- types.variance.repr.covariantA variance conversion
G[S] -> G[T]requires a representation-preservingS -> Tconversion for each covariant argument. - types.variance.repr.contravariantA variance conversion
G[S] -> G[T]requires a representation-preservingT -> Sconversion for each contravariant argument. - types.variance.repr.weakeningPermission weakening,
*mut U -> *U,&mut U -> &U, ormut Suspend[U] -> Suspend[U], is representation-preserving. - types.variance.repr.excludedThese conversions are not representation-preserving:
- construction of a trait value reference such as
&i32 -> &dyn Displayor&User -> &dyn Display; - child-dynamic-trait to supertrait widening;
- optional injection
U -> U?.
- construction of a trait value reference such as
- types.variance.no-insertionVariance never inserts element wrappers, metadata rewrapping, boxing, copies, or per-access conversions. A variance conversion that would need one is an error. Error:
variance-representation-change.
fn demo(items: *mut List[i32]) -> *List[i32]:
items # weakening *mut U to *U keeps the representationSee also: Representation-Preserving Conversions.
#Trait Values And Any
This section defines trait conformance, dynamic trait values, written
&dyn Trait, and the Any trait family.
- types.trait.explicitTrait conformance is explicit.
- types.trait.no-shapeMatching method shape alone does not make a type implement a trait.
- types.trait.staticA generic bound such as
T < Displayuses static dispatch and preserves the concrete type.
struct User:
name: string
fn demo(user: User) -> *dyn Any:
&user # every value satisfies Any#Trait Value Types
- types.trait.value.reference-valuesA trait value type is
&dyn Tror&mut dyn Tr, or counted,*dyn Tror*mut dyn Tr, referring to an object whose concrete type implementsTr. - types.trait.value.contents.reference-valuesA trait value reference holds a reference to an object and dispatch metadata for its trait.
- types.trait.value.dyn-required.reference-valuesA bare trait name in a type position is an error whose fix-it writes
&dyn. Error:trait-used-as-type. - types.trait.value.bound.reference-valuesA dynamic trait value type satisfies a generic bound on its own trait and on each of that trait's supertraits. For example, a
*dyn Displayvalue is a valid argument forT < Display. - types.trait.value.bound.available.reference-valuesIt satisfies such a bound only when every member of the bound's trait is available on the
dynvalue, astrait.dyn.bound.referencestates. - types.trait.value.bindings.reference-valuesA trait value type may bind the trait's associated types, as in
&dyn Supplier[Item = i32], as Bound Associated Types specifies. - types.trait.value.not-targetA dynamic trait value type is still not an implementation target.
- types.trait.value.ref-writtenA trait value reference,
&dyn Traitor&mut dyn Trait, is a trait value that is a reference. A converted&Sor&mut Sis shared, and a value that is no reference does not convert, as Trait Value References states.
trait Greet:
fn greet(self) -> string
fn demo(g: &dyn Greet) -> string:
g.greet()trait Greet:
fn greet(self) -> string
fn demo(g: Greet) -> string: # error: trait-used-as-type
g.greet()Why.
&dynmakes the reference and runtime dispatch explicit. A bound such asT < Greetstays the monomorphized form.
See also: Dynamic Trait Values, which gives the rule.
#Dynamic Safety
- types.trait.dyn.any-traitEvery trait may be used as a
dyntype. There is no per-trait safety check. - types.trait.dyn.memberA member that cannot work through a
dynvalue is unavailable on it, and calling it there is an error at the call. Dynamic Safety lists those members. Error:dyn-member-unavailable. - types.trait.safe.assoc-bound.reference-valuesThe trait value type must bind each associated type of the trait and its supertraits, as in
&dyn Supplier[Item = i32]. - types.trait.safe.method-type-paramA method with a method-level type parameter is available on a
dynvalue whatever the parameter's bounds, as inT < Displayor an unboundedT. - types.trait.safe.method-type-argA call through a trait value may pass any type argument for such a parameter, including an
AnyValtype such asi32or an enum. - types.trait.safe.method-type-arg.any-useThe method's signature and body may use the parameter in any way, including inside a container or a function type.
- types.trait.safe.method-type-arg.boxAn implementation may box a value-typed argument at that call. The box has no identity, so the boxing is not observable.
- types.trait.safe.trait-genericTrait declaration generic parameters are permitted.
trait Versioned:
fn version(self) -> i32
fn newer_than(self, other: Self) -> bool
fn audit(record: &dyn Versioned) -> bool:
if record.version() > 0:
return true
record.newer_than(record) # error: dyn-member-unavailableWhy. One concrete trait instantiation, such as
&dyn Repository[User], fixes the trait declaration's generic parameters before dispatch. A member is judged where it is called, so a trait with one unusual member still works as adyntype.
#Supertrait Widening
- types.trait.childA dynamic child-trait value exposes methods declared by the child and all of its transitive supertraits.
- types.trait.child.widenA dynamic child-trait value may be widened implicitly to a dynamic supertrait value.
- types.trait.child.one-wayThat conversion discards access to child-only methods and cannot be reversed by a conversion; only
Inspectablevalues recover a concrete type. - types.trait.child.reprThis direct widening may rewrap dispatch metadata and is therefore not representation-preserving for a variance conversion.
trait Animal:
fn name(self) -> string
trait Dog < Animal:
fn bark(self) -> string
struct Pup:
name: string
impl Animal for Pup:
fn name(self) -> string: self.name
impl Dog for Pup:
fn bark(self) -> string: "woof"
fn name_of(animal: &dyn Animal) -> string:
animal.name()
fn demo(pup: Pup) -> string:
name_of(&pup) # the reference converts to AnimalSee also: Runtime Type Identity.
#Any
- types.any
Anyis the built-in universal empty trait. - types.any.allEvery value type, including an optional type, satisfies
Anyautomatically. - types.any.never
neversatisfiesAnytoo. Avoidvalue needs no rule of its own, because it is the tuple(). - types.any.erase.reference-valuesThe reference type
&dyn Anyhides the concrete type of its referent. - types.any.one-way
Anyerasure is one-way. - types.any.inspectableA value erased to the sealed trait
std.inspect.Inspectableinstead keeps a runtime record of its concrete type, whichdowncastcompares exactly.
fn demo(value: &dyn Any) -> string:
"got one" # Any erases the concrete typeSee also: Runtime Type Identity.
#AnyVal And AnyRef
- types.sealed.decl
AnyValandAnyRefare the two sealed subtraits ofAny, declared astrait AnyVal < Anyandtrait AnyRef < Any. - types.sealed.markersBoth are empty marker traits that the compiler implements.
- types.sealed.no-implAn explicit implementation of either is an error. Error:
sealed-trait-implementation. - types.sealed.exactly-oneEvery value type implements exactly one of them:
| Trait | Types | Identity |
|---|---|---|
types.sealed.anyval-membersAnyVal |
bool, char, the integer types, f32, f64, string, tuples (void included), enums (optionals and Result included), structs, List, Map, and function types |
These values have no identity. |
types.sealed.anyref-members.reference-valuesAnyRef |
reference types, trait value references included; suspensions and runtime handles with identity | These values have identity. |
- types.sealed.newtypeA newtype has its base type's category:
type Mile(i32)implementsAnyVal, andtype Owner(*User)implementsAnyRef. - types.sealed.never
neverimplements neither, because it has no values. - types.sealed.type-parameterA type parameter implements
AnyValorAnyRefonly through its bound. - types.sealed.bounds
T < AnyValaccepts onlyAnyValtypes, andT < AnyRefaccepts onlyAnyReftypes.
struct Handle: pass
impl AnyRef for Handle # error: sealed-trait-implementation
impl AnyVal for Handle # error: sealed-trait-implementation#Map Key Types
This section defines which types may be map keys, and how keys behave.
- types.map-key.declared-bound
Mapis declared asMap[K < Eq & Hash, V]. A key type that does not implement both traits fails that bound, as any unmet bound does. Error:unsatisfied-trait-bound. - types.map-key.hash
Hashis a standard-library trait instd.hash. - types.map-key.userUser-defined struct and enum types can become keys by explicitly implementing or deriving both traits.
- types.map-key.builtin-hashStandard-library implementations of
Hashcoverbool, integers,char,string, and tuples, optionals, results, and lists of hashable elements. SoList[T]implementsHashwhenT < Hash, andResult[T, E]whenT < HashandE < Hash. - types.map-key.unhashableMaps, floating-point values, functions, suspensions, dynamic trait values, and
Anydo not have built-inHash. - types.map-key.user-enumsEvery user struct and every user enum, including a payload-free one, require an explicit or derived implementation of both traits.
- types.map-key.float-no-hashFloating-point types implement
Eqbut notHash, so they are not valid map keys. - types.map-key.no-map-hashConsequently maps have no built-in hash and impose no order-independent map-hash obligation.
- types.map-key.referenceA reference type implements
EqandHashwhen the type it refers to does, so*Tmay be a key, compared and hashed by value. A map keyed by place usesId[T].
struct UserId:
value: string
fn setup() -> void:
let users: Map[UserId, string] = {} # error: unsatisfied-trait-bound
passWhy. A NaN key is unequal even to itself, so no lookup could find it.
Why. The key traits are an ordinary bound, so a missing trait reads as any other unmet bound.
#Lookup And Order
- types.map.eq-hashMap lookup and duplicate-key replacement use
Eqfor key comparison andHashfor indexing. - types.map.no-lawThe language does not check or impose a law connecting these two implementations.
- types.map.inconsistentIf an implementation hashes values differently that
Eqconsiders equal, lookup and duplicate-key behavior are not guaranteed. - types.map.order.deterministicMap iteration order is deterministic. A program that builds a map by the same operations on the same data iterates it in the same order in every run.
- types.map.order.unspecifiedBeyond that, the order is unspecified. It need not be insertion order.
- types.map.replace.in-placeReplacing the value for an existing key does not change the map's iteration order.
- types.map.semanticsHash values remain outside map value semantics, and map equality remains independent of insertion order.
fn demo() -> List[string]:
m := {"b": +1, "a": +2}
let mut keys: List[string] = []
for entry in m:
keys.push(entry._0)
keys # ["b", "a"] or ["a", "b"]: the map's order, the same in every runWhy. A deterministic order keeps output and tests reproducible. Leaving it unspecified lets the map use a faster layout and hash, as Go and Rust do. Code that needs an order sorts the keys.
#Mutated Keys
- types.map.no-reindexIf a stored key's equality or hash changes after insertion, the map does not automatically reindex it.
- types.map.staleThe entry can remain visible during iteration yet be unreachable by lookup or removal with the mutated key: a stale entry.
- types.map.no-repairSuch mutation does not trigger a compile-time error or an automatic repair.
- types.map.reference-keyA key that holds a
*mutreference, at any depth, changes when the value the reference refers to is written through any path. Its entry then becomes a stale entry.
use std.hash.Hash
@derive(Eq, Hash)
struct Tag:
id: i32
fn demo() -> void:
let mut table: Map[Tag, string] = {}
key := Tag { id: +1 }
table[key] = "one" # keyed by id through Eq and Hash#Least Common Type
types.lct.sitesSeveral constructs infer one type from several values when no expected type is available, as this table lists. An expected type that is a type parameter the enclosing call has not solved yet counts as none: the site joins first, and the call then solves the parameter from the result.
| Construct | Values | Defined in |
|---|---|---|
| List literal | the elements | Expressions |
| Map literal | the keys, and the values | Expressions |
Value-producing if |
the branches | Control Flow |
Value-producing match |
the arm results | Control Flow |
Value-producing while or for |
the break values and the else value |
Control Flow |
| Closure whose result type is inferred | the final value and return operands |
Functions |
| Non-public function whose result type is omitted | the final value and return operands |
Functions |
- types.lct.usesEach of these uses the least common type defined here.
- types.lct.uniqueThe compiler computes a unique least common type of the values' types using only the conversions that
types.lct.conversionslists. - types.lct.conversionsPermission weakening, declared readonly variance, and optional injection may contribute. No other conversion does: no numeric conversion, so
[small, large]with ani16and ani64has no common type. Error:no-common-type. - types.lct.optionalOptional injection adds one layer, as
types.assign.optionalstates. So ani32and ani32?join toi32?, in a list literal, a value-producingiformatch, and an inferred result type alike. - types.lct.optional.one-layerA
Tand aT??have no common type, because injection never adds two layers. Error:no-common-type. - types.lct.no-combineLeast-common-type inference never combines permission weakening with a variance step for the same candidate conversion.
- types.lct.row-union-every-siteAt every construct in the table, function values with different rows are first widened to the union of their rows (Row Union In Literals).
- types.lct.never.droppedThe least common type first drops every value of type
never, then joins the rest. Soif ok: 1 else: return .Nonehas typei32. - types.lct.never.allWhen every value has type
never, the least common type isnever. - types.lct.never.inferA type parameter that only values of type
nevermeet is inferred asnever, as forid(panic("x"))withfn id[T](x: T) -> T.
fn first(values: List[i32]) -> i32?:
let ok = values.len() > 0
value := if ok: values[0] else: return .None
let found: i32? = value
found
fn fail(message: string) -> i32:
if message == "": panic("empty") else: panic(message)
fn fallback(count: i32, found: i32?, flag: bool) -> List[i32?]:
value := if flag: count else: found
values := [count, value]
values
fn nested(count: i32, found: i32??, flag: bool) -> void:
value := if flag: count else: found # error: no-common-typeSee also: a generic call that solves one type parameter from several
arguments uses a narrower join, permission weakening only
(types.generic.infer.join).
#No Implicit Erasure
- types.lct.no-any.reference-valuesThe compiler never falls back to
&dyn Anymerely to make heterogeneous values type-check. - types.lct.no-trait-valueUnconstrained inference also does not introduce a dynamic trait-value conversion.
- types.lct.no-supertrait-wideningIt also never widens a dynamic trait value to a supertrait value, so values of two child traits of one supertrait have no common type.
- types.lct.expected-trait.reference-valuesAn expected type such as
List[*dyn Display]orMap[K, *dyn Display]may request that conversion explicitly. - types.lct.contextualA contextual variant,
.Noneincluded, takes its type only from an expected type, never from the other values. - types.lct.contextual.error
[1, .None]orif c: 1 else: .Nonewithout an expected type is an error. Error:missing-contextual-enum-type. - types.lct.contextual.expected
let values: List[i32?] = [1, .None]supplies the type.
fn pick(flag: bool) -> void:
values := [1, .None] # error: missing-contextual-enum-type
value := if flag: 1 else: .None # error: missing-contextual-enum-typeWhy. Unconstrained inference introduces no trait-value conversion because a concrete type may satisfy multiple unrelated traits.
#Inference Failure
- types.lct.failIf no unique least type exists, inference fails and the user must add an expected type.
- types.lct.no-commonWhen the values have no common type, the failure is an error. Error:
no-common-type. - types.lct.no-leastWhen they have common types but these rules admit no unique least one, the failure is an error. Error:
no-least-common-type.
fn mix(shown: *dyn Shown, tagged: *dyn Tagged) -> void:
items := [shown, tagged] # error: no-common-typeHere Shown and Tagged are traits that both extend Named;
let items: List[*dyn Named] = [shown, tagged] is valid.
#Type Inference Boundaries
types.infer.sitesThe compiler infers the following:
- local binding types;
- closure parameter or result types when an expected function type supplies them;
- generic call arguments when the solution is unambiguous.
types.infer.explicitThe following declarations require explicit types:
- types.infer.explicit.parametersNamed function parameters.
- types.infer.explicit.results-declaredThe results of public functions, public inherent methods, trait methods, and methods of trait implementations, and of at least one function in each recursive cycle.
- types.infer.explicit.fieldsPublic and private struct fields.
- types.infer.explicit.enumEnum payload fields and constructor data.
- types.infer.explicit.trait-methodsTrait method parameters and results.
- types.infer.explicit.fn-typeNamed function type parameters and bounds where applicable.
types.infer.body-result-privateAny other named function, inherent method, or local fn may infer its result from its body, as Parameter And Result Types states.
fn demo() -> i32:
x := +1 # inferred: no annotation needed for a local
xtypes.infer.named-fnFor a named function, inference covers only its result type and its requirement row. Its parameter types, generic parameters, and bounds are always written in its declaration.
#Ambiguous Inference
- types.infer.no-overloadInference must not select among overloaded functions.
- types.infer.ambiguousIf inference has multiple valid solutions, compilation fails and the diagnostic must identify an annotation site that disambiguates the program.
- types.infer.ambiguous.codeThat error is distinct from
cannot-infer-type, which a rule names when no solution exists. Error:ambiguous-type.
trait Repo[T]:
fn number(self) -> i32
struct User: pass
struct Post: pass
struct Job[A, B]:
callback: fn() -> i32 $ Repo[A] + Repo[B]
fn read_both() -> i32 $ Repo[User] + Repo[Post]:
$.use(Repo[User]).number() + $.use(Repo[Post]).number()
fn schedule() -> void:
_ := Job { callback: read_both } # error: ambiguous-type
let job: Job[User, Post] = Job { callback: read_both } # the annotation picks oneThe callback's row fits Repo[A] + Repo[B] with A = User, B = Post
and with the reverse, so the unannotated literal has two solutions.
Why. hd-lang has no function overloading. A separate code tells the reader that inference found several answers, not none, so an annotation picks one.
#Implementation Model (Non-Normative)
This section is non-normative. It describes the reference strategy that the normative rules above are designed to allow, so that implementers and readers can predict costs. An implementation may choose any other representation that preserves the observable semantics.
#Value Categories
Runtime values fall into three categories:
| Category | Types | Identity |
|---|---|---|
| Scalar values | bool, char, the integer types, f32, f64 |
none |
| Identity-free composites | string, tuples, enum values (including Result and optionals), struct values, lists, maps, function values |
none |
| Reference values | references, trait value references included, suspensions, runtime handles | the place a reference refers to; otherwise allocation identity |
The reference values are exactly the implementers of the sealed AnyRef
trait. The scalar values and identity-free composites are exactly the
implementers of the sealed AnyVal trait.
Values without identity are immutable at their top level, or change only through their own place, so storing one by copy or by reference cannot be observed. An implementation may copy, share, or deduplicate such a value freely. A list or a map shares its elements with its copies and copies them before its first change while they are shared.
Writing &e for a value that is no place creates a new object with its
own identity. Converting an existing reference to a trait value reference
shares its place, by Values And References.
See also: Trait Values And Any.
#Shapes and Generic Code
Generic code behaves as if each instantiation were written out for its concrete type arguments. How an implementation compiles it is not observable, and this specification prescribes no strategy. An implementation may emit a body per concrete type, share bodies between types, or pass bounds in any form.
A few rules limit what generic code can observe, so that each strategy stays possible:
| Rule | What it keeps unobservable |
|---|---|
types.generic.sharing |
whether instantiations share a body |
types.generic.identity |
identity on a type that might be a value type |
types.generic.polymorphic-recursion |
whether a growing call cycle could share one body |
| Dynamic Safety | the body behind a trait value's method, which is one per implementation |
A method of a trait value has one body per implementation, so a
method-level type parameter there is the one erased path. A value-typed
argument for it is boxed at the call, which no program can observe, as
Trait Values And Any states. A row parameter
passes its providers as one bundle.
A value with a value layout may stay unboxed in locals, fields,
parameters, generic code, and containers. A program cannot tell a packed
List[i32] from a boxed one.
See also: Name Resolution Across Packages.
#Composite Representation
- A struct is a record of its fields. A field whose type has a value layout is stored unboxed.
- An enum has a value layout that the implementation chooses. A small enum may be a tag and a few unboxed slots, and a larger one an immutable record. A payload of the enum's own type, directly or through other enums, is a reference, so the layout stays finite.
- Shared constructor data is a per-variant constant, stored once in a table indexed by the tag and never in an enum value.
T?is an enum like any other. Because an optional has no identity, a present value may be represented by the payload itself, and.Noneby a null reference or a tag.- A list is a growable array of its element shape, with value-layout elements unboxed. A map is expected to use hashing, with a deterministic iteration order.
- A closure is a function reference plus an environment record. A closure without captures needs no environment. Because a function value has no identity, an implementation may share one value for a named function or allocate one at each use.
- A dynamic trait value is the underlying reference plus a shared method
table for the implementation. For
Inspectableand the traits that extend it, the table also holds one interned descriptor of the recorded type. Adowncastis then a descriptor comparison followed by a cast or an unboxing, and aT < Inspectablebound needs only that descriptor.
#Suspension Frames
A suspending function is compiled to a frame record, a state number, and a poll function. The frame holds the arguments, construction-time providers, the active child suspension, and the locals that are live across suspension points. Frame contents are not observable, so an implementation may keep only live values.
The frame representation as a whole is not observable either. When a
suspension frame is statically known, as for a direct fn! call, an
implementation may skip the uniform Suspend[T] wrapper. It may also skip
the boxing of the frame's result.
#Requirement Rows
How providers for a requirement row are passed is not observable. An implementation may pass them positionally, ordered by a canonical order of the row's keys, instead of through a keyed lookup.
#Representation-Preserving Conversions
A conversion is representation-preserving when the value after conversion is the same runtime value as before. It keeps the same identity and the same stored content, with no wrapper, copy, box, or re-encoding.
Permission weakening &mut U -> &U preserves representation. Conversion to a
trait value or Any, supertrait widening of a dynamic value, and optional
injection do not, which is why Variance excludes them.
#Unsupported Type-System Extensions
This section lists type-system features that hd-lang does not have.
- types.unsupported.type-testThe only runtime type test is exact-type recovery from an
Inspectablevalue. - types.unsupported.no-assertionsThere are no trait-to-trait assertions, no tests on
Anyor on trait values whose trait does not extendInspectable, and no type patterns inmatch. - types.unsupported.no-unionshd-lang has no anonymous union types, including error unions such as
FsError | HttpError. - types.unsupported.error-typeAn error type is a nominal type or a dynamic trait value such as
std.error.Error. - types.unsupported.panic-abiThe exact host representation of a checked runtime panic is an ABI concern; its language-level control-flow semantics are defined in Control Flow.
fn pick(flag: bool) -> i32 | string: # error: syntax-error
if flag: +1 else: "a"See also: Runtime Type Identity, Error Trait.
#Opaque Result Types
An opaque result gives a function a written trait interface while keeping its concrete result type in the defining body.
- types.opaque.occurrenceEach
impl Trait[Args]occurrence in a return annotation denotes an opaque type with the written trait bound. - types.opaque.identity-positionThe declaring function, its generic arguments and the occurrence's position in the return annotation determine the opaque type's static identity.
- types.opaque.distinctResults from different function declarations are distinct types, even when their written bounds and hidden concrete types are equal.
- types.opaque.distinct-positionDifferent opaque occurrences in one return annotation are distinct types, even when their bounds and hidden concrete types are equal.
- types.opaque.head-onlyCallers check against the declared bound, including its supertraits and associated-type bindings, without reading the defining body.
- types.opaque.no-leakageThe hidden concrete type adds no trait implementation or member to the opaque type during caller checking.
- types.opaque.representationThe hidden concrete type is a code-generation fact, not part of a caller's checking dependencies.
- types.opaque.codegenAn opaque result does not introduce a trait value or dynamic trait dispatch.
- types.opaque.recursiveA hidden result type that recursively contains itself is an error. Error:
recursive-value-type.
fn answer() -> impl Display:
+42
pub fn main() -> void $ Console:
println("${answer()}")Open in playgroundfn recursive() -> impl Display: # error: recursive-value-type
recursive()Why. The written bound keeps checking independent of the body. Code generation still sees the concrete representation and selects its trait implementations.
See also: Opaque Results.