hd-lang

#Expressions

Status: language specification draft.

This chapter defines expressions, which compute values.

  1. expr.static-typeEvery expression has a static type.
  2. expr.evaluatedEvery expression is evaluated according to the order defined here and in the relevant feature chapter.
fn demo(x: i32) -> i32:
    x * 2 + 1

#Evaluation Order

This section defines the default order in which subexpressions are evaluated.

  1. expr.order.left-to-rightUnless a construct states otherwise, subexpressions are evaluated from left to right and exactly once.
  2. expr.order.appliesThis rule applies to tuple and collection elements, struct fields, call arguments, operands, and indexing expressions.
  3. expr.order.conditional&&, ||, if, match, loops, optional or result propagation with ?, and comprehension filters evaluate conditionally as described below.
  4. expr.order.reorderA compiler may reorder only when it can prove that the program's observable behavior is unchanged.
fn demo() -> string:
    let mut log: List[string] = []
    fn mark(s: string) -> string:
        log.push(s)
        s
    _ := "${mark("a")}${mark("b")}"
    log.join(",")   # "a,b": left to right

#Expression Categories

This section defines value expressions and place expressions.

  1. expr.category.valueA value expression produces a value.
  2. expr.category.placeA place expression identifies a storage location and may be read or, when it is writable, assigned.
fn demo() -> void:
    let x = +1   # x is a place expression; x * 2 is a value expression
    _ := x * 2

#Places

The core place expressions are locals, fields, index operations, and calls through a callable value that accepts a store:

  1. expr.place.localA reassignable local introduced by let is a core place expression.
  2. expr.place.indexAn index operation whose receiver and indexing protocol expose mutable storage is a core place expression.
  3. expr.place.receiverThe receiver of a field or index place may be any expression of a writable reference type, *mut T or &mut T, including a call that returns a *mut T.
  4. expr.place.call-updateA call is a place only when its callee's type implements Update, as Callable Values defines.
  5. expr.place.value-forms:= bindings, literals, other calls, arithmetic, and temporary values are not places.
  6. expr.place.tuple-elementA tuple element selection such as pair._0 is not a place: tuples are immutable, and a changed tuple is built as a new tuple value.
  7. expr.place.enum-shared-fieldA shared enum field such as status.phrase is not a place: enum values never change once built.
  8. expr.place.struct-fieldA field of a struct value is a place when the struct is in a writable place, as Writable Places defines.
  9. expr.place.deref*r with a writable reference r, *mut or &mut, is a place, and so is a field read through one, such as r.x, as Writing Through A Reference defines.
fn demo() -> void:
    let x = +1
    x = x + 1   # x is a place: a reassignable local

#Assignment

  1. expr.assign.statementAssignment is a statement and requires a writable place on its left side.
  2. expr.assign.targetAssigning to an expression that is not a place is an error. Error: invalid-assignment-target.
  3. expr.assign.orderAssignment evaluates the place before the right-hand side, then performs one store.
  4. expr.assign.order.localA local place requires no subexpression evaluation.
  5. expr.assign.order.fieldA field assignment evaluates its receiver, then the right-hand expression.
  6. expr.assign.order.indexAn indexed assignment evaluates the receiver, the index, and the right-hand expression in that order.
  7. expr.assign.order.callAn assignment through a call evaluates the callee, then the right-hand expression.
  8. expr.assign.abruptIf any step completes abruptly, no store occurs.
Error example
fn invalid() -> (i32, i32):
    let pair: (i32, i32) = (0, 0)
    pair._0 = 1  # error: invalid-assignment-target
    pair

#Compound Assignment

A compound assignment place op= value combines an operator with a store. For every type, p op= e means p = p op e:

use std.ops.Add

type Meters(i64)

impl Add for Meters:
    type Out = Meters
    fn add(self, rhs: Meters) -> Meters:
        Meters(i64(self) + i64(rhs))

struct Tally:
    count: i64

fn record(tally: &mut Tally, hits: List[i64]) -> Meters:
    let log = ""
    let walked = Meters(0)
    for hit in hits:
        tally.count += hit
        walked += Meters(hit)
        log += "."
    walked
  1. expr.assign.compound.formCompound assignment is a statement, written with one of ten operators: +=, -=, *=, /=, %=, &=, |=, ^=, <<=, and >>=.
  2. expr.assign.compound.no-othersThere is no **=, &&=, or ||=.
  3. expr.assign.compound.place-formThe left side must have the form of a place. That is a name, a field selection, an index expression, or a call whose callee's type implements Update. Any other left side, such as a function call, is an error. Error: invalid-assignment-target.
  4. expr.assign.compound.onceThe receiver and index of the place are evaluated once, then the right-hand expression.
  5. expr.assign.compound.meaningFor every type, p op= e means p = p op e: it reads the place, applies the operator to that value and e, and stores the result in the place.
  6. expr.assign.compound.primitiveWhen the place and the right operand have primitive types, p op= e computes p op e by the built-in rules and stores the result in the place. The store follows the rules of p = p op e, so a name must be a reassignable local.
  7. expr.assign.compound.operatorOtherwise the operator follows Operator Traits, so the place's type needs the operator's trait. Without a fitting implementation, the statement is an error. Error: type-mismatch.
  8. expr.assign.compound.store-rulesThe store follows the rules of assignment. So the place must be a reassignable local, a field of a writable place, or an index place that accepts a store. A call place also qualifies.
  9. expr.assign.compound.index-read-writeOn an index place, the read is r[k] and the store is r[k] = v. List and Map use their built-in indexing, and another type needs both Index and IndexSet, as Index Traits defines.
  10. expr.assign.compound.call-onceOn a call place v() op= e, the callee v is evaluated once, then e.
  11. expr.assign.compound.call-read-writeOn a call place, the read is v() and the store is v() = x. So the callee's type needs both Apply and Update, as Callable Values defines.
  12. expr.assign.compound.fresh-valueOn a struct value, the store replaces the place's whole value with the operator's result. A copy made from the place before the store keeps the old value.
  13. expr.assign.compound.no-assign-traitsstd.ops declares no assign trait, and no operator changes a value in place.
Error example
struct Tally:
    count: i64

struct Score:
    points: i64

fn current() -> i64: 1

fn invalid(tally: &Tally, bonus: Score) -> void:
    let score = Score { points: 0 }
    tally.count += 1  # error: not-writable
    score += bonus    # error: type-mismatch
    current() += 1    # error: invalid-assignment-target
fn tally(counts: &mut Map[string, i32], word: string) -> void:
    counts[word] += 1  # panics with index-out-of-bounds when `word` is absent

Note. A diagnostic that rejects p op= e may show p = p op e as a hint. That hint is not normative.

Why. One meaning for every type means += never changes a value in place: the place gets a new value. Accumulators and builders change themselves through ordinary methods, such as sb.push(x).

See also: Operator Traits, Callable Values, Writable Places.

#Primary Expressions

This section defines names, literals, and parenthesized, tuple, collection, and data expressions, which build struct values.

#Names

This section defines identifier, qualified, and contextual variant names.

  1. expr.name.identifierAn identifier expression evaluates the declaration or local binding selected by lexical name resolution.
  2. expr.name.qualifiedA qualified name selects a declaration or enum variant through a module or type namespace.
  3. expr.name.qualified.privateA module path to a declaration that its module declares without pub is an error, as a use of it would be. Error: private-import.
  4. expr.name.qualified.missingA module path to a declaration that its module does not declare is an error, as a use of it would be. Error: unknown-import.
  5. expr.name.contextual.Variant selects a variant only when the expression has an expected type that fixes one nominal enum.
  6. expr.name.contextual.rules.Variant has the same construction and argument rules as Enum.Variant; a payload-bearing variant still requires a call.
  7. expr.name.contextual.no-searchThe compiler does not search all visible enums for a matching variant name.
  8. expr.name.contextual.no-typeWithout a unique expected enum type, .Variant is a type error. Error: missing-contextual-enum-type.
Error example
enum Status:
    Queued

fn invalid() -> void:
    status := .Queued  # error: missing-contextual-enum-type
    pass
Error example
use pkg.words

fn shout(word: string) -> string:
    words.squash(word)                # error: private-import

fn headline(titles: List[string]) -> string:
    words.join_wordz(titles, " ")     # error: unknown-import

Note. A std module is no exception. A module path to a std declaration without pub is private-import by expr.name.qualified.private, as for a module of the package.

See also: Enum Declarations, Use Forms, Use Roots.

#Literals

  1. expr.literal.definedBoolean, integer, floating-point, string, and character literals are defined lexically in Lexical Structure and typed in Type System.
  2. expr.literal.callA suffixed literal or prefixed string is a call, as Literal Suffixes specifies.
  3. expr.literal.passpass is the no-op expression. It has type void and performs no operation.
use std.time.s

fn demo() -> void:
    _ := 5s   # a suffixed literal is a call of the suffix function s

#String Interpolation

An interpreted string may embed expressions, which are formatted through the Display trait. The canonical signature is:

trait Display:
    fn to_string(self) -> string
  1. expr.interp.formsInterpreted strings support $name, $self, and ${expression} interpolation.

  2. expr.interp.self"value: $self" inside a method interpolates the receiver.

  3. expr.interp.orderEmbedded expressions are evaluated from left to right at the position of their segment.

  4. expr.interp.displayEach embedded expression's type must implement the canonical std.format.Display trait.

  5. expr.interp.appendThe compiler appends the string returned by that implementation.

  6. expr.interp.no-displayAn embedded expression whose type does not implement Display is an error. Error: unsatisfied-trait-bound.

  7. expr.interp.no-fallbackThere is no fallback conversion through Any, runtime reflection, or debug output.

  8. expr.interp.stdThe standard library provides Display implementations for ordinary printable primitive types and string.

  9. expr.interp.std.tuple.templateIt also declares a tuple template for Display, so every tuple whose elements implement Display implements it, at every size. It writes the elements' texts inside parentheses, separated by , , as in (1, a).

  10. expr.interp.std.tuple.rest-inlineFor a tuple with a rest element List[T]..., T must implement Display. The rest's items are written inline after the fixed elements, as in (1, 2, 3, 4).

  11. expr.interp.std.tuple.oneA tuple of one element writes a comma after it, as its source syntax does: (1,). A rest tuple counts each of its rest's items as an element.

  12. expr.interp.userOptional and user-defined values are displayable only when the corresponding type implements Display.

  13. expr.interp.prefixedA prefixed string does not append its values: they become the values of a template, as Prefixed Strings specifies.

  14. expr.interp.constantA string with no interpolation segments is an ordinary constant value and performs no Display calls.

  15. expr.interp.referenceA reference interpolates as the value it refers to, since std implements Display for &T when T has it (References).

  16. expr.interp.collection-debug-fixFor a simple name in braced interpolation, a missing Display diagnostic may offer exact debug(name) wrapping for List and Map.

  17. expr.interp.collection-debug-fix.proofThis fix requires proven Debug support and an unshadowed prelude debug.

Note. The empty tuple writes (). A void value is that tuple, so "${log()}" writes () too.

Error example
struct Secret:
    value: i32

fn render(value: Secret) -> string:
    "secret=$value"  # error: unsatisfied-trait-bound

#Literal Suffixes

A suffixed literal or a prefixed string is a call of a literal function. A literal function is marked @num_suffix for a suffix, or @str_prefix for a prefix. This section gives the rules both forms share, then the suffix rules; Prefixed Strings adds the template.

use std.ops.num_suffix

struct Pixels:
    count: i32

@num_suffix
fn px(count: i32) -> Pixels:
    Pixels { count: count }

fn indent() -> Pixels:
    12px  # px(12)

std.ops declares the marker as a typed fact type:

use std.annotation.annotate
use std.num.Num

@annotate::[F](.Fn)
pub struct NumSuffix[F]: pass

pub fn num_suffix[N < Num, R, $Q]() -> NumSuffix[fn(N) -> R $ Q]:
    NumSuffix::[fn(N) -> R $ Q] {}

Rules for every literal function:

  1. expr.literal-fn.markerA suffix function carries a std.ops.NumSuffix value, written @num_suffix, and a prefix function carries a std.ops.StrPrefix value, written @str_prefix. The compiler recognizes both types, and std.ops.Template, by their qualified names.
  2. expr.literal-fn.fn-onlystd.ops declares NumSuffix, num_suffix, StrPrefix, str_prefix, and Template. Both marker types list only .Fn in their @annotate, so either marker before anything but a function is an error. Error: decorator-target-kind.
  3. expr.literal-fn.not-markedA suffix or prefix that resolves to anything but a literal function of its form is an error at the literal. Error: invalid-literal-suffix for a suffix, invalid-string-prefix for a prefix.
  4. expr.literal-fn.no-marker-importThe call needs no import of the marker, its type, or Template: a use of the literal function alone makes the literal valid.
  5. expr.literal-fn.ordinary-callA literal is exactly the call of its literal function wherever it appears, and it is checked and evaluated as an ordinary call there. So argument errors, inferred type arguments, and the rules of a fact or shared enum data position all apply as for any call.
  6. expr.literal-fn.rowA literal function may have a requirement row, and a literal of it needs that row where it appears, as any call does, by req.row.set.call. Error: missing-requirement.

Each marker is a typed fact, checked by annot.typed-fact.check:

Function @num_suffix checks like
fn ms(n: i64) -> Millis let f: NumSuffix[fn(i64) -> Millis] = num_suffix()
fn px(count: i32) -> Pixels $ Console let f: NumSuffix[fn(i32) -> Pixels $ Console] = num_suffix()
fn k[N < Num](n: N) -> N let f: NumSuffix[fn(Num) -> Num] = num_suffix(), with one fixed Num type

num_suffix's signature then holds every shape constraint. A suffix function takes one parameter of a Num type and never suspends. One that does not fit is an error at its decorator. A generic one has its type made monomorphic first, by annot.typed-fact.monomorphic.

Its requirement row may be any row. Q is a row parameter, since it is declared $Q, and so a generic parameter (req.row.parameter). The check infers it from the expected type, as it infers N and R, by annot.typed-fact.check.inferred. So for fn px(count: i32) -> Pixels $ Console, Q is $ Console.

Rules for suffixes:

  1. expr.suffix.user-callA user-suffixed literal Nx calls its suffix function x(N) with the numeric literal as its only argument.
Error example
use std.ops.num_suffix

pub struct Pixels:
    count: i32

fn pt(count: i32) -> Pixels:
    Pixels { count: count }

@num_suffix  # error: unsatisfied-trait-bound
fn em(label: string) -> Pixels:
    Pixels { count: 0 }

@num_suffix  # error: type-mismatch
fn later!(count: i64) -> i64:
    count

@num_suffix  # error: type-mismatch
fn kb(count: i64, unit: i64 = 1024) -> i64:
    count * unit

@num_suffix
fn px(count: i32) -> Pixels $ Console:
    Pixels { count: count }

@num_suffix  # error: unsatisfied-trait-bound
fn same[N](n: N) -> N:
    n

pub fn layout() -> Pixels:
    size := 12pt  # error: invalid-literal-suffix
    12px  # error: missing-requirement

Note. The one parameter may have a default, since a default is not part of a function type. A literal always passes its own value, so in @num_suffix fn unit(count: i64 = 1), 5unit passes 5, and the ordinary call unit() uses the default. A second parameter does not fit, even with a default.

Note. A suffix function may be generic, as in @num_suffix fn k[N < Num](n: N) -> N. A literal infers its type argument as any generic call does: 5k takes N from the literal, or from the expected type, as in let limit: i64 = 5k. A unary minus is not part of the literal, so -5s is -(5s).

A suffixed literal in shared enum data is a call there, as the position rules say:

use std.ops.num_suffix

struct Millis:
    count: i64

@num_suffix
fn ticks(count: i64) -> Millis:
    Millis { count: count }

enum Tier(limit: Millis):
    Fast -> Tier(limit=250ticks)
    Slow -> Tier(limit=5_000ticks)

Why. A suffix is an ordinary function found through use, so libraries can add 12px without new syntax. The literal's type is the function's result, so 12px is a Pixels. A suspending function does not fit num_suffix's result NumSuffix[fn(N) -> R $ Q]. hd marks every suspending call with !, and 5s has no place to show it. A requirement row needs no mark at the call, so a literal function may have one. The marker is checked once, at the decorator, not at each literal.

Note. A compiler may warn when a suffixed literal always overflows, as Rust's unconditional_panic lint does, but none is required.

Note. The standard library's duration suffixes ms, s, min, and h of std.time, and its Duration type, are stdlib tier: Time.

See also: Suffixed Literals, Literal Suffix Names, Target Kinds.

#Prefixed Strings

A prefixed string is a call of its prefix function, a function marked @str_prefix, with a template of its text and values:

use std.ops.{Template, str_prefix}

struct Query:
    text: List[string]
    params: List[i64]

@str_prefix
fn sql(t: Template[i64]) -> Query:
    Query { text: t.raw_parts, params: t.values }

fn by_id(id: i64) -> Query:
    sql"select * from users where id = $id"

std.ops declares the marker and the template:

use std.annotation.annotate

@annotate::[F](.Fn)
pub struct StrPrefix[F]: pass

pub fn str_prefix[T, R, $Q]() -> StrPrefix[fn(Template[T]) -> R $ Q]:
    StrPrefix::[fn(Template[T]) -> R $ Q] {}

pub struct Template[T]:
    pub raw_parts: List[string]
    pub values: List[T]

The rules for every literal function apply to a prefix function. @str_prefix before fn sql(t: Template[i64]) -> Query checks like let f: StrPrefix[fn(Template[i64]) -> Query] = str_prefix(), and its row parameter Q takes the empty row. The rules for prefixes:

  1. expr.prefix.fn-callA prefixed string x"..." is the call x(t) of the prefix function x, with a std.ops.Template value t as its one argument. The compiler never joins the pieces and never calls Display.
  2. expr.prefix.templatet.values holds the n interpolated values in source order, and t.raw_parts holds the n + 1 pieces of text around them.
  3. expr.prefix.partsThe first piece is the text before the first interpolation, and the last piece is the text after the last one. A piece is "" where two interpolations touch, or where one begins or ends the string. So x"a $b c" passes the pieces ["a ", " c"] and the values [b], and x"text" passes ["text"] and [].
  4. expr.prefix.raw-partsEach piece is the text exactly as written, with every backslash kept, as lex.prefix.raw-text says.
  5. expr.prefix.orderThe interpolated expressions are evaluated from left to right before the call, as arguments are.
Error example
use std.ops.{Template, str_prefix}

fn plain(t: Template[string]) -> string:
    "plain"

@str_prefix  # error: type-mismatch
fn count(n: i32) -> i32:
    n

@str_prefix  # error: type-mismatch
fn later!(t: Template[string]) -> string:
    "later"

@str_prefix  # error: type-mismatch
fn tagged(t: Template[string], tag: string = "x") -> string:
    tag

@str_prefix
fn logged(t: Template[string]) -> string $ Console:
    "logged"

pub fn render() -> string:
    first := plain"x"  # error: invalid-string-prefix
    logged"x"  # error: missing-requirement

Why. A prefix is an ordinary function found through use, so a library adds sql"..." without new syntax. The template keeps text and values apart, so sql can send values as parameters instead of splicing them into the query text. As for a suffix, the string is plain call sugar and never suspends, because sql"..." has no place for !.

Note. The standard library's one prefix, the raw-text r of std.text, is stdlib tier: Raw Text Prefix.

See also: Prefixed Strings, String Prefix Names, Prefixed Strings.

#Parenthesized And Tuple Expressions

Parentheses group an expression, and a comma constructs a tuple:

value := (a + b) * c
empty := ()
single := (1,)
point := (10, 20)
  1. expr.paren.groupParentheses group one expression without changing its value.
  2. expr.tuple.commaA comma constructs a tuple.
  3. expr.tuple.singleA one-element tuple requires a trailing comma.
  4. expr.tuple.orderTuple elements evaluate left to right.
  5. expr.tuple.select-underscoreTuple selection uses the member _ followed by the zero-based element index, such as point._0 and point._1. An index past the last element is an error. Error: unknown-field.
  6. expr.tuple.select-underscore.identifierThat member name is an ordinary identifier, so point._0._1 selects through a nested tuple like any other member chain.

Why. An identifier removes the floating-point ambiguity of t.0.1, and works wherever a member does, as in "${point._0}".

See also: Bang And Dot Tokens, which rejects the former spelling point.0.

#Tuple Rest Elements

A tuple expression fills a rest element as a call fills a vararg: it collects separate elements, or it spreads a list:

fn total(values: (usize, usize, List[i32]...)) -> usize:
    values._0 + values._1 + values._2.len()

fn run(xs: List[i32]) -> usize:
    let t: (usize, usize, List[i32]...) = (1, 2, 3, 4)
    total(t) + total((1, 2)) + total((1, 2, xs...))
  1. expr.tuple.rest.collectAgainst an expected tuple type with a rest element List[T]..., a tuple expression's elements fill the fixed elements one each, in order. The elements after them are collected into the rest element, each checked against T. So () against (List[i32]...,) is a tuple whose rest is empty. Too few elements is an error. Error: type-mismatch.
  2. expr.tuple.rest.spreadA spread xs... that ends a tuple expression supplies the rest element. The elements before it are the fixed elements, and xs is the rest element's list. Against an expected type, both must match it. Error: type-mismatch.
  3. expr.tuple.rest.spread.listThe spread operand must be a List[T], which gives the rest element List[T].... A tuple or any other operand is an error. Error: type-mismatch.
  4. expr.tuple.rest.valueThe resulting tuple's rest element is a List[T] that holds the collected elements in order, or the spread operand's list.
Error example
fn short() -> (i32, i32, List[i32]...):
    (1,)  # error: type-mismatch

fn joined(pair: (i32, i32)) -> (i32, i32, i32):
    (1, pair...)  # error: type-mismatch

Why. One collection rule serves calls and tuple expressions, so a value of a function's inputs tuple is written as its call's arguments are. A tuple is never spread into another: there is no tuple concatenation.

#List And Map Expressions

A list literal produces a list, and a map literal produces a map:

names := ["Ada", "Grace"]
scores := {"Ada": 10, "Grace": 12}

#List Literals

  1. expr.list.orderA list literal evaluates its elements left to right.
  2. expr.list.typeA list literal produces List[T], where every element is assignable to T.
  3. expr.list.spreadA list element ending in ... is a spread.
  4. expr.list.spread.onceA spread's operand is evaluated once, in element order.
  5. expr.list.spread.typeA spread's operand must have a list type List[U]. Error: type-mismatch.
  6. expr.list.spread.insertThe spread's elements are inserted at its position, in order.
  7. expr.list.spread.positionA literal may contain several spreads in any position, so [0, xs...] and [xs..., ys...] are valid.
  8. expr.list.spread.expectedWith an expected List[T], U must be assignable to T.
  9. expr.list.spread.inferredWithout an expected List[T], a spread contributes U to the least common type of the elements.
Error example
fn numbers(count: i32) -> List[i32]:
    [0, count...]  # error: type-mismatch

#Map Literals

  1. expr.map.orderA map literal evaluates each key and then its value, processing entries from left to right.
  2. expr.map.duplicate.lastIf two evaluated entries have equal keys, the later value replaces the earlier value.
  3. expr.map.iteration.orderMap iteration follows the map's deterministic order and is not part of map equality.
  4. expr.map.keysKey validity and equality/hash requirements are defined in Type System.
  5. expr.collection.empty.map-constructor-fixAn unambiguous, empty type-qualified Map.new() call gets an exact {} fix when an expected map type determines both argument types.
fn demo() -> Map[string, i32]:
    {"a": +1, "b": +2}

#Element Types

  1. expr.collection.emptyEmpty [] and {} literals require an expected collection type.
  2. expr.collection.empty.errorWithout an expected collection type, an empty literal is an error. Error: cannot-infer-type.
  3. expr.collection.expectedWhen an expected List[T] or Map[K, V] type is available, each literal element is checked directly against the corresponding expected type.
  4. expr.collection.inferredWithout an expected type, a list's element type is the least common type of its elements. A map's key type and value type are the least common types of its keys and of its values.
  5. expr.collection.inferred.rowsFunction values with different rows take the union of their rows first, as Row Union In Literals states.
Error example
fn main() -> void:
    values := []  # error: cannot-infer-type
    others := {}  # error: cannot-infer-type
Open in playground

Why. An empty literal contains no values from which to infer type arguments.

#Data Expressions

A data expression builds a struct value. It names its struct type and provides every required field:

user := User {
    id: "user_123",
    email: "ada@example.com",
}
  1. expr.data.requiredA data expression names its type and provides every required field.
  2. expr.data.onceEvery field may appear at most once.
  3. expr.data.evalField initializers evaluate in source order, not declaration order.
  4. expr.data.shorthandA field written as its bare name, as in Point { x, y }, is field shorthand for name: name. Its initializer is a use of that value name, evaluated in its source position.

See also: Data Literals for field order and unknown fields, and Data Embedding.

#Copy-Update Expressions

Copy-update syntax begins with one spread followed by explicit replacements:

renamed := User {
    ...user,
    display_name: "Ada",
}
  1. expr.update.formCopy-update syntax begins with one spread followed by explicit replacements.
  2. expr.update.one-spreadA data expression permits at most one spread, and it must precede every explicit field.
  3. expr.update.spread-firstThe spread expression is evaluated first.
  4. expr.update.copy-timeEach field not explicitly replaced is read from the spread value when the spread is evaluated, before any explicit field expression runs.
  5. expr.update.exact-typeThe spread expression must have the exact struct type being constructed.
  6. expr.update.copy-fieldsCopy-update is shallow: each field not explicitly replaced is a copy of the spread value's field. A reference field's copy refers to the same object as the source's field.

See also: Copy-Update Literals.

#Postfix Expressions

  1. expr.postfix.bindingPostfix operations bind more tightly than every infix operator.
fn demo(items: List[i32]) -> usize:
    items.len() + 1   # .len() binds tighter than +

#Member Access

value.member selects a member, and tuple._0 selects a tuple element.

  1. expr.member.select-identifiervalue.member selects a member named by an identifier, and tuple._0 selects a tuple element.
  2. expr.member.field-readWithout an argument clause, value.name reads a field. A field the type does not declare is an error. Error: unknown-field.
  3. expr.member.lookupMember Resolution defines field and method lookup, including promoted members and the declaration-time check ambiguous-promoted-member.
  4. expr.member.enum-variantEnum.Variant is not member access: it names an enum variant.
  5. expr.member.variant-fn-valueA variant constructor with exactly one payload field is a function value.
struct Point:
    x: i32
    y: i32

fn demo(p: Point) -> i32:
    p.x + p.y

See also: Enum Declarations.

#Method Calls

  1. expr.member.method-callWhen a member suffix is immediately followed by an argument clause, value.name(arguments...) is a method call. Member lookup selects a method, which is called with value as its receiver.
  2. expr.member.no-field-callA method call never reads a field.
  3. expr.member.stored-fnA function stored in a field is called by parenthesizing the field read, as in (handler.callback)(event).
  4. expr.member.stored-fn.errorhandler.callback(event) looks for a method named callback, and is an error when there is none. Error: unknown-method.
  5. expr.member.stored-fn.hintWhen a field named callback exists, the unknown-method message should suggest (handler.callback)(event).
  6. expr.member.trait-availableA method call never finds a method of a trait that is not available at the call, even when the receiver's type implements it.
  7. expr.member.trait-hintWhen such a method is the only one with the name, the unknown-method message should suggest a use declaration for its trait.
  8. expr.member.method-not-valueA method is not a value: value.method without an argument clause is a field read.
  9. expr.member.method-referencesA method is used as a value through a :: method reference, Type::name or value::name.
  10. expr.member.closure-adaptExplicit closures can adapt method calls where a function value is needed.
Error example
struct Button:
    on_click: fn(i32) -> i32

fn invalid(button: Button) -> i32:
    button.on_click(41)  # error: unknown-method

See also: Method References.

#Indexing

  1. expr.index.orderreceiver[index] evaluates the receiver, then the index, and invokes the receiver type's indexing behavior.
  2. expr.index.expectedFor a List or string receiver, the index is checked with usize as its expected type, so an integer literal index is a usize.
  3. expr.index.expected.rangeFor such a receiver, an index that is a range expression gives each of its bounds usize as the expected type. So items[1..3] slices with a Range[usize].
  4. expr.index.negative-literalA negated literal index or slice bound, as in items[-1] or items[-1..], negates an unsigned value. It is an error. Error: type-mismatch.
fn demo(items: List[i32]) -> i32:
    items[0] + items[1]

#List Indexing

  1. expr.index.list.unsignedFor List[T], an index must have an unsigned integer type, such as usize. A signed index is an error. Error: type-mismatch.
  2. expr.index.list.rangeA list index must be non-negative and less than the list length.
  3. expr.index.list.panicA failed check causes the standard checked runtime panic.
  4. expr.index.list.readReading a list element yields its declared element type T, whether the list is reached directly or through a reference.
  5. expr.index.list.assignAssigning items[index] = value requires a list in a writable place or reached through a &mut reference, and an in-range index.
Error example
fn previous(items: List[i32], i: usize) -> i32:
    items[i - 1]  # in a debug or test build, panics with integer-overflow when i is 0

fn invalid(items: List[i32], i: i32) -> void:
    a := items[-1]  # error: type-mismatch
    b := items[i]   # error: type-mismatch

Why. An index is unsigned, so no index counts from the end. With negative indices counting from the end, as in Python, items[i - 1] with i == 0 would read the last element. Here the subtraction panics instead.

#Map Indexing

  1. expr.index.map.typeFor Map[K, V], the index must have type K.
  2. expr.index.map.read-valueReading entries[key] returns V. When the map holds no equal key, the read is a checked runtime panic. Panic: index-out-of-bounds.
  3. expr.index.map.value-typeThe read's type is the declared V, whether the map is reached directly or through a reference.
  4. expr.index.map.getThe optional read is the built-in method get: entries.get(key) returns V?, and .None means no equal key exists.
  5. expr.index.map.assignAssigning entries[key] = value requires a map in a writable place or reached through a &mut reference, and inserts or replaces the entry.
  6. expr.index.map.libraryRemoval and entry APIs are standard-library methods rather than special syntax.
fn score(scores: Map[string, i32], name: string) -> i32:
    match scores.get(name):
        .Some(value) => value
        .None => 0

fn strict(scores: Map[string, i32], name: string) -> i32:
    scores[name]  # panics with index-out-of-bounds when `name` is absent

Why. A map read means one thing everywhere: m[k], m[k] += v, and Index::index all read V, as a list index reads T. Rust and Python also panic or raise on a missing key and offer get for the optional read.

See also: Built-In Methods.

#String Indexing

A string index reads one byte:

Error example
fn first(text: string) -> u8:
    text[0]

fn invalid(text: string, i: i32) -> void:
    text[0] = 65  # error: invalid-assignment-target
    b := text[i]  # error: type-mismatch
  1. expr.index.string.byteFor string, text[index] reads the byte at that byte offset, as a u8, in constant time.
  2. expr.index.string.offsetThe index counts bytes, not scalar values, so "รฉ"[0] is 0xC3, the first byte of its encoding.
  3. expr.index.string.unsignedThe index must have an unsigned integer type, such as usize. A signed index is an error. Error: type-mismatch.
  4. expr.index.string.rangeThe index must be non-negative and less than the string's length in bytes.
  5. expr.index.string.panicA failed check causes the standard checked runtime panic, as a failed list index does.
  6. expr.index.string.no-assignA string is immutable, so text[index] is not a place. Assigning to it is an error. Error: invalid-assignment-target.

See also: Strings.

#Slicing

An index of a range type selects a slice of a string or a list:

fn parts(text: string, items: List[i32]) -> (string, string, List[i32], List[i32]):
    (text[0..3], text[2..], items[..2], items[1..=2])

fn whole(text: string, items: List[i32]) -> (string, List[i32], string):
    (text[..], items[..], text[..=1])
Rule Index Selects the offsets
expr.index.slice.half-openHalf-open a..b from a up to, not including, b
expr.index.slice.fromFrom a.. from a up to the length
expr.index.slice.toTo ..b from 0 up to, not including, b
expr.index.slice.inclusiveInclusive a..=b from a through b, the same as a..b + 1 computed without overflow
expr.index.slice.to-inclusiveTo inclusive ..=b from 0 through b, the same as ..b + 1 computed without overflow
expr.index.slice.fullFull .. from 0 up to the length, so text[..] is the whole string and items[..] a copy of the whole list
  1. expr.index.slice.callFor a List or string receiver, an index of a range type is the call Index::[K]::index(r, k) of an implementation that Built-In Implementations lists.
  2. expr.index.slice.stringA string slice is the string of the bytes at the selected byte offsets.
  3. expr.index.slice.string.sharedA string slice shares the original string's bytes rather than copying them, as module.string.slice.shared states for slice.
  4. expr.index.slice.string.rangeA string slice whose start or end offset is greater than the length is a checked runtime panic. Panic: index-out-of-bounds.
  5. expr.index.slice.string.reversedA string slice whose start is greater than its end is a checked runtime panic. Panic: index-out-of-bounds.
  6. expr.index.slice.string.boundaryA string slice whose start or end offset is not a scalar boundary is a checked runtime panic. Panic: index-out-of-bounds.
  7. expr.index.slice.listA list slice is a new list that holds the selected elements in order. It is not a view: later changes to either list do not change the other.
  8. expr.index.slice.list.type-valueA list slice of a List[T] has type List[T], the Out of its implementation. It is a new list value, so a let mut binding of it may change it. Its elements keep the type T.
  9. expr.index.slice.list.rangeA list slice whose start or end offset is greater than the length is a checked runtime panic. Panic: index-out-of-bounds.
  10. expr.index.slice.list.reversedA list slice whose start is greater than its end is a checked runtime panic. Panic: index-out-of-bounds.
  11. expr.index.slice.no-storeNo range type has an IndexSet implementation for List or string, so assigning to a slice is an error. Error: invalid-assignment-target.
  12. expr.index.slice.no-mapMap has no slicing: an index on a map is a key of type K, whatever its type.
  13. expr.index.slice.unsignedA slice's bounds must have an unsigned integer type. A range of a signed type is an error, as no implementation takes it. Error: type-mismatch.
Error example
fn grow(items: List[i32]) -> List[i32]:
    let mut part = items[0..2]  # valid: the slice is a new List[i32]
    part.push(9)
    part

fn invalid(items: &mut List[i32]) -> void:
    items[0..2] = [7, 8]  # error: invalid-assignment-target
fn panics(text: string, items: List[i32]) -> void:
    a := "hรฉllo"[0..2]  # panics with index-out-of-bounds: offset 2 is inside รฉ
    b := text[3..1]     # panics with index-out-of-bounds: start after end
    c := items[0..9]    # panics with index-out-of-bounds when items has fewer than 9
Error example
fn invalid(items: List[i32], start: i32) -> void:
    a := items[-1..]      # error: type-mismatch
    b := items[start..]   # error: type-mismatch

Why. A slice goes through std.ops.Index, as Rust's does, so generic code bounded by Index[Range[usize]] accepts strings and lists. A list slice is a copy, so later changes to the list never reach it. A string slice shares its bytes because strings are immutable.

Note. A read-only window on a list with no copy is the view method of Collections.

See also: String Methods, Range Expressions.

#Index Traits

Other types get [] by implementing the std.ops traits Index and IndexSet:

use std.ops.{Index, IndexSet}

struct Ring:
    items: *mut List[i32]

impl Index[usize] for Ring:
    type Out = i32
    fn index(self, key: usize) -> i32:
        self.items[key % self.items.len()]

impl IndexSet[usize, i32] for Ring:
    fn index_set(&mut self, key: usize, value: i32) -> void:
        self.items[key % self.items.len()] = value

fn rotate(ring: &mut Ring) -> i32:
    ring[5] = (*ring)[0]
    (*ring)[5]

std.ops declares them in this shape:

pub trait Index[K]:
    type Out
    fn index(self, key: K) -> Self::Out

pub trait IndexSet[K, V]:
    fn index_set(&mut self, key: K, value: V) -> void
  1. expr.index.trait.std-refstd.ops declares Index[K], with an associated type Out, and IndexSet[K, V], whose index_set takes &mut self, as shown above.
  2. expr.index.trait.read-otherFor a receiver whose type is not List, Map, or string, a type parameter included, reading r[k] is the call Index::[K]::index(r, k). Its type is that implementation's Out.
  3. expr.index.trait.write-refAssigning r[k] = v to such a receiver is the call IndexSet::[K, V]::index_set(&mut r, k, v). The call takes the address of the place r, as a &mut self call does by fn.method.auto-address. When r is a &mut reference, the call passes it, so the store changes the value it refers to.
  4. expr.index.trait.choiceThe candidates are chosen as for a binary operator, by the receiver's type, then by the key and, for a store, the value.
  5. expr.index.trait.no-useNeither call needs a use of the trait.
  6. expr.index.trait.mut-refSince index_set takes &mut self, the receiver must be a writable place or a &mut reference. A store into any other receiver, such as a := binding, a value parameter, or a read-only reference, is an error. Error: not-writable.
  7. expr.index.trait.placer[k] is a place only when the receiver's type implements IndexSet. Assigning to it otherwise is an error. Error: invalid-assignment-target.
  8. expr.index.trait.no-readReading r[k] when the receiver's type has no fitting Index implementation is an error. Error: type-mismatch.
  9. expr.index.trait.builtin-directA receiver whose type is List, Map, or string keeps the built-in indexing above, even though these types implement the traits, as Built-In Implementations states.
  10. expr.index.trait.independentThe two traits are independent: a type may implement either one alone.
Error example
use std.ops.Index

struct Row:
    cells: List[i32]

impl Index[usize] for Row:
    type Out = i32
    fn index(self, key: usize) -> i32:
        self.cells[key]

struct Plain:
    value: i32

fn invalid(row: &mut Row, plain: Plain) -> i32:
    row[0] = 1  # error: invalid-assignment-target
    plain[0]    # error: type-mismatch
#Built-In Implementations

The standard library implements the index traits for List, Map, and string, so generic code bounded by them accepts these types:

use std.ops.{Index, IndexSet}

fn first[C < Index[usize]](items: C) -> C::Out:
    items[0]

fn reset[C < IndexSet[usize, i32]](items: &mut C) -> void:
    items[0] = 0

fn lead(counts: &mut List[i32], text: string) -> u8:
    reset(counts)
    first(text)
Rule Type Implementations
expr.index.std.list.usizeList List[T] Index[usize] with Out = T, and IndexSet[usize, T]
expr.index.std.mapMap Map[K, V] Index[K] with Out = V, and IndexSet[K, V]
expr.index.std.string.usizeString string Index[usize] with Out = u8, and no IndexSet
expr.index.std.string.range.unsignedString slice string Index[R[I]] with Out = string, for each range type R and each unsigned integer type I
expr.index.std.list.range.unsigned-valueList slice List[T] Index[R[I]] with Out = List[T], for each range type R and each unsigned integer type I
expr.index.std.string.fullWhole string string Index[RangeFull] with Out = string
expr.index.std.list.full-valueWhole list List[T] Index[RangeFull] with Out = List[T]
  1. expr.index.std.intrinsicThe body of each implementation in the table is a compiler intrinsic. It behaves as the built-in indexing of its type, including the checks and their panics, so Map's index panics when no equal key exists.
  2. expr.index.std.map-storeMap's index_set inserts or replaces the entry, as entries[key] = value does.
  3. expr.index.std.string-no-index-setstring implements no IndexSet, so a bound such as IndexSet[usize, u8] rejects it. Error: unsatisfied-trait-bound.
Error example
use std.ops.IndexSet

fn store[C < IndexSet[usize, u8]](items: &mut C, byte: u8) -> void:
    items[0] = byte

fn invalid(text: string) -> void:
    let mut copy = text
    store(&mut copy, 65)  # error: unsatisfied-trait-bound

Note. Through a bound, items[k] has type Out, the declared element or value type, as built-in indexing gives.

See also: Operator Traits, Compound Assignment.

#Calls

A call applies a callable to positional and named arguments:

resize(640, height=480)
  1. expr.call.orderA call evaluates the callable first, then arguments from left to right.
  2. expr.call.callableThe callable may be any expression of function type, including a parenthesized field read such as (handler.callback)(event).
  3. expr.call.positional-firstPositional arguments must precede named arguments.

#Named Arguments

  1. expr.call.namedEach named argument identifies a parameter by its declared name.
  2. expr.call.exactly-onceA parameter must receive exactly one argument after defaults are applied.
  3. expr.call.duplicateSupplying one parameter more than once, such as positionally and again by name, is an error. Error: duplicate-argument.
  4. expr.call.source-orderEvaluation order follows source argument order, not parameter declaration order.
Error example
fn resize(width: i32, height: i32) -> i32: width * height

fn area() -> i32:
    unknown := resize(640, depth=480)  # error: unknown-named-argument
    twice := resize(640, width=480)    # error: duplicate-argument
    unknown + twice

See also: fn.arg.unknown-name, which makes an unknown named argument an error.

#Positional Spreads

A positional spread x... passes the value x in place of separate arguments. A tuple fills the callee's remaining inputs, and a vararg takes a value of its own type:

fn add(a: i32, b: i32) -> i32: a + b
fn count(values...: List[i32]) -> usize: values.len()
fn g(a: usize, b: usize, xs...: List[i32]) -> usize: a + b + xs.len()

fn spreads(pair: (i32, i32), items: List[i32], t: (usize, usize, List[i32]...)) -> (i32, usize):
    (add(pair...), count(items...) + count(1, 2) + g(t...))
  1. expr.call.spreadAn argument ending in ... is a positional spread.
  2. expr.call.spread.positionA call has at most one positional spread; except for expr.call.spread.vararg-duplicate, it must be the final positional argument and therefore precedes every named argument. Error: nonfinal-positional-spread.
  3. expr.call.spread.fillsA spread operand of type X is evaluated once and fills what X describes, as the table below states.
Rule Where the spread stands X must be It fills
expr.call.spread.at-varargAt a vararg the next positional parameter is a vararg assignable to the vararg's type the vararg, as its collected value
expr.call.spread.tuple-vararg-tailAfter a tuple vararg's arguments it follows one or more separate arguments of a tuple-typed or Tuple-bounded vararg what a tuple expression's final spread needs the rest element, as fn.vararg.collect.tuple-expr states
expr.call.spread.inputsBefore fixed parameters any other position the same type as the tuple of the callee's remaining inputs, which keeps a rest element each remaining parameter with one element, in order, and a List[T] vararg with the rest element's list
  1. expr.call.spread.vararg-duplicateA call that supplies a vararg through a positional spread and through any separate positional value supplies it twice. Error: duplicate-argument.
  2. expr.call.spread.mismatchAn operand that does not meet the table's requirement is an error. Error: type-mismatch.
  3. expr.call.spread.list-needs-varargA List[T] operand where the next positional parameter is not a vararg is an error. Error: positional-spread-needs-vararg.
  4. expr.call.vararg-by-name.exclusiveA call that passes a vararg by name must not also supply positional values for that vararg.
  5. expr.call.spread.inputs.onlyA value of type Fn[Args, O, $ R] takes its inputs only as one spread. A call with separate arguments, a trailing block, or more positional values is an error. Error: type-mismatch.
Error example
fn add(a: i32, b: i32) -> i32: a + b
fn fixed(value: i32) -> i32: value
fn collect(values...: List[i32]) -> List[i32]: values

fn triple(values: (i32, i32, i32)) -> i32: add(values...)  # error: type-mismatch
fn total(values: List[i32]) -> i32: fixed(values...)       # error: positional-spread-needs-vararg
fn duplicate(nums: List[i32]) -> List[i32]: collect(nums..., 4)  # error: duplicate-argument

A function with a List[T] vararg takes a tuple whose rest element stands for it. A function with a plain List[T] parameter takes a tuple without one:

Error example
fn g(a: usize, b: usize, xs...: List[i32]) -> usize: a + b + xs.len()
fn h(a: usize, b: usize, xs: List[i32]) -> usize: a + b + xs.len()

fn valid(t: (usize, usize, List[i32]...), u: (usize, usize, List[i32])) -> usize:
    g(t...) + h(u...)

fn crossed(t: (usize, usize, List[i32]...), u: (usize, usize, List[i32])) -> usize:
    g(u...)  # error: type-mismatch
    h(t...)  # error: type-mismatch

Note. For a function value of type Fn[Args, O, $ R], the remaining parameters are Args, so f(args...) with args: Args calls it. A suspending callee is written f!(args...).

See also: Varargs.

#Explicit Generic Arguments

Explicit generic arguments occur before the call argument list:

first::[string](names)
  1. expr.call.generic.positionGeneric arguments, when explicit, occur before the call argument list.
  2. expr.call.generic.trailingAn explicit list may omit trailing arguments, which are inferred or defaulted as Explicit Type Arguments states.
  3. expr.call.generic.targetsIn hd-lang, explicit arguments may specialize a named module function or qualified function introduced by a use declaration.

See also: Functions, and Generic Methods And Qualified Calls for generic methods.

#Callable Values

A callable value is a value whose type implements the std.ops trait Apply: v() reads it. When the type also implements Update, v() is a call place, and v() = x stores through it. This example declares a live cell:

use std.ops.{Apply, Update}

struct Cell[T]:
    value: T

impl[T] Apply for Cell[T]:
    type Out = T
    fn apply(self) -> T: self.value

impl[T] Update[T] for Cell[T]:
    fn update(&mut self, value: T) -> void:
        self.value = value

fn live[T](value: T) -> Cell[T]:
    Cell { value: value }

struct Panel:
    count: Cell[i32]

fn parent(panel: &mut Panel) -> string:
    let mut count = live(0)
    count() = 1
    count() += 1
    (panel.count)() += 1
    badge(count)

fn badge(count: Cell[i32]) -> string:
    "clicked ${count()}"

std.ops declares them in this shape:

pub trait Apply:
    type Out
    fn apply(self) -> Self::Out

pub trait Update[V]:
    fn update(&mut self, value: V) -> void
  1. expr.call.apply.std-refstd.ops declares Apply, with an associated type Out, and Update[V], whose update takes &mut self, as shown above.
  2. expr.call.apply.zero-keysNeither trait takes a key: a callable value is called with no arguments.
  3. expr.call.apply.readWhen the callee's type is not a function type and implements Apply, a type parameter included, the call v() is the call Apply::apply(v).
  4. expr.call.apply.read-typeThe type of v() is that implementation's Out.
  5. expr.call.apply.argumentsA call that passes a callable value any argument, positional or named, is an error. Error: argument-count.
  6. expr.call.apply.write-refAssigning v() = x is the call Update::[V]::update(&mut v, x), which takes the address of the place v, or passes v when it is a &mut reference, as an index store does by expr.index.trait.write-ref.
  7. expr.call.apply.choiceThe Update implementation is chosen by the callee's type, then by the type of x, as an index store's is.
  8. expr.call.apply.no-useNeither call needs a use of the trait.
  9. expr.call.apply.placev() is a place only when the callee's type implements Update. Assigning to any other call, including a function call, is an error. Error: invalid-assignment-target.
  10. expr.call.apply.mut-refA store through v() changes v in place, so v must be a writable place or a &mut reference.
  11. expr.call.apply.readonly-refA store through any other callee, such as a := binding, a value parameter, or a read-only reference, is an error. Error: not-writable.
  12. expr.call.apply.not-callableCalling a value whose type is neither a function type nor implements Apply is an error. Error: not-callable.
  13. expr.call.apply.independentThe two traits are independent: a type may implement either one alone.
  14. expr.call.apply.fieldA callable value in a field is called by parenthesizing the field read, as in (panel.count)(), as expr.member.stored-fn states for a stored function.

A value with Apply alone reads but does not store, and a callable value takes no keys:

Error example
use std.ops.Apply

struct Counter:
    hits: i32

impl Apply for Counter:
    type Out = i32
    fn apply(self) -> i32: self.hits

fn current() -> i32: 1

fn invalid(counter: Counter, tags: List[string]) -> void:
    counter() = 2  # error: invalid-assignment-target
    current() = 2  # error: invalid-assignment-target
    counter(1)     # error: argument-count
    tags(0)        # error: not-callable

A store needs a cell it can change. With Cell and live from the first example, a := binding never changes, and a parameter typed Cell[i32] is the function's own copy:

Error example
fn invalid(count: Cell[i32]) -> void:
    view := live(0)
    view() = 1   # error: not-writable
    count() = 2  # error: not-writable

Note. let mut count = live(0) is a writable place, so its stores change it. view := live(0) reads view() but does not store. A function that stores into its caller's cell takes it as &mut Cell[i32].

Why. Keyed reads have one syntax, [], so Apply and Update take no keys, and a grid keeps g[(0, 1)]. update takes &mut self, so a store through a cell changes a writable place or what a &mut reference refers to, as every other &mut self call does.

See also: Index Traits, Compound Assignment, Operator Traits.

#Suspension Calls

  1. expr.call.suspensionSuspension calls with ! construct and drive a child suspension as specified in Requirements and Suspension.
  2. expr.call.suspension.orderThe callee and arguments are evaluated left to right before the child begins execution.
fn fetch!(key: string) -> string: "value of $key"

fn demo!() -> string:
    fetch!("a") + fetch!("b")   # left to right: "value of avalue of b"

#Propagation

Postfix ? handles either an optional or a Result value:

Rule Operand Value case Early return
expr.try.optionOptional T? .Some(value) produces value as T. .None immediately returns .None from the nearest function.
expr.try.resultResult Result[T, E] .Ok(value) produces the declared T. .Err(error) immediately returns .Err from the nearest function, holding the error converted as Error Conversion describes.
  1. expr.try.onceThe operand is evaluated once.
  2. expr.try.no-panic? does not catch runtime panics and does not interact with suspension by itself.
  3. expr.try.expectedWhen x? has an expected type T, its operand gets an expected type as an inference hint.
  4. expr.try.expected.resultThe hint is Result[T, E] when the nearest function returns Result[U, E].
  5. expr.try.expected.optionThe hint is T? when the nearest function returns an optional.
  6. expr.try.expected.hint-onlyThe hint only solves what the operand leaves open, such as a call's type arguments; it never coerces the operand.
fn empty_ok[T]() -> Result[List[T], string]:
    .Ok([])

fn no_ports() -> Result[List[i32], string]:
    let ports: List[i32] = empty_ok()?
    .Ok(ports)

In no_ports, the hint Result[List[i32], string] solves empty_ok's open type argument T as i32.

#Error Conversion

  1. expr.try.convertLet the nearest enclosing named function or closure return Result[U, F]. The error of type E becomes the returned error by the first of these steps that applies.

The steps are:

  1. expr.try.convert.assignableAssignability. If E is assignable to F by one rule of Assignability And Coercion, the returned error is the error converted by that rule.
  2. expr.try.convert.erased-referenceAn error implementing the referent trait propagates into a trait value reference error type by sharing an existing reference or copying a value into a new object. This step does not perform optional injection.
  3. expr.try.convert.fromConversion. Otherwise, if F implements From[E], the returned error is the result of that implementation's from applied to the error.
  4. expr.try.convert.noneOtherwise the ? is an error. Error: invalid-result-propagation.
use std.convert.From

enum FsError:
    NotFound(path: string)

enum HttpError:
    Timeout

enum SyncError:
    Fs(error: FsError)
    Http(error: HttpError)

impl From[FsError] for SyncError:
    fn from(value: FsError) -> SyncError: SyncError.Fs(value)

impl From[HttpError] for SyncError:
    fn from(value: HttpError) -> SyncError: SyncError.Http(value)

fn read_config(path: string) -> Result[string, FsError]:
    .Err(FsError.NotFound(path))

fn fetch(url: string) -> Result[string, HttpError]:
    .Err(HttpError.Timeout)

fn sync(path: string) -> Result[string, SyncError]:
    url := read_config(path)?
    body := fetch(url)?
    .Ok(body)

These rules refine the steps:

  1. expr.try.convert.assignable.covers.reference-valuesThe assignability step covers identical types, reference weakening, variance, supertrait widening and optional injection.
  2. expr.try.convert.no-importThe code using ? does not need to import From.
  3. expr.try.convert.message.reference-valuesThe failed propagation's message should name E and F. It should suggest an implementation of From[E] for F or an explicit mapping of the error.
  4. expr.try.convert.one-stepExactly one step converts the error.
  5. expr.try.convert.no-combine? never combines an assignability rule with a conversion, and it never chains conversions.
  6. expr.try.convert.no-chainWith impl From[A] for B and impl From[B] for C, a ? on Result[T, A] in a function returning Result[U, C] is an error. Error: invalid-result-propagation.
  7. expr.try.convert.no-injectionWith the same implementations, a ? on Result[T, A] in a function returning Result[U, B?] is also an error. The conversion to B would need an optional injection after it. Error: invalid-result-propagation.
  8. expr.try.convert.single-rule.reference-valuesAssignability is itself one rule: an A that implements the trait Tr does not propagate into Result[U, *dyn Tr?].
  9. expr.try.convert.only-tryOnly ? calls a conversion. return .Err(error) and every other .Err construction use ordinary assignability.
  10. expr.try.convert.before-deferThe conversion is part of the propagated value, so it runs before any deferred cleanup.
  11. expr.try.convert.pureA conversion neither suspends nor uses a requirement, because from has neither.
  12. expr.try.convert.inferred-closureWhen the nearest function is a closure whose result type is neither written nor supplied by an expected function type, ? performs no conversion. There, E itself contributes to the inferred result type.

See also: Error Trait, Conversion Trait, Deferred Cleanup.

#Propagation Targets

  1. expr.try.target.nearest-functionPostfix ? is an error when its nearest enclosing named function or closure does not provide the required optional or Result return type. Error: invalid-result-propagation.
  2. expr.try.target.module-top-levelModule top-level statements do not provide an implicit propagation target.
  3. expr.try.target.test-bodyA test body is a closure, so it is the propagation target of its ?, with the result type Propagation In Test Blocks gives it.
  4. expr.try.misuseEvery misuse of ? is an error. Error: invalid-result-propagation.
  5. expr.try.misuse.operandAn operand that is neither optional nor a Result is a misuse.
  6. expr.try.misuse.targetA ? whose enclosing function or closure does not return a compatible optional or a Result is a misuse.
  7. expr.try.misuse.unconvertedA Result error that neither conversion step converts to the enclosing error type is a misuse.
Error example
fn first(value: i32?) -> i32:
    value?  # error: invalid-result-propagation

#Propagation In Test Blocks

A test body written as a trailing block has a fixed result type, and ? propagates to it by the ordinary rules:

use std.error.Error
enum DigitError:
    NotDigit(text: string)

impl Display for DigitError:
    fn to_string(self) -> string:
        match self:
            DigitError.NotDigit(text) => "not a digit: " + text

impl Error for DigitError

fn parse_digit(text: string) -> Result[i32, DigitError]:
    if text == "7": .Ok(7) else: .Err(DigitError.NotDigit(text))

tests:
    use std.testing.assert_equal

    it("parses a digit"):
        digit := parse_digit("7")?
        assert_equal(digit, 7, reason="the digit parses")
        .Ok(())

    it("a body without ? is void"):
        match parse_digit("x"):
            .Ok(_) => panic("x is not a digit")
            .Err(error) => assert_equal(error.to_string(), "not a digit: x", reason="the helper rejects x")
  1. expr.try.test.fixed-resultWhen a trailing block is the body of an it call, its result type is fixed rather than inferred.
  2. expr.try.test.with-try.reference-valuesIf the block contains a ? outside any nested closure, its result type is Result[void, *dyn Error], where *dyn Error is the erased std.error.Error.
  3. expr.try.test.without-tryOtherwise its result type is void.
  4. expr.try.test.converts.reference-valuesAn error implementing Error propagates into the test block's *dyn Error, sharing an existing reference or copying a value into a new object. An error that does not implement Error is rejected. Error: invalid-result-propagation.
  5. expr.try.test.final-valueThe block's final value must be assignable to its result type, as for a function body. So a block that uses ? usually ends in .Ok(()).
  6. expr.try.test.explicit-closureA body passed as an explicit closure keeps its written or inferred result type. That type must implement std.process.Termination, the bound on it. Error: unsatisfied-trait-bound.
  7. expr.try.test.explicit-result-errorAn explicit test body returning Result[T, E] requires T < Termination and E < Error. Error: unsatisfied-trait-bound.
  8. expr.try.test.closureInside a closure nested in a test body, that closure is the nearest function, and these rules do not apply to it.
Error example
struct Hidden: pass

fn hidden() -> Result[void, Hidden]:
    .Err(Hidden {})

fn maybe() -> i32?:
    .None

fn lookup(key: i64) -> Result[i32, i64]:
    .Err(key)

tests:
    use std.testing.assert

    it("an error without Display", fn!() -> Result[void, Hidden]: hidden())  # error: unsatisfied-trait-bound
    it("an optional result", fn!() -> i32?: maybe())                         # error: unsatisfied-trait-bound

    it("an error that is not an Error does not convert"):
        value := lookup(80)?  # error: invalid-result-propagation
        assert(value > 0, reason="a positive port")
        .Ok(())

Why. A fixed Result[void, *dyn Error] lets one test body use ? on several error types, as Zig's inferred anyerror!void test bodies do.

Note. The body closures of it_each, it_prop, and it_prop_with get their result types by rules 2 and 3, by module.testing.reg.body-closure-result.reference-values.

See also: Error Trait, Standard Testing.

#Unary And Binary Operators

This section defines operator precedence and the meaning of each operator.

#Precedence

  1. expr.op.precedenceOperators are ordered from highest to lowest precedence as this table shows:
Precedence Operators Associativity
Postfix ., [], (), !(), postfix ? left
Power ** right
Unary +, -, ~, prefix ! right
Multiplicative *, /, % left
Additive +, - left
Shift <<, >> left
Bitwise AND & left
Bitwise XOR ^ left
Bitwise OR | left
Pipe |> left
Comparison ==, !=, <, <=, >, >=, is non-associative
Logical AND && left, short-circuiting
Logical OR || left, short-circuiting
Range .., ..= non-associative
Control and closure if, match, for, while, fn structural
Binding := right
  1. expr.op.suspension-callSuspension-call postfix ! has the same precedence as an ordinary call.
  2. expr.op.notPrefix ! is logical negation.
  3. expr.op.not.positionPrefix ! is recognized only at the start of an operand. So it never conflicts with the suspension suffix f!(args) or the fn! type marker, which always follow a name or fn.
  4. expr.op.not.combineThe two forms may combine: inside a suspending function, !fetch!(id) negates the bool result of the suspending call fetch!(id).
  5. expr.op.not-equalBy longest match, f!=g lexes as f, !=, g and is an inequality comparison, not a suspension call.
  6. expr.op.complement~ remains bitwise complement on integers.
  7. expr.op.not.groupinga && !b || c groups as (a && (!b)) || c: prefix ! binds tighter than every binary operator except **, and && binds tighter than ||.
  8. expr.op.power.groupingExponentiation binds less tightly on its right than unary negation, following the grammar: 2 ** -3 parses as 2 ** (-3), while -2 ** 2 means -(2 ** 2).
  9. expr.op.power.grouping.signedWith an integer base, 2 ** -3 is then rejected because its exponent is signed, as Exponentiation describes.
  10. expr.op.no-chainComparisons do not chain.
  11. expr.op.prefix-referenceThe prefix & and &mut are unary operators in the row of prefix !, and the prefix * binds tighter than **. Precedence states how they group.

Note. Write low <= value && value < high rather than low <= value < high.

fn demo() -> i32:
    2 + 3 * 4   # 14: * binds tighter than +

#Arithmetic Operators

  1. expr.arith.primitive-numericBetween primitive operands, arithmetic operators require compatible numeric operands. Other operands use Operator Traits.
  2. expr.arith.non-numeric-no-implA binary +, -, *, /, %, or ** is an error when an operand is a non-numeric primitive, as in true + false. It is also an error when no operator trait implementation fits, as in [1] * [2]. Error: type-mismatch.
  3. expr.arith.string-primitivestring + string is the only arithmetic form on non-numeric primitives.
  4. expr.arith.definedMixed-width result types, overflow, division, and shifts are defined in Type System.
  5. expr.arith.castSigned/unsigned and integer/floating mixing requires an explicit cast.
  6. expr.arith.int.checkedFor compatible integer operands, +, -, and * produce the common integer type. They are checked in a debug or test build and wrap in a release build, by types.arith.checked.
  7. expr.arith.int.divide/ truncates toward zero, % produces the corresponding remainder, and a zero divisor panics.
  8. expr.arith.unary-plusUnary + accepts all numeric types, preserves its operand's type and value, and evaluates the operand once. Any other operand is an error. Error: type-mismatch.
  9. expr.arith.unary-minusUnary - accepts signed integers and floating-point values, but not unsigned integers. Negating an unsigned operand is an error. Error: type-mismatch.
Error example
fn sum(a: bool, b: bool) -> bool: a + b                     # error: type-mismatch
fn product(a: List[i32], b: List[i32]) -> List[i32]: a * b  # error: type-mismatch

#Bitwise Operators

  1. expr.bit.primitive-integerBetween primitive operands, ~, &, |, and ^ accept integer values only and produce the operand common type. Other operands use Operator Traits.
  2. expr.bit.non-integer-no-implA binary &, |, or ^ with a primitive operand that is not an integer is an error. bool, floating-point, and string operands are examples. One with a non-primitive operand is an error too when no operator trait implementation fits. Error: type-mismatch.
Error example
fn both(a: bool, b: bool) -> bool: a & b          # error: type-mismatch
fn either(a: f64, b: f64) -> f64: a | b           # error: type-mismatch
fn toggle(a: string, b: string) -> string: a ^ b  # error: type-mismatch

#Shifts

A shift moves the left operand's bits by an unsigned integer count:

fn mask(x: i64, n: u32) -> i64:
    (x << n) | (x >> 3)

fn indexed_mask(x: i64, i: usize) -> i64:
    x << i
  1. expr.shift.unificationShifts are the exception to ordinary binary numeric unification.
  2. expr.shift.left-anyThe left operand may have any integer type, and the result has the left operand's type.
  3. expr.shift.count-unsignedThe right operand, the shift count, must have an unsigned integer type. Its type may be u8, u16, u32, u64, or usize.
  4. expr.shift.count-literalAn unsuffixed integer literal count has type u32, so x << 3 needs no suffix.
  5. expr.shift.count-invalidA signed integer count or a count of any non-integer type is an error. A numeric count's fix-it converts it, as in u32(n). Error: type-mismatch.
  6. expr.shift.countA negative count or a count at least as large as the left operand's bit width is an invalid shift, by types.arith.shift-count.
  7. expr.shift.fixed-widthThe shift itself is a fixed-width bit operation; left-shifted high bits are discarded rather than reported as arithmetic overflow.
Error example
fn scale(x: i64, n: i32) -> i64:
    x << n  # error: type-mismatch

Why. Accepting every unsigned width lets an index or a compact count shift without a cast, while excluding negative counts statically. A literal still takes u32, the exponent type of ** and the count type of Rust's checked_shl.

#Exponentiation

  1. expr.power.int.exponentFor an integer base, ** requires an exponent of an unsigned integer type, so a negative exponent cannot occur.
  2. expr.power.int.literalAn unsuffixed integer literal in exponent position has type u32.
  3. expr.power.int.signedA signed integer exponent is an error. Error: type-mismatch.
  4. expr.power.negated-literalA negated literal is signed: in 2 ** -1 the literal 1 is the operand of unary -, not the exponent itself. So -1 has a signed type, and the expression is a compile-time error. Error: type-mismatch.
  5. expr.power.negated-literal.not-otherThat error is not a runtime panic.
  6. expr.power.checkedInteger exponentiation uses checked multiplication in the base's result type.
  7. expr.power.float.same-typeFor a floating-point base, the exponent must have the base's type, as for a binary numeric operator. A floating-point literal exponent takes that type. Error: type-mismatch.
  8. expr.power.float.powFloating ** computes IEEE 754-2019 pow as specified in clause 9.2, including its special cases, and rounds the result correctly to the destination format.
  9. expr.power.mixedInteger and floating operands do not mix without an explicit cast; a mixed power expression is an error. Error: type-mismatch.
  10. expr.power.type-parameterFor a base of a type parameter, its bound picks the rule: T < Integer takes the integer rules, and T < Float the float rules. Any other type parameter base is an error. Error: type-mismatch.
Error example
fn power(exponent: i32) -> i32: 2 ** exponent  # error: type-mismatch
fn inverse() -> i32: 2 ** -1                   # error: type-mismatch
fn main() -> f64: 2 ** 2.0                     # error: type-mismatch
Open in playground

#Floating-Point Arithmetic

  1. expr.float.basicFloating +, -, *, and / use the corresponding required IEEE 754 basic operation, including infinities, signed zero, and NaN.
  2. expr.float.powerFloating ** uses the pow rule of Exponentiation.
  3. expr.float.remainder-truncatedFloating % returns the remainder of division truncated toward zero, as C fmod and Rust % do. The result is exact and has the dividend's sign.
  4. expr.float.remainder-specialA zero divisor, an infinite dividend, or a NaN operand gives NaN. A finite dividend with an infinite divisor gives the dividend.
fn demo() -> f64:
    5.5 % 2.0   # 1.5: truncated toward zero, with the dividend's sign

#Logical Operators

  1. expr.logic.notPrefix ! requires bool.
  2. expr.logic.bool&& and || require bool operands and produce bool.
  3. expr.logic.short-circuit&& and || evaluate the right operand only when needed.
fn demo(a: bool, b: bool) -> bool:
    a && !b   # b is read only when a is true

#Equality

  1. expr.eq.calls-eq== calls Eq.eq and != negates that result.
  2. expr.eq.stdStandard-library implementations provide value equality for primitives, optional and result values, tuples, lists, and maps when their elements support equality.
  3. expr.eq.map-orderMap equality is independent of entry order.
  4. expr.eq.no-implicitA user-defined struct or enum type has no implicit Eq implementation, even if all its members are comparable. Its author must explicitly implement or request derivation of the trait.
  5. expr.eq.no-identity-fallbackEquality never silently falls back to reference identity.
  6. expr.eq.floatFloating-point equality follows IEEE 754, so NaN is unequal even to itself, although floating-point types implement Eq.
  7. expr.eq.std.intrinsicThe Eq implementations of the number types, char, and bool are intrinsic methods. The one for string is not.
  8. expr.eq.functionsFunction and closure values do not implement Eq; applying == or != to them is an error. Error: type-mismatch.
  9. expr.eq.contextual-operandIn == and !=, a contextual variant operand, such as .None, .Ok(1), or .Some(x), takes the other operand's type as its expected type. This holds on either side.
  10. expr.eq.contextual-bothWhen both operands are contextual variants, neither has an expected type, so .None == .None is an error. Error: missing-contextual-enum-type.
  11. expr.eq.enum-hint== or != on an enum that does not implement Eq is an error whose message suggests adding @derive(Eq) to the enum. Error: type-mismatch.
  12. expr.eq.enum-hint.fixWhen the enum is declared in the same file as the comparison, the error has a fix-it. The fix-it inserts @derive(Eq) on its own line before the enum's declaration.
  13. expr.eq.references== and != between two references are an error, by ref.identity.no-equality. Error: reference-equality.
Error example
fn invalid(left: fn() -> void, right: fn() -> void) -> bool:
    left == right  # error: type-mismatch
Error example
fn absent(found: i32?) -> bool:
    found == .None

fn first(found: i32?) -> bool:
    .Some(1) != found

fn neither() -> bool:
    .None == .None  # error: missing-contextual-enum-type

Why. Equality stays opt-in, by expr.eq.no-implicit, so the error names the one-line opt-in rather than leaving the reader to find it.

#Ordering

  1. expr.ord.partial-cmp<, <=, >, and >= use PartialOrd.partial_cmp.
  2. expr.ord.stdThe standard library implements PartialOrd.partial_cmp for compatible numeric values, characters, strings, tuples, lists, and optionals.
  3. expr.ord.std.char-scalarCharacters are ordered by Unicode scalar value.
  4. expr.ord.std.string-bytesStrings are ordered lexicographically by byte, which for valid UTF-8 is the order by scalar value.
  5. expr.ord.std.sequencesTuples and lists are ordered lexicographically.
  6. expr.ord.std.optionalOptionals are ordered with .None before every present value.
  7. expr.ord.compositeComposite ordering is available when the corresponding elements implement the comparison trait, and it stops at the first unequal or unordered element.
  8. expr.ord.userUsers can implement comparison traits for their own types.
  9. expr.ord.totalOrd is the total-order refinement; floating-point types have PartialOrd but not Ord because NaN is unordered.
  10. expr.ord.unorderedAn unordered comparison makes all four relational operators false.
  11. expr.ord.std.intrinsicThe PartialOrd implementations of the number types and char, and the Ord implementations of the integer types and char, are intrinsic methods. Those for string are not.
fn demo(names: List[string]) -> List[string]:
    names.sorted()   # byte order: "Zebra" before "apple"

#Unsigned Comparisons With Zero

A comparison of an unsigned value with zero in one of four forms has a result fixed by the type, so it gets a warning:

Rule Form Result for an unsigned t
expr.ord.unsigned-zero.at-leastAt least zero t >= 0 always true
expr.ord.unsigned-zero.at-mostZero at most 0 <= t always true
expr.ord.unsigned-zero.belowBelow zero t < 0 always false
expr.ord.unsigned-zero.aboveZero above 0 > t always false
  1. expr.ord.unsigned-zero.lintA comparison in the table whose t has an unsigned integer type produces a lint finding without changing program validity. Warning: unsigned-comparison-always.
  2. expr.ord.unsigned-zero.literalThe 0 of a form is any unsuffixed integer literal whose value is zero, in any radix, such as 0 or 0x0.
  3. expr.ord.unsigned-zero.binding-typeThe type of t is its type at the comparison. That is a declared unsigned type, or the usize default of the literal its binding was initialized with.
  4. expr.ord.unsigned-zero.messageThe warning must name the operand and say whether the comparison is always true or always false. One phrasing is "t is unsigned, so t >= 0 is always true". When t has the usize default, it should also suggest a signed literal such as +10 or another condition.
fn countdown() -> void:
    let t = 10
    while t >= 0:  # warning: unsigned-comparison-always
        t = t - 1

fn below(value: u32) -> bool:
    value < 0  # warning: unsigned-comparison-always

Why. With the usize default, while t >= 0 never ends normally: t - 1 panics at zero instead. A countdown that means to reach -1 writes let t = +10.

#Identity

is compares identity without invoking user code.

  1. expr.is.no-user-codeis compares identity without invoking user code.
  2. expr.is.referencesFor two references, is compares the places they refer to, as Identity defines.
struct Account:
    owner: string

fn demo(a: &Account, b: &Account) -> bool:
    a is b   # identity, not contents

#Allocation Identity

  1. expr.is.conversionConverting a reference to a trait value reference preserves the identity of the place it refers to.
  2. expr.is.function-noneA function value has no identity. This covers named functions, generic instantiations, one-payload variant constructors, and closures.
  3. expr.is.function-sharingAn implementation may share one function value between evaluations or allocate a new one at each evaluation. No program can observe the choice.
  4. expr.is.no-wrapperA direct conversion of a reference does not allocate an identity wrapper.
trait Named:
    fn name(&self) -> string

struct User:
    name: string

impl Named for User:
    fn name(&self) -> string: self.name

fn demo(user: &mut User) -> bool:
    let first: &mut dyn Named = user
    let second: &mut dyn Named = user
    first is second   # one place, one identity

#Identity Operands

  1. expr.is.tupleTuples have no identity, and using is with a tuple is an error even if it contains references. Error: identity-requires-references.
  2. expr.is.value-operandPrimitive values, strings, enum values, optionals included, and function values likewise cannot be compared with is: an operand type must implement AnyRef, not AnyVal. Error: identity-requires-references.
  3. expr.is.compatible.referentBoth operands must otherwise have compatible types. Two reference types are compatible when the types they refer to are equal, whether each reference is & or &mut.
  4. expr.is.compatible.trait-valueA trait value or Any type is also compatible with a type that converts to it.
  5. expr.is.incompatible.dataTwo operands that are not compatible, such as &User and &Order, are an error. Error: incompatible-identity-operands.
  6. expr.is.function.operandAn is with an operand whose static type is a function type is an error, as for a value type. Error: identity-requires-references.
  7. expr.is.function.boundA function type does not implement AnyRef, so generic code over T < AnyRef never receives a function value. Error: unsatisfied-trait-bound.
Error example
struct User:
    name: string

struct Order:
    total: i32

fn same(user: &User, order: &Order) -> bool:
    user is order  # error: incompatible-identity-operands

fn tuples(left: (i32, i32), right: (i32, i32)) -> bool:
    left is right  # error: identity-requires-references

enum Color:
    Red
    Green

fn colors(left: Color, right: Color) -> bool:
    left is right  # error: identity-requires-references
Error example
fn aliases() -> bool:
    callback := fn() -> i32: 1
    alias := callback
    callback is alias  # error: identity-requires-references

Note. Use !(a is b) for distinct identities. Code that must later remove a registered callback keeps a handle returned at registration instead of comparing function values.

Why. Enum and function values have no identity, so an implementation may copy, share, or allocate them freely. A comparison would expose that choice. An enum never closes a reference cycle by itself, so cycle checks need no enum identity either.

See also: Trait Values And Any.

#Operator Traits

An operator trait is a std.ops trait that gives a type one operator. A library type gets the operator by implementing the trait:

use std.ops.{Add, Mul, Neg}

struct Money:
    cents: i64

impl Add for Money:
    type Out = Money
    fn add(self, rhs: Money) -> Money:
        Money { cents: self.cents + rhs.cents }

impl Mul[i64] for Money:
    type Out = Money
    fn mul(self, rhs: i64) -> Money:
        Money { cents: self.cents * rhs }

impl Neg for Money:
    type Out = Money
    fn neg(self) -> Money:
        Money { cents: -self.cents }

fn net(price: Money, refund: Money) -> Money:
    price * 3 + -refund

std.ops declares each binary operator trait in this shape, and the two unary ones without the argument:

pub trait Add[Rhs = Self]:
    type Out
    fn add(self, rhs: Rhs) -> Self::Out

pub trait Neg:
    type Out
    fn neg(self) -> Self::Out
Rule Operator Trait Method
expr.op.trait.addAdd a + b Add[Rhs] add
expr.op.trait.subSubtract a - b Sub[Rhs] sub
expr.op.trait.mulMultiply a * b Mul[Rhs] mul
expr.op.trait.divDivide a / b Div[Rhs] div
expr.op.trait.remRemainder a % b Rem[Rhs] rem
expr.op.trait.negNegate -a Neg neg
expr.op.trait.bit-andBitwise AND a & b BitAnd[Rhs] bit_and
expr.op.trait.bit-orBitwise OR a | b BitOr[Rhs] bit_or
expr.op.trait.bit-xorBitwise XOR a ^ b BitXor[Rhs] bit_xor
expr.op.trait.notComplement ~a Not not
expr.op.trait.shlShift left a << b Shl[Rhs] shl
expr.op.trait.shrShift right a >> b Shr[Rhs] shr
  1. expr.op.builtinArithmetic and bitwise operators are built in for the numeric types specified by this chapter and Type System.
  2. expr.op.concatstring + string concatenates strings.
  3. expr.op.trait.stdstd.ops declares the twelve operator traits in the table above.
  4. expr.op.trait.shape-defaultA binary operator trait takes the right operand's type as its one argument Rhs, whose default is Self. It declares an associated type Out and one method fn m(self, rhs: Rhs) -> Self::Out.
  5. expr.op.trait.rhs-selfSo impl Add for Money implements Add[Money], and the bound T < Add[Out = T] means T < Add[T, Out = T].
  6. expr.op.trait.rhs-explicitThe explicit form, such as impl Add[Money] for Money, stays valid and names the same trait.
  7. expr.op.trait.unary-shapeNeg and Not take no argument. Each declares Out and one method fn m(self) -> Self::Out.
  8. expr.op.primitive.typesWhen every operand is primitive after literal typing, the built-in rules of this chapter and Type System type the operator. They accept operands of one type or reject them, never converting an operand, and no trait is searched.
  9. expr.op.primitive.callThe operator then calls the trait method that expr.op.desugar names for it, with the operands. So a + b, with two i64 operands, calls Add::[i64]::add(a, b).
  10. expr.op.desugarOtherwise a op b is the trait-qualified call Op::[R]::m(a, b) of the operator's trait, as in Add::[R]::add(a, b). Likewise -a is Neg::neg(a) and ~a is Not::not(a).
  11. expr.op.no-useThe call needs no use of the trait.
  12. expr.op.left-dispatchThe left operand's type selects the implementation. Its instantiations of the trait are the candidates, and Instantiations Of One Generic Trait chooses among them by the right operand.
  13. expr.op.left-dispatch.exampleSo price * 3 checks 3 against i64 in Mul[i64].
  14. expr.op.left-dispatch.exact-functionA left operand of function type selects implementations by its own type. Row subsumption does not apply, so an implementation for a function type with a wider row does not fit. Error: type-mismatch.
  15. expr.op.left-dispatch.exact-function.messageThat error's message should suggest a binding typed with the implementation's function type, as in let handler: fn(i32) -> i32 $ Db = get.
  16. expr.op.genericWhen an operand's type is a type parameter, the candidates come from its bounds and their supertraits.
  17. expr.op.outThe operator's result type is the chosen implementation's Out. Implementations are unique per trait instantiation and target, so a + b has one type.
  18. expr.op.orderThe left operand is evaluated, then the right one, and then the method is called.
  19. expr.op.left-literal.joinA literal left operand with a non-primitive right operand takes its width by types.literal.local.class.meet. An implementation alone never names a width, so with only impl Mul[Money] for i64, write i64(3) * price.
  20. expr.op.left-literal.no-fitWith only impl Mul[i64] for Money, no width of 3 lets 3 * price type-check, so it is an error; write price * 3. Error: type-mismatch.
  21. expr.op.no-implAn operator for which no implementation fits is an error, and its message should name the missing trait. Error: type-mismatch.
  22. expr.op.newtypeA newtype has only the operators its author implements. It inherits none from its base type, and no derivation supplies an operator trait.
  23. expr.op.fixedOperator traits never change precedence or associativity, and they add no operator symbols.
  24. expr.op.not-overloaded&&, ||, prefix !, unary +, **, is, =, :=, and postfix ? have no trait and keep their built-in meaning.
  25. expr.op.comparison==, !=, and the relational operators call Eq and PartialOrd, as Equality and Ordering define. std.ops declares no comparison trait.
Error example
use std.ops.Mul

struct Money:
    cents: i64

impl Mul[i64] for Money:
    type Out = Money
    fn mul(self, rhs: i64) -> Money:
        Money { cents: self.cents * rhs }

type Meters(f64)

fn triple(price: Money) -> Money:
    3 * price  # error: type-mismatch

fn total(a: Meters, b: Meters) -> Meters:
    a + b  # error: type-mismatch

#Primitive Implementations

The standard library implements the operator traits for the primitive number types, and Add for string. So generic code bounded by an operator trait accepts them:

use std.ops.Add

fn sum[T < Add[Out = T]](items: List[T], zero: T) -> T:
    let total = zero
    for item in items:
        total = total + item
    total

fn count(items: List[i32]) -> i32:
    sum(items, 0)
Rule Traits Implementations, with Out = T
expr.op.std.arithArithmetic Add, Sub, Mul, Div, Rem impl Add for T and the like, for every integer and floating-point type T
expr.op.std.negNegation Neg every signed integer and floating-point type
expr.op.std.bitwiseBitwise BitAnd, BitOr, BitXor impl BitAnd for T and the like, for every integer type T
expr.op.std.notComplement Not every integer type
expr.op.std.string-addConcatenation Add impl Add for string
expr.op.std.shift-unsignedShifts Shl, Shr impl Shl[C] for T and impl Shr[C] for T, for every integer type T and every unsigned integer type C
  1. expr.op.std.intrinsic-methodEach method of the number types' implementations in the table is an intrinsic method. It computes the operation that this chapter and Type System define for its type, including checked overflow and its panics.
  2. expr.op.std.string-not-intrinsicimpl Add for string is an ordinary implementation, not an intrinsic method.
  3. expr.op.std.one-typeThe arithmetic and bitwise implementations are same-type only, as the built-in typing of primitive operands is, so neither generic nor primitive code widens an operand.
  4. expr.op.std.string-genericimpl Add for string concatenates, as string + string does, so generic code such as T < Add[Out = T] accepts string.
  5. expr.op.std.bool-charThe standard library declares no operator trait implementation for bool or char.

Note. An implementation may compile an operator on primitive operands to inline code rather than a call, as trait.impl.intrinsic.inline allows. The result is the same.

Why. This is Rust's shape. The right operand is a trait argument, so a type may scale by i64 and add its own kind. As in Rust, the argument defaults to Self, so the common same-type case omits it. The output is an associated type that the operands fix, so x := a + b never becomes ambiguous when an implementation is added. Primitive operands skip trait search, so numeric code compiles as before. Type checking never searches across numeric types, the cost Swift pays for overloaded operators.

See also: Compound Assignment, Index Traits, Numeric Traits.

#Pipe Expressions

A pipe expression value |> step passes a value to a step, so a chain of calls reads left to right:

fn tag(label: string, level: i32) -> string:
    "$label:$level"

fn clean(raw: string) -> string:
    "[$raw]"

fn label(raw: string) -> string:
    raw |> clean |> tag(_, 2)
  1. expr.pipe.formA pipe expression is a value, the |> token, and a step.
  2. expr.pipe.left-assoc|> is left-associative: a |> f |> g is (a |> f) |> g.
  3. expr.pipe.precedence|> binds more loosely than | and more tightly than comparison, so n + 1 |> twice == 4 is ((n + 1) |> twice) == 4.
  4. expr.pipe.step-kindsA step is either a substitution step, which contains _, or a bare step, which is a name or path without _.
  5. expr.pipe.orderThe piped value is evaluated first and exactly once, before any part of the step, including the step's callee.
  6. expr.pipe.valueThe pipe expression's type and value are those of its step.

#Substitution Steps

A substitution step marks the piped value's slot with _:

struct Point:
    x: usize
    y: usize

fn shift(point: Point, by: usize) -> usize:
    point.x + by

fn measure(raw: string, start: usize) -> usize:
    width := raw |> _.len()
    moved := start |> Point { x: _, y: 0 } |> shift(_, 3)
    width + moved |> _ * 2
  1. expr.pipe.slot.oneA substitution step contains exactly one _, which stands for the piped value.
  2. expr.pipe.slot.anyThe step may be any expression at its precedence, not only a call, such as _.len(), _ * 2, or Point { x: _, y: 0 }.
  3. expr.pipe.slot.meaningThe step is evaluated as if _ were a name bound to the piped value, with that value's type and access.
  4. expr.pipe.slot.suffixSuffixes in the step apply inside it: in x |> parse(_)?, ? propagates from parse(x), and x |> load!(_) is a suspension call.
  5. expr.pipe.slot.duplicateA step with two or more _ is an error. Error: duplicate-pipe-placeholder.
  6. expr.pipe.slot.closureA _ inside a closure nested in the step is an error. Error: pipe-placeholder-in-closure.
  7. expr.pipe.slot.nestedA _ belongs to the innermost pipe step that contains it, and the rules above count only a step's own _. In x |> f(_, y |> g(_)), the second _ belongs to g(_), so the outer step has one _.
Error example
fn add(left: i32, right: i32) -> i32: left + right

fn apply(value: i32, f: fn(i32) -> i32) -> i32: f(value)

fn doubled(n: i32) -> i32:
    n |> add(_, _)  # error: duplicate-pipe-placeholder

fn later(n: i32) -> i32:
    n |> apply(1, fn(v): v + _)  # error: pipe-placeholder-in-closure

Why. _ always shows where the value goes. A closure may run later, or many times, so a _ inside one would not say which value it means.

#Bare Steps

A bare step names the function to call, so x |> f means f(x):

fn clean(raw: string) -> string:
    "[$raw]"

fn tidy(raw: string) -> string:
    raw |> clean
  1. expr.pipe.bare.formA bare step is an identifier, or identifiers joined by ., with no suffix after it.
  2. expr.pipe.bare.callvalue |> path evaluates as the call path(value), except that value is evaluated first.
  3. expr.pipe.bare.methodWhen the path's prefix names a value, the call is a method call on that value: x |> user.greet means user.greet(x).
  4. expr.pipe.bare.no-suspendA bare step whose callee is a suspending function is an error. Write a substitution step, as in x |> load!(_). Error: suspending-pipe-step.
  5. expr.pipe.bare.needs-placeholderA step without _ that is not a bare step is an error. Error: pipe-step-needs-placeholder.
  6. expr.pipe.bare.needs-placeholder.formsThat covers a call such as x |> f(y), an index such as x |> f[0], type arguments such as x |> parse::[i32], and a suffix such as x |> f?.
  7. expr.pipe.bare.method-referenceA method reference without type arguments, such as Config::parse, is also a bare step: raw |> Config::parse means Config::parse(raw).
Error example
fn scale(value: i32, by: i32) -> i32: value * by

fn fetch!(id: i32) -> i32: id

fn tripled(n: i32) -> i32:
    n |> scale(3)  # error: pipe-step-needs-placeholder

fn loaded!(n: i32) -> i32:
    n |> fetch  # error: suspending-pipe-step

Why. Elixir reads x |> f(y) as f(x, y), and F# reads it as f(y)(x). Rejecting the form removes both readings. A bare step is only a name or path, so an index or type arguments after it need _ too. A suspending bare step would suspend with no visible !.

#Pipe Layout

  1. expr.pipe.single-lineA step must not contain an indented suite, such as a multi-line match, if, or closure body. Error: multi-line-pipe-step.
  2. expr.pipe.single-line.inlineA same-line closure or conditional inside a step is valid.
  3. expr.pipe.no-trailing-blockA step takes no trailing block, because a trailing block call must be a complete statement or right-hand side, as fn.trailing.position says.
  4. expr.pipe.linesA chain may continue on lines that start with |>. A leading-dot line inside a chain is an error, as Leading-Pipe Continuation specifies. Error: syntax-error.
Error example
fn apply(value: i32, f: fn(i32) -> i32) -> i32: f(value)

fn bumped(n: i32) -> i32:
    n |> apply(_, fn(v):  # error: multi-line-pipe-step
        v + 1
    )

Why. One line per step keeps a chain readable as a list of steps. For a longer step, bind a name first or extract a function.

#The Placeholder Outside Pipes

  1. expr.pipe.placeholder-only_ has an expression meaning only in a pipe step.
  2. expr.pipe.placeholder-outside_ as an expression anywhere else is an error. Error: placeholder-outside-pipe.
  3. expr.pipe.no-partialhd has no function placeholder: the partial application f(_, a) does not create a function.
Error example
fn format(value: i32, width: i32) -> string:
    "$value/$width"

fn apply(value: i32, f: fn(i32) -> string) -> string: f(value)

fn run() -> string:
    apply(3, format(_, 8))  # error: placeholder-outside-pipe

Note. Write the callback as a closure, such as fn(value): format(value, 8), or as a method reference.

See also: Precedence, Leading-Pipe Continuation.

#Range Expressions

A range expression builds a range value from its bounds:

use std.ops.{Range, RangeFrom}

fn halves(n: i32) -> (Range[i32], RangeFrom[i32]):
    middle := n / 2
    (0..middle, middle..)

fn wide(limit: i64) -> Range[i64]:
    0..limit

fn closed(limit: i64) -> Range[i64]:
    0..=limit

fn unit() -> Range[f64]:
    0.0..=1.0
Rule Form Type Holds
expr.range.form.half-openHalf-open a..b Range[T] the values from a up to, not including, b
expr.range.form.fromFrom a.. RangeFrom[T] the values from a up, with no end
expr.range.form.toTo ..b RangeTo[T] the values below b, with no start
expr.range.form.throughInclusive a..=b Range[T] the values from a through b
expr.range.form.to-throughTo inclusive ..=b RangeTo[T] the values up to and including b, with no start
expr.range.form.fullFull .. RangeFull every value, with no start and no end

std.ops declares the range types in this shape:

pub struct Range[T]:
    pub start: T
    pub end: T
    pub inclusive: bool

pub struct RangeFrom[T]:
    pub start: T

pub struct RangeTo[T]:
    pub end: T
    pub inclusive: bool

pub struct RangeFull: pass
  1. expr.range.declared.structstd.ops declares the four range types as structs whose fields are public, as shown above. A range is a value, so copying one copies its fields, and a change to one copy leaves the other as it was.
  2. expr.range.value.fieldsA range expression builds the range type of its form, with its bounds as the fields: a..b is Range { start: a, end: b, inclusive: false }.
  3. expr.range.inclusive-fieldThe inclusive field of a Range or RangeTo is true for a ..= form and false for a .. form, so ..=b is RangeTo { end: b, inclusive: true }.
  4. expr.range.not-preludeThe range types are not prelude names. Code that names one in a type imports it, as in use std.ops.Range, but a range expression needs no use.
  5. expr.range.orderA range expression evaluates its start bound, then its end bound.
  6. expr.range.no-checkBuilding a range never compares its bounds, so 5..2 is a valid, empty range.
  7. expr.range.bound.numericEach bound must have an integer or a floating-point type, or be a type parameter bounded by Integer or Float. A bound of any other type is an error. Error: type-mismatch.
  8. expr.range.bound.operandsThe two bounds of a..b or a..=b are typed as the operands of a binary numeric operator. A literal takes the other bound's type, and bounds of two types of one family are an error. Error: type-mismatch.
  9. expr.range.bound.signednessA signed and an unsigned bound are an error, as for a binary numeric operator. Error: mixed-signedness.
  10. expr.range.element-typeThe range's element type T is the bounds' common type, or the one bound's type for a.., ..b, and ..=b. RangeFull has no bound and no element type.
  11. expr.range.expectedAn expected range type gives each bound its element type as the bound's expected type, so let r: Range[i64] = 0..10 has i64 bounds.
  12. expr.range.literal-boundsWith no expected type, literal bounds are one literal class: 0..3 is a Range[usize], and -3..3 is a Range[i32].
  13. expr.range.float-literal-boundsFloat literal bounds default as a float literal does, so 0.0..=1.0 and 0.5..2.0 are Range[f64]. Bounds of type f32 make a Range[f32].
Error example
fn invalid(name: string, count: u32, limit: i32, large: i64) -> void:
    a := name..name    # error: type-mismatch
    b := count..limit  # error: mixed-signedness
    c := limit..large  # error: type-mismatch

Note. for iterates a..b, a.., and a..=b of an integer type, as Range Iteration states. A float range is not iterable. An index of a range type slices a string or a list with unsigned bounds, as Slicing states. A match takes the range forms but ..b and .. as range patterns.

Why. As in Rust, a range is an ordinary std.ops value. One syntax then serves loops, slicing, and any function that takes a range, with no separate range function.

Why. Float bounds let a function that takes a range of floats read like one that takes integers, as c.float(0.0..=1.0) reads like c.int(1..=6). Rust's range expressions also take float bounds and do not iterate them.

Why. a..b and a..=b share one type, as ..b and ..=b do, so a function that takes a Range[i64] accepts both forms. The types still keep a range with no start, which is not iterable, apart from one with a start.

See also: Range Syntax, Binary Numeric Operators.

#Binding Expressions

:= binds names inside an expression:

if (size := input.len()) > 0:
    println(size)
  1. expr.bind.one-name:= introduces one inferred, non-reassignable name and evaluates to the initializer's value.
  2. expr.bind.precedence:= has the lowest precedence.
  3. expr.bind.parensParentheses are required when a binding appears as an operand of another expression, as above.

Note. Destructuring is a let statement, as in let (a, b) = pair, never a := binding, as grammar.stmt.short-binding.let-only states.

See also: Binding Expressions, which defines the binding's scope.

#Comprehensions

A list comprehension builds a list from nested iteration:

pairs := [for x in xs for y in ys if x.id == y.owner_id => (x, y)]
  1. expr.comp.listA list comprehension evaluates clauses from left to right and appends one result for every path that reaches =>.
  2. expr.comp.shapeA comprehension is equivalent in iteration shape to nested loops, with an if clause filtering at the exact point where it appears.
  3. expr.comp.visibilityNames introduced by earlier clauses are visible to later clauses and the result.

#Map Comprehensions

A map comprehension generates keys and values:

by_id := {for user in users if user.active => user.id: user}
  1. expr.comp.mapA map comprehension evaluates the key and then the value for every successful path.
  2. expr.comp.map.duplicateDuplicate keys use the later generated value.

#Comprehension Restrictions

  1. expr.comp.eagerComprehensions are eager.
  2. expr.comp.suspensionA comprehension follows the loops it abbreviates. So a bang call inside it is valid exactly where it would be valid in those loops: in a driver context. Outside one it is an error. Error: bang-call-outside-suspension.
  3. expr.comp.suspension.sequentialThe bang calls of a comprehension run one at a time, in the order the loops reach them. Each call completes before the comprehension evaluates any later clause, guard, key, or element.
  4. expr.comp.no-jumpsreturn, break, and continue are not valid inside a comprehension.
  5. expr.comp.no-letThere is no comprehension let clause.
Error example
fn fetch!(id: i32) -> string: "user"

fn names!(ids: List[i32]) -> List[string]:
    [for id in ids => fetch!(id)]

fn invalid(ids: List[i32]) -> List[string]:
    [for id in ids => fetch!(id)]  # error: bang-call-outside-suspension

Note. Postfix ? inside a comprehension returns from the nearest enclosing function or closure, as it would from the loop. The comprehension then stops: it evaluates no later clause, guard, or element.

In place of a let clause, use a parenthesized := binding in a guard or result expression:

sizes := [for user in users
          if (size := user.name.len()) > 0
          => size]

#Closures And Control Expressions

  1. expr.closureClosures are expressions described in Functions.
  2. expr.controlif, match, and loops are value-capable expressions described in Control Flow.
fn demo(flag: bool) -> i32:
    apply := fn(x: i32) -> i32: x * 2
    f := if flag: apply else: fn(x: i32) -> i32: x
    f(+21)

#Unsupported Expression Extensions

  1. expr.unsupported.custom-operatorshd-lang has no user-defined operator symbols, and no overloading of the operators that expr.op.not-overloaded lists.
  2. expr.unsupported.chaininghd-lang has no comparison chaining.
  3. expr.unsupported.any-fallback.reference-valueshd-lang has no fallback conversion of heterogeneous literals to &dyn Any.
  4. expr.unsupported.try-mappingPostfix ? has no mapping clause.
  5. expr.unsupported.try-mapping.explicitA site that needs a different error conversion maps the Result explicitly before ?. One way is a function that takes a single-payload variant constructor as its mapper.
Error example
fn inside(value: i32) -> bool:
    0 < value < 10   # error: comparison-chaining

See also: Enum Declarations.