#Expressions
Status: language specification draft.
This chapter defines expressions, which compute values.
- expr.static-typeEvery expression has a static type.
- 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.
- expr.order.left-to-rightUnless a construct states otherwise, subexpressions are evaluated from left to right and exactly once.
- expr.order.appliesThis rule applies to tuple and collection elements, struct fields, call arguments, operands, and indexing expressions.
- expr.order.conditional
&&,||,if,match, loops, optional or result propagation with?, and comprehension filters evaluate conditionally as described below. - 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.
- expr.category.valueA value expression produces a value.
- 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:
- expr.place.localA reassignable local introduced by
letis a core place expression. - expr.place.indexAn index operation whose receiver and indexing protocol expose mutable storage is a core place expression.
- expr.place.receiverThe receiver of a field or index place may be any expression of a writable reference type,
*mut Tor&mut T, including a call that returns a*mut T. - expr.place.call-updateA call is a place only when its callee's type implements
Update, as Callable Values defines. - expr.place.value-forms
:=bindings, literals, other calls, arithmetic, and temporary values are not places. - expr.place.tuple-elementA tuple element selection such as
pair._0is not a place: tuples are immutable, and a changed tuple is built as a new tuple value. - expr.place.enum-shared-fieldA shared enum field such as
status.phraseis not a place: enum values never change once built. - expr.place.struct-fieldA field of a struct value is a place when the struct is in a writable place, as Writable Places defines.
- expr.place.deref
*rwith a writable referencer,*mutor&mut, is a place, and so is a field read through one, such asr.x, as Writing Through A Reference defines.
fn demo() -> void:
let x = +1
x = x + 1 # x is a place: a reassignable local#Assignment
- expr.assign.statementAssignment is a statement and requires a writable place on its left side.
- expr.assign.targetAssigning to an expression that is not a place is an error. Error:
invalid-assignment-target. - expr.assign.orderAssignment evaluates the place before the right-hand side, then performs one store.
- expr.assign.order.localA local place requires no subexpression evaluation.
- expr.assign.order.fieldA field assignment evaluates its receiver, then the right-hand expression.
- expr.assign.order.indexAn indexed assignment evaluates the receiver, the index, and the right-hand expression in that order.
- expr.assign.order.callAn assignment through a call evaluates the callee, then the right-hand expression.
- expr.assign.abruptIf any step completes abruptly, no store occurs.
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- expr.assign.compound.formCompound assignment is a statement, written with one of ten operators:
+=,-=,*=,/=,%=,&=,|=,^=,<<=, and>>=. - expr.assign.compound.no-othersThere is no
**=,&&=, or||=. - 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. - expr.assign.compound.onceThe receiver and index of the place are evaluated once, then the right-hand expression.
- expr.assign.compound.meaningFor every type,
p op= emeansp = p op e: it reads the place, applies the operator to that value ande, and stores the result in the place. - expr.assign.compound.primitiveWhen the place and the right operand have primitive types,
p op= ecomputesp op eby the built-in rules and stores the result in the place. The store follows the rules ofp = p op e, so a name must be a reassignable local. - 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. - 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.
- expr.assign.compound.index-read-writeOn an index place, the read is
r[k]and the store isr[k] = v.ListandMapuse their built-in indexing, and another type needs bothIndexandIndexSet, as Index Traits defines. - expr.assign.compound.call-onceOn a call place
v() op= e, the calleevis evaluated once, thene. - expr.assign.compound.call-read-writeOn a call place, the read is
v()and the store isv() = x. So the callee's type needs bothApplyandUpdate, as Callable Values defines. - 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.
- expr.assign.compound.no-assign-traits
std.opsdeclares no assign trait, and no operator changes a value in place.
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-targetfn tally(counts: &mut Map[string, i32], word: string) -> void:
counts[word] += 1 # panics with index-out-of-bounds when `word` is absentNote. A diagnostic that rejects
p op= emay showp = p op eas 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 assb.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.
- expr.name.identifierAn identifier expression evaluates the declaration or local binding selected by lexical name resolution.
- expr.name.qualifiedA qualified name selects a declaration or enum variant through a module or type namespace.
- expr.name.qualified.privateA module path to a declaration that its module declares without
pubis an error, as a use of it would be. Error:private-import. - 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. - expr.name.contextual
.Variantselects a variant only when the expression has an expected type that fixes one nominal enum. - expr.name.contextual.rules
.Varianthas the same construction and argument rules asEnum.Variant; a payload-bearing variant still requires a call. - expr.name.contextual.no-searchThe compiler does not search all visible enums for a matching variant name.
- expr.name.contextual.no-typeWithout a unique expected enum type,
.Variantis a type error. Error:missing-contextual-enum-type.
enum Status:
Queued
fn invalid() -> void:
status := .Queued # error: missing-contextual-enum-type
passuse 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-importNote. A
stdmodule is no exception. A module path to astddeclaration withoutpubisprivate-importbyexpr.name.qualified.private, as for a module of the package.
See also: Enum Declarations, Use Forms, Use Roots.
#Literals
- expr.literal.definedBoolean, integer, floating-point, string, and character literals are defined lexically in Lexical Structure and typed in Type System.
- expr.literal.callA suffixed literal or prefixed string is a call, as Literal Suffixes specifies.
- expr.literal.pass
passis the no-op expression. It has typevoidand 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-
expr.interp.formsInterpreted strings support
$name,$self, and${expression}interpolation. -
expr.interp.self
"value: $self"inside a method interpolates the receiver. -
expr.interp.orderEmbedded expressions are evaluated from left to right at the position of their segment.
-
expr.interp.displayEach embedded expression's type must implement the canonical
std.format.Displaytrait. -
expr.interp.appendThe compiler appends the string returned by that implementation.
-
expr.interp.no-displayAn embedded expression whose type does not implement
Displayis an error. Error:unsatisfied-trait-bound. -
expr.interp.no-fallbackThere is no fallback conversion through
Any, runtime reflection, or debug output. -
expr.interp.stdThe standard library provides
Displayimplementations for ordinary printable primitive types andstring. -
expr.interp.std.tuple.templateIt also declares a tuple template for
Display, so every tuple whose elements implementDisplayimplements it, at every size. It writes the elements' texts inside parentheses, separated by,, as in(1, a). -
expr.interp.std.tuple.rest-inlineFor a tuple with a rest element
List[T]...,Tmust implementDisplay. The rest's items are written inline after the fixed elements, as in(1, 2, 3, 4). -
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. -
expr.interp.userOptional and user-defined values are displayable only when the corresponding type implements
Display. -
expr.interp.prefixedA prefixed string does not append its values: they become the values of a template, as Prefixed Strings specifies.
-
expr.interp.constantA string with no interpolation segments is an ordinary constant value and performs no
Displaycalls. -
expr.interp.referenceA reference interpolates as the value it refers to, since
stdimplementsDisplayfor&TwhenThas it (References). -
expr.interp.collection-debug-fixFor a simple name in braced interpolation, a missing
Displaydiagnostic may offer exactdebug(name)wrapping forListandMap. -
expr.interp.collection-debug-fix.proofThis fix requires proven
Debugsupport and an unshadowed preludedebug.
Note. The empty tuple writes
(). Avoidvalue is that tuple, so"${log()}"writes()too.
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:
- expr.literal-fn.markerA suffix function carries a
std.ops.NumSuffixvalue, written@num_suffix, and a prefix function carries astd.ops.StrPrefixvalue, written@str_prefix. The compiler recognizes both types, andstd.ops.Template, by their qualified names. - expr.literal-fn.fn-only
std.opsdeclaresNumSuffix,num_suffix,StrPrefix,str_prefix, andTemplate. Both marker types list only.Fnin their@annotate, so either marker before anything but a function is an error. Error:decorator-target-kind. - 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-suffixfor a suffix,invalid-string-prefixfor a prefix. - expr.literal-fn.no-marker-importThe call needs no import of the marker, its type, or
Template: auseof the literal function alone makes the literal valid. - 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.
- 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:
- expr.suffix.user-callA user-suffixed literal
Nxcalls its suffix functionx(N)with the numeric literal as its only argument.
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-requirementNote. 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),5unitpasses5, and the ordinary callunit()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:5ktakesNfrom the literal, or from the expected type, as inlet limit: i64 = 5k. A unary minus is not part of the literal, so-5sis-(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 add12pxwithout new syntax. The literal's type is the function's result, so12pxis aPixels. A suspending function does not fitnum_suffix's resultNumSuffix[fn(N) -> R $ Q]. hd marks every suspending call with!, and5shas 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_paniclint does, but none is required.
Note. The standard library's duration suffixes
ms,s,min, andhofstd.time, and itsDurationtype, 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:
- expr.prefix.fn-callA prefixed string
x"..."is the callx(t)of the prefix functionx, with astd.ops.Templatevaluetas its one argument. The compiler never joins the pieces and never callsDisplay. - expr.prefix.template
t.valuesholds theninterpolated values in source order, andt.raw_partsholds then + 1pieces of text around them. - 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. Sox"a $b c"passes the pieces["a ", " c"]and the values[b], andx"text"passes["text"]and[]. - expr.prefix.raw-partsEach piece is the text exactly as written, with every backslash kept, as
lex.prefix.raw-textsays. - expr.prefix.orderThe interpolated expressions are evaluated from left to right before the call, as arguments are.
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-requirementWhy. A prefix is an ordinary function found through
use, so a library addssql"..."without new syntax. The template keeps text and values apart, sosqlcan 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, becausesql"..."has no place for!.
Note. The standard library's one prefix, the raw-text
rofstd.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) * cempty := ()
single := (1,)
point := (10, 20)- expr.paren.groupParentheses group one expression without changing its value.
- expr.tuple.commaA comma constructs a tuple.
- expr.tuple.singleA one-element tuple requires a trailing comma.
- expr.tuple.orderTuple elements evaluate left to right.
- expr.tuple.select-underscoreTuple selection uses the member
_followed by the zero-based element index, such aspoint._0andpoint._1. An index past the last element is an error. Error:unknown-field. - expr.tuple.select-underscore.identifierThat member name is an ordinary identifier, so
point._0._1selects 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...))- 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 againstT. So()against(List[i32]...,)is a tuple whose rest is empty. Too few elements is an error. Error:type-mismatch. - expr.tuple.rest.spreadA spread
xs...that ends a tuple expression supplies the rest element. The elements before it are the fixed elements, andxsis the rest element's list. Against an expected type, both must match it. Error:type-mismatch. - expr.tuple.rest.spread.listThe spread operand must be a
List[T], which gives the rest elementList[T].... A tuple or any other operand is an error. Error:type-mismatch. - 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.
fn short() -> (i32, i32, List[i32]...):
(1,) # error: type-mismatch
fn joined(pair: (i32, i32)) -> (i32, i32, i32):
(1, pair...) # error: type-mismatchWhy. 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
- expr.list.orderA list literal evaluates its elements left to right.
- expr.list.typeA list literal produces
List[T], where every element is assignable toT. - expr.list.spreadA list element ending in
...is a spread. - expr.list.spread.onceA spread's operand is evaluated once, in element order.
- expr.list.spread.typeA spread's operand must have a list type
List[U]. Error:type-mismatch. - expr.list.spread.insertThe spread's elements are inserted at its position, in order.
- expr.list.spread.positionA literal may contain several spreads in any position, so
[0, xs...]and[xs..., ys...]are valid. - expr.list.spread.expectedWith an expected
List[T],Umust be assignable toT. - expr.list.spread.inferredWithout an expected
List[T], a spread contributesUto the least common type of the elements.
fn numbers(count: i32) -> List[i32]:
[0, count...] # error: type-mismatch#Map Literals
- expr.map.orderA map literal evaluates each key and then its value, processing entries from left to right.
- expr.map.duplicate.lastIf two evaluated entries have equal keys, the later value replaces the earlier value.
- expr.map.iteration.orderMap iteration follows the map's deterministic order and is not part of map equality.
- expr.map.keysKey validity and equality/hash requirements are defined in Type System.
- 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
- expr.collection.emptyEmpty
[]and{}literals require an expected collection type. - expr.collection.empty.errorWithout an expected collection type, an empty literal is an error. Error:
cannot-infer-type. - expr.collection.expectedWhen an expected
List[T]orMap[K, V]type is available, each literal element is checked directly against the corresponding expected type. - 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.
- expr.collection.inferred.rowsFunction values with different rows take the union of their rows first, as Row Union In Literals states.
fn main() -> void:
values := [] # error: cannot-infer-type
others := {} # error: cannot-infer-typeOpen in playgroundWhy. 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",
}- expr.data.requiredA data expression names its type and provides every required field.
- expr.data.onceEvery field may appear at most once.
- expr.data.evalField initializers evaluate in source order, not declaration order.
- expr.data.shorthandA field written as its bare name, as in
Point { x, y }, is field shorthand forname: 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",
}- expr.update.formCopy-update syntax begins with one spread followed by explicit replacements.
- expr.update.one-spreadA data expression permits at most one spread, and it must precede every explicit field.
- expr.update.spread-firstThe spread expression is evaluated first.
- 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.
- expr.update.exact-typeThe spread expression must have the exact struct type being constructed.
- 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
- 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.
- expr.member.select-identifier
value.memberselects a member named by an identifier, andtuple._0selects a tuple element. - expr.member.field-readWithout an argument clause,
value.namereads a field. A field the type does not declare is an error. Error:unknown-field. - expr.member.lookupMember Resolution defines field and method lookup, including promoted members and the declaration-time check
ambiguous-promoted-member. - expr.member.enum-variant
Enum.Variantis not member access: it names an enum variant. - 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.ySee also: Enum Declarations.
#Method Calls
- 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 withvalueas its receiver. - expr.member.no-field-callA method call never reads a field.
- expr.member.stored-fnA function stored in a field is called by parenthesizing the field read, as in
(handler.callback)(event). - expr.member.stored-fn.error
handler.callback(event)looks for a method namedcallback, and is an error when there is none. Error:unknown-method. - expr.member.stored-fn.hintWhen a field named
callbackexists, theunknown-methodmessage should suggest(handler.callback)(event). - 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.
- expr.member.trait-hintWhen such a method is the only one with the name, the
unknown-methodmessage should suggest a use declaration for its trait. - expr.member.method-not-valueA method is not a value:
value.methodwithout an argument clause is a field read. - expr.member.method-referencesA method is used as a value through a
::method reference,Type::nameorvalue::name. - expr.member.closure-adaptExplicit closures can adapt method calls where a function value is needed.
struct Button:
on_click: fn(i32) -> i32
fn invalid(button: Button) -> i32:
button.on_click(41) # error: unknown-methodSee also: Method References.
#Indexing
- expr.index.order
receiver[index]evaluates the receiver, then the index, and invokes the receiver type's indexing behavior. - expr.index.expectedFor a
Listorstringreceiver, the index is checked withusizeas its expected type, so an integer literal index is ausize. - expr.index.expected.rangeFor such a receiver, an index that is a range expression gives each of its bounds
usizeas the expected type. Soitems[1..3]slices with aRange[usize]. - expr.index.negative-literalA negated literal index or slice bound, as in
items[-1]oritems[-1..], negates an unsigned value. It is an error. Error:type-mismatch.
fn demo(items: List[i32]) -> i32:
items[0] + items[1]#List Indexing
- expr.index.list.unsignedFor
List[T], an index must have an unsigned integer type, such asusize. A signed index is an error. Error:type-mismatch. - expr.index.list.rangeA list index must be non-negative and less than the list length.
- expr.index.list.panicA failed check causes the standard checked runtime panic.
- expr.index.list.readReading a list element yields its declared element type
T, whether the list is reached directly or through a reference. - expr.index.list.assignAssigning
items[index] = valuerequires a list in a writable place or reached through a&mutreference, and an in-range index.
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-mismatchWhy. An index is unsigned, so no index counts from the end. With negative indices counting from the end, as in Python,
items[i - 1]withi == 0would read the last element. Here the subtraction panics instead.
#Map Indexing
- expr.index.map.typeFor
Map[K, V], the index must have typeK. - expr.index.map.read-valueReading
entries[key]returnsV. When the map holds no equal key, the read is a checked runtime panic. Panic:index-out-of-bounds. - expr.index.map.value-typeThe read's type is the declared
V, whether the map is reached directly or through a reference. - expr.index.map.getThe optional read is the built-in method
get:entries.get(key)returnsV?, and.Nonemeans no equal key exists. - expr.index.map.assignAssigning
entries[key] = valuerequires a map in a writable place or reached through a&mutreference, and inserts or replaces the entry. - 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 absentWhy. A map read means one thing everywhere:
m[k],m[k] += v, andIndex::indexall readV, as a list index readsT. Rust and Python also panic or raise on a missing key and offergetfor the optional read.
See also: Built-In Methods.
#String Indexing
A string index reads one byte:
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- expr.index.string.byteFor
string,text[index]reads the byte at that byte offset, as au8, in constant time. - expr.index.string.offsetThe index counts bytes, not scalar values, so
"รฉ"[0]is0xC3, the first byte of its encoding. - expr.index.string.unsignedThe index must have an unsigned integer type, such as
usize. A signed index is an error. Error:type-mismatch. - expr.index.string.rangeThe index must be non-negative and less than the string's length in bytes.
- expr.index.string.panicA failed check causes the standard checked runtime panic, as a failed list index does.
- 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 |
- expr.index.slice.callFor a
Listorstringreceiver, an index of a range type is the callIndex::[K]::index(r, k)of an implementation that Built-In Implementations lists. - expr.index.slice.stringA string slice is the string of the bytes at the selected byte offsets.
- expr.index.slice.string.sharedA string slice shares the original string's bytes rather than copying them, as
module.string.slice.sharedstates forslice. - 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. - expr.index.slice.string.reversedA string slice whose start is greater than its end is a checked runtime panic. Panic:
index-out-of-bounds. - 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. - 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.
- expr.index.slice.list.type-valueA list slice of a
List[T]has typeList[T], theOutof its implementation. It is a new list value, so alet mutbinding of it may change it. Its elements keep the typeT. - 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. - expr.index.slice.list.reversedA list slice whose start is greater than its end is a checked runtime panic. Panic:
index-out-of-bounds. - expr.index.slice.no-storeNo range type has an
IndexSetimplementation forListorstring, so assigning to a slice is an error. Error:invalid-assignment-target. - expr.index.slice.no-map
Maphas no slicing: an index on a map is a key of typeK, whatever its type. - 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.
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-targetfn 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 9fn invalid(items: List[i32], start: i32) -> void:
a := items[-1..] # error: type-mismatch
b := items[start..] # error: type-mismatchWhy. A slice goes through
std.ops.Index, as Rust's does, so generic code bounded byIndex[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
viewmethod 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- expr.index.trait.std-ref
std.opsdeclaresIndex[K], with an associated typeOut, andIndexSet[K, V], whoseindex_settakes&mut self, as shown above. - expr.index.trait.read-otherFor a receiver whose type is not
List,Map, orstring, a type parameter included, readingr[k]is the callIndex::[K]::index(r, k). Its type is that implementation'sOut. - expr.index.trait.write-refAssigning
r[k] = vto such a receiver is the callIndexSet::[K, V]::index_set(&mut r, k, v). The call takes the address of the placer, as a&mut selfcall does byfn.method.auto-address. Whenris a&mutreference, the call passes it, so the store changes the value it refers to. - 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.
- expr.index.trait.no-useNeither call needs a
useof the trait. - expr.index.trait.mut-refSince
index_settakes&mut self, the receiver must be a writable place or a&mutreference. A store into any other receiver, such as a:=binding, a value parameter, or a read-only reference, is an error. Error:not-writable. - expr.index.trait.place
r[k]is a place only when the receiver's type implementsIndexSet. Assigning to it otherwise is an error. Error:invalid-assignment-target. - expr.index.trait.no-readReading
r[k]when the receiver's type has no fittingIndeximplementation is an error. Error:type-mismatch. - expr.index.trait.builtin-directA receiver whose type is
List,Map, orstringkeeps the built-in indexing above, even though these types implement the traits, as Built-In Implementations states. - expr.index.trait.independentThe two traits are independent: a type may implement either one alone.
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] |
- 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'sindexpanics when no equal key exists. - expr.index.std.map-store
Map'sindex_setinserts or replaces the entry, asentries[key] = valuedoes. - expr.index.std.string-no-index-set
stringimplements noIndexSet, so a bound such asIndexSet[usize, u8]rejects it. Error:unsatisfied-trait-bound.
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-boundNote. Through a bound,
items[k]has typeOut, 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)- expr.call.orderA call evaluates the callable first, then arguments from left to right.
- expr.call.callableThe callable may be any expression of function type, including a parenthesized field read such as
(handler.callback)(event). - expr.call.positional-firstPositional arguments must precede named arguments.
#Named Arguments
- expr.call.namedEach named argument identifies a parameter by its declared name.
- expr.call.exactly-onceA parameter must receive exactly one argument after defaults are applied.
- expr.call.duplicateSupplying one parameter more than once, such as positionally and again by name, is an error. Error:
duplicate-argument. - expr.call.source-orderEvaluation order follows source argument order, not parameter declaration order.
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 + twiceSee 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...))- expr.call.spreadAn argument ending in
...is a positional spread. - 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. - expr.call.spread.fillsA spread operand of type
Xis evaluated once and fills whatXdescribes, 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 |
- 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. - expr.call.spread.mismatchAn operand that does not meet the table's requirement is an error. Error:
type-mismatch. - 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. - expr.call.vararg-by-name.exclusiveA call that passes a vararg by name must not also supply positional values for that vararg.
- 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.
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-argumentA 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:
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-mismatchNote. For a function value of type
Fn[Args, O, $ R], the remaining parameters areArgs, sof(args...)withargs: Argscalls it. A suspending callee is writtenf!(args...).
See also: Varargs.
#Explicit Generic Arguments
Explicit generic arguments occur before the call argument list:
first::[string](names)- expr.call.generic.positionGeneric arguments, when explicit, occur before the call argument list.
- expr.call.generic.trailingAn explicit list may omit trailing arguments, which are inferred or defaulted as Explicit Type Arguments states.
- 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- expr.call.apply.std-ref
std.opsdeclaresApply, with an associated typeOut, andUpdate[V], whoseupdatetakes&mut self, as shown above. - expr.call.apply.zero-keysNeither trait takes a key: a callable value is called with no arguments.
- expr.call.apply.readWhen the callee's type is not a function type and implements
Apply, a type parameter included, the callv()is the callApply::apply(v). - expr.call.apply.read-typeThe type of
v()is that implementation'sOut. - expr.call.apply.argumentsA call that passes a callable value any argument, positional or named, is an error. Error:
argument-count. - expr.call.apply.write-refAssigning
v() = xis the callUpdate::[V]::update(&mut v, x), which takes the address of the placev, or passesvwhen it is a&mutreference, as an index store does byexpr.index.trait.write-ref. - expr.call.apply.choiceThe
Updateimplementation is chosen by the callee's type, then by the type ofx, as an index store's is. - expr.call.apply.no-useNeither call needs a
useof the trait. - expr.call.apply.place
v()is a place only when the callee's type implementsUpdate. Assigning to any other call, including a function call, is an error. Error:invalid-assignment-target. - expr.call.apply.mut-refA store through
v()changesvin place, sovmust be a writable place or a&mutreference. - 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. - expr.call.apply.not-callableCalling a value whose type is neither a function type nor implements
Applyis an error. Error:not-callable. - expr.call.apply.independentThe two traits are independent: a type may implement either one alone.
- expr.call.apply.fieldA callable value in a field is called by parenthesizing the field read, as in
(panel.count)(), asexpr.member.stored-fnstates for a stored function.
A value with Apply alone reads but does not store, and a callable value
takes no keys:
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-callableA 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:
fn invalid(count: Cell[i32]) -> void:
view := live(0)
view() = 1 # error: not-writable
count() = 2 # error: not-writableNote.
let mut count = live(0)is a writable place, so its stores change it.view := live(0)readsview()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,
[], soApplyandUpdatetake no keys, and a grid keepsg[(0, 1)].updatetakes&mut self, so a store through a cell changes a writable place or what a&mutreference refers to, as every other&mut selfcall does.
See also: Index Traits, Compound Assignment, Operator Traits.
#Suspension Calls
- expr.call.suspensionSuspension calls with
!construct and drive a child suspension as specified in Requirements and Suspension. - 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. |
- expr.try.onceThe operand is evaluated once.
- expr.try.no-panic
?does not catch runtime panics and does not interact with suspension by itself. - expr.try.expectedWhen
x?has an expected typeT, its operand gets an expected type as an inference hint. - expr.try.expected.resultThe hint is
Result[T, E]when the nearest function returnsResult[U, E]. - expr.try.expected.optionThe hint is
T?when the nearest function returns an optional. - 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
- expr.try.convertLet the nearest enclosing named function or closure return
Result[U, F]. The error of typeEbecomes the returned error by the first of these steps that applies.
The steps are:
- expr.try.convert.assignableAssignability. If
Eis assignable toFby one rule of Assignability And Coercion, the returned error is the error converted by that rule. - 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.
- expr.try.convert.fromConversion. Otherwise, if
FimplementsFrom[E], the returned error is the result of that implementation'sfromapplied to the error. - 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:
- expr.try.convert.assignable.covers.reference-valuesThe assignability step covers identical types, reference weakening, variance, supertrait widening and optional injection.
- expr.try.convert.no-importThe code using
?does not need to importFrom. - expr.try.convert.message.reference-valuesThe failed propagation's message should name
EandF. It should suggest an implementation ofFrom[E]forFor an explicit mapping of the error. - expr.try.convert.one-stepExactly one step converts the error.
- expr.try.convert.no-combine
?never combines an assignability rule with a conversion, and it never chains conversions. - expr.try.convert.no-chainWith
impl From[A] for Bandimpl From[B] for C, a?onResult[T, A]in a function returningResult[U, C]is an error. Error:invalid-result-propagation. - expr.try.convert.no-injectionWith the same implementations, a
?onResult[T, A]in a function returningResult[U, B?]is also an error. The conversion toBwould need an optional injection after it. Error:invalid-result-propagation. - expr.try.convert.single-rule.reference-valuesAssignability is itself one rule: an
Athat implements the traitTrdoes not propagate intoResult[U, *dyn Tr?]. - expr.try.convert.only-tryOnly
?calls a conversion.return .Err(error)and every other.Errconstruction use ordinary assignability. - expr.try.convert.before-deferThe conversion is part of the propagated value, so it runs before any deferred cleanup.
- expr.try.convert.pureA conversion neither suspends nor uses a requirement, because
fromhas neither. - 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,Eitself contributes to the inferred result type.
See also: Error Trait, Conversion Trait, Deferred Cleanup.
#Propagation Targets
- expr.try.target.nearest-functionPostfix
?is an error when its nearest enclosing named function or closure does not provide the required optional orResultreturn type. Error:invalid-result-propagation. - expr.try.target.module-top-levelModule top-level statements do not provide an implicit propagation target.
- 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. - expr.try.misuseEvery misuse of
?is an error. Error:invalid-result-propagation. - expr.try.misuse.operandAn operand that is neither optional nor a
Resultis a misuse. - expr.try.misuse.targetA
?whose enclosing function or closure does not return a compatible optional or aResultis a misuse. - expr.try.misuse.unconvertedA
Resulterror that neither conversion step converts to the enclosing error type is a misuse.
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")- expr.try.test.fixed-resultWhen a trailing block is the body of an
itcall, its result type is fixed rather than inferred. - expr.try.test.with-try.reference-valuesIf the block contains a
?outside any nested closure, its result type isResult[void, *dyn Error], where*dyn Erroris the erasedstd.error.Error. - expr.try.test.without-tryOtherwise its result type is
void. - expr.try.test.converts.reference-valuesAn error implementing
Errorpropagates into the test block's*dyn Error, sharing an existing reference or copying a value into a new object. An error that does not implementErroris rejected. Error:invalid-result-propagation. - 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(()). - 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 onit. Error:unsatisfied-trait-bound. - expr.try.test.explicit-result-errorAn explicit test body returning
Result[T, E]requiresT < TerminationandE < Error. Error:unsatisfied-trait-bound. - 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.
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 inferredanyerror!voidtest bodies do.
Note. The body closures of
it_each,it_prop, andit_prop_withget their result types by rules 2 and 3, bymodule.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
- 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 |
- expr.op.suspension-callSuspension-call postfix
!has the same precedence as an ordinary call. - expr.op.notPrefix
!is logical negation. - expr.op.not.positionPrefix
!is recognized only at the start of an operand. So it never conflicts with the suspension suffixf!(args)or thefn!type marker, which always follow a name orfn. - expr.op.not.combineThe two forms may combine: inside a suspending function,
!fetch!(id)negates theboolresult of the suspending callfetch!(id). - expr.op.not-equalBy longest match,
f!=glexes asf,!=,gand is an inequality comparison, not a suspension call. - expr.op.complement
~remains bitwise complement on integers. - expr.op.not.grouping
a && !b || cgroups as(a && (!b)) || c: prefix!binds tighter than every binary operator except**, and&&binds tighter than||. - expr.op.power.groupingExponentiation binds less tightly on its right than unary negation, following the grammar:
2 ** -3parses as2 ** (-3), while-2 ** 2means-(2 ** 2). - expr.op.power.grouping.signedWith an integer base,
2 ** -3is then rejected because its exponent is signed, as Exponentiation describes. - expr.op.no-chainComparisons do not chain.
- expr.op.prefix-referenceThe prefix
&and&mutare unary operators in the row of prefix!, and the prefix*binds tighter than**. Precedence states how they group.
Note. Write
low <= value && value < highrather thanlow <= value < high.
fn demo() -> i32:
2 + 3 * 4 # 14: * binds tighter than +#Arithmetic Operators
- expr.arith.primitive-numericBetween primitive operands, arithmetic operators require compatible numeric operands. Other operands use Operator Traits.
- expr.arith.non-numeric-no-implA binary
+,-,*,/,%, or**is an error when an operand is a non-numeric primitive, as intrue + false. It is also an error when no operator trait implementation fits, as in[1] * [2]. Error:type-mismatch. - expr.arith.string-primitive
string + stringis the only arithmetic form on non-numeric primitives. - expr.arith.definedMixed-width result types, overflow, division, and shifts are defined in Type System.
- expr.arith.castSigned/unsigned and integer/floating mixing requires an explicit cast.
- 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, bytypes.arith.checked. - expr.arith.int.divide
/truncates toward zero,%produces the corresponding remainder, and a zero divisor panics. - 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. - 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.
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
- expr.bit.primitive-integerBetween primitive operands,
~,&,|, and^accept integer values only and produce the operand common type. Other operands use Operator Traits. - expr.bit.non-integer-no-implA binary
&,|, or^with a primitive operand that is not an integer is an error.bool, floating-point, andstringoperands are examples. One with a non-primitive operand is an error too when no operator trait implementation fits. Error:type-mismatch.
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- expr.shift.unificationShifts are the exception to ordinary binary numeric unification.
- expr.shift.left-anyThe left operand may have any integer type, and the result has the left operand's type.
- expr.shift.count-unsignedThe right operand, the shift count, must have an unsigned integer type. Its type may be
u8,u16,u32,u64, orusize. - expr.shift.count-literalAn unsuffixed integer literal count has type
u32, sox << 3needs no suffix. - 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. - 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. - expr.shift.fixed-widthThe shift itself is a fixed-width bit operation; left-shifted high bits are discarded rather than reported as arithmetic overflow.
fn scale(x: i64, n: i32) -> i64:
x << n # error: type-mismatchWhy. 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'schecked_shl.
#Exponentiation
- expr.power.int.exponentFor an integer base,
**requires an exponent of an unsigned integer type, so a negative exponent cannot occur. - expr.power.int.literalAn unsuffixed integer literal in exponent position has type
u32. - expr.power.int.signedA signed integer exponent is an error. Error:
type-mismatch. - expr.power.negated-literalA negated literal is signed: in
2 ** -1the literal1is the operand of unary-, not the exponent itself. So-1has a signed type, and the expression is a compile-time error. Error:type-mismatch. - expr.power.negated-literal.not-otherThat error is not a runtime panic.
- expr.power.checkedInteger exponentiation uses checked multiplication in the base's result type.
- 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. - expr.power.float.powFloating
**computes IEEE 754-2019powas specified in clause 9.2, including its special cases, and rounds the result correctly to the destination format. - expr.power.mixedInteger and floating operands do not mix without an explicit cast; a mixed power expression is an error. Error:
type-mismatch. - expr.power.type-parameterFor a base of a type parameter, its bound picks the rule:
T < Integertakes the integer rules, andT < Floatthe float rules. Any other type parameter base is an error. Error:type-mismatch.
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-mismatchOpen in playground#Floating-Point Arithmetic
- expr.float.basicFloating
+,-,*, and/use the corresponding required IEEE 754 basic operation, including infinities, signed zero, and NaN. - expr.float.powerFloating
**uses thepowrule of Exponentiation. - expr.float.remainder-truncatedFloating
%returns the remainder of division truncated toward zero, as Cfmodand Rust%do. The result is exact and has the dividend's sign. - 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
- expr.logic.notPrefix
!requiresbool. - expr.logic.bool
&&and||requirebooloperands and producebool. - 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
- expr.eq.calls-eq
==callsEq.eqand!=negates that result. - expr.eq.stdStandard-library implementations provide value equality for primitives, optional and result values, tuples, lists, and maps when their elements support equality.
- expr.eq.map-orderMap equality is independent of entry order.
- expr.eq.no-implicitA user-defined struct or enum type has no implicit
Eqimplementation, even if all its members are comparable. Its author must explicitly implement or request derivation of the trait. - expr.eq.no-identity-fallbackEquality never silently falls back to reference identity.
- expr.eq.floatFloating-point equality follows IEEE 754, so NaN is unequal even to itself, although floating-point types implement
Eq. - expr.eq.std.intrinsicThe
Eqimplementations of the number types,char, andboolare intrinsic methods. The one forstringis not. - expr.eq.functionsFunction and closure values do not implement
Eq; applying==or!=to them is an error. Error:type-mismatch. - 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. - expr.eq.contextual-bothWhen both operands are contextual variants, neither has an expected type, so
.None == .Noneis an error. Error:missing-contextual-enum-type. - expr.eq.enum-hint
==or!=on an enum that does not implementEqis an error whose message suggests adding@derive(Eq)to the enum. Error:type-mismatch. - 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. - expr.eq.references
==and!=between two references are an error, byref.identity.no-equality. Error:reference-equality.
fn invalid(left: fn() -> void, right: fn() -> void) -> bool:
left == right # error: type-mismatchfn absent(found: i32?) -> bool:
found == .None
fn first(found: i32?) -> bool:
.Some(1) != found
fn neither() -> bool:
.None == .None # error: missing-contextual-enum-typeWhy. 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
- expr.ord.partial-cmp
<,<=,>, and>=usePartialOrd.partial_cmp. - expr.ord.stdThe standard library implements
PartialOrd.partial_cmpfor compatible numeric values, characters, strings, tuples, lists, and optionals. - expr.ord.std.char-scalarCharacters are ordered by Unicode scalar value.
- expr.ord.std.string-bytesStrings are ordered lexicographically by byte, which for valid UTF-8 is the order by scalar value.
- expr.ord.std.sequencesTuples and lists are ordered lexicographically.
- expr.ord.std.optionalOptionals are ordered with
.Nonebefore every present value. - expr.ord.compositeComposite ordering is available when the corresponding elements implement the comparison trait, and it stops at the first unequal or unordered element.
- expr.ord.userUsers can implement comparison traits for their own types.
- expr.ord.total
Ordis the total-order refinement; floating-point types havePartialOrdbut notOrdbecause NaN is unordered. - expr.ord.unorderedAn unordered comparison makes all four relational operators false.
- expr.ord.std.intrinsicThe
PartialOrdimplementations of the number types andchar, and theOrdimplementations of the integer types andchar, are intrinsic methods. Those forstringare 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 |
- expr.ord.unsigned-zero.lintA comparison in the table whose
thas an unsigned integer type produces a lint finding without changing program validity. Warning:unsigned-comparison-always. - expr.ord.unsigned-zero.literalThe
0of a form is any unsuffixed integer literal whose value is zero, in any radix, such as0or0x0. - expr.ord.unsigned-zero.binding-typeThe type of
tis its type at the comparison. That is a declared unsigned type, or theusizedefault of the literal its binding was initialized with. - expr.ord.unsigned-zero.messageThe warning must name the operand and say whether the comparison is always true or always false. One phrasing is "
tis unsigned, sot >= 0is always true". Whenthas theusizedefault, it should also suggest a signed literal such as+10or 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-alwaysWhy. With the
usizedefault,while t >= 0never ends normally:t - 1panics at zero instead. A countdown that means to reach-1writeslet t = +10.
#Identity
is compares identity without invoking user code.
- expr.is.no-user-code
iscompares identity without invoking user code. - expr.is.referencesFor two references,
iscompares 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
- expr.is.conversionConverting a reference to a trait value reference preserves the identity of the place it refers to.
- expr.is.function-noneA function value has no identity. This covers named functions, generic instantiations, one-payload variant constructors, and closures.
- 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.
- 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
- expr.is.tupleTuples have no identity, and using
iswith a tuple is an error even if it contains references. Error:identity-requires-references. - expr.is.value-operandPrimitive values, strings, enum values, optionals included, and function values likewise cannot be compared with
is: an operand type must implementAnyRef, notAnyVal. Error:identity-requires-references. - 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. - expr.is.compatible.trait-valueA trait value or
Anytype is also compatible with a type that converts to it. - expr.is.incompatible.dataTwo operands that are not compatible, such as
&Userand&Order, are an error. Error:incompatible-identity-operands. - expr.is.function.operandAn
iswith an operand whose static type is a function type is an error, as for a value type. Error:identity-requires-references. - expr.is.function.boundA function type does not implement
AnyRef, so generic code overT < AnyRefnever receives a function value. Error:unsatisfied-trait-bound.
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-referencesfn aliases() -> bool:
callback := fn() -> i32: 1
alias := callback
callback is alias # error: identity-requires-referencesNote. 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 + -refundstd.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 |
- expr.op.builtinArithmetic and bitwise operators are built in for the numeric types specified by this chapter and Type System.
- expr.op.concat
string + stringconcatenates strings. - expr.op.trait.std
std.opsdeclares the twelve operator traits in the table above. - expr.op.trait.shape-defaultA binary operator trait takes the right operand's type as its one argument
Rhs, whose default isSelf. It declares an associated typeOutand one methodfn m(self, rhs: Rhs) -> Self::Out. - expr.op.trait.rhs-selfSo
impl Add for MoneyimplementsAdd[Money], and the boundT < Add[Out = T]meansT < Add[T, Out = T]. - expr.op.trait.rhs-explicitThe explicit form, such as
impl Add[Money] for Money, stays valid and names the same trait. - expr.op.trait.unary-shape
NegandNottake no argument. Each declaresOutand one methodfn m(self) -> Self::Out. - 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.
- expr.op.primitive.callThe operator then calls the trait method that
expr.op.desugarnames for it, with the operands. Soa + b, with twoi64operands, callsAdd::[i64]::add(a, b). - expr.op.desugarOtherwise
a op bis the trait-qualified callOp::[R]::m(a, b)of the operator's trait, as inAdd::[R]::add(a, b). Likewise-aisNeg::neg(a)and~aisNot::not(a). - expr.op.no-useThe call needs no
useof the trait. - 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.
- expr.op.left-dispatch.exampleSo
price * 3checks3againsti64inMul[i64]. - 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. - 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. - expr.op.genericWhen an operand's type is a type parameter, the candidates come from its bounds and their supertraits.
- expr.op.outThe operator's result type is the chosen implementation's
Out. Implementations are unique per trait instantiation and target, soa + bhas one type. - expr.op.orderThe left operand is evaluated, then the right one, and then the method is called.
- 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 onlyimpl Mul[Money] for i64, writei64(3) * price. - expr.op.left-literal.no-fitWith only
impl Mul[i64] for Money, no width of3lets3 * pricetype-check, so it is an error; writeprice * 3. Error:type-mismatch. - expr.op.no-implAn operator for which no implementation fits is an error, and its message should name the missing trait. Error:
type-mismatch. - 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.
- expr.op.fixedOperator traits never change precedence or associativity, and they add no operator symbols.
- expr.op.not-overloaded
&&,||, prefix!, unary+,**,is,=,:=, and postfix?have no trait and keep their built-in meaning. - expr.op.comparison
==,!=, and the relational operators callEqandPartialOrd, as Equality and Ordering define.std.opsdeclares no comparison trait.
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 |
- 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.
- expr.op.std.string-not-intrinsic
impl Add for stringis an ordinary implementation, not an intrinsic method. - 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.
- expr.op.std.string-generic
impl Add for stringconcatenates, asstring + stringdoes, so generic code such asT < Add[Out = T]acceptsstring. - expr.op.std.bool-charThe standard library declares no operator trait implementation for
boolorchar.
Note. An implementation may compile an operator on primitive operands to inline code rather than a call, as
trait.impl.intrinsic.inlineallows. The result is the same.
Why. This is Rust's shape. The right operand is a trait argument, so a type may scale by
i64and add its own kind. As in Rust, the argument defaults toSelf, so the common same-type case omits it. The output is an associated type that the operands fix, sox := a + bnever 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)- expr.pipe.formA pipe expression is a value, the
|>token, and a step. - expr.pipe.left-assoc
|>is left-associative:a |> f |> gis(a |> f) |> g. - expr.pipe.precedence
|>binds more loosely than|and more tightly than comparison, son + 1 |> twice == 4is((n + 1) |> twice) == 4. - expr.pipe.step-kindsA step is either a substitution step, which contains
_, or a bare step, which is a name or path without_. - expr.pipe.orderThe piped value is evaluated first and exactly once, before any part of the step, including the step's callee.
- 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- expr.pipe.slot.oneA substitution step contains exactly one
_, which stands for the piped value. - expr.pipe.slot.anyThe step may be any expression at its precedence, not only a call, such as
_.len(),_ * 2, orPoint { x: _, y: 0 }. - expr.pipe.slot.meaningThe step is evaluated as if
_were a name bound to the piped value, with that value's type and access. - expr.pipe.slot.suffixSuffixes in the step apply inside it: in
x |> parse(_)?,?propagates fromparse(x), andx |> load!(_)is a suspension call. - expr.pipe.slot.duplicateA step with two or more
_is an error. Error:duplicate-pipe-placeholder. - expr.pipe.slot.closureA
_inside a closure nested in the step is an error. Error:pipe-placeholder-in-closure. - expr.pipe.slot.nestedA
_belongs to the innermost pipe step that contains it, and the rules above count only a step's own_. Inx |> f(_, y |> g(_)), the second_belongs tog(_), so the outer step has one_.
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-closureWhy.
_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- expr.pipe.bare.formA bare step is an identifier, or identifiers joined by
., with no suffix after it. - expr.pipe.bare.call
value |> pathevaluates as the callpath(value), except thatvalueis evaluated first. - expr.pipe.bare.methodWhen the path's prefix names a value, the call is a method call on that value:
x |> user.greetmeansuser.greet(x). - 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. - expr.pipe.bare.needs-placeholderA step without
_that is not a bare step is an error. Error:pipe-step-needs-placeholder. - expr.pipe.bare.needs-placeholder.formsThat covers a call such as
x |> f(y), an index such asx |> f[0], type arguments such asx |> parse::[i32], and a suffix such asx |> f?. - expr.pipe.bare.method-referenceA method reference without type arguments, such as
Config::parse, is also a bare step:raw |> Config::parsemeansConfig::parse(raw).
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-stepWhy. Elixir reads
x |> f(y)asf(x, y), and F# reads it asf(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
- 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. - expr.pipe.single-line.inlineA same-line closure or conditional inside a step is valid.
- 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.positionsays. - 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.
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
- expr.pipe.placeholder-only
_has an expression meaning only in a pipe step. - expr.pipe.placeholder-outside
_as an expression anywhere else is an error. Error:placeholder-outside-pipe. - expr.pipe.no-partialhd has no function placeholder: the partial application
f(_, a)does not create a function.
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-pipeNote. 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- expr.range.declared.struct
std.opsdeclares 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. - expr.range.value.fieldsA range expression builds the range type of its form, with its bounds as the fields:
a..bisRange { start: a, end: b, inclusive: false }. - expr.range.inclusive-fieldThe
inclusivefield of aRangeorRangeToistruefor a..=form andfalsefor a..form, so..=bisRangeTo { end: b, inclusive: true }. - 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. - expr.range.orderA range expression evaluates its start bound, then its end bound.
- expr.range.no-checkBuilding a range never compares its bounds, so
5..2is a valid, empty range. - expr.range.bound.numericEach bound must have an integer or a floating-point type, or be a type parameter bounded by
IntegerorFloat. A bound of any other type is an error. Error:type-mismatch. - expr.range.bound.operandsThe two bounds of
a..bora..=bare 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. - expr.range.bound.signednessA signed and an unsigned bound are an error, as for a binary numeric operator. Error:
mixed-signedness. - expr.range.element-typeThe range's element type
Tis the bounds' common type, or the one bound's type fora..,..b, and..=b.RangeFullhas no bound and no element type. - expr.range.expectedAn expected range type gives each bound its element type as the bound's expected type, so
let r: Range[i64] = 0..10hasi64bounds. - expr.range.literal-boundsWith no expected type, literal bounds are one literal class:
0..3is aRange[usize], and-3..3is aRange[i32]. - expr.range.float-literal-boundsFloat literal bounds default as a float literal does, so
0.0..=1.0and0.5..2.0areRange[f64]. Bounds of typef32make aRange[f32].
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-mismatchNote.
foriteratesa..b,a.., anda..=bof 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. Amatchtakes the range forms but..band..as range patterns.
Why. As in Rust, a range is an ordinary
std.opsvalue. One syntax then serves loops, slicing, and any function that takes a range, with no separaterangefunction.
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 likec.int(1..=6). Rust's range expressions also take float bounds and do not iterate them.
Why.
a..banda..=bshare one type, as..band..=bdo, so a function that takes aRange[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)- expr.bind.one-name
:=introduces one inferred, non-reassignable name and evaluates to the initializer's value. - expr.bind.precedence
:=has the lowest precedence. - expr.bind.parensParentheses are required when a binding appears as an operand of another expression, as above.
Note. Destructuring is a
letstatement, as inlet (a, b) = pair, never a:=binding, asgrammar.stmt.short-binding.let-onlystates.
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)]- expr.comp.listA list comprehension evaluates clauses from left to right and appends one result for every path that reaches
=>. - expr.comp.shapeA comprehension is equivalent in iteration shape to nested loops, with an
ifclause filtering at the exact point where it appears. - 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}- expr.comp.mapA map comprehension evaluates the key and then the value for every successful path.
- expr.comp.map.duplicateDuplicate keys use the later generated value.
#Comprehension Restrictions
- expr.comp.eagerComprehensions are eager.
- 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. - 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.
- expr.comp.no-jumps
return,break, andcontinueare not valid inside a comprehension. - expr.comp.no-letThere is no comprehension
letclause.
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-suspensionNote. 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
- expr.closureClosures are expressions described in Functions.
- expr.control
if,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
- expr.unsupported.custom-operatorshd-lang has no user-defined operator symbols, and no overloading of the operators that
expr.op.not-overloadedlists. - expr.unsupported.chaininghd-lang has no comparison chaining.
- expr.unsupported.any-fallback.reference-valueshd-lang has no fallback conversion of heterogeneous literals to
&dyn Any. - expr.unsupported.try-mappingPostfix
?has no mapping clause. - expr.unsupported.try-mapping.explicitA site that needs a different error conversion maps the
Resultexplicitly before?. One way is a function that takes a single-payload variant constructor as its mapper.
fn inside(value: i32) -> bool:
0 < value < 10 # error: comparison-chainingSee also: Enum Declarations.