Scope and ownership
LinkThis page targets Topaz 5.21 language mode and the public 5.21.3 CLI. The rules cite SELF source and its product runtime at af88afbc1d99a8c0220b5984f9d74f895f8156f2.
This page owns the concurrent forms, arm results, completion, failure, and timeout behavior. Functions and generics owns calls. Blocks and control flow owns general control transfer. Bindings and scope owns captures and mutation. Failure and propagation owns language-level propagation. Thread and scheduler implementation details are outside this page.
Concurrent forms
LinkConcurrent = Join | Timed ;Join = "concurrent" "{" ArmSequence? "}" ;Timed = "concurrent" "(" "timeout" ":" Expression ")""{" ArmSequence? "}" "else" Block ;Arm = Identifier ":" Expression ;DurationLiteral = DecimalInteger DurationUnit ;DurationUnit = "ms" | "s" | "m" ;
This grammar imports separators, expression grouping, blocks, and decimal integer spelling from their language chapters. Arm names the arm sequence.
Source evidence
- parseConcurrentArm, parseConcurrentArms, parseConcurrentElse, parseConcurrent at
af88afbc1d99· 782-907 - parseConcurrentTimeout at
af88afbc1d99· 722-780 - decodeDurationMilliseconds at
af88afbc1d99· 70-83 - pull/41 at
271329b482bc - pull/41 at
271329b482bc
Named arm expressions
Each concurrent arm has the form name: expression, where name is an identifier.
Why
Arm labels become record fields. The same name: value shape connects each computation to the field that will hold its result.
Source evidence
- parseConcurrentArm, parseConcurrentArms, parseConcurrentElse, parseConcurrent at
af88afbc1d99· 782-907
Concurrent groups may contain zero arms
A concurrent group may contain zero arms. An untimed empty group completes immediately with the zero-field structural record. Its type is written {} in type positions, and it is distinct from unit (). The ordinary empty expression block {} retains its unit result.
A timed empty group evaluates its Duration expression exactly once before the group, then follows the same deadline/completion arbitration as every timed group. A zero deadline selects the else block. The fallback must satisfy the same zero-field record result type. An untimed concurrent {} can supply that value.
Why
Each concurrent arm contributes one named result field. With no arms, the result is the zero-field structural record, and an untimed group is already complete. The matching empty structural type can be written explicitly so adding a type annotation does not change which result is expressible. Timed empty groups retain the existing deadline priority, including a zero deadline.
Source evidence
- pull/41 at
271329b482bc - parseConcurrentArms rejects zero arms at
271329b482bc· 801-841 - inferConcurrentExpression constructs structural(names, values) at
271329b482bc· 601-681 - ConcurrentJoin returns a record when no pending arms remain at
271329b482bc· 135-170 - empty pending collection skips the expired-deadline branch at
271329b482bc· 240-270 - issues/31 at
570530e69141
Timeout and fallback appear together
The concurrent(timeout: . form requires an else block. An else block requires the timeout form. Either missing partner is rejected with TPZ2005.
Why
A timed group can finish without a complete result record. A fallback supplies the value or control transfer for that path.
Source evidence
- parseConcurrentArm, parseConcurrentArms, parseConcurrentElse, parseConcurrent at
af88afbc1d99· 782-907
A timeout accepts a Duration expression
The timeout operand is an ordinary expression whose resolved type is Duration. Parentheses, bindings, calls, and conditionals obey the same expression and type rules. An int or float does not implicitly convert to Duration. An incompatible operand produces TPZ5001 during checking. Evaluate the operand exactly once, before starting its deadline or any arm. If that evaluation fails, propagate its original failure and location without starting an arm or evaluating the fallback. After it succeeds, start the deadline and apply the existing completion/deadline arbitration. A zero deadline wins even against immediately ready arms.
Why
A duration keeps its value and type when it is parenthesized, bound to a name, returned from a function, or selected by an expression. Checking that value against Duration gives timeout one ordinary expression rule. Evaluating it once before the group starts prevents duplicated effects and keeps computing a budget outside the budget it establishes.
Source evidence
- pull/41 at
271329b482bc - parseConcurrentTimeout and ordinary duration rejection at
271329b482bc· 722-780,1254-1259 - literal-only timeout lowering at
271329b482bc· 1550-1588 - inferConcurrentExpression, durationFitsMilliseconds at
af88afbc1d99· 601-687 - issues/31 at
570530e69141
Arms and result fields
LinkDistinct arm names
Arm names in one concurrent expression must be distinct. A repeated arm name is rejected with TPZ5008.
Why
Each arm supplies one result field. Repeated names would make that field ambiguous or silently discard an arm value.
Source evidence
- inferConcurrentExpression, durationFitsMilliseconds at
af88afbc1d99· 601-687
The result record type
When the arm result types are known, a concurrent expression has a structural record type. Each arm name becomes a field name. Each field type is the widened type inferred for its arm expression.
Why
Named fields preserve which computation produced each value regardless of completion order. Ordinary record access can then read the results.
Source evidence
- inferConcurrentExpression, durationFitsMilliseconds at
af88afbc1d99· 601-687
Arm names, enclosing scope, and visible effects
Arms resolve enclosing names using the ordinary lexical rules. Names introduced inside an arm block stay in that block. Arm labels name result fields and do not introduce bindings. Start order, completion order, and interleaving of visible effects are unspecified. Read completed fields after the expression returns when output order matters.
Progress and effects when work is abandoned follow timed group progress.
Why
Lexical scope gives enclosing names their usual meaning inside an arm. Scheduling remains independent of source order so arm placement does not promise effect order.
Source evidence
- openNodeScope at
151141d7bb44· 928-975 - walkNode lexical lookup and scope exit at
151141d7bb44· 1123-1168 - eval_concurrent environment cloning and polling at
151141d7bb44· 219-287 - Control output order at
151141d7bb44· 94-98 - issues/31 at
1cbf38d3d422
Each arm has its own control boundary
Each concurrent arm is an independent control boundary. A return or postfix ? cannot target a function outside the arm. A break or continue cannot target an outer loop. Missing arm-local function or unlabelled loop targets are rejected during checking with TPZ5001. A loop label without a target inside the arm is rejected during checking with TPZ5720. Functions called by the arm and loops introduced inside it consume their own control transfers.
An arm ends the search for enclosing return and loop targets. TPZ5001 already diagnoses a return or failure propagation without a function context, and unlabelled loop control without a loop context. TPZ5720 already diagnoses a label without an enclosing loop target. These existing diagnostics apply after the arm boundary removes outer targets from that search.
Why
Each arm runs independently. Keeping control transfers inside its boundary avoids making sibling cancellation depend on a return or loop outside the arm.
Source evidence
- issues/31 at
570530e69141 - inferConcurrentExpression retains ambient return and loop contexts at
570530e69141· 601-680 - resolveLoopTarget existing missing-context and missing-label diagnostics at
570530e69141· 339-399 - checkReturn emits TPZ5001 without an enclosing function context at
570530e69141· 613-662 - inferTryExpression uses the enclosing function context and TPZ5001 guard at
570530e69141· 1122-1166 - eval_concurrent rejects return, propagation, break, and continue escaping an arm at
570530e69141· 219-260
Completion and failure
LinkCollect completed values
When every arm finishes successfully before any applicable timeout, the concurrent expression produces a record containing the named arm values.
Why
A completed record must contain every declared field. Waiting for all arms prevents callers from receiving missing results.
Source evidence
- lowerKindPlan concurrent case at
af88afbc1d99· 118-118 - expression/concurrent dispatch to eval_concurrent at
af88afbc1d99 - eval_concurrent at
af88afbc1d99· 180-297 - issues/31 at
570530e69141
Result values remain field values
An arm that completes with an ordinary Result value stores that value in its result field, preserving Ok(. or Err(.. The concurrent expression does not apply postfix propagation to that value.
Why
Result variants are ordinary values until a program handles or propagates them. Preserving each variant keeps concurrency from adding implicit propagation.
Source evidence
- lowerKindPlan concurrent case at
af88afbc1d99· 118-118 - expression/concurrent dispatch to eval_concurrent at
af88afbc1d99 - eval_concurrent at
af88afbc1d99· 180-297 - data at
af88afbc1d99· 24-39
An arm execution failure leaves the expression
If an arm reports an execution failure before the timeout is reached, that failure leaves the concurrent expression without evaluating its fallback. The same failure transfer applies to the form without a timeout.
Why
The fallback handles an expired deadline. Using it for an arm fault would hide that failure and give the fallback two different meanings.
Source evidence
- lowerKindPlan concurrent case at
af88afbc1d99· 118-118 - expression/concurrent dispatch to eval_concurrent at
af88afbc1d99 - eval_concurrent at
af88afbc1d99· 180-297 - issues/31 at
570530e69141
Evaluate the fallback after timeout
When the deadline is reached before all arms complete, or coincides with completion, unfinished arms are abandoned and collected result values are discarded. The concurrent expression then evaluates its else block.
Progress and effects when work is abandoned follow timed group progress.
Why
The fallback replaces the group result as a whole. Discarding collected values prevents the returned fields from depending on scheduling.
Source evidence
- lowerKindPlan concurrent case at
af88afbc1d99· 118-118 - expression/concurrent dispatch to eval_concurrent at
af88afbc1d99 - eval_concurrent at
af88afbc1d99· 180-297 - issues/31 at
570530e69141 - issues/31 at
1cbf38d3d422
Deadline priority and competing faults
Timeout takes priority when reaching the deadline coincides with arm completion, including a zero-millisecond timeout. The else block determines the result in that case. When arm faults occur simultaneously, arm declaration order gives no fault priority.
Progress and effects when work is abandoned follow timed group progress.
Why
Giving the deadline priority makes a boundary case reproducible, including a zero timeout. Fault priority does not change when independent arms are reordered.
Source evidence
- issues/31 at
570530e69141 - eval_concurrent polls a final ready arm before requiring a nonempty pending set for timeout at
570530e69141· 261-294 - issues/31 at
1cbf38d3d422
Timed group progress
A timed concurrent reaches its fallback after the deadline even when an arm cannot finish, including an infinite loop or deep recursion. A host call already in progress finishes without interruption and can delay group completion. An abandoned arm produces no effects after the group finishes. Effects that occurred before completion remain. A group without a timeout makes no fairness guarantee.
See timeout fallback, deadline races, and the remaining abandoned-arm cleanup question.
Why
A timeout must let a group finish even when an arm does not. Host calls finish as one operation. Untimed fairness would impose scheduling work on every program.
Source evidence
- issues/31 at
1cbf38d3d422
Duration and fallback values
LinkDuration units
Why
A shared literal definition keeps timeout expressions consistent with ordinary expressions.
Source evidence
- parseConcurrentTimeout at
af88afbc1d99· 722-780 - decodeDurationMilliseconds at
af88afbc1d99· 70-83
Duration literal bounds
Why
Keeping literal bounds in one place prevents timeout syntax from acquiring a separate value range.
Source evidence
- decodeDurationMilliseconds at
af88afbc1d99· 70-83 - inferConcurrentExpression, durationFitsMilliseconds at
af88afbc1d99· 601-687
The fallback result type
When the arm result types are known, a value-producing fallback is checked against their record type. A fallback that terminates control flow is checked without that expected result type.
Why
Successful completion and the fallback feed the same expression. Matching their record types lets callers use the result without testing which path completed.
Source evidence
- inferConcurrentExpression, durationFitsMilliseconds at
af88afbc1d99· 601-687
Examples and expected checks
LinkRead both completed arm values
Linkimport std.iolet joined = concurrent {first: 2second: 3}io.writeLine("{joined.first}")io.writeLine("{joined.second}")
2 3
Expected result
Expected result
Execution completes. Exit code 0.
23
Observed assertions · passed · 5.21.3
Exit code 0.
23
No stderr.
- Passed · expected 0; observed 0
- Passed · stdout matches the authored expectation
- Passed · stderr matches the authored expectation
Source evidence
- lowerKindPlan concurrent case at
af88afbc1d99· 118-118 - expression/concurrent dispatch to eval_concurrent at
af88afbc1d99 - eval_concurrent at
af88afbc1d99· 180-297
Keep an Err value in the result record
Linkimport std.iofunction completeError() -> Result<int, string> {Err("missing")}let joined = concurrent {value: completeError()count: 7}match (joined.value) {case Ok(_) => io.writeLine("unexpected")case Err(message) => io.writeLine(message)}io.writeLine("{joined.count}")
missing 7
Expected result
Expected result
Execution completes. Exit code 0.
missing7
Observed assertions · passed · 5.21.3
Exit code 0.
missing7
No stderr.
- Passed · expected 0; observed 0
- Passed · stdout matches the authored expectation
- Passed · stderr matches the authored expectation
Source evidence
- lowerKindPlan concurrent case at
af88afbc1d99· 118-118 - expression/concurrent dispatch to eval_concurrent at
af88afbc1d99 - eval_concurrent at
af88afbc1d99· 180-297 - data at
af88afbc1d99· 24-39
Distinct arm names and result fields
Linklet joined = concurrent {first: 1second: 2}let first: int = joined.firstlet second: int = joined.second
No stdout.
Expected result
Expected result
Checking accepts the program. Exit code 0.
Observed assertions · passed · 5.21.3
Exit code 0.
No stdout.
No stderr.
- Passed · expected 0; observed 0
- Passed · stdout matches the authored expectation
- Passed · stderr matches the authored expectation
Source evidence
- parseConcurrentArm, parseConcurrentArms, parseConcurrentElse, parseConcurrent at
af88afbc1d99· 782-907 - inferConcurrentExpression, durationFitsMilliseconds at
af88afbc1d99· 601-687
A fallback with the corresponding result fields
Linklet joined = concurrent(timeout: 25ms) {count: 2 + 3label: "ready"} else {{ count: 0, label: "timeout" }}let count: int = joined.countlet label: string = joined.label
No stdout.
Expected result
Expected result
Checking accepts the program. Exit code 0.
Observed assertions · passed · 5.21.3
Exit code 0.
No stdout.
No stderr.
- Passed · expected 0; observed 0
- Passed · stdout matches the authored expectation
- Passed · stderr matches the authored expectation
Source evidence
- parseConcurrentArm, parseConcurrentArms, parseConcurrentElse, parseConcurrent at
af88afbc1d99· 782-907 - parseConcurrentTimeout at
af88afbc1d99· 722-780 - inferConcurrentExpression, durationFitsMilliseconds at
af88afbc1d99· 601-687
The three duration units
Linklet milliseconds = concurrent(timeout: 1ms) { value: 1 } else { { value: 0 } }let seconds = concurrent(timeout: 1s) { value: 1 } else { { value: 0 } }let minutes = concurrent(timeout: 1m) { value: 1 } else { { value: 0 } }
No stdout.
Expected result
Expected result
Checking accepts the program. Exit code 0.
Observed assertions · passed · 5.21.3
Exit code 0.
No stdout.
No stderr.
- Passed · expected 0; observed 0
- Passed · stdout matches the authored expectation
- Passed · stderr matches the authored expectation
Source evidence
- parseConcurrentTimeout at
af88afbc1d99· 722-780 - decodeDurationMilliseconds at
af88afbc1d99· 70-83 - inferConcurrentExpression, durationFitsMilliseconds at
af88afbc1d99· 601-687
Repeated name in one concurrent expression
Linklet joined = concurrent {value: 1value: 2}
No stdout.
error[TPZ5008]: duplicate concurrent arm `value` --> example.tpz:3:5 | 3 | value: 2 | ^^^^^ example.tpz: 1 type diagnostic
Expected result
Expected result
Rejection during check. Nonzero exit.
TPZ5008duplicate-concurrent-arm atexample.tpz:3:5
Observed assertions · passed · 5.21.3
Exit code 1.
No stdout.
error[TPZ5008]: duplicate concurrent arm `value`--> example.tpz:3:5|3 | value: 2| ^^^^^example.tpz: 1 type diagnostic
- Passed · expected nonzero; observed 1
- Passed · checking TPZ5008; cause duplicate-concurrent-arm; primary location asserted; additional diagnostics allow
- Passed · stdout matches the authored expectation
Source evidence
- parseConcurrentArm, parseConcurrentArms, parseConcurrentElse, parseConcurrent at
af88afbc1d99· 782-907 - inferConcurrentExpression, durationFitsMilliseconds at
af88afbc1d99· 601-687
Timeout with no fallback block
Linklet joined = concurrent(timeout: 25ms) {count: 2 + 3label: "ready"}
No stdout.
error[TPZ2005]: the `concurrent(timeout: ...)` form requires an `else` block
--> example.tpz:1:14
|
1 | let joined = concurrent(timeout: 25ms) {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
example.tpz: 1 diagnostic
Expected result
Expected result
Rejection during parse. Nonzero exit.
TPZ2005concurrent-timeout-without-else atexample.tpz:1:14
Observed assertions · passed · 5.21.3
Exit code 1.
No stdout.
error[TPZ2005]: the `concurrent(timeout: ...)` form requires an `else` block--> example.tpz:1:14|1 | let joined = concurrent(timeout: 25ms) {| ^^^^^^^^^^^^^^^^^^^^^^^^^^^example.tpz: 1 diagnostic
- Passed · expected nonzero; observed 1
- Passed · checking TPZ2005; cause concurrent-timeout-without-else; primary location asserted; additional diagnostics allow
- Passed · stdout matches the authored expectation
Source evidence
- parseConcurrentArm, parseConcurrentArms, parseConcurrentElse, parseConcurrent at
af88afbc1d99· 782-907 - parseConcurrentTimeout at
af88afbc1d99· 722-780
An untimed group may have no arms
Linklet joined = concurrent {}
No stdout.
Expected result
Expected result
Checking accepts the program. Exit code 0.
Source evidence
- pull/41 at
271329b482bc - parseConcurrentArms rejects zero arms at
271329b482bc· 801-841 - inferConcurrentExpression constructs structural(names, values) at
271329b482bc· 601-681 - ConcurrentJoin returns a record when no pending arms remain at
271329b482bc· 135-170 - empty pending collection skips the expired-deadline branch at
271329b482bc· 240-270 - issues/31 at
570530e69141 - parseRecordType currently rejects zero fields at
271329b482bc· 145-184 - unit is primitive and the zero-field structural record is a distinct kind at
271329b482bc· 111-159 - assignable at
af88afbc1d99· 388-407 - structural at
af88afbc1d99· 632-675 - formRecordType at
af88afbc1d99· 298-341 - reportExpectedType diagnoses the initializer expression at
271329b482bc· 422-475
Arm blocks keep their own names and read the enclosing binding
Linkimport std.iolet base: int = 10let joined = concurrent {first: {let branchLocal = base + 1branchLocal}second: {let branchLocal = base + 2branchLocal}}io.writeLine("{joined.first},{joined.second}")
11,12
Expected result
Expected result
Execution completes. Exit code 0.
11,12
Source evidence
- openNodeScope at
151141d7bb44· 928-975 - walkNode lexical lookup and scope exit at
151141d7bb44· 1123-1168 - eval_concurrent environment cloning and polling at
151141d7bb44· 219-287 - Control output order at
151141d7bb44· 94-98
An arm label does not bind a value
Linklet joined = concurrent {first: 1second: first}
No stdout.
Expected result
Expected result
Rejection during check. Nonzero exit.
TPZ5002concurrent-arm-label-not-bound atexample.tpz:3:13
Source evidence
- openNodeScope at
151141d7bb44· 928-975 - walkNode lexical lookup and scope exit at
151141d7bb44· 1123-1168
An arm block does not expose its local to a sibling
Linklet joined = concurrent {first: {let branchLocal = 1branchLocal}second: branchLocal}
No stdout.
Expected result
Expected result
Rejection during check. Nonzero exit.
TPZ5002concurrent-sibling-local-not-bound atexample.tpz:6:13
Source evidence
- openNodeScope at
151141d7bb44· 928-975 - walkNode lexical lookup and scope exit at
151141d7bb44· 1123-1168
A noncompleting arm selects the timeout fallback
Linkimport std.iofunction waitForever() -> int {while (true) {}0}let joined = concurrent(timeout: 0ms) {value: waitForever()} else {{ value: 7 }}io.writeLine("{joined.value}")
7
Expected result
Expected result
Execution completes. Exit code 0.
7
The arm cannot complete, so this example does not choose a rule for completion coinciding with a deadline. It expects the complete fallback record.
Source evidence
- eval_concurrent runtime outcomes at
151141d7bb44· 261-294 - eval_while asynchronous body evaluation at
151141d7bb44· 545-583
Called functions and inner loops consume their own control transfer
Linkimport std.iofunction advance(value: Result<int, string>) -> Result<int, string> {let number: int = value?return Ok(number + 1)}let joined = concurrent {result: advance(Err("missing"))count: {let mut count = 0while (count < 3) {count += 1if (count < 3) { continue }break}count}}match (joined.result) {case Ok(_) => io.writeLine("unexpected")case Err(message) => io.writeLine(message)}io.writeLine("{joined.count}")
missing 3
Expected result
Expected result
Execution completes. Exit code 0.
missing3
Source evidence
- eval_concurrent rejects escaped arm control at
151141d7bb44· 219-260 - inferConcurrentExpression retains ambient return and loop contexts at
151141d7bb44· 601-687 - resolveLoopTarget at
151141d7bb44· 339-399 - checkReturn at
151141d7bb44· 613-662 - issues/31 at
570530e69141 - inferConcurrentExpression retains ambient return and loop contexts at
570530e69141· 601-680 - resolveLoopTarget existing missing-context and missing-label diagnostics at
570530e69141· 339-399 - checkReturn emits TPZ5001 without an enclosing function context at
570530e69141· 613-662 - inferTryExpression uses the enclosing function context and TPZ5001 guard at
570530e69141· 1122-1166 - eval_concurrent rejects return, propagation, break, and continue escaping an arm at
570530e69141· 219-260
Zero duration takes the fallback for one ready arm
Linkimport std.iolet joined = concurrent(timeout: 0ms) {value: 1} else {{ value: 7 }}io.writeLine("{joined.value}")
7
Expected result
Expected result
Execution completes. Exit code 0.
7
Source evidence
- issues/31 at
570530e69141 - eval_concurrent polls a final ready arm before requiring a nonempty pending set for timeout at
570530e69141· 261-294
Zero duration takes the complete fallback for multiple ready arms
Linkimport std.iolet joined = concurrent(timeout: 0ms) {left: 1right: 2} else {{ left: 7, right: 8 }}io.writeLine("{joined.left}:{joined.right}")
7:8
Expected result
Expected result
Execution completes. Exit code 0.
7:8
Source evidence
- issues/31 at
570530e69141 - eval_concurrent polls a final ready arm before requiring a nonempty pending set for timeout at
570530e69141· 261-294
A zero-duration fallback can raise its own runtime fault
Linkfunction divide(divisor: int) -> int {42 / divisor}let joined = concurrent(timeout: 0ms) {value: 1} else {{ value: divide(0) }}
No stdout.
Expected result
Source evidence
- issues/31 at
570530e69141 - eval_concurrent polls a final ready arm before requiring a nonempty pending set for timeout at
570530e69141· 261-294
An arm cannot return from its enclosing function
Linkfunction escape() -> int {let joined = concurrent {value: {return 1}}0}
No stdout.
Expected result
Expected result
Rejection during check. Nonzero exit.
TPZ5001concurrent-arm-function-boundary atexample.tpz:4:13
Source evidence
- issues/31 at
570530e69141 - inferConcurrentExpression retains ambient return and loop contexts at
570530e69141· 601-680 - resolveLoopTarget existing missing-context and missing-label diagnostics at
570530e69141· 339-399 - checkReturn emits TPZ5001 without an enclosing function context at
570530e69141· 613-662 - inferTryExpression uses the enclosing function context and TPZ5001 guard at
570530e69141· 1122-1166 - eval_concurrent rejects return, propagation, break, and continue escaping an arm at
570530e69141· 219-260
An arm cannot break its enclosing loop
Linkwhile (true) {let joined = concurrent {value: {break}}break}
No stdout.
Expected result
Expected result
Rejection during check. Nonzero exit.
TPZ5001concurrent-arm-loop-boundary atexample.tpz:4:13
Source evidence
- issues/31 at
570530e69141 - inferConcurrentExpression retains ambient return and loop contexts at
570530e69141· 601-680 - resolveLoopTarget existing missing-context and missing-label diagnostics at
570530e69141· 339-399 - checkReturn emits TPZ5001 without an enclosing function context at
570530e69141· 613-662 - inferTryExpression uses the enclosing function context and TPZ5001 guard at
570530e69141· 1122-1166 - eval_concurrent rejects return, propagation, break, and continue escaping an arm at
570530e69141· 219-260
An arm cannot continue its enclosing loop
Linkwhile (true) {let joined = concurrent {value: {continue}}break}
No stdout.
Expected result
Expected result
Rejection during check. Nonzero exit.
TPZ5001concurrent-arm-loop-boundary atexample.tpz:4:13
Source evidence
- issues/31 at
570530e69141 - inferConcurrentExpression retains ambient return and loop contexts at
570530e69141· 601-680 - resolveLoopTarget existing missing-context and missing-label diagnostics at
570530e69141· 339-399 - checkReturn emits TPZ5001 without an enclosing function context at
570530e69141· 613-662 - inferTryExpression uses the enclosing function context and TPZ5001 guard at
570530e69141· 1122-1166 - eval_concurrent rejects return, propagation, break, and continue escaping an arm at
570530e69141· 219-260
An arm cannot propagate a Result to its enclosing function
Linkfunction escape(value: Result<int, string>) -> Result<int, string> {let joined = concurrent {result: value?}Ok(0)}
No stdout.
Expected result
Expected result
Rejection during check. Nonzero exit.
TPZ5001concurrent-arm-function-boundary atexample.tpz:3:17
Source evidence
- issues/31 at
570530e69141 - inferConcurrentExpression retains ambient return and loop contexts at
570530e69141· 601-680 - resolveLoopTarget existing missing-context and missing-label diagnostics at
570530e69141· 339-399 - checkReturn emits TPZ5001 without an enclosing function context at
570530e69141· 613-662 - inferTryExpression uses the enclosing function context and TPZ5001 guard at
570530e69141· 1122-1166 - eval_concurrent rejects return, propagation, break, and continue escaping an arm at
570530e69141· 219-260
An arm cannot break to an outer loop label
Linkloop 'outer {let joined = concurrent {value: {loop {break 'outer}}}break}
No stdout.
Expected result
Expected result
Rejection during check. Nonzero exit.
TPZ5720concurrent-arm-loop-label atexample.tpz:5:17
Source evidence
- issues/31 at
570530e69141 - inferConcurrentExpression retains ambient return and loop contexts at
570530e69141· 601-680 - resolveLoopTarget existing missing-context and missing-label diagnostics at
570530e69141· 339-399 - checkReturn emits TPZ5001 without an enclosing function context at
570530e69141· 613-662 - inferTryExpression uses the enclosing function context and TPZ5001 guard at
570530e69141· 1122-1166 - eval_concurrent rejects return, propagation, break, and continue escaping an arm at
570530e69141· 219-260
An arm cannot continue to an outer loop label
Linkloop 'outer {let joined = concurrent {value: {loop {continue 'outer}}}break}
No stdout.
Expected result
Expected result
Rejection during check. Nonzero exit.
TPZ5720concurrent-arm-loop-label atexample.tpz:5:17
Source evidence
- issues/31 at
570530e69141 - inferConcurrentExpression retains ambient return and loop contexts at
570530e69141· 601-680 - resolveLoopTarget existing missing-context and missing-label diagnostics at
570530e69141· 339-399 - checkReturn emits TPZ5001 without an enclosing function context at
570530e69141· 613-662 - inferTryExpression uses the enclosing function context and TPZ5001 guard at
570530e69141· 1122-1166 - eval_concurrent rejects return, propagation, break, and continue escaping an arm at
570530e69141· 219-260
A function declared inside an arm consumes its own control transfers
Linkimport std.iolet joined = concurrent {result: {function advance(value: Result<int, string>) -> Result<int, string> {let number: int = value?return Ok(number + 1)}advance(Ok(4))}}match (joined.result) {case Ok(value) => io.writeLine("{value}")case Err(message) => io.writeLine(message)}
5
Expected result
Expected result
Execution completes. Exit code 0.
5
Source evidence
- issues/31 at
570530e69141 - inferConcurrentExpression retains ambient return and loop contexts at
570530e69141· 601-680 - resolveLoopTarget existing missing-context and missing-label diagnostics at
570530e69141· 339-399 - checkReturn emits TPZ5001 without an enclosing function context at
570530e69141· 613-662 - inferTryExpression uses the enclosing function context and TPZ5001 guard at
570530e69141· 1122-1166 - eval_concurrent rejects return, propagation, break, and continue escaping an arm at
570530e69141· 219-260
Parentheses preserve a timeout value
Linkimport std.iolet joined = concurrent(timeout: (0ms)) { value: 1 } else { { value: 7 } }io.writeLine("{joined.value}")
7
Expected result
Expected result
Execution completes. Exit code 0.
7
Source evidence
- pull/41 at
271329b482bc - parseConcurrentTimeout and ordinary duration rejection at
271329b482bc· 722-780,1254-1259 - literal-only timeout lowering at
271329b482bc· 1550-1588 - inferConcurrentExpression, durationFitsMilliseconds at
af88afbc1d99· 601-687 - issues/31 at
570530e69141
A bound Duration is the same timeout value
Linkimport std.iolet budget: Duration = 0mslet joined = concurrent(timeout: budget) { value: 1 } else { { value: 7 } }io.writeLine("{joined.value}")
7
Expected result
Expected result
Execution completes. Exit code 0.
7
Source evidence
- pull/41 at
271329b482bc - parseConcurrentTimeout and ordinary duration rejection at
271329b482bc· 722-780,1254-1259 - literal-only timeout lowering at
271329b482bc· 1550-1588 - inferConcurrentExpression, durationFitsMilliseconds at
af88afbc1d99· 601-687 - issues/31 at
570530e69141
Evaluate the timeout once before any arm
Linkimport std.iofunction budget() -> Duration {io.writeLine("budget")0ms}let joined = concurrent(timeout: budget()) {value: { io.writeLine("arm")1 }} else {io.writeLine("fallback"){ value: 7 }}io.writeLine("{joined.value}")
budget fallback 7
Expected result
Expected result
Execution completes. Exit code 0.
budgetfallback7
The adopted zero-deadline rule selects the fallback before any arm. The budget marker occurs once and before fallback evaluation; no arm marker is permitted.
Source evidence
- pull/41 at
271329b482bc - parseConcurrentTimeout and ordinary duration rejection at
271329b482bc· 722-780,1254-1259 - literal-only timeout lowering at
271329b482bc· 1550-1588 - inferConcurrentExpression, durationFitsMilliseconds at
af88afbc1d99· 601-687 - issues/31 at
570530e69141
Functions and conditionals preserve Duration
Linkfunction choose(first: bool) -> Duration { if (first) { 0ms } else { 1s } }let budget: Duration = choose(true)let joined = concurrent(timeout: if (true) { budget } else { 2m }) { value: 1 } else { { value: 7 } }
No stdout.
Expected result
Expected result
Checking accepts the program. Exit code 0.
Source evidence
- pull/41 at
271329b482bc - parseConcurrentTimeout and ordinary duration rejection at
271329b482bc· 722-780,1254-1259 - literal-only timeout lowering at
271329b482bc· 1550-1588 - inferConcurrentExpression, durationFitsMilliseconds at
af88afbc1d99· 601-687 - issues/31 at
570530e69141
An integer does not implicitly become Duration
Linklet joined = concurrent(timeout: 1) { value: 1 } else { { value: 7 } }
No stdout.
Expected result
Expected result
Rejection during check. Exit code 1.
TPZ5001concurrent-timeout-duration-type atexample.tpz:1:34
The target uses the existing expected-type mismatch identity. Current SELF rejects earlier with TPZ2001; that already-nonzero exit is not excused.
Source evidence
- pull/41 at
271329b482bc - parseConcurrentTimeout and ordinary duration rejection at
271329b482bc· 722-780,1254-1259 - literal-only timeout lowering at
271329b482bc· 1550-1588 - inferConcurrentExpression, durationFitsMilliseconds at
af88afbc1d99· 601-687 - issues/31 at
570530e69141
A timeout expression fault precedes the group
Linkimport std.iofunction budget(divisor: int) -> Duration {io.writeLine("budget")let ratio = 42 / divisor0ms}let joined = concurrent(timeout: budget(0)) {value: { io.writeLine("arm")1 }} else {io.writeLine("fallback"){ value: 7 }}
budget
Expected result
Expected result
Rejection during run. Exit code 1.
TPZ4002diagnostic-tpz4002 atexample.tpz:4:17
budget
Source evidence
- pull/41 at
271329b482bc - parseConcurrentTimeout and ordinary duration rejection at
271329b482bc· 722-780,1254-1259 - literal-only timeout lowering at
271329b482bc· 1550-1588 - inferConcurrentExpression, durationFitsMilliseconds at
af88afbc1d99· 601-687 - issues/31 at
570530e69141
An empty concurrent record passes through an explicit record signature
Linkimport std.iofunction keep(value: {}) -> {} { value }let joined: {} = keep(concurrent {})io.writeLine("completed")
completed
Expected result
Expected result
Execution completes. Exit code 0.
completed
Source evidence
- pull/41 at
271329b482bc - parseConcurrentArms rejects zero arms at
271329b482bc· 801-841 - inferConcurrentExpression constructs structural(names, values) at
271329b482bc· 601-681 - ConcurrentJoin returns a record when no pending arms remain at
271329b482bc· 135-170 - empty pending collection skips the expired-deadline branch at
271329b482bc· 240-270 - issues/31 at
570530e69141 - parseRecordType currently rejects zero fields at
271329b482bc· 145-184 - unit is primitive and the zero-field structural record is a distinct kind at
271329b482bc· 111-159 - assignable at
af88afbc1d99· 388-407 - structural at
af88afbc1d99· 632-675 - formRecordType at
af88afbc1d99· 298-341 - reportExpectedType diagnoses the initializer expression at
271329b482bc· 422-475
A zero deadline wins over empty-group completion
Linkimport std.iofunction deadline() -> Duration { io.writeLine("timeout")0ms }let joined: {} = concurrent(timeout: deadline()) {} else {io.writeLine("fallback")concurrent {}}io.writeLine("completed")
timeout fallback completed
Expected result
Expected result
Execution completes. Exit code 0.
timeoutfallbackcompleted
Source evidence
- pull/41 at
271329b482bc - parseConcurrentArms rejects zero arms at
271329b482bc· 801-841 - inferConcurrentExpression constructs structural(names, values) at
271329b482bc· 601-681 - ConcurrentJoin returns a record when no pending arms remain at
271329b482bc· 135-170 - empty pending collection skips the expired-deadline branch at
271329b482bc· 240-270 - issues/31 at
570530e69141 - parseRecordType currently rejects zero fields at
271329b482bc· 145-184 - unit is primitive and the zero-field structural record is a distinct kind at
271329b482bc· 111-159 - assignable at
af88afbc1d99· 388-407 - structural at
af88afbc1d99· 632-675 - formRecordType at
af88afbc1d99· 298-341 - reportExpectedType diagnoses the initializer expression at
271329b482bc· 422-475
The zero-field record is distinct from unit
Linklet joined: () = concurrent {}
No stdout.
Expected result
Expected result
Rejection during check. Nonzero exit.
TPZ5001concurrent-empty-result-is-not-unit atexample.tpz:1:18
Source evidence
- pull/41 at
271329b482bc - parseConcurrentArms rejects zero arms at
271329b482bc· 801-841 - inferConcurrentExpression constructs structural(names, values) at
271329b482bc· 601-681 - ConcurrentJoin returns a record when no pending arms remain at
271329b482bc· 135-170 - empty pending collection skips the expired-deadline branch at
271329b482bc· 240-270 - issues/31 at
570530e69141 - parseRecordType currently rejects zero fields at
271329b482bc· 145-184 - unit is primitive and the zero-field structural record is a distinct kind at
271329b482bc· 111-159 - assignable at
af88afbc1d99· 388-407 - structural at
af88afbc1d99· 632-675 - formRecordType at
af88afbc1d99· 298-341 - reportExpectedType diagnoses the initializer expression at
271329b482bc· 422-475
Use the empty structural type in aliases, parameters, and results
Linktype Empty = {}function keep(value: Empty) -> {} { value }
No stdout.
Expected result
Expected result
Checking accepts the program. Exit code 0.
Source evidence
- pull/41 at
271329b482bc - parseConcurrentArms rejects zero arms at
271329b482bc· 801-841 - inferConcurrentExpression constructs structural(names, values) at
271329b482bc· 601-681 - ConcurrentJoin returns a record when no pending arms remain at
271329b482bc· 135-170 - empty pending collection skips the expired-deadline branch at
271329b482bc· 240-270 - issues/31 at
570530e69141 - parseRecordType currently rejects zero fields at
271329b482bc· 145-184 - unit is primitive and the zero-field structural record is a distinct kind at
271329b482bc· 111-159 - assignable at
af88afbc1d99· 388-407 - structural at
af88afbc1d99· 632-675 - formRecordType at
af88afbc1d99· 298-341 - reportExpectedType diagnoses the initializer expression at
271329b482bc· 422-475
A positive deadline ends an infinite loop
Linkimport std.iolet joined = concurrent(timeout: 1ms) {value: {while (true) {}0}} else {io.writeLine("fallback"){ value: 7 }}io.writeLine("{joined.value}")
fallback 7
Expected result
Expected result
Execution completes. Exit code 0.
fallback7
Source evidence
- issues/31 at
1cbf38d3d422 - eval_while asynchronous body evaluation at
151141d7bb44· 545-583
Pending contracts
LinkRelated language and diagnostic references
LinkFunctions and generics defines the calls used by arms. Failure and propagation defines the language propagation form. Types, Bindings and scope, and Blocks and control flow own their respective general rules.
The diagnostic catalog owns complete diagnostic descriptions. The following entries are referenced here for the specific causes isolated by these examples.