Control Flow
Overview
rill provides singular control flow—no exceptions, no try/catch. Errors halt execution. Recovery requires explicit conditionals.
| Syntax | Description |
|---|---|
cond ? then ! else | Conditional (if-else) |
$val -> ? then ! else | Piped conditional (uses $ as cond) |
(cond) @ body | While loop (cond is bool) |
@ body ? cond | Do-while (body first) |
break / $val -> break | Exit loop |
return / $val -> return | Exit block |
assert cond / assert cond "msg" | Validate condition, halt on failure |
error "msg" / $val -> error | Halt execution with error message |
Conditionals
? is the conditional operator. The condition precedes ?, and ! introduces the else clause.
Syntax Forms
condition ? then-body
condition ? then-body ! else-body
$val -> ? then-body ! else-body # piped form: $ is the condition
# Multi-line forms (? and ! work as line continuations)
condition
? then-body
! else-body
value -> is_valid
? "ok"
! "error"Standalone Form
Condition precedes ?:
true ? "yes" ! "no" # "yes"
false ? "yes" ! "no" # "no"
(5 > 3) ? "big" ! "small" # grouped comparison as conditionPiped Form
Use $ as condition:
true -> ? "yes" ! "no" # "yes" (pipe value must be bool)
5 -> ($ > 3) ? "big" ! "small" # "big"Method Conditions
Methods that return booleans work directly as conditions:
"hello" -> .contains("ell") ? "found" ! "missing" # "found"
"abc" -> !.empty ? "has content" ! "empty" # "has content"Condition Forms
"test" -> ($ == "test") ? "match" ! "no" # grouped comparison
"test" -> .eq("test") ? "match" ! "no" # comparison method
"xyz" -> .contains("x") ? "found" ! "no" # method as conditionOptional Else
The else branch (! ...) is optional:
true ? "executed" # only runs if true
false ? "skipped" # returns empty stringElse-If Chains
"B" => $val
$val -> .eq("A") ? "a" ! .eq("B") ? "b" ! "other" # "b"Multi-line else-if chains improve readability:
"B" => $val
$val -> .eq("A") ? "a"
! .eq("B") ? "b"
! "other"
# Result: "b"Return Value
Conditionals return the last expression of the executed branch:
true -> ? "yes" ! "no" => $result # "yes"
false -> ? "yes" ! "no" => $result # "no"Block Bodies
Use braces for multi-statement branches:
true -> ? {
"step 1" -> log
"step 2" -> log
"done"
} ! {
"skipped"
}Block bodies work with multi-line conditionals:
"data" => $input
$input -> .empty
? { error "Empty input" }
! { $input -> .upper }
# Result: "DATA"While Loop
Pre-condition loop. Condition is evaluated before each iteration. The body result becomes the next iteration’s $.
Note: There is no
whilekeyword. Use(condition) @ { body }syntax. Loop bodies cannot modify outer-scope variables—use$to carry all state. For multiple values, pack them in a dict.
Syntax
initial -> (condition) @ { body }Basic Usage
# Count to 5
0 -> ($ < 5) @ { $ + 1 } # Result: 5
# String accumulation
"" -> (.len < 5) @ { "{$}x" } # Result: "xxxxx"Condition Forms
0 -> ($ < 10) @ { $ + 1 } # comparison condition
"" -> (.len < 5) @ { "{$}x" } # method call conditionInfinite Loop with Break
0 -> (true) @ {
$ + 1 -> ($ > 5) ? break ! $
} # Result: 6Loop Limits
Use ^(limit: N) annotation to set maximum iterations (default: 10,000):
^(limit: 100) 0 -> ($ < 10) @ { $ + 1 } # Runs 10 iterations, returns 10Exceeding the limit throws RuntimeError with code RUNTIME_LIMIT_EXCEEDED.
Multiple State Values
When you need to track multiple values across iterations, use $ as a state dict:
# Track iteration count, text, and done flag
[iter: 0, text: $input, done: false]
-> (!$.done && $.iter < 3) @ {
$.iter + 1 => $i
app::process($.text) => $result
$result.finished
? [iter: $i, text: $.text, done: true]
! [iter: $i, text: $result.text, done: false]
}
# Access final state: $.text, $.iterThis pattern replaces the common (but invalid) approach of trying to modify outer variables from inside the loop.
Do-While Loop
Post-condition loop. Body executes first, then condition is checked. Use when you want at least one execution.
Syntax
initial -> @ { body } ? (condition)Basic Usage
# Execute at least once, continue while condition holds
0 -> @ { $ + 1 } ? ($ < 5) # Returns 5
# String accumulation
"" -> @ { "{$}x" } ? (.len < 3) # Returns "xxx"When to Use
- While
(condition) @ { body }: condition checked BEFORE body (may execute 0 times) - Do-while
@ { body } ? (condition): condition checked AFTER body (executes at least once)
Retry Pattern
Do-while is ideal for retry patterns:
^(limit: 5) @ {
app::prompt("Perform operation")
} ? (.contains("RETRY"))
# Loop exits when result doesn't contain RETRYLoop Limit
^(limit: 100) 0 -> @ { $ + 1 } ? ($ < 10) # Returns 10Break
Exit a loop early. Returns the value piped to break, or current $ if bare.
Syntax
break # exit with current $
$value -> break # exit with valueIn Each Loop
[1, 2, 3, 4, 5] -> each {
($ > 3) ? ("found {$}" -> break)
$
}
# Returns "found 4"In While Loop
0 -> (true) @ {
($ + 1) -> ($ > 3) ? break ! $
}
# Returns 4Break Value
In each, break returns partial results collected before the break:
["a", "b", "STOP", "c"] -> each {
($ == "STOP") ? break
$
}
# Returns ["a", "b"] (partial results before break)Break Not Allowed
break is not supported in map, filter, or fold (parallel operations):
[1, 2, 3] -> map { break } # ERROR: break not supported in mapReturn
Exit a block early. Returns the value piped to return, or current $ if bare.
Syntax
return # exit with current $
$value -> return # exit with valueIn Blocks
{
5 => $x
($x > 3) ? ("big" -> return)
"small"
}
# Returns "big"Multi-Phase Pipeline
{
"content" => $data
$data -> .contains("ERROR") ? ("Read failed" -> return)
"processed: {$data}"
}
# Returns "processed: content" or "Read failed"Assert
Validate conditions during execution. Halts the script with a clear error if the assertion fails.
Syntax
assert condition
assert condition "error message"
$value -> assert conditionBasic Usage
Assert halts execution when the condition evaluates to false. If the condition is true, the piped value passes through unchanged.
5 -> assert ($ > 0) # Returns 5 (condition true)
-1 -> assert ($ > 0) # Error: Assertion failedCustom Error Messages
Provide a descriptive message as the second argument:
"" -> assert !.empty "Empty input not allowed"
# Error: Empty input not allowed
[1, 2, 3] -> assert (.len > 0) "List cannot be empty"
# Returns [1, 2, 3] (assertion passes)Type Assertions
Combine with type checks to validate input:
"hello" -> assert $:?string # Returns "hello" (type check passes)
42 -> assert $:?string # Error: Assertion failedIn Loops
Assert validates each iteration. The loop halts on the first failing assertion:
[1, 2, 3] -> each {
assert ($ > 0) "Must be positive"
}
# Returns [1, 2, 3] (all elements valid)
[1, 0, 3] -> each {
assert ($ > 0) "Must be positive"
}
# Error: Must be positivePipe Passthrough
When the assertion passes, the piped value flows through unchanged:
"data" => $input
$input
-> assert !.empty "Input required"
-> .upper
-> assert (.len > 0) "Processed value required"
# Returns "DATA"Error Behavior
Assert throws RuntimeError when:
| Condition | Error Code | Message |
|---|---|---|
Condition is false | RUNTIME_ASSERTION_FAILED | Custom message or “Assertion failed” |
| Condition is not boolean | RUNTIME_TYPE_ERROR | “assert requires boolean condition, got {type}” |
# Non-boolean condition
"test" -> assert $ # Error: assert requires boolean condition, got string
# Failed assertion with location
-1 -> assert ($ > 0) # Error: Assertion failedValidation Patterns
Guard clauses at function start:
|data| {
assert $data:?list "Expected list"
assert !$data.empty "List cannot be empty"
$data -> each { $ * 2 }
} => $processMulti-step validation:
$input
-> assert $:?string "Input must be string"
-> .trim
-> assert !.empty "Trimmed input cannot be empty"
-> assert (.len >= 5) "Input too short (min 5 chars)"
-> app::process()Error
Halt execution immediately with a custom error message. Unlike assert, which validates a condition, error always halts.
Syntax
error "message" # Direct form
$value -> error # Piped formBasic Usage
Use error with a string literal to halt with a message:
error "Something went wrong"
# Halts execution with: Something went wrongThe message argument accepts string literals or piped string values (see Piped Form below).
Piped Form
Pipe a string value to error to use dynamic error messages:
"Operation failed" -> error
# Halts with: Operation failedThe piped value must be a string:
"Error occurred" => $msg
$msg -> error
# Halts with: Error occurred
"Status: " => $prefix
404 => $code
"{$prefix}{$code}" -> error
# Halts with: Status: 404Piping non-string values throws a type error:
42 -> error # Error: error requires string, got numberString Interpolation
Use interpolation for dynamic error messages:
404 => $code
error "Unexpected status: {$code}"
# Halts with: Unexpected status: 4043 => $step
"timeout" => $reason
error "Failed at step {$step}: {$reason}"
# Halts with: Failed at step 3: timeoutConditional Usage
Combine error with conditionals for guard clauses:
5 => $x
($x < 0) ? { error "Number must be non-negative" } ! $x
# Returns 5 (condition false, proceeds with else branch)$data -> .empty ? { error "Data cannot be empty" } ! $data
# Proceeds with $data if not emptyIn Blocks
Use error in blocks for multi-step validation:
|age| {
($age < 0) ? { error "Age cannot be negative: {$age}" }
($age > 150) ? { error "Age out of range: {$age}" }
"Valid age: {$age}"
} => $validate_ageError Behavior
Error throws RuntimeError with code RUNTIME_ERROR_RAISED:
| Pattern | Halts With | Message Source |
|---|---|---|
error "msg" | RUNTIME_ERROR_RAISED | String literal |
$val -> error | RUNTIME_ERROR_RAISED | Piped value (must be string) |
error "" | RUNTIME_ERROR_RAISED | Empty message |
error 123 | Parse error | PARSE_INVALID_SYNTAX |
All error responses include the source location from the error statement.
Multiline Messages
Use triple-quoted strings for formatted error messages:
error """
Error occurred:
- Line 1
- Line 2
"""Comparison with Assert
| Statement | Condition | Behavior |
|---|---|---|
assert cond "msg" | Validates condition | Halts if condition is false, passes through if true |
error "msg" | None | Always halts with message |
Use assert when you need to validate a condition. Use error when you’ve already determined that execution cannot continue.
In Loops
Error halts the loop immediately:
[1, 2, 3] -> each {
($ == 2) ? { error "Halted at 2" }
$ * 2
}
# Halts on second iteration with: Halted at 2Pass
The pass keyword returns the current pipe value ($) unchanged. Use it for explicit identity pass-through in conditional branches and dict values.
In Conditionals
Use pass when one branch should preserve the piped value:
"input" -> .contains("in") ? pass ! "fallback"
# Returns "input" (condition true, pass preserves $)"data" -> .empty ? { error "Empty input" } ! pass
# Returns "data" (condition false, pass preserves $)In Dict Values
Use pass to include the piped value in dict construction:
"success" -> { [status: pass, code: 0] }
# Returns [status: "success", code: 0]In Collection Operators
Preserve elements conditionally:
[1, -2, 3, -4] -> map { ($ > 0) ? pass ! 0 }
# Returns [1, 0, 3, 0]Why Use Pass?
The pass keyword provides clearer intent than bare $:
# Less clear - what does $ mean here?
$cond ? do_something() ! $
# More explicit - reader knows this is intentional no-op
$cond ? do_something() ! passPass Behavior
| Pattern | Returns | Context |
|---|---|---|
cond ? pass ! alt | $ if true, alt if false | Conditional branch |
cond ? alt ! pass | alt if true, $ if false | Conditional branch |
[key: pass] | Dict with $ as value | Dict construction |
-> { pass } | $ | Block body |
Note: pass requires pipe context. Using pass without $ bound throws an error.
Control Flow Summary
| Statement | Scope | Effect |
|---|---|---|
break | Loop | Exit loop with current $ |
$val -> break | Loop | Exit loop with value |
return | Block/Script | Exit block or script with current $ |
$val -> return | Block/Script | Exit block or script with value |
pass | Any | Returns current $ unchanged |
assert cond | Any | Halt if condition false, pass through on success |
assert cond "msg" | Any | Halt with custom message if condition false |
error "msg" | Any | Always halt with error message |
$val -> error | Any | Always halt with piped error message (must be string) |
Patterns
Guard Clauses
Exit early on invalid conditions (assumes host provides error()):
|data| {
$data -> .empty ? app::error("Empty input")
$data -> :?list ? $ ! app::error("Expected list")
$data -> each { $ * 2 }
} => $processRetry with Limit
^(limit: 3) @ {
app::prompt("Try operation")
} ? (.contains("RETRY"))
.contains("SUCCESS") ? [0, "Done"] ! [1, "Failed"]State Machine
"start" -> ($ != "done") @ {
($ == "start") ? "processing" ! ($ == "processing") ? "validating" ! ($ == "validating") ? "done" ! $
}
# Walks through states: start -> processing -> validating -> doneFind First Match
[1, 2, 3, 4, 5] -> each {
($ > 3) ? ($ -> break)
$
}
# Returns 4 (first element > 3)See Also
- Variables — Scope rules and
$binding - Collections —
each,map,filter,folditeration - Operators — Comparison and logical operators
- Reference — Quick reference tables