hd-lang

#Type System

Status: language specification draft.

This chapter defines types, assignability, value types and writable places, generics, and variance.

  1. types.statichd-lang is statically typed: every expression has a compile-time type.
  2. types.no-executeA program with a type error must not execute.
  3. 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 type

See also: Type Inference Boundaries.

#Type Forms

  1. 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]
  1. types.suspendSuspend[T] is the dynamic one-shot computation protocol.
fn demo() -> (List[i32], string?):
    ([+1, +2], .Some("a"))   # a tuple of a list and an optional

See also: Requirements and Suspension.

#The never Type

  1. types.nevernever is the uninhabited bottom type.
  2. types.never.assignablenever is assignable to every type, and no ordinary value is assignable to it.
  3. types.never.abruptAn expression that completes abruptly has type never on that control-flow path.
  4. types.never.abrupt-formsThese expressions complete abruptly: an unconditional return, break, or continue, propagation that exits the current body, and a call to panic.
  5. types.never.formsExactly six forms have type never: return, break, continue, a call to panic, and any other call whose result type is never. The sixth is an infinite loop that no break targets.
  6. types.never.fitsEach of them therefore fits any expected type, as in a match arm .None => continue or a branch else: 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
    sum

Note. This explains why every reachable branch of a value-producing if or match may 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
  1. types.prim.no-aliasesThere are no core aliases named int, uint, or float.
  2. types.prim.decimaldecimal may be a standard-library type but is not primitive.
  3. types.prim.no-bytehd-lang has no primitive byte or bytes type and no byte literal syntax.
  4. types.prim.by-valuePrimitive values are passed and returned by value.
  5. types.prim.no-mutPrimitive types do not accept the mut modifier.
  6. types.prim.no-mut.errorA primitive type written with mut, as in let n: mut i32 = 0, is an error. Error: mut-on-primitive.
Error example
fn invalid(scale: mut f64) -> void:  # error: mut-on-primitive
    let n: mut i32 = 0  # error: mut-on-primitive

Why. Primitive types do not expose mutable reference state.

  1. types.prim.no-mut.selfA mut self receiver in an impl whose Self is primitive is valid: it is not a primitive type written with mut.
trait Counter:
    fn reset(mut self) -> void

impl Counter for i32:
    fn reset(mut self) -> void: pass  # valid

Why. An impl repeats its trait method's signature. A trait with a mut self method must therefore stay implementable for a primitive Self.

  1. types.prim.no-mut.self-typeIn such a method the mut is dropped, because a primitive has no mut form: self has the plain type Self, as in i32.
  2. 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
  1. types.usize.primitiveusize is a primitive unsigned integer type. It is distinct from u32 and from every other numeric type.
  2. types.usize.widthusize is an unsigned integer with the target's pointer width. On the Wasm32 target it is 32 bits.
  3. 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.
  4. types.usize.preludeusize is a prelude name, so every module may write it without a use.
  5. types.usize.sizesEvery length, index, count, and byte offset that the language defines has type usize.
  6. types.usize.convertA usize and a u32 convert only by a written cast, as usize(limit) or u32(size), by types.num.no-implicit. A value of one where the other is expected is an error. Error: type-mismatch.
Error example
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 usize has the representation of a u32, a Wasm i32. That is a fact about representation: the two stay distinct types.

Note. A size is never negative, so len() - 1 on an empty collection is below the range of usize. In a debug or test build it panics with integer-overflow by types.arith.checked, as in Rust. A release build wraps by types.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 usize and Go's int. 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

  1. types.voidvoid is an alias for the empty tuple type (), so the two spellings name one type and are interchangeable.
  2. types.unitThe empty tuple value is ().
  3. types.void.roleA function that produces no useful value returns (), and its result type is usually spelled -> void.
  4. types.unit.role() is a tuple value and tuple type.
fn log_start() -> void:
    pass

fn same() -> ():
    log_start()  # valid: void and () are one type

Note. The type has one value, (). A void result, a statement result, and pass all produce it, as Swift's typealias 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"
  1. types.string.utf8-bytesA string is a sequence of bytes whose contents cannot be mutated.
  2. types.string.valid-utf8Every string value is valid UTF-8.
  3. types.string.literal-utf8The value of a string literal is the UTF-8 encoding of its Unicode scalar values.
  4. types.string.concat-bytesConcatenation joins the bytes of its operands, so its result is valid UTF-8.
  5. types.string.from-bytesA conversion from arbitrary bytes to string must check that they are valid UTF-8, and must fail when they are not.
  6. types.string.boundaryA scalar boundary of a string is a byte offset from 0 to its length. It does not fall inside the encoding of a scalar value.
  7. types.string.split-scalarAn operation given a byte offset that is not a scalar boundary is a checked runtime panic.
  8. types.string.no-normalizationSource text and runtime operations do not perform Unicode normalization.
  9. types.string.compare-bytesEquality and ordering compare bytes in sequence.
  10. types.string.encodingsCanonically equivalent but differently encoded scalar sequences are distinct.
  11. types.string.len-bytesstring.len() is the number of bytes, and takes constant time.
  12. types.string.graphemesLibraries may provide grapheme traversal.
  13. 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 len and s[i] count bytes, a string is not Iterable. 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

  1. 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.
  2. types.literal.int-no-widenAn integer literal does not automatically choose a wider type.
  3. 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.
Error example
let small: i8 = 1    # valid
let bad: u8 = 300    # error: integer-literal-range
let ratio: f64 = 1   # error: type-mismatch
  1. types.literal.int-diagnosticThe diagnostic must identify the literal, the target range, and an appropriate wider type when one exists.
  2. types.literal.int-not-floatAn integer literal never takes a floating-point type. An integer literal whose expected type is f32 or f64 is an error; write 1.0. Error: type-mismatch.
  3. types.literal.unary-plusAn expected numeric type passes through unary + to a numeric literal, so let positive: u8 = +1 checks the literal against the u8 range.

#Negated Integer Literals

  1. 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.
  2. types.literal.negation.minimumThis makes let minimum: i8 = -128 valid even though positive 128 does not fit in i8.
  3. types.literal.negation.unsignedA negated literal for an unsigned expected type is an error. Error: type-mismatch.
  4. 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

  1. types.literal.float-localA floating-point literal with no expected type takes its width from its expression, by types.literal.local.class.meet, or else f64. It must be representable as a finite value of the type it takes.
  2. types.literal.float-expectedWhen an expected f32 or f64 type is available, the literal is converted directly to that type.
  3. 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
  1. types.literal.local.expectedAn unsuffixed numeric literal with an expected type takes that type, by Integer Literals and Floating-Point Literals.
  2. types.literal.local.signedA signed literal is an integer literal written directly after unary - or +, as in -1 or +5.
  3. 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.
  4. types.literal.local.defaultA literal class's default type is i32 for integers when a member is a signed literal, and usize otherwise. For floating-point literals it is f64.
  5. 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.
  6. types.literal.local.class.openA literal class that meets no type by the end of its statement takes its default type.
  7. 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.
  8. types.literal.local.statementNothing crosses a statement. A binding takes the type of its initializer, and no later statement changes it.
  9. 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.
Error example
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-mismatch

Two 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 i32

The 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
  1. 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.
  2. 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.
  3. 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.
  4. 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 = &42 refers to a usize.
  5. 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.
Error example
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
  1. 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, as let total = +0 or {"tea": +3}, and an annotation for another width, as let total: i64 = 0.

Note. With no annotation, let balance = 100 followed by balance = balance - 150 is a usize subtraction below zero, which panics with integer-overflow in a debug or test build. Write +100 for a value that may go negative. The panic report says that the type is a literal's default type, by flow.panic.report.fallback.

Why. An unannotated integer usually counts or indexes, so usize lets let i = 0 meet len() 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 = 0 gets the warning of types.bind.let-mut-primitive-lint unless &mut i is 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 only impl Halve for i64, is an error: write i64(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
  1. types.literal.duration.fixedA duration literal has type std.time.Duration, independent of its expected type and names in scope.
  2. types.literal.duration.unitsUnits ms, s, min, and h multiply their integer counts by 1, 1000, 60000, and 3600000 milliseconds, respectively.
  3. types.literal.duration.rangeA duration literal must represent between zero and 9223372036854775807 milliseconds, inclusive. Error: integer-literal-range.
  4. types.literal.duration.callA duration literal evaluates as the corresponding qualified std.time unit function applied to its integer count.
  5. 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
  1. types.literal.user-call-resultA user-suffixed literal or prefixed string has the result type of its literal function.
  2. 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 i8 range-checks 300b.
  3. types.literal.primitive.fixedA primitive-suffixed literal has the suffix's primitive type, independent of its expected type or literal class.
  4. 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.
  5. types.literal.primitive.integer-rangeA primitive-suffixed integer literal must fit its suffixed type's range. Error: integer-literal-range.
  6. types.literal.primitive.float-rangeA primitive-suffixed floating-point literal must be finite under its suffixed type's rounding rules. Error: float-literal-range.
  7. types.literal.primitive.negationUnary - directly on a primitive-suffixed signed integer literal checks the negated value, so -128i8 is valid.
Error example
use std.ops.num_suffix

@num_suffix
fn secs(count: i64) -> i64: count * 1000

fn wait() -> void:
    half := 1.5secs  # error: type-mismatch
    pass

Note. Argument checking rejects a literal of the wrong kind, so 1.5s is invalid when s takes an i64; write 1500ms.

Note. -5s is the ordinary negation -(5s): unary - applies to the result of s(5), so a non-primitive result type needs Neg. The minus is not part of the literal, so -128b checks 128 against an i8 parameter and is out of range; write b(-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:

Error example
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
  1. types.literal.prefixed.valuesEach interpolated expression is checked with the T of the prefix function's Template[T] parameter as its expected type, as an argument is. It converts to T by the same rules.

Note. So the argument errors apply at each value: a string where T is i32 is type-mismatch, and an i64 there is implicit-narrowing. When T is the trait value type Display, every value whose type implements Display converts, as Dynamic Trait Values specifies.

See also: Prefixed Strings.

#Other Literals

  1. types.literal.booltrue and false have type bool.
  2. types.literal.stringA string literal has type string, and a character literal has type char.
  3. 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: string

UserId and PostId are distinct even though their field shapes match.

  1. types.nominal.distinctEach struct and enum declaration introduces a distinct nominal type.
  2. 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.

  1. types.tuple.structuralTuple types are structural.
  2. types.tuple.sameTwo tuples have the same type when they have the same arity and pairwise-equal element types.
  3. types.tuple.one-elementOne-element tuples require a trailing comma.
  4. 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 written

See 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
  1. 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.
  2. types.tuple.rest.listA rest element whose type is not List[T], as in (i32, i32...), is an error. Error: type-mismatch.
  3. 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, so values._1 above is a List[i32].
  4. 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.
  5. 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.
Error example
fn plain(values: (i32, List[i32]...)) -> (i32, List[i32]):
    values  # error: type-mismatch

Why. The rest element lets a function's inputs tuple say that it takes varargs, so fn(i32, List[i32]...) and fn(i32, List[i32]) are different types. Only a List tail is allowed: there is no tuple concatenation in types.

See also: Vararg Inputs, Tuple Rest Elements.

#Function Type Identity

  1. types.fn.constructorA function type is an application of the standard constructor Fn or SuspendFn, and the fn(...) -> T spelling is exact sugar for it.
  2. 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.
  3. 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
  1. types.alias.sameA transparent alias introduces another name for the same type.
  2. types.alias.identicalUserName and string are identical for assignability, method lookup, trait conformance, and runtime representation.
  3. types.alias.rowA transparent alias may also name a requirement row, as Row Aliases defines.
  4. types.alias.cycleAn alias that expands to itself, directly or through other aliases, is an error. Error: alias-cycle.
  5. types.alias.cycle.reportedThe error is reported once per cycle, on the declaration of the cycle that comes first in the source.
  6. 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.
  7. 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.
Error example
type Left = List[Right]  # error: alias-cycle

type Right = Left?
Error example
type Owner = Account  # error: unknown-type
type AppRow = $ Ledger  # error: unknown-trait

Why. A transparent alias is replaced by its right side, so a cycle never ends. A recursive type needs a struct or enum declaration.

#Newtypes

A parenthesized type declaration creates a nominal single-field newtype:

type Mile(i32)
  1. types.newtype.declA parenthesized type declaration creates a nominal single-field newtype.
  2. types.newtype.distinctMile is distinct from i32.
  3. types.newtype.constructConstructor syntax creates the newtype, and the base type constructor unwraps it:
m := Mile(10)
n := i32(m)
  1. types.newtype.no-implicitNo implicit conversion exists in either direction.
  2. types.newtype.no-inheritA newtype does not inherit trait implementations from its underlying type.
  3. types.newtype.map-keyIn particular, a newtype is not a valid map key until it explicitly implements or derives both Eq and Hash.
  4. types.newtype.construct-refConstructing a newtype over an AnyRef base creates no new object: the newtype value wraps the base value.
  5. types.newtype.construct-valueA newtype construction over an AnyVal base, such as Mile(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
  1. types.option.declThe prelude declares the ordinary generic enum Option.
  2. types.option.sugarT? is exact sugar for Option[T]: the two spellings denote the same type everywhere a type may appear, including implementation targets.
  3. types.option.distinctT and T? are different types; a non-optional type never contains an absent value.
  4. types.option.prelude-nameOption is the only prelude name this adds; Some and None are variants, not prelude names.
  5. types.option.bare-variantA bare None or Some(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

  1. types.option.noneThe absent value is written .None where an optional type is expected, or Option.None.
  2. types.option.someA present value is written .Some(value) or Option.Some(value).
  3. types.option.constructionThese spellings follow the ordinary enum construction rules.
  4. types.option.none-contextIn particular, .None without an expected optional type is an error. Error: missing-contextual-enum-type.
  5. types.option.patternsOptionals are matched with ordinary enum patterns, .Some(pattern), .None, and their Option.-qualified forms.
Error example
let value: i32? = None  # error: unknown-name
missing := .None        # error: missing-contextual-enum-type

See also: Enum Declarations, Match Expressions.

#Implicit Wrapping And Nesting

  1. types.option.wrapA value of T is also implicitly accepted where T? is expected, constructing .Some(value).
  2. types.option.wrap.sitesThe implicit wrap applies to assignments, arguments, and return values.
  3. types.option.wrap.onceThe source expression is evaluated once.
  4. types.option.wrap.one-layerThe implicit wrap adds one layer only.
  5. types.option.nestOptionality may nest: T?? is Option[Option[T]]. It preserves the distinction between an absent outer value and a present outer value containing an absent inner value.
  6. types.option.nest.acceptA T? value is accepted where T?? is expected, but a plain T is not.
  7. types.option.nest.explicitThe inner layer needs an explicit .Some(...), as in let nested: i32?? = .Some(1).
  8. types.option.nest.none.None with an expected T?? 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

  1. types.option.reprThe representation of Option[T] is an implementation detail; for example, an implementation may represent .None as a null reference.
  2. types.option.propagatePostfix ? on an optional expression either produces its contained value or returns .None from the nearest function.
  3. types.option.propagate.resultThat function must itself return a compatible optional type.
  4. types.option.propagate.one-layerPostfix ? removes and propagates one optional layer at a time.
  5. types.option.propagate.permissionA present value keeps the optional's declared contained type T, including *mut U when T = *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

  1. types.option.ordinaryOptionals follow the ordinary enum rules in every other respect.
  2. types.option.any.reference-valuesA reference to an optional value may be erased to &dyn Any like a reference to any other enum value.
  3. types.option.valueAn optional is an enum, so it is a value without identity and implements AnyVal, whatever its payload type.
  4. types.option.identity-erroris with an optional operand is an error, as for every enum. Error: identity-requires-references.
  5. types.option.invariantOption declares its parameter unmarked, so an optional is invariant in its contained type, as every unmarked parameter is.
  6. types.option.invariant.resultResult[T, E] likewise declares both parameters unmarked, so it is invariant in both T and E.
Error example
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-variance

Why. The standard methods on optionals and results take T as a parameter, as unwrap_or(self, fallback: T) does. That is a negative position on the public surface, so a covariant T would 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.

  1. types.result.declRecoverable errors use the prelude enum enum Result[T, E]: Ok(value: T); Err(error: E).
  2. types.result.prelude-nameResult is a prelude name; Ok and Err are its variants, not prelude names.
  3. types.result.constructionValues follow the ordinary enum construction rules: .Ok(value) and .Err(error) where a Result type is expected, or Result.Ok(value) and Result.Err(error).
  4. types.result.ok-contextIn particular, .Ok(value) without an expected Result type is an error. Error: missing-contextual-enum-type.
  5. types.result.bare-variantA bare Ok(value) or Err(error) expression resolves like any other identifier. It is an error unless a declaration in scope supplies that name. Error: unknown-name.
  6. types.result.patternsResults are matched with the patterns .Ok(pattern), .Err(pattern), and their Result.-qualified forms.
Error example
fn parse() -> Result[i32, string]:
    Ok(1)  # error: unknown-name

See also: Enum Declarations, Match Expressions.

#Void Results

  1. types.result.unit-okWhen T is void, the success value is (), so the constructor is written .Ok(()) or Result.Ok(()).
  2. types.result.no-ok-omitNo shorthand omits that (), so .Ok() passes no argument for value. Error: argument-count.
  3. types.result.no-ok-pass.Ok(pass) is not the source spelling for this case.
  4. types.result.unit-patternA match names that success with the unit pattern, as in .Ok(()) => ..., so construction and matching read alike.
Error example
fn save(ready: bool) -> Result[void, string]:
    if ready:
        return .Ok(())
    .Ok()  # error: argument-count

#Result Propagation

  1. types.result.propagatePostfix ? on Result[T, E] either produces the success value or immediately returns the error from the nearest function.
  2. types.result.propagate.enclosingThe enclosing function must return a Result[U, F].
  3. types.result.propagate.one-stepThe error reaches F in one step: by one rule of Assignability And Coercion, or otherwise by one call of F's From[E] implementation.
  4. types.result.propagate.not-bothThe step is never both, and conversions are never chained.
  5. types.result.err-valueReturning an .Err value without ? does not itself alter control flow, and it never calls a conversion.
  6. types.result.propagate.permissionThe success value retains its declared generic type T, including *mut U when T = *mut U.
  7. 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 once

See 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:

Error example
fn total(small: i16, large: i64) -> i64:
    let wide: i64 = small  # error: type-mismatch
    i64(small) + large + 1

The numeric types form three families, each ordered from narrower to wider:

i8, i16, i32, i64
u8, u16, u32, usize, u64
f32, f64
  1. types.num.familiesThe signed integers, the unsigned integers, and the floating-point types are the three numeric families above, each ordered by width.
  2. 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) or f64(ratio).
  3. types.num.no-implicit.widerSo a value where a wider type of its family is expected is an error. Examples are an i16 passed to an i64 parameter and an f32 assigned to an f64. Error: type-mismatch.
  4. types.num.no-implicit.fixThat diagnostic should offer a fix-it that writes the conversion around the value, as in i64(small).
  5. types.num.narrowingA value where a narrower type of its family is expected is an error. Error: implicit-narrowing.
  6. types.num.usize-widthusize is the unsigned type of the target's width, by types.usize.width. On Wasm32 it is wider than u16, narrower than u64, and as wide as u32.
  7. types.num.same-widthA value where another type of its family with the same width is expected is an error, as a u32 passed to a usize parameter. Error: type-mismatch.
  8. types.num.no-sign-changeThere is no implicit conversion between signed and unsigned integers.
  9. types.num.no-int-floatThere is no implicit integer-to-floating or floating-to-integer conversion in the current core.
  10. 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 = 300 is valid, and let y: f64 = 1 is 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

  1. 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.
  2. types.num.binary.literal-leftSo an unsuffixed integer literal on the left takes the right operand's integer type. 0xFFFF_FFFF_FFFF_FFFF - count with a u64 count is valid, and 1 < count compares two u64 values.
  3. types.num.binary.same-typeOtherwise both operands must have one type, and the result has that type, so f32 op f32 produces f32. Two types of one family are an error, as small + large with an i16 and an i64, or an f32 and an f64 operand. Error: type-mismatch.
  4. 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.
  5. types.num.binary.castThe user must cast one operand explicitly in those cases.
Error example
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)
  1. types.cast.syntaxExplicit numeric conversion uses constructor-style casts.
  2. types.cast.wrapAn integer-to-integer cast wraps at run time, as Go conversions and Rust as do. The result keeps the low bits of the source value's two's-complement form, read in the target type. It never panics.
  3. types.cast.wrap.exampleSo u8(x) with x = 300 gives 44, and i8(x) with x = 200 gives -56.
  4. 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.
  5. types.cast.fallibleLibraries may provide separate fallible conversion functions returning Result.
Error example
fn narrow(wide: i64) -> u8:
    u8(wide)          # wraps: 300 gives 44
fn fixed() -> u8:
    u8(300)           # error: integer-literal-range

The 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
  1. types.cast.saturateA floating-to-integer cast saturates, as Rust as does. 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.
  2. types.cast.saturate.exampleSo i8(x) with x = 300.0 gives 127, u8(x) with x = -1.5 gives 0, and i32(x) gives 0 when x is NaN.
  3. types.cast.no-panicNo numeric cast panics.
  4. types.cast.precisionAn explicit numeric cast may lose precision according to these rules.
  5. types.cast.exactLibraries may expose exact or fallible conversions when loss must be rejected.
  6. 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.

  1. types.arith.checkedIn a debug or test build, an integer overflow or underflow of +, -, *, **, or unary - panics. Panic: integer-overflow.
  2. types.arith.releaseIn a release build, those operations wrap to the type's width as two's complement, as in Rust.
  3. 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.
  4. 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 the checked_*, wrapping_*, and saturating_* library operations.
  5. types.arith.divisionInteger division truncates toward zero.
  6. types.arith.remainderInteger remainder has the sign of the dividend.
  7. types.arith.shift-countA shift count is invalid when it is negative or not smaller than the bit width of the shifted value.
  8. types.arith.shift-count.debugIn a debug or test build, an invalid shift count panics. Panic: invalid-shift.
  9. types.arith.shift-count.releaseIn a release build, an invalid shift count is masked to the bit width of the shifted value. So x << n shifts by n % bits, as in Rust.
  10. types.arith.shift-rightRight shift of a signed integer is arithmetic and sign-extending.
  11. types.arith.min-divisionFor every signed width, MIN / -1 panics with integer-overflow in every build.
  12. types.arith.min-remainderFor every signed width, MIN % -1 produces 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 build

Why. This is Rust's model: overflow checks help while a program is written and tested, and cost speed once it ships. hd test always 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

  1. types.display.intDisplay formats integers in base ten.
  2. types.display.floatDisplay formats 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.
  3. types.display.widthThe digits round-trip at the value's own width, so an f32 displays its f32 digits even when formatted through generic code.
  4. types.display.notationFinite floats use fixed notation when the normalized decimal exponent is in [-6, 21) and lowercase scientific notation otherwise.
  5. types.display.exponentScientific exponents always include + or - and no leading zeroes.
  6. types.display.fixedFixed notation always contains a decimal point and at least one fractional digit, so 1.0 remains visibly floating.
  7. types.display.specialNegative zero is -0.0; infinities are inf and -inf; every NaN is NaN.
  8. types.display.nan-canonicalBefore hashing, boundary serialization, or Display, every NaN is replaced with the one canonical quiet-NaN value for its width.
  9. types.display.nan-compareNaN comparison continues to follow IEEE 754.
fn demo() -> string:
    "${1.5}"   # "1.5": shortest round-trip digits

Note. 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:

  1. types.assign.identicalS and T are identical after expanding transparent aliases.
  2. types.assign.weakenS is mut Suspend[U] and T is its readonly form Suspend[U], or S is a writable reference and T the read-only one of its kind, as *mut U and *U, by ref.convert.weaken.
  3. types.assign.varianceA declared generic variance conversion permits the readonly outer type to change its type arguments.
  4. types.assign.optionalA value of T is injected into T?. The injection adds one layer only, so a T is not injected into T??.
  5. types.assign.row-subsumptionS and T are function types, and T's row entails every key of S's row. Then S with T's row is assignable to T, as Row Subsumption states.
  6. types.assign.neverS is never, as types.never.assignable states.
  7. types.assign.trait-value.refT is a trait value reference, such as &dyn Trait or *mut dyn Trait, and S converts 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

  1. types.assign.no-subtypingNo inheritance or structural record subtyping exists.
  2. types.assign.binding-typeAssignment never changes the declared or inferred type of a binding.
  3. types.assign.let-declaredIn particular, later assignment to a let binding must remain assignable to the type established at its declaration. A literal initializer's width is fixed there, by types.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.

  1. types.mut.weakenA mut Suspend[T] may be used as a Suspend[T], a *mut T as a *T, and a &mut T as a &T.
  2. types.mut.no-upgradeA Suspend[T] must never be upgraded to mut Suspend[T], nor a *T or &T to *mut T or &mut T. An upgrade is an error. Error: mutable-upgrade.
  3. types.mut.no-upgrade.inferenceThe error includes an upgrade that generic inference would produce. Given keep[T](value: T) -> T, binding keep(user) with user: *User to a *mut User declaration is rejected rather than inferring T as *mut User.
  4. types.mut.argumentA readonly Suspend[T] argument cannot satisfy a mut Suspend[T] parameter. Error: readonly-argument-to-mutable-parameter.
  5. 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.

Error example
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-upgrade

See 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
  1. types.value.structA struct type is a value type. Each struct declaration introduces a distinct nominal type, as an enum declaration does.
  2. 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.
  3. 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.
  4. types.value.no-viewA struct type has no readonly or mut view, so a struct literal or copy-update has the plain struct type.
  5. types.value.no-mut.writtenA type written with mut, as in mut Point, mut List[i32], mut (i32, i32), or mut T for a type parameter T, is an error unless it is a Suspend[T]. Error: mut-on-struct.
  6. types.value.field-typeA field read through a struct value has the field's declared type, whatever holds the struct.
  7. types.value.anyvalA struct type implements AnyVal, so is with a struct operand is an error. Error: identity-requires-references.
Error example
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-references

Note. A primitive type written with mut is the error of types.prim.no-mut.error instead.

Why. A struct value has no identity, so a mut view of one would name no shared object, and is would have nothing to compare.

#Writable Places

A field of a struct value changes in place only through a writable place.

  1. types.value.writable-placesA place is writable when it is a let mut binding, local or top-level, a mut parameter, or mut self. A value reached through a &mut reference 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.
  2. types.value.let-mutlet mut name = value with a struct value is valid, with or without a struct annotation, and makes name writable.
  3. types.value.field-writeAssigning a struct field, with = or a compound assignment, requires the struct to be in a writable place.
  4. types.value.write-onceThe assignment changes that place alone, since no other place shares its value.
  5. types.value.reassign-bindingA plain let binding of a struct and a mut parameter may be reassigned as a whole. A := binding and a parameter that is not mut never 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
Error example
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
    p

Every other write to a place that is not writable has one code:

  1. 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 self method on the place. A field write takes the code that the table above gives. A call of a mut self method on a readonly Suspend[T] is the same error. Error: not-writable.
Error example
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-writable

Note. A struct's mut self method takes its own copy of the receiver, by fn.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.

  1. types.view.non-reassignableA binding is non-reassignable when its name cannot be rebound, as a := binding and a parameter that is not mut cannot.
  2. types.bind.let-rebindA let binding without mut may 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: reassignment
Error example
fn invalid() -> void:
    let user = User { name: "Ada" }
    user.name = "Grace"  # error: not-writable
  1. types.bind.let-mut-primitive-locallet mut on a local whose type is primitive, as in let mut n = 0, is valid, since &mut n needs a writable place.
  2. types.bind.let-mut-primitive-lintWhen no &mut of such a local is taken, its redundant mut produces a lint finding with a removal fix-it. Warning: redundant-let-mut.
  3. types.bind.let-mut-primitive-toplet mut on a top-level binding whose type is primitive is an error. Error: mut-on-primitive.
  4. types.bind.let-mut-primitive.hintThe diagnostic says that a plain let is already reassignable.
  5. types.bind.let-mut-tuple-validlet mut on a name whose type is a tuple, as in let 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
  1. types.bind.let-pattern-mutA name written mut in a let pattern is a writable binding, as in a match arm; a name without mut is not. In let (mut log, db) = pair, log is writable and db is not.
  2. types.bind.let-pattern-mut.data-fieldThe same holds in a struct or variant pattern. In let Profile { mut tags, name } = profile, tags is a writable copy of the field.
Error example
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-writable
struct 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)
    name

Note. 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 mut says at the declaration which locals change in place.

  1. types.bind.neverA binding without a type annotation whose initializer has type never is an error. Error: uninhabited-binding.
Error example
fn first_even(values: List[i32]) -> i32:
    found := panic("no even value")  # error: uninhabited-binding
    found

Why. No value of type never exists, so the binding could never be read. The initializer alone, without found :=, 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
  1. types.coll.valueList[T] and Map[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.
  2. 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.
  3. types.coll.no-identityA list or a map has no identity. List and Map implement AnyVal, so is with one is an error. Error: identity-requires-references.
  4. types.coll.let-mutlet mut name = value with a list or map value is valid, with no annotation or a List[T] or Map[K, V] one, and makes name writable.
  5. 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 as push, or an element store.
  6. types.coll.writable.callA &mut self method such as push, called on a list or a map that is no writable place, is an error. A mut self method 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.
  7. 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.
  8. types.coll.element-placeAn element of a list in a writable place is a writable place. So xs[i] = v, xs[i].x = v, and xs[i].push(v) change xs in place.
  9. types.coll.nestedA change through a path, such as m[k].push(x), team.members.push(x), or xs[i].name = n, changes the inner value where it is stored. A mut self method called on such a path changes only its own copy (fn.method.mut-self-struct), so the inner value stays as it was.
  10. 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.
Error example
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-references

Why. 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 *mut or &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.

  1. types.coll.map-valuem[k] = v and m[k] op= v replace a map value. A changing method called on one, such as m[k].push(x), changes it in place.
  2. types.coll.map-value.placeA value of a map in a writable place is a writable place. So m[k].x = v and m[k].x += 1 change the value in place, as xs[i].x = v changes a list element.
  3. 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 of m[k] is. Panic: index-out-of-bounds.
  4. types.coll.map-value.reference&m[k] and &mut m[k] are element references, which live for one call, as Element References states.
Error example
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.count

See also: Snapshot Iteration, Writable Places.

#Generics

This section defines generic parameters, bounds, arguments, and their runtime representation.

  1. types.generic.completeGeneric type and function parameters denote complete types.
  2. types.generic.namingBy convention, generic parameters, including row parameters, use uppercase names such as T, U, K, V, and R.
  3. 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)
  1. types.generic.bound.any-orderIn a generic declaration of any kind, a bound may name any parameter of the same list, earlier or later.
  2. types.generic.bound.mutualBounds may name each other, as S < Source[U] and U < Sink[S] do above. Such mutual references are not a circularity error.
  3. types.generic.bound.defaults-apartDefaults keep their own order: they are trailing, as types.generic.default.order states, and see only earlier parameters, as types.generic.default.later states.

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 Tank pairs with both Pipe and Hose.

See also: Function And Closure Scopes.

#Generic Arguments

  1. types.generic.inferGeneric arguments are inferred at call sites when unambiguous.
  2. types.generic.explicitCallers may supply the complete generic argument list explicitly.
  3. types.generic.short-listAn explicit list in an expression may omit trailing slots. Each omitted slot is inferred as a _ slot is, then defaulted.
  4. types.generic.placeholder-slotIn a generic function's or method's explicit list, _ may occupy any slot and requests inference for that argument.
  5. types.generic.placeholder.not-type_ is not itself a type. Its use in an ordinary type application is an error. Error: syntax-error.
  6. 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.
Error example
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:

  1. 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 a Suspend[X] meet at Suspend[X].
  2. types.generic.infer.join.no-trait-valueA trait-value conversion never applies, as types.lct.no-trait-value states for the least common type. So cmp(user, label) with a User and a Display argument is an error. Error: no-common-type.
  3. 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.
  4. types.generic.infer.join.other-conflictAny other conflict between the arguments' types is an error. Examples are choose(1, true), max(small, large) with an i32 and an i64, a *mut User and a *User, and a T and a T?. Error: type-mismatch. The caller writes a cast, as in max(i64(small), large).
  5. 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 an i64 large solves T = i64.
  6. 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, as types.lct.expected-trait.reference-values allows.
  7. types.generic.infer.join.not-lctThis join is narrower than the least common type, and is not one of that section's constructs.
Error example
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-type
struct 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 T to 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
  1. 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 U in read[U, S < Source[U]](source: S).
  2. types.generic.infer.bound.afterCall inference solves a parameter named only in bounds after the other parameters.
  3. 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.
  4. types.generic.infer.bound.noneIf the type implements the bound's trait for no instantiation, the call is an error. Error: unsatisfied-trait-bound.
  5. types.generic.infer.bound.ambiguousIf the type implements the bound's trait for several instantiations, this step leaves the parameter unsolved.
  6. types.generic.infer.bound.defaultA parameter named only in bounds and left unsolved takes its type-argument default, as types.generic.default.fill gives, and the bound is then checked.
  7. 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.
  8. types.generic.infer.bound.no-default.ambiguousWhen a bound of the parameter allows several instantiations, the error is ambiguous-type, by types.infer.ambiguous.code. Error: ambiguous-type.
  9. types.generic.infer.bound.no-default.disagreeWhen each bound allows exactly one instantiation and they differ, no solution exists. Error: cannot-infer-type.
  10. 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.
  11. 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.
  12. types.generic.infer.bound.disagreeOtherwise, when bounds allow different instantiations or one allows several, the parameter is left unsolved, as for one bound.
  13. 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.
  14. 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.
  15. types.generic.infer.bound.placeholderAn explicit list may mix written arguments with _ slots, as in read::[string, _](both); each _ slot is inferred by the steps above.
  16. 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.explicit requires.
Error example
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-bound

A 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
    found

A 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
    text

Two bounds that name one parameter must agree on it:

Error example
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-type

Note. The std adapter zip[U, I < Iterable[U]](self, other: I) (More Adapters) relies on this step. With names: List[string], xs.iter().zip(names) solves I from the argument, then U = string.

Why. When the argument's type has one implementation, the trait argument is a fact of that type, as Rust's associated IntoIterator::Item is. 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
  1. types.generic.default.formA generic parameter of a function, method, struct, enum, trait, or type declaration may declare a default.
  2. 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.
  3. types.generic.default.scopeA default may name earlier parameters of the same list, the parameters of an enclosing declaration, and Self where Self is in scope.
  4. types.generic.default.laterA default that names its own parameter or a later one is an error. Error: binding-not-yet-visible.
  5. 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.
  6. 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.
  7. 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
  1. 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.
  2. 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.
  3. types.generic.default.argument-winsA default never replaces a solution, so an argument or expected type that solves the parameter wins: widen(3) above has T = i32.
  4. types.generic.default.unsolvedA parameter left unsolved that has no default stays an error. Error: cannot-infer-type.
  5. 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.
  6. types.generic.default.bareA name written in a type without a list, or with bindings only, omits every positional slot. So impl Same for Money is valid for trait Same[Other = Self].
  7. types.generic.default.written-missingA written type that omits a slot without a default is an error. Error: partial-generic-arguments.
Error example
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
    0

Why. 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

  1. types.generic.unobservableThe runtime representation of generic code is not observable.
  2. 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 < Inspectable bound;
    • is on a type parameter requires T < AnyRef;
    • variance conversions must be representation-preserving.
  3. types.generic.interfacesPackage interfaces therefore carry the bodies of generic functions needed by downstream compilation.
  4. 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.
  5. 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.
  6. types.generic.call-cycle.stepA call passes type parameter P of its body to parameter Q of its callee when its type argument for Q holds P.
  7. types.generic.call-cycleA generic call cycle is a chain of such steps that leads a type parameter back to itself.
  8. types.generic.call-cycle.growsA step grows when its type argument is larger than P, such as List[P], (P, P), or P?.
  9. 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.
  10. 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.
  11. types.generic.call-cycle.impl-paramsEach method of an implementation has its own copy of the implementation's type parameters.
  12. types.generic.call-cycle.projectionA type argument does not hold a type parameter that occurs only inside a projection, such as P.Item.
  13. 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.
  14. 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 way
Error example
fn nest[T](value: T, depth: i32) -> i32:
    if depth == 0: return depth
    nest((value, value), depth - 1)  # error: instantiation-too-deep

Why. 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

  1. types.generic.identityIdentity comparison is on a type parameter is permitted only with the sealed T < AnyRef bound. Without it, the comparison is an error. Error: identity-requires-references.
  2. 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
  1. types.variance.markersGeneric type declarations mark covariance with +T, contravariance with -T, and invariance by leaving T unmarked.
  2. types.variance.verifiedThe compiler verifies each declared parameter against its use on the type's surface.
  3. 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 self receiver, private methods included.
  4. types.variance.trait-implA separate trait implementation does not alter the nominal type declaration's variance; its own instantiated signatures must still type-check.
  5. types.variance.trait-paramsA trait's generic parameters are invariant. A variance marker on one is an error. Error: invalid-variance.
Error example
struct Box[+T]:
    value: T

impl[T] Box[T]:
    fn replace(self, next: T) -> void:  # error: invalid-variance
        pass

Why. 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:

  1. types.variance.surface.mut-selfAn inherent method with a mut self, &self, or &mut self receiver counts toward declared variance, as a method with a self receiver does.
Error example
struct Box[+T]:
    value: T

impl[U] Box[U]:
    pub fn set(&mut self, value: U) -> void:  # error: invalid-variance
        self.value = value

An associated function with no receiver is not on that surface:

  1. types.variance.surface.no-receiverAn inherent associated function with no self receiver 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:

  1. types.polarity.positiveA returned value and an ordinary field are positive positions.
  2. types.polarity.negativeA function or method parameter is a negative position.
  3. types.polarity.functionEntering a function parameter reverses polarity, while entering a function result preserves it.
  4. types.polarity.genericApplying a covariant generic argument preserves polarity, a contravariant argument reverses it, and an invariant argument makes the occurrence invariant.
  5. types.polarity.mutOccurrence beneath &mut is invariant, and an embedded field's type counts as beneath &mut.
  6. types.polarity.bothA parameter used in both positive and negative positions must be invariant.

Why. The storage beneath &mut can be both read and written. Access through an embedded field follows its container.

  1. types.variance.covariant-checkA declared +T is rejected if any occurrence is negative or invariant. Error: invalid-variance.
  2. types.variance.contravariant-checkA declared -T is rejected if any occurrence is positive or invariant. Error: invalid-variance.
  3. types.variance.unmarkedAn unmarked invariant parameter may occur in any position.
struct Box[+T]:
    value: T

See 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
  1. types.variance.target.derivedFor an inherent impl[U, ...] G[A1, ..., An], each impl parameter U has a derived variance: its variance in the target type, which the Polarity rules compute.
  2. types.variance.target.composeA covariant parameter of G keeps U's variance in its argument, a contravariant one reverses it, and an unmarked one makes it invariant.
  3. types.variance.target.bothA U that the target places both covariantly and contravariantly, as in impl[U] Pair[U, U] for Pair[+A, -B], is invariant.
  4. types.variance.target.checkEach method signature of that implementation is checked as if U were declared with its derived variance: covariant U by types.variance.covariant-check, contravariant U by types.variance.contravariant-check. Error: invalid-variance.
  5. types.variance.target.invariantAn invariant U, as in impl[U] Box[U?], may occur in any position, as types.variance.unmarked allows.
Error example
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 is T'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. So U can 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

  1. types.variance.readonly-onlyVariance conversion applies only to a readonly outer type: one not written *mut, &mut, or mut, such as G[T], *G[T], or &G[T].
  2. types.variance.mut-invariantEvery writable reference, *mut G[T] or &mut G[T], and every mut Suspend[T], is invariant in all generic arguments.
  3. types.variance.listThe built-in List declares a covariant element parameter.
  4. types.variance.mapMap[K, V] is invariant in K and covariant in V.
  5. types.variance.functionThe function type constructors Fn and SuspendFn are 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

  1. types.variance.repr.covariantA variance conversion G[S] -> G[T] requires a representation-preserving S -> T conversion for each covariant argument.
  2. types.variance.repr.contravariantA variance conversion G[S] -> G[T] requires a representation-preserving T -> S conversion for each contravariant argument.
  3. types.variance.repr.weakeningPermission weakening, *mut U -> *U, &mut U -> &U, or mut Suspend[U] -> Suspend[U], is representation-preserving.
  4. types.variance.repr.excludedThese conversions are not representation-preserving:
    • construction of a trait value reference such as &i32 -> &dyn Display or &User -> &dyn Display;
    • child-dynamic-trait to supertrait widening;
    • optional injection U -> U?.
  5. 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 representation

See also: Representation-Preserving Conversions.

#Trait Values And Any

This section defines trait conformance, dynamic trait values, written &dyn Trait, and the Any trait family.

  1. types.trait.explicitTrait conformance is explicit.
  2. types.trait.no-shapeMatching method shape alone does not make a type implement a trait.
  3. types.trait.staticA generic bound such as T < Display uses 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

  1. types.trait.value.reference-valuesA trait value type is &dyn Tr or &mut dyn Tr, or counted, *dyn Tr or *mut dyn Tr, referring to an object whose concrete type implements Tr.
  2. types.trait.value.contents.reference-valuesA trait value reference holds a reference to an object and dispatch metadata for its trait.
  3. 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.
  4. 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 Display value is a valid argument for T < Display.
  5. types.trait.value.bound.available.reference-valuesIt satisfies such a bound only when every member of the bound's trait is available on the dyn value, as trait.dyn.bound.reference states.
  6. 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.
  7. types.trait.value.not-targetA dynamic trait value type is still not an implementation target.
  8. types.trait.value.ref-writtenA trait value reference, &dyn Trait or &mut dyn Trait, is a trait value that is a reference. A converted &S or &mut S is 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()
Error example
trait Greet:
    fn greet(self) -> string

fn demo(g: Greet) -> string:  # error: trait-used-as-type
    g.greet()

Why. &dyn makes the reference and runtime dispatch explicit. A bound such as T < Greet stays the monomorphized form.

See also: Dynamic Trait Values, which gives the rule.

#Dynamic Safety

  1. types.trait.dyn.any-traitEvery trait may be used as a dyn type. There is no per-trait safety check.
  2. types.trait.dyn.memberA member that cannot work through a dyn value is unavailable on it, and calling it there is an error at the call. Dynamic Safety lists those members. Error: dyn-member-unavailable.
  3. 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].
  4. types.trait.safe.method-type-paramA method with a method-level type parameter is available on a dyn value whatever the parameter's bounds, as in T < Display or an unbounded T.
  5. types.trait.safe.method-type-argA call through a trait value may pass any type argument for such a parameter, including an AnyVal type such as i32 or an enum.
  6. 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.
  7. 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.
  8. types.trait.safe.trait-genericTrait declaration generic parameters are permitted.
Error example
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-unavailable

Why. 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 a dyn type.

#Supertrait Widening

  1. types.trait.childA dynamic child-trait value exposes methods declared by the child and all of its transitive supertraits.
  2. types.trait.child.widenA dynamic child-trait value may be widened implicitly to a dynamic supertrait value.
  3. types.trait.child.one-wayThat conversion discards access to child-only methods and cannot be reversed by a conversion; only Inspectable values recover a concrete type.
  4. 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 Animal

See also: Runtime Type Identity.

#Any

  1. types.anyAny is the built-in universal empty trait.
  2. types.any.allEvery value type, including an optional type, satisfies Any automatically.
  3. types.any.nevernever satisfies Any too. A void value needs no rule of its own, because it is the tuple ().
  4. types.any.erase.reference-valuesThe reference type &dyn Any hides the concrete type of its referent.
  5. types.any.one-wayAny erasure is one-way.
  6. types.any.inspectableA value erased to the sealed trait std.inspect.Inspectable instead keeps a runtime record of its concrete type, which downcast compares exactly.
fn demo(value: &dyn Any) -> string:
    "got one"   # Any erases the concrete type

See also: Runtime Type Identity.

#AnyVal And AnyRef

  1. types.sealed.declAnyVal and AnyRef are the two sealed subtraits of Any, declared as trait AnyVal < Any and trait AnyRef < Any.
  2. types.sealed.markersBoth are empty marker traits that the compiler implements.
  3. types.sealed.no-implAn explicit implementation of either is an error. Error: sealed-trait-implementation.
  4. 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.
  1. types.sealed.newtypeA newtype has its base type's category: type Mile(i32) implements AnyVal, and type Owner(*User) implements AnyRef.
  2. types.sealed.nevernever implements neither, because it has no values.
  3. types.sealed.type-parameterA type parameter implements AnyVal or AnyRef only through its bound.
  4. types.sealed.boundsT < AnyVal accepts only AnyVal types, and T < AnyRef accepts only AnyRef types.
Error example
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.

  1. types.map-key.declared-boundMap is declared as Map[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.
  2. types.map-key.hashHash is a standard-library trait in std.hash.
  3. types.map-key.userUser-defined struct and enum types can become keys by explicitly implementing or deriving both traits.
  4. types.map-key.builtin-hashStandard-library implementations of Hash cover bool, integers, char, string, and tuples, optionals, results, and lists of hashable elements. So List[T] implements Hash when T < Hash, and Result[T, E] when T < Hash and E < Hash.
  5. types.map-key.unhashableMaps, floating-point values, functions, suspensions, dynamic trait values, and Any do not have built-in Hash.
  6. 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.
  7. types.map-key.float-no-hashFloating-point types implement Eq but not Hash, so they are not valid map keys.
  8. types.map-key.no-map-hashConsequently maps have no built-in hash and impose no order-independent map-hash obligation.
  9. types.map-key.referenceA reference type implements Eq and Hash when the type it refers to does, so *T may be a key, compared and hashed by value. A map keyed by place uses Id[T].
Error example
struct UserId:
    value: string

fn setup() -> void:
    let users: Map[UserId, string] = {}  # error: unsatisfied-trait-bound
    pass

Why. 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

  1. types.map.eq-hashMap lookup and duplicate-key replacement use Eq for key comparison and Hash for indexing.
  2. types.map.no-lawThe language does not check or impose a law connecting these two implementations.
  3. types.map.inconsistentIf an implementation hashes values differently that Eq considers equal, lookup and duplicate-key behavior are not guaranteed.
  4. 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.
  5. types.map.order.unspecifiedBeyond that, the order is unspecified. It need not be insertion order.
  6. types.map.replace.in-placeReplacing the value for an existing key does not change the map's iteration order.
  7. 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 run

Why. 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

  1. types.map.no-reindexIf a stored key's equality or hash changes after insertion, the map does not automatically reindex it.
  2. types.map.staleThe entry can remain visible during iteration yet be unreachable by lookup or removal with the mutated key: a stale entry.
  3. types.map.no-repairSuch mutation does not trigger a compile-time error or an automatic repair.
  4. types.map.reference-keyA key that holds a *mut reference, 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
  1. types.lct.usesEach of these uses the least common type defined here.
  2. types.lct.uniqueThe compiler computes a unique least common type of the values' types using only the conversions that types.lct.conversions lists.
  3. types.lct.conversionsPermission weakening, declared readonly variance, and optional injection may contribute. No other conversion does: no numeric conversion, so [small, large] with an i16 and an i64 has no common type. Error: no-common-type.
  4. types.lct.optionalOptional injection adds one layer, as types.assign.optional states. So an i32 and an i32? join to i32?, in a list literal, a value-producing if or match, and an inferred result type alike.
  5. types.lct.optional.one-layerA T and a T?? have no common type, because injection never adds two layers. Error: no-common-type.
  6. types.lct.no-combineLeast-common-type inference never combines permission weakening with a variance step for the same candidate conversion.
  7. 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).
  8. types.lct.never.droppedThe least common type first drops every value of type never, then joins the rest. So if ok: 1 else: return .None has type i32.
  9. types.lct.never.allWhen every value has type never, the least common type is never.
  10. types.lct.never.inferA type parameter that only values of type never meet is inferred as never, as for id(panic("x")) with fn id[T](x: T) -> T.
Error example
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-type

See 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

  1. types.lct.no-any.reference-valuesThe compiler never falls back to &dyn Any merely to make heterogeneous values type-check.
  2. types.lct.no-trait-valueUnconstrained inference also does not introduce a dynamic trait-value conversion.
  3. 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.
  4. types.lct.expected-trait.reference-valuesAn expected type such as List[*dyn Display] or Map[K, *dyn Display] may request that conversion explicitly.
  5. types.lct.contextualA contextual variant, .None included, takes its type only from an expected type, never from the other values.
  6. types.lct.contextual.error[1, .None] or if c: 1 else: .None without an expected type is an error. Error: missing-contextual-enum-type.
  7. types.lct.contextual.expectedlet values: List[i32?] = [1, .None] supplies the type.
Error example
fn pick(flag: bool) -> void:
    values := [1, .None]             # error: missing-contextual-enum-type
    value := if flag: 1 else: .None  # error: missing-contextual-enum-type

Why. Unconstrained inference introduces no trait-value conversion because a concrete type may satisfy multiple unrelated traits.

#Inference Failure

  1. types.lct.failIf no unique least type exists, inference fails and the user must add an expected type.
  2. types.lct.no-commonWhen the values have no common type, the failure is an error. Error: no-common-type.
  3. 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.
Error example
fn mix(shown: *dyn Shown, tagged: *dyn Tagged) -> void:
    items := [shown, tagged]  # error: no-common-type

Here 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:

  1. types.infer.explicit.parametersNamed function parameters.
  2. 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.
  3. types.infer.explicit.fieldsPublic and private struct fields.
  4. types.infer.explicit.enumEnum payload fields and constructor data.
  5. types.infer.explicit.trait-methodsTrait method parameters and results.
  6. 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
    x

types.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

  1. types.infer.no-overloadInference must not select among overloaded functions.
  2. types.infer.ambiguousIf inference has multiple valid solutions, compilation fails and the diagnostic must identify an annotation site that disambiguates the program.
  3. types.infer.ambiguous.codeThat error is distinct from cannot-infer-type, which a rule names when no solution exists. Error: ambiguous-type.
Error example
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 one

The 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 .None by 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 Inspectable and the traits that extend it, the table also holds one interned descriptor of the recorded type. A downcast is then a descriptor comparison followed by a cast or an unboxing, and a T < Inspectable bound 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.

  1. types.unsupported.type-testThe only runtime type test is exact-type recovery from an Inspectable value.
  2. types.unsupported.no-assertionsThere are no trait-to-trait assertions, no tests on Any or on trait values whose trait does not extend Inspectable, and no type patterns in match.
  3. types.unsupported.no-unionshd-lang has no anonymous union types, including error unions such as FsError | HttpError.
  4. types.unsupported.error-typeAn error type is a nominal type or a dynamic trait value such as std.error.Error.
  5. 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.
Error example
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.

  1. types.opaque.occurrenceEach impl Trait[Args] occurrence in a return annotation denotes an opaque type with the written trait bound.
  2. 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.
  3. types.opaque.distinctResults from different function declarations are distinct types, even when their written bounds and hidden concrete types are equal.
  4. types.opaque.distinct-positionDifferent opaque occurrences in one return annotation are distinct types, even when their bounds and hidden concrete types are equal.
  5. types.opaque.head-onlyCallers check against the declared bound, including its supertraits and associated-type bindings, without reading the defining body.
  6. types.opaque.no-leakageThe hidden concrete type adds no trait implementation or member to the opaque type during caller checking.
  7. types.opaque.representationThe hidden concrete type is a code-generation fact, not part of a caller's checking dependencies.
  8. types.opaque.codegenAn opaque result does not introduce a trait value or dynamic trait dispatch.
  9. 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 playground
Error example
fn 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.