Scope and ownership
СсылкаThis page owns block execution, conditional and iterative control flow, labels, explicit transfers, and deferred cleanup.
Pattern syntax belongs to Pattern matching. Range operators belong to Expressions and operators. Function return inference belongs to Functions and generics. Collection construction and comprehensions belong to Collections and comprehensions. Concurrent timeout behavior and CLI lifecycle are outside this page.
Control-flow syntax
СсылкаBlock = "{" Statement* Expression? "}" ;IfExpression = "if" IfHead Block ("else" (IfExpression | Block))? ;IfHead = ControlOperand | "let" Pattern "=" ControlOperand ;WhileStatement = "while" WhileHead Block ;WhileHead = ControlOperand | "let" Pattern "=" ControlOperand ;ForExpression = "for" Pattern "in" ControlOperand Block ;LoopExpression = "loop" Label? Block ;BreakStatement = "break" Label? Expression? ;ContinueStatement = "continue" Label? ;ReturnStatement = "return" Expression? ;DeferStatement = "defer" Block ;ControlOperand = "(" Expression ")" ;
This is a manually transcribed source-syntax excerpt. Statement boundaries belong to Lexical structure. Patterns belong to Pattern matching. Call arguments belong to Functions and generics.
A label starts with an apostrophe, as in loop 'outer { ., break 'outer 7, or continue 'outer. The label follows loop, break, or continue.
The iterator in for item in (expression) { . can be a range. In for n in (0., by 2 is part of the range expression. Range construction and bounds belong to Expressions and operators.
Blocks and final values
СсылкаBlock execution and final value
On normal completion, a block executes its statements in source order. It then evaluates its optional final expression and produces that value. Trailing line breaks, blank lines, and comments preserve that final value. Without a final expression, it produces (). Write let _ to evaluate an expression once without retaining its value. A final discard binding leaves the block without a tail, so the block produces (). A statement separator never consumes a value.
Причина
A block has one final-value rule. Formatting the closing delimiter, adding blank lines, or ending a line comment cannot consume that value. A discard binding names the intention to run an expression once and finish with unit. Statement and control-flow effect contexts retain their existing result rules.
If, else, and if let
СсылкаChoosing an if branch
An if condition has type bool. A true condition selects the then branch. A false condition selects the else arm when present and otherwise skips the then branch. An else arm can be a block or another if expression. The unselected branch is not evaluated. An if expression with an else arm produces the selected branch's value. Without an else arm, the expression always has the unit type and produces () on normal completion, including in a value position. A selected then branch runs for its effects and its value is discarded.
Причина
Boolean conditions avoid implicit truth conversions. An if without else has the same unit result in statement and value positions.
Conditional pattern binding
if let evaluates its scrutinee once. A successful pattern selects the then block and makes its bindings available in that branch. Failure selects the else arm when present. The result follows the if expression result rules. Without an else arm, the expression has the unit type and produces () on normal completion whether the pattern matches or fails. A selected then block discards its value when the else arm is absent.
Причина
Evaluating the scrutinee once avoids repeating its effects. Keeping bindings in the successful branch prevents access to values from a failed match.
While, while let, and for
СсылкаTesting before each while iteration
A while loop tests its bool condition before each body execution. A true condition runs the body, and a false condition finishes the loop. A continue targeting this loop resumes at the next condition test. A break targeting it transfers control after the loop.
Причина
Testing before the body lets an initially false condition skip the loop. Testing again after continue preserves the condition for every iteration.
Matching before each while let iteration
while let evaluates its scrutinee before each body execution. A successful pattern runs the body with its bindings. Pattern failure finishes the loop. A continue targeting this loop resumes at the next scrutinee evaluation.
Причина
A fresh match checks each iteration against the current value. Its bindings describe the value that admitted that iteration.
Statement and value uses of for
A for expression evaluates its iterator expression once, visits elements in iterator order, and uses its pattern to bind each element.
Every for runs its body for effects, discards each completed body value, and produces (). This type and its break and continue targets stay the same in a statement, parentheses, initializer, argument, or function tail. An empty iterator also produces ().
Construct an array with [for item in (values) => expression]. Set and map comprehensions use their corresponding collection delimiters. A for expression does not collect body results. Requiring an array where a for produces () is an ordinary type mismatch.
Iterable admission and element-pattern failure are specified below.
Причина
A for expression performs iteration for effects, while a comprehension constructs a collection. Giving every for the same unit result prevents parentheses, binding, and argument position from changing its operation or loop targets. The existing collection delimiters make collection construction explicit. Collecting values with an early stop has a different control-flow intent. Use an accumulator and break for that case. A comprehension expresses collection that runs to completion.
Iterable admission and element-pattern failure
For expressions admit Array, Set, Range and bind their element type in every context. Other resolved concrete types are rejected with TPZ5001. Iterate map keys or entries explicitly. Convert a string with scalars() or graphemes() to choose the text unit.
A generic, foreign, or unresolved inferred type defers the element decision. At runtime the value must still be an array, set, or range. Array and set iteration traverse the collection value at entry under collection value semantics.
A refutable element pattern must match each visited element. A miss raises a runtime type fault before the body executes for that element.
Причина
The collection value at entry keeps an iteration stable when the source variable changes. A failed element pattern faults before the body can use missing bindings.
Loop labels, break, continue, and return
СсылкаSelecting a break or continue target
break, continue can include a target label. Lookup stays inside the current function and respects concurrent arm boundaries. An unlabeled transfer selects the innermost loop frame. A labeled transfer selects the nearest enclosing loop with that label. Targets include while loops, for expressions in every context, and loop expressions.
A transfer without an enclosing loop is rejected during checking with TPZ5001. An unknown loop label is rejected during checking with TPZ5720.
Причина
Function boundaries prevent loop transfers from escaping into a caller. A label makes the intended enclosing loop visible when loops are nested.
Leaving a loop with a value
A loop label appears after loop. A break targeting the loop completes it with the evaluated break operand, or () when the operand is absent. A continue targeting it repeats the body. Without a contextual result type, break types must be assignable in at least one direction after widening. Disagreement is rejected during checking with TPZ5721.
Причина
The break operand supplies the result at the exit point. Checking its type against other exits gives the loop a usable result type.
Returning from a function
A return is a statement and requires a function context. Its target cannot cross a concurrent arm boundary. Its value expression is evaluated before cleanup. A return without a value supplies (). Cleanup for crossed scopes runs before control transfers to the enclosing function's caller. A return outside a function is rejected during checking with TPZ5001.
A return inside a lambda ends that lambda invocation. The same callable boundary determines lambda and callback propagation.
Причина
Evaluating the return value before cleanup preserves the selected result when cleanup changes state. The enclosing function fixes the transfer destination.
Break values in while and for loops
A break targeting a while loop or for expression evaluates its operand once before cleanup. The operand result is discarded. The target loop completes with (). A break without an operand follows the same cleanup and exit order. These rules also apply when the for expression is grouped or bound to a name.
Причина
Evaluating the operand before cleanup preserves its effects. All for expressions and while loops complete with unit, so grouping or binding does not change the meaning of a break. Cleanup order remains consistent with return and value-producing break.
Deferred cleanup
СсылкаRegistering a deferred block
A defer operand must be a block. A call without a block and other operands are rejected during parsing with TPZ2004. Write defer { cleanup() } to defer a call. The block is registered when execution reaches the defer statement and runs during cleanup.
Причина
A block gives a single call and a sequence of cleanup steps the same syntax. Ordinary block rules determine how the action runs. A call form would evaluate its arguments when cleanup is registered, although cleanup happens when the scope exits. A block keeps evaluation at cleanup time. Use let to capture a value earlier when that is intended.
Cleanup order on normal completion and control transfer
A reached defer belongs to its enclosing block. Cleanup runs on normal block completion and when return, break, continue, or an Err propagated by ? leaves that scope. An Ok operand of ? continues without leaving the scope or running its cleanup.
Actions run in reverse registration order within each scope. Crossed scopes are cleaned up from innermost to outermost. The block or transfer result is evaluated before cleanup. An ordinary runtime fault follows the fault cleanup rule.
Deadline abandonment follows abandoned arm cleanup. Remaining language cleanup is skipped and runtime handle reclamation belongs to the group.
Причина
An Err propagated by ? leaves scopes just as an explicit return does. A common cleanup order keeps resources available until their dependents finish.
Runtime faults skip remaining cleanup
An ordinary runtime fault is an uncatchable termination. Remaining defer blocks and using cleanup do not run. Normal completion and structured exits retain the defer order.
Normal completion and structured exits follow the defer order.
Deadline abandonment follows abandoned arm cleanup. Remaining language cleanup is skipped and runtime handle reclamation belongs to the group.
Причина
A single fault outcome makes output reproducible. Skipping cleanup also prevents a second cleanup fault after termination.
Complete examples and expected results
СсылкаEach example below is a complete file. Its expected result is specified before execution.
A block value and a selected branch
Ссылкаimport std.iofunction choose() -> int {let value = {let base = 4base + 3}if (value > 5) {value * 2} else {0}}io.writeLine("{choose()}")
14
Ожидаемый результат
Ожидаемый результат
Выполнение завершается. Код завершения 0.
14
Наблюдаемые утверждения · Пройдено · 5.21.3
Код завершения 0.
14
Нет вывода ошибок.
- Пройдено · expected 0; observed 0
- Пройдено · stdout matches the authored expectation
- Пройдено · stderr matches the authored expectation
- Пройдено · stdout matches the authored expectation
Expected stdout is 14 followed by a newline.
Using if let and while let
Ссылкаimport std.iolet mut next: Option<int> = Some(3)if let Some(value) = (next) {io.writeLine("ready: {value}")} else {io.writeLine("empty")}while let Some(value) = (next) {io.writeLine("{value}")next = None()}io.writeLine("done")
ready: 3
3
done
Ожидаемый результат
Ожидаемый результат
Выполнение завершается. Код завершения 0.
ready: 33done
Наблюдаемые утверждения · Пройдено · 5.21.3
Код завершения 0.
ready: 33done
Нет вывода ошибок.
- Пройдено · expected 0; observed 0
- Пройдено · stdout matches the authored expectation
- Пройдено · stderr matches the authored expectation
- Пройдено · stdout matches the authored expectation
The while-let body changes the next scrutinee to None(). The following test ends the loop.
Continue in while and break in for
Ссылкаimport std.iolet mut tick = 0while (tick < 3) {tick += 1if (tick == 2) {continue}io.writeLine("{tick}")}for n in (0..<8 by 2) {if (n == 4) {break}io.writeLine("{n}")}io.writeLine("done")
1
3
0
2
done
Ожидаемый результат
Ожидаемый результат
Выполнение завершается. Код завершения 0.
1302done
Наблюдаемые утверждения · Пройдено · 5.21.3
Код завершения 0.
1302done
Нет вывода ошибок.
- Пройдено · expected 0; observed 0
- Пройдено · stdout matches the authored expectation
- Пройдено · stderr matches the authored expectation
- Пройдено · stdout matches the authored expectation
- Пройдено · stdout matches the authored expectation
Both loops are used for their effects. The range syntax and step are defined under Expressions and operators.
Breaking out of an enclosing labeled loop
Ссылкаimport std.iolet result: int = loop 'outer {loop {break 'outer 7}}io.writeLine("{result}")
7
Ожидаемый результат
Ожидаемый результат
Выполнение завершается. Код завершения 0.
7
Наблюдаемые утверждения · Пройдено · 5.21.3
Код завершения 0.
7
Нет вывода ошибок.
- Пройдено · expected 0; observed 0
- Пройдено · stdout matches the authored expectation
- Пройдено · stderr matches the authored expectation
- Пройдено · stdout matches the authored expectation
The label selects the enclosing loop. The break supplies its result.
Running deferred actions before returning
Ссылкаimport std.iofunction result() -> int {io.writeLine("value")7}function finish() -> int {defer { io.writeLine("first") }defer {io.writeLine("second")}io.writeLine("body")return result()}io.writeLine("{finish()}")
body
value
second
first
7
Ожидаемый результат
Ожидаемый результат
Выполнение завершается. Код завершения 0.
bodyvaluesecondfirst7
The return operand runs before cleanup. Deferred blocks run in reverse registration order.
Cleaning up scopes during error propagation
Ссылкаimport std.iofunction fail() -> Result<int, string> {io.writeLine("operand")Err("stop")}function finish() -> Result<int, string> {defer { io.writeLine("outer") }let value = {defer { io.writeLine("inner-first") }defer { io.writeLine("inner-second") }fail()?}io.writeLine("unreachable")Ok(value)}io.writeLine(match (finish()) {case Ok(value) => "value={value}"case Err(error) => error})
operand
inner-second
inner-first
outer
stop
Ожидаемый результат
Ожидаемый результат
Выполнение завершается. Код завершения 0.
operandinner-secondinner-firstouterstop
The operand runs once before cleanup. Error propagation runs inner defers in reverse order, then the outer defer, before returning the error.
Rejecting an invalid deferred expression
Ссылкаdefer 5
Ожидаемый результат
Ожидаемый результат
Этап отклонения parse. Ненулевой код завершения.
defer-body-not-block at
example.tpz:1:7
The integer operand is rejected at the start of 5. Replace it with a block that performs cleanup.
Break operands run before cleanup in statement loops
Ссылкаimport std.iofunction operand(label: string) -> int {io.writeLine(label)7}while (true) {defer { io.writeLine("while cleanup") }break operand("while value")}for item in ([1, 2]) {defer { io.writeLine("for cleanup") }break operand("for value")}io.writeLine("done")
while value
while cleanup
for value
for cleanup
done
Ожидаемый результат
Ожидаемый результат
Выполнение завершается. Код завершения 0.
while valuewhile cleanupfor valuefor cleanupdone
The expected result follows from current SELF source. Execution against public Topaz 5.21.3 is pending CI.
Break in a bound for preserves its unit type
Ссылкаlet result: () = for item in ([1]) {break 7}
Нет стандартного вывода.Ожидаемый результат
Ожидаемый результат
Проверка принимает программу. Код завершения 0.
The explicit unit annotation also applies when the for is a binding initializer. The break operand does not become a collected element.
Arrays, sets, ranges, and matching element patterns
Ссылкаimport std.iofor item in ([1, 2]) {io.writeLine("array {item}")}for item in (set { 3, 1, 3 }) {io.writeLine("set {item}")}for item in (2..4) {io.writeLine("range {item}")}for Some(item) in ([Some(7), Some(8)]) {io.writeLine("matched {item}")}
array 1
array 2
set 3
set 1
range 2
range 3
range 4
matched 7
matched 8
Ожидаемый результат
Ожидаемый результат
Выполнение завершается. Код завершения 0.
array 1array 2set 3set 1range 2range 3range 4matched 7matched 8
The expected result follows from current SELF source. Execution against public Topaz 5.21.3 is pending CI.
A string must be converted to scalar elements before iteration
Ссылкаfor item in ("hi") {}
Ожидаемый результат
Ожидаемый результат
Этап отклонения check. Ненулевой код завершения.
for-non-iterable-type at
example.tpz:1:14
The expected result follows from current SELF source. Execution against public Topaz 5.21.3 is pending CI.
An omitted else discards values and preserves selected effects
Ссылкаimport std.iolet taken = if (true) {io.writeLine("then")7}let skipped = if (false) {io.writeLine("unexpected")9}io.writeLine("{taken}")io.writeLine("{skipped}")
then
()
()
Ожидаемый результат
Ожидаемый результат
Выполнение завершается. Код завершения 0.
then()()
An omitted else cannot produce an int result
Ссылкаlet value: int = if (true) { 7 }
Нет стандартного вывода.Ожидаемый результат
Ожидаемый результат
Этап отклонения check. Ненулевой код завершения.
if-without-else-result-type-mismatch at
example.tpz:1:18
Rejecting a deferred call without a block
Ссылкаimport std.iofunction finish() -> () {defer io.writeLine("cleanup")}finish()
Ожидаемый результат
Ожидаемый результат
Этап отклонения parse. Ненулевой код завершения.
defer-body-not-block at
example.tpz:4:11
Wrap the cleanup call in a defer block. The current SELF parser still accepts this removed form.
Running a deferred block at scope exit
Ссылкаimport std.iofunction finish() -> () {defer { io.writeLine("cleanup") }io.writeLine("body")}finish()
body
cleanup
Ожидаемый результат
Ожидаемый результат
Выполнение завершается. Код завершения 0.
bodycleanup
The body prints before the registered cleanup block runs.
Returning through a match arm block
Ссылкаimport std.iofunction choose(flag: bool) -> int {while (true) {let value = match (flag) {case true => { return 7 }case false => 3}return value}0}io.writeLine("{choose(true)}")io.writeLine("{choose(false)}")
7
3
Ожидаемый результат
Ожидаемый результат
Выполнение завершается. Код завершения 0.
73
The true arm returns from the function through the surrounding binding and loop. The false arm supplies a value to the ordinary return statement.
For results stay unit in grouped and bound positions
Ссылкаimport std.iofunction items(label: string) -> Array<int> {io.writeLine(label)[1, 2]}let bound = for item in (items("bound")) {io.writeLine("{item}")item * 2}let grouped = (for item in (items("grouped")) {io.writeLine("{item}")item * 3})let emptyItems: Array<int> = []let empty = for item in (emptyItems) { item }io.writeLine("{bound} {grouped} {empty}")
bound
1
2
grouped
1
2
() () ()
Ожидаемый результат
Ожидаемый результат
Выполнение завершается. Код завершения 0.
bound12grouped12() () ()
The iterator calls and body output establish evaluation order. Binding, parentheses, and an empty iterator all retain the unit result.
Break and continue survive grouping and binding
Ссылкаimport std.iofunction grouped() -> () {(for item in ([1, 2, 3, 4]) {if (item == 2) { continue }if (item == 4) { break }io.writeLine("grouped {item}")})}grouped()let result = for item in ([1, 2, 3, 4]) {if (item == 2) { continue }if (item == 4) { break }io.writeLine("bound {item}")}io.writeLine("{result}")
grouped 1
grouped 3
bound 1
bound 3
()
Ожидаемый результат
Ожидаемый результат
Выполнение завершается. Код завершения 0.
grouped 1grouped 3bound 1bound 3()
Both loops skip item 2 and stop before item 4. Grouping a for at a function tail and binding its result preserve the same loop targets.
For cannot supply an array result
Ссылкаlet values: Array<int> = for item in ([1, 2]) { item * 2 }
Нет стандартного вывода.Ожидаемый результат
Ожидаемый результат
Этап отклонения check. Ненулевой код завершения.
for-result-type-mismatch at
example.tpz:1:26
A for expression has type (). An array annotation therefore rejects this initializer. Use an array comprehension to collect the body values.
Collect values with an array comprehension
Ссылкаimport std.iolet values: Array<int> = [for item in ([1, 2]) => item * 2]io.writeLine("{values[0]} {values[1]}")
2 4
Ожидаемый результат
Ожидаемый результат
Выполнение завершается. Код завершения 0.
2 4
The collection delimiters request array construction. The body supplies one element per visited input.
Trailing layout preserves values and discard evaluates once
Ссылкаimport std.iofunction inlineTail() -> int { 7 }function linesTail() -> int {7}function commentedTail() -> int {7 // Trailing layout preserves this value.}function mark(value: int) -> int {io.writeLine("mark {value}")value}let discarded: () = {let _ = mark(9)}let bound: int = { 11 }io.writeLine("{inlineTail()}|{linesTail()}|{commentedTail()}|{bound}")io.writeLine("done")
mark 9
7|7|7|11
done
Ожидаемый результат
Ожидаемый результат
Выполнение завершается. Код завершения 0.
mark 97|7|7|11done
A unit context accepts successful and failed if let expressions without else
Ссылкаlet takenInput: int = 7let skippedInput: int = 8let taken: () = if let 7 = (takenInput) { 9 }let skipped: () = if let 7 = (skippedInput) { "discarded" }
Нет стандартного вывода.Ожидаемый результат
Ожидаемый результат
Проверка принимает программу. Код завершения 0.
An if let without else discards branch values and preserves selected effects
Ссылкаimport std.iolet takenInput: int = 7let skippedInput: int = 8let taken = if let 7 = (takenInput) {io.writeLine("then")9}let skipped = if let 7 = (skippedInput) {io.writeLine("unexpected")"discarded"}io.writeLine("{taken}")io.writeLine("{skipped}")
then
()
()
Ожидаемый результат
Ожидаемый результат
Выполнение завершается. Код завершения 0.
then()()
A runtime fault leaves defer output absent
Ссылкаimport std.iofunction divide(divisor: int) -> int {defer { io.writeLine("cleanup") }io.writeLine("before fault")12 / divisor}io.writeLine("{divide(0)}")
before fault
Ожидаемый результат
Ожидаемый результат
Этап отклонения run. Ненулевой код завершения.
diagnostic-tpz4002 at
example.tpz:5:5
before fault
The registered defer is not executed. The program prints before fault, then terminates at the division.
Normal completion runs the registered defer
Ссылкаimport std.iofunction divide(divisor: int) -> int {defer { io.writeLine("cleanup") }io.writeLine("before result")12 / divisor}io.writeLine("{divide(2)}")
before result
cleanup
6
Ожидаемый результат
Ожидаемый результат
Выполнение завершается. Код завершения 0.
before resultcleanup6
Related rules and diagnostics
СсылкаSee Pattern matching for pattern forms and matching, Expressions and operators for range expressions, and Functions and generics for function declarations and return inference.